本篇介绍如何实现MMO类游戏常见的功能:当玩家接取任务,引导玩家前往指定地点的UI提示功能。文章前半部分介绍实现原理,完整代码及项目文件在文章结尾。
效果演示
需求分析
- 当目标在屏幕内时,UI锁定在目标的位置,并显示玩家当前与该目标的距离
- 当目标在屏幕外时,距离提示切换为箭头提示
- 当目标在屏幕外时,UI的运动轨迹是一个椭圆型
关键方法
Camera.WorldToScreenPoint(Vector3 worldPosition)
该方法会将传入的物体的世界坐标位置转换成该摄像机中屏幕坐标位置
屏幕坐标的范围为(以1080p为例)
左下角为(0,0),右上角为(1920,1080),如果转换后的点超过这个范围,说明物体在屏幕之外。
Rectransform.anchoredPosition
当前UI元素距离锚点(Anchor)的位置,该位置是相对位置,与锚点有关
例如:如果想让UI元素位于屏幕正中间
当锚点为中心点时( Min(0.5,0.5),Max(0.5,0.5) )anchoredPosition为(0,0)
当锚点为左下角时( Min(0,0),Max(0,0) )anchoredPosition为(940,540)
Vector3.Dot(Vector3 dir1,Vector3 dir2)
计算两个向量的点积,如果值为负说明两向量夹角大于90度
常用于判断玩家与敌人是背对还是面朝的关系
例如传入playerTransform.forward和enemyTransform.forward,如果结果为负,说明玩家目前背对着敌人
Vector2.SignedAngle(Vector2 dir1, Vector2 dir2)
计算向量dir1到dir2的有符号角度。
角度正负判断可以使用左手大拇指指向屏幕,四指指向dir1,然后向dir2弯曲,如果手指是自然弯曲,那么角度为正。
SignedAngle方法的原理为使用向量点积计算角度,使用向量叉乘计算正负,Unity中为左手坐标系,所以使用左手判断正负。
完整代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class UIGuideIcon : MonoBehaviour
{
private Camera Cam;//主摄像机
[SerializeField] private TMP_Text MeterText;//
[SerializeField] private GameObject Arrow;
[SerializeField] private Transform TargetTransform;//目标的Transorm
[SerializeField] private Transform PlayerTransform;//玩家自己的Transfom,用于计算与目标的距离
public Vector2 EllipticalRadius;//椭圆轨迹的长半轴和短半轴
private RectTransform IconRectransform;//UIIcon自身的RectTransform,用于更新Icon的位置
private Vector2 ScreenSize;//屏幕大小
private Vector2 ScreenCenter;//屏幕中心位置
void Start()
{
Cam = Camera.main;
IconRectransform = GetComponent<RectTransform>();
ScreenSize.x = Screen.width;
ScreenSize.y = Screen.height;
ScreenCenter.x = ScreenSize.x / 2;
ScreenCenter.y = ScreenSize.y / 2;
}
// Update is called once per frame
private void LateUpdate()
{
if (TargetTransform != null && PlayerTransform != null)
{
UpdateIconPosition();
}
else
{
Debug.LogWarning("请确认目标以及玩家的Transform都已指定");
}
}
private void UpdateIconPosition()
{
IconRectransform.anchoredPosition = CalculatePos();
if (MeterText.IsActive())
{
int distance = (int)Vector3.Distance(TargetTransform.position, PlayerTransform.position);
MeterText.SetText($"{distance}米");
}
}
public void SetTargetSelf(Transform target, Transform player)
{
TargetTransform = target;
PlayerTransform = player;
if (target == null)
{
gameObject.SetActive(false);
}
}
private Vector2 CalculatePos()
{
Vector2 screenPos = (Vector2)Cam.WorldToScreenPoint(TargetTransform.position);
Vector3 dir = Cam.transform.forward;
Vector3 dir2 = TargetTransform.transform.position - Cam.transform.position;
bool IsOutScreen=false;
if (Vector3.Dot(dir, dir2) <= 0)//判断摄像机是否背对目标
{
screenPos.x = ScreenSize.x - screenPos.x;
screenPos.y = ScreenSize.y - screenPos.y;
IsOutScreen = true;
}
if(screenPos.x < 0 || screenPos.x > ScreenSize.x || screenPos.y < 0 || screenPos.y > ScreenSize.y)
{
IsOutScreen= true;
}
screenPos -= ScreenCenter;
if (IsOutScreen|| Mathf.Pow(screenPos.x / EllipticalRadius.x, 2) + Mathf.Pow(screenPos.y / EllipticalRadius.y, 2) > 1)//如果超出屏幕或者超出椭圆的范围,将位置约束在椭圆上
{
float x = Mathf.Sqrt(Mathf.Pow(EllipticalRadius.x * EllipticalRadius.y * screenPos.x, 2) / (Mathf.Pow(screenPos.x * EllipticalRadius.y, 2) + Mathf.Pow(EllipticalRadius.x * screenPos.y, 2)));
if (Mathf.Sign(x) != Mathf.Sign(screenPos.x))
{
x *= -1;
}
float y = x * (screenPos.y / screenPos.x);
screenPos.x = x;
screenPos.y = y;
if (MeterText.enabled )
{
MeterText.enabled = false;
Arrow.SetActive(true);
}
Arrow.transform.localEulerAngles = new Vector3(0, 0, Vector2.SignedAngle(Vector2.right, screenPos));
}
else
{
if (!MeterText.enabled )
{
MeterText.enabled = true;
Arrow.SetActive(false);
}
}
return screenPos;
}
}
思路:
首先将世界位置映射到屏幕位置,同时检测摄像机是否背对目标,如果背对目标,说明物体一定在屏幕外,当摄像机背对目标时,转换出来的坐标会是对称于中心点的,因此使用屏幕右上角减去当前位置即可,计算出来的位置的原点在屏幕左下角,由于UI的锚点中心在中间,因此减去屏幕中心点平移到中心点
如果屏幕位置超出屏幕或者在椭圆之外,将其限制在椭圆中
椭圆公式:
,a为长半轴,b为短半轴
如果将某点坐标带入,结果小于1,说明在椭圆内,否则在椭圆外
直线AO的方程为:y=kx,k=y1/x1
将直线方程与椭圆方程联立,即可解出B点坐标,根据A点的符号确定解出坐标的符号。
箭头的中心点在星星的中心,计算出Vector2.right与指向椭圆位置的向量的夹角并赋值为ArrowPivot即可让箭头指向目标
注:CanvasScaler的缩放需要为Constant Pixel Size,且ScaleFactor为1,否则会导致UI位置不准确(因为UI被以锚点为中心缩放了)
链接:https://pan.baidu.com/s/1ZCWec9qISR4FSsYwbDmtxg?pwd=s0xz
提取码:s0xz
博客创作不易,求点赞求关注~