系统分析

对于每个能被锁定的物体,都挂载一个Target Object脚本,用来记录当前位置以及判断是否能被加入到当前锁定列表。这里使用了isVisible变量来判断是否能被加入到锁定列表中,在Update中使用IsVisibleAndCloseEnough不断更新isVisible的值。

系统的流程图如下

TargetSystem

TargetSystem使用单例模式,每次帧更新中都会刷新锁定列表TargetList中的值,并且从锁定列表中选取距离屏幕中心最近的目标作为当前的锁定目标currentTarget。此外,TargetSystem还有FreeModeSelectionMode两种模式,在FreeMode下,每次帧更新都会更新当前目标,而SelectionMode中有按下Lock按键才能更新当前目标。

image-20231205172933913

Target Object

通过相机识别带有TargetObject的物体,可以使用TestPlanesAABB函数,判断Plane数组是否在视锥体中来测试边界框,此外还设置了一个最大值maxDistance,用来剔除超过最大边界的物体。

1
2
3
4
5
6
7
8
9
10
11
12
13
public bool IsVisibleAndCloseEnough(Camera camera, float maxDistance)
{
Plane[] planes = GeometryUtility.CalculateFrustumPlanes(camera);

if (!GeometryUtility.TestPlanesAABB(planes, GetComponent<Renderer>().bounds))
{
return false;
}

float distanceToCamera = Vector3.Distance(transform.position, camera.transform.position);
return distanceToCamera <= maxDistance;

}

然后在Update中更新isVisible值

1
2
3
4
5
private void Update()
{
position = transform.position;
isVisible = IsVisibleAndCloseEnough(m_Camera, TargetSystem.instance.maxVisibleDistance);
}

Free Mode

在自由模式下,会不断地刷新锁定目标,流程和代码如下:

  • ClearTargetList:清除列表缓冲

    1
    2
    3
    4
    private void ClearTargetList()
    {
    TargetList.Clear();
    }
  • AddTarget:将所有可见的目标加入到TargetList

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    private void AddTarget()
    {
    TargetObject[] allTargetObjects = FindObjectsOfType<TargetObject>();

    foreach (var target in allTargetObjects)
    {
    if (target.isVisible)
    {
    if (!TargetList.Contains(target))
    {
    TargetList.Add(target);
    }
    }
    }
    }
  • ChangeCurrentTarget:遍历列表,找出距离屏幕中心最近的物体,加载到当前锁定目标中;如果当前锁定目标不是最近的物体,说明发生了转换,播放锁定动画

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    private void ChangeCurrentTarget()
    {
    Vector3 screenCenter = new Vector3(Screen.width / 2, Screen.height / 2, 0);
    float minDistance = float.MaxValue;

    TargetObject closestTargetObject = null;

    if (TargetList.Count > 0)
    {
    foreach (var target in TargetList)
    {
    // 将 Target 的世界坐标转换为屏幕坐标
    Vector3 targetScreenPos = m_Camera.WorldToScreenPoint(target.position);
    float distanceToScreenCenter = Vector3.Distance(screenCenter, targetScreenPos);

    if (distanceToScreenCenter < minDistance)
    {
    closestTargetObject = target;
    minDistance = distanceToScreenCenter;
    }
    }

    }

    if (closestTargetObject != currentTarget)
    {
    StartCoroutine(ScaleAnimation());
    }

    currentTarget = closestTargetObject;
    }
  • SetUI:如果当前锁定目标不为空,则将UI的位置设置到该物体的位置;如果当前的距离超出了maxVisibleDistance,则不再可以看见物体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    private void SetUI()
    {
    if (currentTarget != null)
    {
    TargetPointUI.rectTransform.position = ClampedScreenPosition(currentTarget.position);
    TargetPointUI.gameObject.SetActive(true);
    }
    else
    {
    TargetPointUI.gameObject.SetActive(false);
    }


    float distanceToCamera = Vector3.Distance(currentTarget.position, m_Camera.transform.position);
    if (distanceToCamera > maxVisibleDistance)
    {
    TargetPointUI.gameObject.SetActive(false);
    }
    }

Selection Mode

选择模式下,只有按下了Lock键才能更换选择的目标

1
2
3
4
5
6
7
8
9
10
public void OnLock()
{
Debug.Log("Locking enemy");

if (TargetSystem.instance.currentTargetMode == TargetSystem.TargetMode.SelectionMode)
{
TargetSystem.instance.InitiateTarget();
}

}

InitiateTarget函数如下,基本是使用了Free Mode中的函数:

1
2
3
4
5
6
7
public void InitiateTarget()
{
ClearTargetList();
AddTarget();
ChangeCurrentTarget();
SetUI();
}

此外,在Update中,还要不断使用SetUI更新UI的位置

UI

画了个图标,作为锁定效果的UI,图标如下:

TargetUI

Animation

使用一个协程函数来实现动画效果,动画是一个简单的缩放动画,需要提前记录下原本的UI大小,然后使用插值位置进行动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private IEnumerator ScaleAnimation()
{
float duration = 0.25f;
float elapsed = 0.0f;

Vector3 startScale = TargetPointUI.rectTransform.localScale + new Vector3(2.0f, 2.0f, 2.0f);
Vector3 targetScale = m_TargetScale;

while (elapsed < duration)
{
TargetPointUI.rectTransform.localScale = Vector3.Lerp(startScale, targetScale, elapsed / duration);
elapsed += Time.deltaTime;
yield return null;
}

// 确保最终大小为目标大小
TargetPointUI.rectTransform.localScale = targetScale;
}