先决条件
在一切之前,请检查这些东西:
- 指示器所在画布的
Canvas
组件渲染模式为“屏幕空间-覆盖”; - 指示器的锚点必须在左下角;
- 指示器的轴心为 ( 0.5 , 0.5 ) (0.5, 0.5) (0.5,0.5)。
请注意,以上三点非常重要。尤其是第二、三点,决定了指示器的最终显示位置是否正确(当然,不这么做也是可以通过代码来修正的)。
实现逻辑
因为本文讲的是目标指示器的实现方法,所以我们先把实现它所需要的代码给预备好:
public Transform target; //追踪目标
public RectTransform indicator; //指示器
void Update()
{
UpdateIndicatorState();
}
/// <summary>
/// 更新指示器状态
/// </summary>
void UpdateIndicatorState()
{
if (target)
{
indicator.gameObject.SetActive(true);
//我们即将实现的内容
}
else
{
indicator.gameObject.SetActive(false);
}
}
/// <summary>
/// 检查目标是否在摄像机前面
/// </summary>
/// <returns>若是,则返回true,否则返回false</returns>
public bool IsTargetInCameraFront()
{
Vector3 directionToTarget = (target.transform.position - transform.position).normalized;
return Vector3.Dot(directionToTarget, transform.forward) > 0;
}
以上代码在Update()
时会调用我们的处理函数UpdateIndicatorState()
,我们将在这个函数中实现我们的逻辑。
首先,我们需要获取到目标点的屏幕坐标:
//函数 UpdateIndicatorState() 中
Vector3 targetScreenPoint = Camera.main.WorldToScreenPoint(target.position);
由于屏幕坐标等于锚点必须在左下角的UI元素的anchoredPosition
(前提是该UI元素所在画布的Canvas
组件渲染模式为“屏幕空间-覆盖”),因此在拿到目标点的屏幕坐标后,我们就可以使指示器指向目标点了:
//函数 UpdateIndicatorState() 中
indicator.anchoredPosition = targetScreenPoint;
但是这样,我们遇到了第一个问题,即:当目标点在消失屏幕外时,指示器也会跟着消失,所以我们需要限制指示器的位置在屏幕范围内:
//函数 UpdateIndicatorState() 中
Vector2 indicatorAnchoredPosition = new Vector2(Mathf.Clamp(targetScreenPoint.x, 0, Screen.width), Mathf.Clamp(targetScreenPoint.y, 0, Screen.height));
这样就解决了指示器消失的问题,值得注意的是:当画布Canvas Scaler
的缩放模式不为“恒定像素大小”时,Screen.width
和Screen.height
并不能表示画布真实的长宽。顺带一提,我们还可以让指示器到屏幕边缘保持一段距离:
//在脚本类中
public float indicatorMargin;
//函数 UpdateIndicatorState() 中,刚才的代码
Vector2 indicatorAnchoredPosition = new Vector2(Mathf.Clamp(targetScreenPoint.x, indicatorMargin, Screen.width - indicatorMargin), Mathf.Clamp(targetScreenPoint.y, indicatorMargin, Screen.height - indicatorMargin));
然而,当目标点在摄像机背面时,指示器也会跑到屏幕中间。因此当目标点在摄像机后面时,我们要给targetScreenPoint
乘一个极小的负数,以使指示器能够继续靠边。
//函数 UpdateIndicatorState() 中
if (!IsEnemyInPlayerFront())
{
targetScreenPoint *= (Screen.width * Screen.height * -1);
}
然后,运行代码,你的指示器已经初具雏形了。接下来请按照你的需求而自由改造它。
完整代码
using UnityEngine;
public class IndicatorManagerSimple : MonoBehaviour
{
public Transform target; //追踪目标
public RectTransform indicator; //指示器
public float indicatorMargin; //指示器活动范围到屏幕的距离
void Update()
{
UpdateIndicatorState();
}
/// <summary>
/// 更新指示器状态
/// </summary>
void UpdateIndicatorState()
{
if (target)
{
indicator.gameObject.SetActive(true);
//核心代码部分
//获取目标点屏幕坐标
Vector3 targetScreenPoint = Camera.main.WorldToScreenPoint(target.position);
if (!IsTargetInCameraFront())
{
targetScreenPoint *= (Screen.width * Screen.height * -1); //当目标点在摄像机后面时,乘上一个极小的负数
}
//对 targetScreenPoint 的范围进行限定
Vector2 indicatorAnchoredPosition = new Vector2(Mathf.Clamp(targetScreenPoint.x, indicatorMargin, Screen.width - indicatorMargin), Mathf.Clamp(targetScreenPoint.y, indicatorMargin, Screen.height - indicatorMargin));
indicator.anchoredPosition = indicatorAnchoredPosition;
}
else
{
indicator.gameObject.SetActive(false);
}
}
/// <summary>
/// 检查目标是否在摄像机前面
/// </summary>
/// <returns>若是,则返回true,否则返回false</returns>
public bool IsTargetInCameraFront()
{
Vector3 directionToTarget = (target.transform.position - transform.position).normalized;
return Vector3.Dot(directionToTarget, transform.forward) > 0;
}
}