Unity开发:两种屏幕外目标点标记的实现方法 | indienova 独立游戏 转载
以上转载的文章,适用于第一人称视角游戏,但对于mmo类型游戏,相机围绕角色转动,使用该文章代码,如果相机偏顶视角向下看角色和目标点,表现正常,但如果相机偏平视角看角色和目标点,会出现目标点从世界空间转换后的屏幕空间有偏差。
为解决该问题,提供以下代码
using System;
using UnityEngine;
using XLua;
public class ShowTargetDistance:MonoBehaviour
{
private float refreshTime = 0.01f;
private float refreshTimer;
private float offsetLeft = 200;
private float offsetRight = 200;
private float offsetUp = 120;
private float offsetDown = 120;
private bool isInSideView = false;
private bool isShowTarget;
private Unit targetUnit;
private Vector3 mainPlayerPos;
private Vector3 targetPos;
private Transform targetTransform;
private GameObject targetSelectNode;
private GameObject showTargetMoveNode;
private GameObject showTargetRotateNode;
private LText showTargetDisText;
private Transform camTransform;
private Vector3 playerScreenPos;
private Vector3 targetScreenPos;
private Vector3 targetVector;
private float targetShowRotate;
private float targetScreenEuler;
private float distanceToTarget;
private float lastDisShow;
private int screenWidth;
private int screenHeight;
private void Start()
{
InitParams();
}
private void OnDestroy()
{
Clear();
}
private void Update( )
{
if (isShowTarget)
{
if (GameUtil.GetPlayerMgr().GetMainPlayer()!=null)
{
mainPlayerPos = GameUtil.GetPlayerMgr().GetMainPlayer().GetActorMeterPos();
targetPos = targetUnit.GetActorMeterPos();
refreshTimer += Time.deltaTime;
if (refreshTimer>refreshTime)
{
RefreshTargetScreenPos();
RefreshTargetScreenEuler();
RefreshTargetDistance();
RefreshUI();
refreshTimer = 0;
}
}
}
}
[WhiteList]
public void SetShowTargetUI(GameObject moveNode,GameObject rotateNode,LText showDisText)
{
showTargetMoveNode = moveNode;
showTargetRotateNode = rotateNode;
showTargetDisText = showDisText;
}
[WhiteList]
public void SetShowTargetState(bool _isShow,Unit _target=null)
{
refreshTimer = refreshTime;
if (_isShow)
{
targetUnit = _target;
targetTransform = targetUnit.actor.transform;
// targetSelectNode = targetUnit.actor.GetActorNode("fx_body");
// if (targetSelectNode == null)
// {
// targetSelectNode = targetTransform.gameObject;
// }
}
else
{
Clear();
}
isShowTarget = _isShow;
}
[WhiteList]
public void RefreshOffset(float _offsetLeft, float _offsetRight, float _offsetUp, float _offsetDown,float _refreshTime)
{
offsetLeft = _offsetLeft;
offsetRight = _offsetRight;
offsetUp = _offsetUp;
offsetDown = _offsetDown;
refreshTime = _refreshTime;
}
//可以打开界面的时候再初始化,因为这个时候拿到的屏幕尺寸没问题
private void InitParams()
{
screenWidth = Screen.width;
screenHeight = Screen.height;
}
private void Clear()
{
isShowTarget = false;
targetUnit = null;
}
//计算起始点目标点的连线和边界框的交叉点
private Vector3 GetBoundPos(Vector3 targetScreenPos)
{
float x = targetScreenPos.x;
float y = targetScreenPos.y;
Vector2 center = playerScreenPos;
float k = (targetScreenPos.y - center.y) / (targetScreenPos.x - center.x);
float b = targetScreenPos.y - k * targetScreenPos.x;
if (targetScreenPos.x=offsetDown&&y<=screenHeight-offsetUp)
{
x = offsetLeft;
}
else
{
if (yscreenHeight-offsetUp)
{
y = screenHeight-offsetUp;
}
x = (y - b) / k;
}
}else if (targetScreenPos.x>offsetLeft&&targetScreenPos.xscreenHeight-offsetUp)
{
y = screenHeight - offsetUp;
}else if (targetScreenPos.y=offsetDown&&y<=screenHeight-offsetUp)
{
x = screenWidth-offsetRight;
}
else
{
if (yscreenHeight-offsetUp)
{
y = screenHeight-offsetUp;
}
x = (y - b) / k;
}
}
targetScreenPos.x = x;
targetScreenPos.y = y;
return targetScreenPos;
}
private Vector3 KClamp(Vector3 targetScreenPos,Vector3 playerScreenPos)
{
if (targetScreenPos.y>=offsetDown&&targetScreenPos.y<=screenHeight-offsetUp&&targetScreenPos.x>=offsetLeft&&targetScreenPos.x<=screenWidth-offsetRight)
{
//在限制框范围内,不显示
isInSideView = true;
}
else
{
//超出限制范围,则在边框处显示
targetScreenPos = GetBoundPos(targetScreenPos);
isInSideView = false;
}
return targetScreenPos;
}
private void RefreshTargetScreenEuler()
{
targetVector = (targetScreenPos-playerScreenPos).normalized;
// 计算向量和Y轴正方向的夹角
float angle = Math.Abs(Vector3.Angle(targetVector, Vector3.up));
if (targetVector.x>=0)
{
if (targetVector.y>=0)
{
//1象限
angle = 90 - angle;
}
else
{
//4
angle = 360 - (angle - 90);
}
}
else
{
if (targetVector.y>=0)
{
//2
angle = angle+90;
}
else
{
//3
angle = angle+90;
}
}
//图片0度箭头向上
targetShowRotate = angle-90;
}
//这种计算方式是以相机为起始点,适用于第一人称视角的游戏,但现在的情况是第三人称,以角色为起始点,用该方法会出现当相机从偏向顶视角向下看的时候,目标点位置有偏移
private Vector3 WorldPosToScreenPos(Vector3 worldPos)
{
Vector3 screenPos;
camTransform = Camera.main.transform;
var vFov = Camera.main.fieldOfView;
var radHFov = 2 * Mathf.Atan(Mathf.Tan(vFov * Mathf.Deg2Rad / 2) * Camera.main.aspect);
var hFov = Mathf.Rad2Deg * radHFov;
Vector3 deltaUnitVec = (worldPos - camTransform.position).normalized;
float vdegobj = Vector3.Angle(Vector3.up, deltaUnitVec) - 90f;
float vdegcam = Vector3.SignedAngle(Vector3.up, camTransform.forward, camTransform.right) - 90f;
float vdeg = vdegobj - vdegcam;
float hdeg = Vector3.SignedAngle(Vector3.ProjectOnPlane(camTransform.forward, Vector3.up), Vector3.ProjectOnPlane(deltaUnitVec, Vector3.up), Vector3.up);
vdeg = Mathf.Clamp(vdeg, -89f, 89f);
hdeg = Mathf.Clamp(hdeg, hFov * -0.5f, hFov * 0.5f);
Vector3 projectedPos = Quaternion.AngleAxis(vdeg, camTransform.right) * Quaternion.AngleAxis(hdeg, camTransform.up) * camTransform.forward;
Vector3 newPos = Camera.main.WorldToScreenPoint(camTransform.position + projectedPos);
//newPos.z = 0;
screenPos = newPos;
return screenPos;
}
// 用点积判断目标点是否处于相机背后,用 Camera.main.WorldToScreenPoint 会出现当目标点接近相机背后时,出现转换的屏幕空间位置出现在了屏幕顶部
private Vector3 WorldPosToScreenPosNew(Vector3 worldPos)
{
Vector3 newPos;
camTransform = Camera.main.transform;
Vector3 delta = worldPos - camTransform.position;
float dot = Vector3.Dot(camTransform.forward, delta);
if (dot < 0)
{
Vector3 projectedPos = camTransform.position + (delta - camTransform.forward * dot * 1.01f);
newPos = Camera.main.WorldToScreenPoint(projectedPos);
}
else
{
newPos = Camera.main.WorldToScreenPoint(worldPos);
}
return newPos;
}
private void RefreshTargetScreenPos()
{
playerScreenPos = Camera.main.WorldToScreenPoint(mainPlayerPos);
//targetScreenPos = WorldPosToScreenPosNew(targetSelectNode.transform.position);
//策划希望指向脚下
targetScreenPos = WorldPosToScreenPosNew(targetPos);
targetScreenPos = KClamp(targetScreenPos,playerScreenPos);
}
private void RefreshTargetDistance()
{
distanceToTarget = Vector3.Distance(mainPlayerPos, targetPos);
if (showTargetMoveNode.activeInHierarchy!=!isInSideView)
{
showTargetMoveNode.SetActive(!isInSideView);
}
}
private void RefreshUI()
{
if (showTargetMoveNode)
{
showTargetMoveNode.transform.position = Vector3.Lerp(showTargetMoveNode.transform.position, targetScreenPos, 20 * Time.deltaTime);
}
if (showTargetRotateNode)
{
showTargetRotateNode.transform.rotation = Quaternion.Euler(0, 0, targetShowRotate);
}
if (showTargetDisText)
{
float roundedNum = (float)Math.Round(distanceToTarget, 1);
if (Mathf.Abs(lastDisShow-roundedNum)>0.2f)
{
showTargetDisText.text = roundedNum.ToString()+"m";
}
lastDisShow = roundedNum;
}
}
}
设置了四个方向的缩进距离形成的视野限制框,目标点进入限制框后,目标在边框上显示。