虽然三维软件提供了基本的物体RTS操作,但是对于用户来说过于复杂。
这些操作方式需要用户理解什么是三维空间、XYZ坐标系、欧拉角等。但是用户视角下,就一个二维屏幕+动来动去的鼠标光标。
之前写过一套RTM组件,RTM组件,讲解了移动操作。
这次基于Camera视口(二维屏幕)+屏幕坐标(鼠标光标)来实现一套全新的物体RTS操作。
一.Translate
移动的原理是根据RayHitPoint获取视椎平面,使物体在视椎平面相对移动,如下:
根据hitpoint到hp1的相对坐标计算center到c1的坐标
接下来实现代码:
using UnityEngine;
using UnityEngine.EventSystems;
public abstract class RTSBaseComp : MonoBehaviour
{
public bool isOperating = false;
protected Vector3 rayHitPoint; //交点
protected Vector3 worldBaisPos; //世界坐标偏移量
protected float planeDistance; //交点平面距离
protected virtual void Awake()
{
}
protected virtual void Start()
{
EventTriggerListener.Get(gameObject).onLeftPointDown.AddListener(LeftPointDownCallback);
EventTriggerListener.Get(gameObject).onLeftPointUp.AddListener(LeftPointUpCallback);
}
protected virtual void Update()
{
}
protected virtual void LeftPointDownCallback(GameObject go, PointerEventData data)
{
if (CameraControl.Instance.IsPointRaycastHit(out var rayhit))
{
rayHitPoint = rayhit.point;
worldBaisPos = transform.position - rayHitPoint;
planeDistance = CameraControl.Instance.GetParallelPlaneDistance(rayHitPoint);
isOperating = true;
}
}
protected virtual void LeftPointUpCallback(GameObject go, PointerEventData data)
{
isOperating = false;
}
protected virtual void OnDestroy()
{
EventTriggerListener.Get(gameObject).onLeftPointDown.RemoveListener(LeftPointDownCallback);
EventTriggerListener.Get(gameObject).onLeftPointUp.RemoveListener(LeftPointUpCallback);
}
}
基类中封装最基本的用户鼠标操作。
/// <summary>
/// 根据世界坐标获取平行于视锥体平面
/// 获取平面距离
/// </summary>
/// <param name="screenPos"></param>
/// <param name="wpos"></param>
/// <returns></returns>
public float GetParallelPlaneDistance(Vector3 wpos)
{
Vector3 from = mainTransform.position;
Vector3 end = wpos;
Vector3 f2e = end - from;
float f2eDistance = Vector3.Distance(from, end);
float deg = Vector3.Angle(f2e, mainTransform.forward);
float pdistance = Mathf.Cos(deg * Mathf.Deg2Rad) * f2eDistance;
return pdistance;
}
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
/// <summary>
/// 基于HitPoint视锥平面
/// </summary>
public class RTSTranslateComp : RTSBaseComp
{
public UnityAction<Vector3> OnTranslatingListener;
public UnityAction<Vector3> OnEndTranslateListener;
protected override void Update()
{
base.Update();
if (isOperating)
{
Vector2 csPos = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
Vector3 cwPos = CameraControl.Instance.GetScreenToWorldPos(csPos, planeDistance);
transform.position = cwPos + worldBaisPos;
OnTranslatingListener?.Invoke(transform.position);
}
}
protected override void LeftPointUpCallback(GameObject go, PointerEventData data)
{
base.LeftPointUpCallback(go, data);
OnEndTranslateListener?.Invoke(transform.position);
}
}
PS:其中有一些依赖函数是我框架代码内的,只标注意义,因为以前都有讲解过原理,所以节省篇幅。
效果如下:
二.Rotate
旋转的原理是根据RayHitPoint获取视椎平面Plane。物体中心到Plane投影点为旋转中心,Camera坐标系为基准。HitPoint绕Z轴旋转,依据左手定则,如下:
需要注意的是基于RayHitPoint视椎平面的旋转。
接下来实现代码:
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
/// <summary>
/// 基于RayHitPoint视锥平面
/// </summary>
public class RTSRotateComp : RTSBaseComp
{
public UnityAction<Vector3> OnRotatingListener;
public UnityAction<Vector3> OnEndRotateListener;
private Vector3 rotateAxis; //旋转轴
private Vector3 rotateCenter; //旋转中心
private Vector3 lastHitPoint;
private Vector3 crtHitPoint;
protected override void LeftPointDownCallback(GameObject go, PointerEventData data)
{
base.LeftPointDownCallback(go, data);
rotateAxis = CameraControl.Instance.mainTransform.forward;
Vector2 csPos = CameraControl.Instance.GetWorldToScreenPos(transform.position);
rotateCenter = CameraControl.Instance.GetScreenToWorldPos(csPos, planeDistance);
lastHitPoint = rayHitPoint;
}
protected override void Update()
{
base.Update();
#if UNITY_EDITOR
//辅助坐标系
Debug.DrawLine(rotateCenter, rotateCenter + CameraControl.Instance.mainTransform.right, Color.red);
Debug.DrawLine(rotateCenter, rotateCenter + CameraControl.Instance.mainTransform.up, Color.green);
Debug.DrawLine(rotateCenter, rotateCenter + rotateAxis, Color.blue);
#endif
if (isOperating)
{
crtHitPoint = CameraControl.Instance.GetScreenToWorldPos(Input.mousePosition, planeDistance);
Vector3 f = lastHitPoint - rotateCenter;
Vector3 t = crtHitPoint - rotateCenter;
//f到t的角度,左手定则,逆时针
float deltaAngle = Vector3.SignedAngle(f, t, rotateAxis);
transform.RotateAround(rotateCenter, rotateAxis, deltaAngle);
lastHitPoint = crtHitPoint;
OnRotatingListener?.Invoke(transform.eulerAngles);
}
}
protected override void LeftPointUpCallback(GameObject go, PointerEventData data)
{
base.LeftPointUpCallback(go, data);
OnEndRotateListener?.Invoke(transform.eulerAngles);
}
}
效果如下:
三.Scale
常见的缩放功能是鼠标滚轮缩放物体,但是并非基于RayHitPoint,所以视觉上会偏移,为了修正偏移,实现基于RayHitPoint的缩放。
原理是在缩放的同时,根据RayHitPoint相对位移计算物体中心位移,依据视椎平面相对位移*相对缩放即可。如下:
代码实现如下:
using UnityEngine;
using UnityEngine.EventSystems;
public class RTSScaleComp : MonoBehaviour
{
[Range(0.5f, 2f)]
public float ScrollSpeed = 1.0f;
private bool isPointEnter = false;
private Vector3 rayHitPoint; //交点
private Vector3 lastLocalScale; //当前缩放值
private Vector3 worldBaisPos; //世界坐标偏移量
private float planeDistance; //交点平面距离
private
void Start()
{
EventTriggerListener.Get(gameObject).onPointEnter.AddListener(PointEnterCallback);
EventTriggerListener.Get(gameObject).onPointExit.AddListener(PointExitCallback);
}
private void PointEnterCallback(GameObject go, PointerEventData data)
{
isPointEnter = true;
}
private void PointExitCallback(GameObject go, PointerEventData data)
{
isPointEnter = false;
}
void Update()
{
if (isPointEnter)
{
float val = Input.GetAxis("Mouse ScrollWheel");
if (val == 0)
{
if (CameraControl.Instance.IsPointRaycastHit(out var rayhit))
{
rayHitPoint = rayhit.point;
worldBaisPos = transform.position - rayHitPoint;
lastLocalScale = transform.localScale;
planeDistance = CameraControl.Instance.GetParallelPlaneDistance(rayHitPoint);
}
}
else
{
ScrollMove(val);
Vector2 csPos = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
Vector3 cwPos = CameraControl.Instance.GetScreenToWorldPos(csPos, planeDistance);
//计算缩放
Vector3 sca = transform.localScale.Division(lastLocalScale);
transform.position = cwPos + worldBaisPos.Multiply(sca);
}
}
}
public void ScrollMove(float val)
{
transform.localScale += (Vector3.one * val * ScrollSpeed);
}
private void OnDestroy()
{
EventTriggerListener.Get(gameObject).onPointEnter.RemoveListener(PointEnterCallback);
EventTriggerListener.Get(gameObject).onPointExit.RemoveListener(PointExitCallback);
}
}
效果如下:
这样就实现了一套Camera(用户)视角下,精准的RTS操作。