先给出效果
基本思路
箭头指示由两点组成,首先是定位,其次是旋转。旋转不是问题,所以主要思考如何定位。
最直接想到的思路:想象屏幕外物体和屏幕中心有一条连线,然后这条交点,就是定位点了。所以只需确认这条连线与屏幕矩形的哪一条边相交,再求这个定位点在连线上的比例。
初中学的相似三角形就可以解决它了。
三种方法
四边对应的四区域判定
最早想到的是,直接根据物体的坐标来判断边。
当物体在区域外时,继续以下判断:
[屏幕左下角是(0,0)]
当y在0~height[屏幕高度]之间时,看x是小于0还是大于width[屏幕宽度],这就两个区域了。
当x在0~width之间时,看y是小于0还是大于height。
四个角咋办?就被我漏了,只能直接定位到角落。
可以注意到,在角落有一个很突兀的跳动。
代码:
/// <summary>
/// 得到【屏幕外物体位置到屏幕中心的连线】与屏幕边界的交点,无法过渡四角。
/// </summary>
/// <param name="x">物体X坐标</param>
/// <param name="y">物体Y坐标</param>
/// <param name="width">屏幕宽度</param>
/// <param name="height">屏幕高度</param>
/// <returns></returns>
private static Vector2 CalculateIntersection(float x, float y, float width, float height)
{
Vector2 position = new Vector2();
if (CheckInView(x, y, width, height))
{
position.x = x;
position.y = y;
}
if (0 <= y && y <= height)
{
if (x < 0)
{
position.x = 0;
position.y = height / 2 + (y - (height / 2)) * (width / 2) / (width / 2 - x);
}
else if (x > width)
{
position.x = width;
position.y = height / 2 + (y - (height / 2)) * (width / 2) / (x - width / 2);
}
}
else if (0 <= x && x <= width)
{
if (y < 0)
{
position.y = 0;
position.x = width / 2 + (x - (width / 2)) * (height / 2) / (height / 2 - y);
}
else if (y > height)
{
position.y = height;
position.x = width / 2 + (x - (width / 2)) * (height / 2) / (y - height / 2);
}
}
else//四角当如何?
{
position.x = x < 0 ? 0 : width;
position.y = y < 0 ? 0 : height;
}
return position;
}
★横轴纵轴对角线八分区域★
那么就需要处理好四个角。用高宽比得到对角线斜率之后[camera自带的属性aspect可以提供宽高比],用物体和中心连线的斜率与之对比,那么就可以进一步区分。
当然,使用的位置赋值公式还是没变。
这种方法效果是最好的,相当的丝滑。
看看现在的角落:
代码:
/// <summary>
/// 得到【屏幕外物体位置到屏幕中心的连线】与屏幕边界的交点,无死角。
/// </summary>
/// <param name="x">物体X坐标</param>
/// <param name="y">物体Y坐标</param>
/// <param name="width">屏幕宽度</param>
/// <param name="height">屏幕高度</param>
/// <returns></returns>
private static Vector2 CalculateIntersectionBetter(float x, float y, float width, float height)
{
Vector2 position = new Vector2();
if (CheckInView(x, y, width, height))
{
position.x = x;
position.y = y;
return position;
}
float aspectRatio = height / width;
float relativeY = y - height / 2;
float relativeX = x - width / 2;
float k = relativeY / CommonParameter.GetSafeFloatDivisor(relativeX);//GetSafeFloatDivisor : return value = value == 0 ? 0.01f : value;
/*
*
* |
* 2 | 3
* |
* |
* |
* 1 | 4
* |
* |
*————————————————————|————————————————————h/2
* |
* |
* 8 | 5
* |
* |
* |
* 7 | 6
* |
* w/2
*
*
* 8=1 2=3 4=5 6=7
*/
if (y > height / 2)
{
if (x < width / 2)
{
if (-aspectRatio < k) //1
{
position.x = 0;
position.y = height / 2 + (y - (height / 2)) * (width / 2) / (width / 2 - x);
}
else //2
{
position.x = width / 2 + (x - (width / 2)) * (height / 2) / (y - height / 2);
position.y = height;
}
}
else
{
if (aspectRatio < k) //3
{
position.x = width / 2 + (x - (width / 2)) * (height / 2) / (y - height / 2);
position.y = height;
}
else //4
{
position.x = width;
position.y = height / 2 + (y - (height / 2)) * (width / 2) / (x - width / 2);
}
}
}
else
{
if (x > width / 2)
{
if (-aspectRatio < k) //5
{
position.x = width;
position.y = height / 2 + (y - (height / 2)) * (width / 2) / (x - width / 2);
}
else //6
{
position.y = 0;
position.x = width / 2 + (x - (width / 2)) * (height / 2) / (height / 2 - y);
}
}
else
{
if (aspectRatio < k) //7
{
position.y = 0;
position.x = width / 2 + (x - (width / 2)) * (height / 2) / (height / 2 - y);
}
else //8
{
position.x = 0;
position.y = height / 2 + (y - (height / 2)) * (width / 2) / (width / 2 - x);
}
}
}
return position;
}
其实仍然是四区域的四种赋值,但是因为斜率的符号问题…以我的水平只能用这种粗暴的方法了。
要套三层if else,其实我是拒绝的。可惜没水平做到以不降低效率的方式简化代码,这里层层二分已经很快了。
根据角度与屏幕矩形取比例
另辟蹊径:我想着,屏幕这四周这个矩形的周长是固定的,那么按照角度来定位到矩形的不同位置,是不是也可以呢?
然后我写了一下
float C = 2 * (width + height);
float angle = Vector3.SignedAngle(Vector3.right, direction, Vector3.forward);
float ratio = angle / 360f;
float L = ratio * C;
效果是这样的:
整体效果看起来没大问题,只是箭头定位点没那么准。角度转换到线段上,这个变化并不是很平滑。
代码:
/// <summary>
/// 利用物体位置所在角度和屏幕周长比例,
/// 得到【屏幕外物体位置到屏幕中心的连线】
/// 与屏幕边界的交点
/// </summary>
/// <param name="x">物体X坐标</param>
/// <param name="y">物体Y坐标</param>
/// <param name="width">屏幕宽度</param>
/// <param name="height">屏幕高度</param>
/// 有一些误差
private static Vector2 CalculateIntersectionByPerimeter(float x, float y, float width, float height)
{
Vector2 position = new Vector2();
Vector3 direction = new Vector2(x - width / 2, y - height / 2).normalized;
/*
* ▷ w + h / 2 2 ▷ h / 2
* |————————————————|————————————————|
* | | |
* | +90 |
* | | |
* 1| + | + |
* | | |
* | | |
* | | |
* ——|——————180———————|————————0———————|————h/2 3
* | | |
* | | |
* | | |
* 5| - | - |
* | | |
* | -90 |
* | | |
* |————————————————|————————————————|
* ▷ -h / 2 - w w/2 ▷ -h / 2
* 0,0 4
*
*/
float C = 2 * (width + height);
//AXIS做大拇指,四指从from转到to,其实就是叉乘。 这是左手系。 正的Z是向里的。
float angle = Vector3.SignedAngle(Vector3.right, direction, Vector3.forward);
float ratio = angle / 360f;
float L = ratio * C;
if (width + height / 2 <= L && L < C / 2) //1
{
position.x = 0;
position.y = C / 2 - L + height / 2;
}
else if (height / 2 <= L && L < width + height / 2) //2
{
position.x = width + height / 2 - L;
position.y = height;
}
else if (-height / 2 <= L && L < height / 2) //3
{
position.x = width;
position.y = L + height / 2;
}
else if (-height / 2 - width <= L && L < -height / 2)//4
{
position.x = L + width + height / 2;
position.y = 0;
}
else if (-C / 2 <= L && L < -height / 2 - width) //5
{
position.x = 0;
position.y = -C / 2 - L + height / 2;
}
return position; ;
}
下面是整个类的代码,因为篇幅问题,上面的三个方法就不再重复了。
using UnityEngine;
using UnityEngine.UI;
public class DrawArrowForOutOfSight : MonoBehaviour
{
public Transform arrow;
public Transform root;
private static Transform s_arrow;
private static Transform canvasTransform;
private static Transform arrowObject;
private void Awake()
{
s_arrow = Component.FindObjectOfType<DrawArrowForOutOfSight>().arrow;
canvasTransform = Component.FindObjectOfType<DrawArrowForOutOfSight>().root;
}
private void FixedUpdate()
{
Vector3 objectPosition = Camera.main.ViewportToWorldPoint
(
new Vector3(Input.mousePosition.x / Screen.width, Input.mousePosition.y / Screen.height, 0f)
);
arrowObject= DrawArrow(arrowObject, objectPosition);
}
/// <summary>
/// 在Canvas设置指向屏幕外部物体的箭头,返回此箭头对象。
/// </summary>
/// <param name="arrowObject">箭头对象</param>
/// <param name="objectPosition">目标物体的位置</param>
public static Transform DrawArrow(Transform arrowObject, Vector3 objectPosition)
{
Vector2 objectScreenPoint = Camera.main.WorldToScreenPoint(objectPosition);
if (!CheckInView(objectScreenPoint.x, objectScreenPoint.y, Screen.width, Screen.height))
{
Vector2 arrowPosition = CalculateIntersectionBetter(objectScreenPoint.x, objectScreenPoint.y, Screen.width, Screen.height);
Vector3 direction = (arrowPosition - new Vector2(Screen.width / 2, Screen.height / 2)).normalized;
if (arrowObject == null)
{
arrowObject = Instantiate(s_arrow, arrowPosition, Quaternion.AngleAxis(Vector3.SignedAngle(Vector3.up, direction, Vector3.forward), Vector3.forward), canvasTransform);
}
else
{
arrowObject.gameObject.SetActive(true);
arrowObject.position = arrowPosition;
arrowObject.rotation = Quaternion.AngleAxis(Vector3.SignedAngle(Vector3.up, direction, Vector3.forward), Vector3.forward);
}
}
else if (arrowObject != null)
{
arrowObject.gameObject.SetActive(false);
}
return arrowObject;
}
public static bool SetArrowColor(Transform arrowObject, Color color)
{
if (arrowObject == null) return false;
arrowObject.GetComponent<Image>().color = color;
return true;
}
/// <summary>
/// 确认目标是否在视野内
/// </summary>
/// <param name="x">物体X坐标</param>
/// <param name="y">物体Y坐标</param>
/// <param name="width">屏幕宽度</param>
/// <param name="height">屏幕高度</param>
private static bool CheckInView(float x, float y, float width, float height)
{
return x > 0 && x < width && y > 0 && y < height;
}
private static Vector2 CalculateIntersection(float x, float y, float width, float height){}
private static Vector2 CalculateIntersectionBetter(float x, float y, float width, float height){}
private static Vector2 CalculateIntersectionByPerimeter(float x, float y, float width, float height){}
}
这里的FixedUpdate是为了用鼠标指针来演示。
使用的时候,需求对象类内直接调用静态方法,传入信息就可以了,但对象类要多持有一个Transform变量。如:
public class Planet : CelestialBodyBase
{
private Transform arrowObject;
private bool isArrowUpdated;
void OnEnable()
{
isArrowUpdated = false;
}
void FixedUpdate()
{
arrowObject = DrawArrowForOutOfSight.DrawArrow(arrowObject, transform.position);
if (!isArrowUpdated)
{
isArrowUpdated = DrawArrowForOutOfSight.SetArrowColor(arrowObject, gameObject.GetComponent<SpriteRenderer>().color);
}
}
我是把这个类的脚本挂到场景object上,然后放一个箭头预制体进去。
ArrowRoot是一个空物体,以免Hierarchy太乱。