虽然三维软件提供了基本的物体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);
#endifif (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; //交点平面距离privatevoid 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操作。