用于场景中镜头的移动/缩放行为管理:
- 场景固定,移动和缩放的是镜头.
- 边界控制,是通过直接限定摄像机的移动范围来做的.
- 缩放是通过摄像机的高度变化来实现的.
- 进行定点缩放, editor模式下: 按住ctrl键设置缩放中心点,按住alt围绕此点进行缩放,即在该点在缩放过程中不会发生位置偏移.
- 需要在Hierarchy中添加EasyTouch
- 移动时会出现偏差,不能完全匹配手指(鼠标),通过移动地图的方式可以解决. 具体参考: 镜头移动/缩放管理(镜头固定,地图移动方式)
主要是通过摄像机距离目标的距离和透视值(FiledOfView)换算得到相关值,参考图(来自https://www.jianshu.com/p/148725feecfa):
using UnityEngine;
using HedgehogTeam.EasyTouch;
#if UNITY_EDITOR
using UnityEditor;
#endif
/// <summary>
/// 摄像机管理类.
/// 挂载到摄像机所在的GameObject.
/// </summary>
public class SceneCameraView : MonoBehaviour
{
/// 摄像机距离
private float distance = 100;
[Tooltip("边界,顺序:左上->右上->右下->左下")]
public Vector3[] rect = new Vector3[4]{
new Vector3(70.0f, 160.0f, -65.1f),
new Vector3(324.6f, 160.0f, -65.1f),
new Vector3(324.6f, 160.0f, -182.5f),
new Vector3(70.0f, 160.0f, -182.5f)
};
[Tooltip("缩放时的最高高度")]
public float scaleMaxY = 160;
[Tooltip("缩放时的最低高度")]
public float scaleMinY = 100;
private Camera mainCamera;
[Tooltip("摄像机移动到的目标点"),SerializeField]
private Vector3 lerpMoveTarget = Vector3.zero;
/// swipe结束后需要继续滑动的系数,是对swipe的gesture.deltaPosition的缩放,值越大滑动得越远
[Tooltip("手势滑动结束后,需要继续移动的系数,值越大移动得越远")]
public float lerpGoOnMoveScale = 6f;
/// lerpMove速度,值越大滑动得越快
[Tooltip("手势滑动结束后,继续(减速)移动的速度,值越大移动得越快")]
public float lerpMoveSpeed = 10f;
[Tooltip("射线最大检测距离")]
public float rayDistance = 2000;
/// 是否要进行lerpMove
internal bool lerpMove = false;
[SerializeField]
internal bool showDebugLines = false;
private bool isPinching = false;
private void Awake()
{
this.mainCamera = this.gameObject.GetComponent<Camera>();
this.lerpMove = false;
lerpMoveTarget = this.transform.localPosition;
this.GetCameraToTargetDistance();
EasyTouch.On_Pinch += EasyTouch_On_Pinch;
EasyTouch.On_DragStart += EasyTouch_On_DragStart;
EasyTouch.On_SwipeStart += EasyTouch_On_SwipeStart;
EasyTouch.On_Drag += EasyTouch_On_Drag;
EasyTouch.On_Swipe += EasyTouch_On_Swipe;
EasyTouch.On_DragEnd += EasyTouch_On_DragEnd;
EasyTouch.On_SwipeEnd += EasyTouch_On_SwipeEnd;
EasyTouch.On_TouchUp2Fingers += EasyTouch_On_TouchUp2Fingers;
}
private void OnDestroy()
{
EasyTouch.On_DragStart -= EasyTouch_On_DragStart;
EasyTouch.On_SwipeStart -= EasyTouch_On_SwipeStart;
EasyTouch.On_Drag -= EasyTouch_On_Drag;
EasyTouch.On_Swipe -= EasyTouch_On_Swipe;
EasyTouch.On_DragEnd -= EasyTouch_On_DragEnd;
EasyTouch.On_SwipeEnd -= EasyTouch_On_SwipeEnd;
EasyTouch.On_TouchUp2Fingers -= EasyTouch_On_TouchUp2Fingers;
}
/// <summary>
/// 缩放
/// </summary>
/// <param name="gesture"></param>
private void EasyTouch_On_Pinch(Gesture gesture)
{
this.isPinching = true;
// 往外扩(放大)是负数,往内聚(缩小)是整数
float scaleDelta = gesture.deltaPinch * UnityEngine.Time.deltaTime;
// 缩放中心点(相对于屏幕左下角)
Vector2 scaleCenterPos = gesture.position;
// 计算摄像机视口(摄像机显示画面)的宽高
float halfFOV = (this.mainCamera.fieldOfView * 0.5f) * Mathf.Deg2Rad;
float aspect = this.mainCamera.aspect;
// 视口在Z轴上变化时(相当于缩放效果),对应的宽高变化量,相当于直接使用scaleDelta作为Z轴的变化距离
float scaleH = scaleDelta * Mathf.Tan(halfFOV) * 2;
float scaleW = scaleH * aspect;
// 缩放中心点在屏幕中的比例,减0.5f,因为世界坐标是相对于屏幕的中心
float cpRateX = scaleCenterPos.x / Screen.width - 0.5f;
float cpRateY = scaleCenterPos.y / Screen.height - 0.5f;
Vector3 pos = this.transform.localPosition;
// scaleW*cpRateX 表示视口画面宽度变化偏移度.
// 如果cpRateX,cpRateY都为0,表示X轴,Y轴上无变化,则只以transform.forward为实际变化,效果为沿着视口中心的路径上(Z轴)前进/后退.
// 比如cpRateX为0.2f,表示在屏幕中心右侧20%位置处作为手势缩放中心点进行操作,
// scaleW此时假如为-5(表示放大),则transform.right就还需要往左走-1f,
// 最终效果为transform.forward按scaleDelta前进,同时X轴往左移动,这样视觉上20%位置处没有发生任何偏移. Y轴同理
pos += transform.right * (scaleW * cpRateX);
pos += transform.up * (scaleH * cpRateY);
pos += transform.forward * scaleDelta;
if (pos.y <= scaleMaxY && pos.y >= scaleMinY)
{
this.transform.localPosition = pos;
var borderScVec = new Vector3(scaleW * .5f, 0, scaleH * .5f);
// 边界跟随
for (int i = 0; i < rect.Length; i++)
{
rect[i] += transform.right * (scaleW * cpRateX);
rect[i] += transform.up * (scaleH * cpRateY);
rect[i] += transform.forward * scaleDelta;
}
}
this.GetCameraToTargetDistance();
}
private void EasyTouch_On_TouchUp2Fingers(Gesture gesture)
{
this.isPinching = false;
}
/// <summary>
/// 开始拖
/// </summary>
/// <param name="gesture"></param>
private void EasyTouch_On_DragStart(Gesture gesture)
{
EasyTouch_On_SwipeStart(gesture);
}
/// <summary>
/// 开始划
/// </summary>
/// <param name="gesture"></param>
private void EasyTouch_On_SwipeStart(Gesture gesture)
{
this.lerpMoveTarget = Vector3.zero;
this.lerpMove = false;
}
/// <summary>
/// 拖
/// </summary>
/// <param name="gesture"></param>
private void EasyTouch_On_Drag(Gesture gesture)
{
EasyTouch_On_Swipe(gesture);
}
/// <summary>
/// 划
/// </summary>
/// <param name="gesture"></param>
private void EasyTouch_On_Swipe(Gesture gesture)
{
if (this.isPinching) return;
this.lerpMoveTarget = Vector3.zero;
this.lerpMove = false;
// 计算摄像机视口(摄像机显示画面)的宽高
float halfFOV = (this.mainCamera.fieldOfView * 0.5f) * Mathf.Deg2Rad;
float aspect = this.mainCamera.aspect;
// 从camera开始到当前屏幕点击点对应世界坐标下的distance
var screenPosition = gesture.position;
var ray = this.mainCamera.ScreenPointToRay(screenPosition);
RaycastHit hit;
var screenPointDistance = this.distance;
if (Physics.Raycast(ray, out hit, this.rayDistance))
{
screenPointDistance = hit.distance;
}
// float halfH = this.distance * Mathf.Tan(halfFOV);
float halfH = screenPointDistance * Mathf.Tan(halfFOV);
float halfW = halfH * aspect;
// gesturePos是相对于屏幕的操作偏移,通过与Screen的比例乘以halfW,halfH,得到最终在视口上的位移
// 通过这个计算,最终效果是在拖动时也不能做到完全同步:即拖动场景中某个点到屏幕任意位置,该点依然精确的位于鼠标(手指)处.
var offx = (-gesture.deltaPosition.x / Screen.width) * halfW;
var offy = (-gesture.deltaPosition.y / Screen.height) * halfH;
var v3pos = new Vector3(offx, 0, offy);
// 只使用y的旋转信息构建一个新的四元数
var qua = Quaternion.identity;
qua.y = this.transform.rotation.y;
// 旋转vector:vector3的各分量被四元数按旋转角度计算新值
v3pos = qua * v3pos;
var tagPos = this.transform.localPosition + v3pos;
if (tagPos.x < rect[0].x)
{
tagPos.x = rect[0].x; v3pos.x = 0;
}
else
{
if (tagPos.x > rect[1].x)
{
tagPos.x = rect[1].x; v3pos.x = 0;
}
}
if (tagPos.z < rect[3].z)
{
tagPos.z = rect[3].z; v3pos.z = 0;
}
else
{
if (tagPos.z > rect[0].z)
{
tagPos.z = rect[0].z; v3pos.z = 0;
}
}
this.transform.Translate(v3pos, Space.World);
// 额外增加一个分量来,使得lerpMove时进行更多偏移, 效果为:减速滑动得更远
var extPos = v3pos * lerpGoOnMoveScale; // new Vector3(offx * lerpGoOnScale, 0, offy * lerpGoOnScale);
extPos = qua * extPos;
this.lerpMoveTarget = this.WrapPosInRect(tagPos + extPos);
}
private void EasyTouch_On_DragEnd(Gesture gesture)
{
EasyTouch_On_SwipeEnd(gesture);
}
/// <summary>
/// 开始划
/// </summary>
/// <param name="gesture"></param>
private void EasyTouch_On_SwipeEnd(Gesture gesture)
{
this.lerpMove = true;
}
private void LateUpdate()
{
if (this.lerpMove && this.lerpMoveTarget != Vector3.zero)
{
var dist = Vector3.Distance(this.transform.position, this.lerpMoveTarget);
if (dist >= 0.01f)
{
// var curPos = this.WrapPosInRect(Vector3.Lerp(this.transform.position, this.lerpMoveTarget, Time.deltaTime * this.lerpMoveSpeed));
var curPos = Vector3.Lerp(this.transform.position, this.lerpMoveTarget, Time.deltaTime * this.lerpMoveSpeed);
this.transform.position = curPos;
}
else
{
this.lerpMove = false;
}
}
#if UNITY_EDITOR
if (!showDebugLines) return;
// 可拖动区域
Debug.DrawLine(rect[0], rect[1], Color.blue);
Debug.DrawLine(rect[1], rect[2], Color.blue);
Debug.DrawLine(rect[2], rect[3], Color.blue);
Debug.DrawLine(rect[3], rect[0], Color.blue);
// 视口
Debug.DrawLine(transform.position, transform.position + transform.forward * 1000, Color.red);
Vector3[] corners = GetCorners(this.distance);
Debug.DrawLine(corners[0], corners[1], Color.red); // UpperLeft -> UpperRight
Debug.DrawLine(corners[1], corners[3], Color.red); // UpperRight -> LowerRight
Debug.DrawLine(corners[3], corners[2], Color.red); // LowerRight -> LowerLeft
Debug.DrawLine(corners[2], corners[0], Color.red); // LowerLeft -> UpperLeft
#endif
}
private Vector3 WrapPosInRect(Vector3 pos)
{
if (pos.x < rect[0].x) pos.x = rect[0].x;
else
if (pos.x > rect[1].x) pos.x = rect[1].x;
if (pos.z < rect[3].z) pos.z = rect[3].z;
else
if (pos.z > rect[0].z) pos.z = rect[0].z;
return pos;
}
///<summary>移动到目标位置,并使其与摄像机中心位置对齐</summary>
public void LookAt(Vector3 tagV3)
{
this.lerpMove = false;
this.lerpMoveTarget = Vector3.zero;
// 要lookAt指定目标点,使用向量减法,向量减法常用于取得一个对象到另一个对象之间的方向和距离
var qua = Quaternion.Euler(transform.eulerAngles);
var tagPos = tagV3 - (qua * Vector3.forward * this.distance + qua * Vector3.right + qua * Vector3.up);
if (Vector3.Distance(this.transform.position, tagPos) < 0.01) return;
this.lerpMove = true;
this.lerpMoveTarget = this.WrapPosInRect(tagPos);
}
/// 得到摄像机与指定对象的距离
private float GetCameraToTargetDistance()
{
Ray ray = new Ray(transform.position, transform.forward);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 1000))
{
distance = hit.distance;
}
return distance;
}
///<summary>
/// 计算一个点是否在一个多边形范围内
/// 如果过该点的线段与多边形的交点不为零且距该点左右方向交点数量都为奇数时 该点再多边形范围内
/// <summary>
/// <param name="point">测试点</param>
/// <param name="vertexs">多边形的顶点集合</param>
/// <returns><returns>
public static bool PolygonIsContainPoint(Vector3 point, Vector3[] vertexs)
{
//判断测试点和横坐标方向与多边形的边的交叉点
int leftNum = 0; //左方向上的交叉点数
int rightNum = 0; //右方向上的交叉点数
int index = 1;
for (int i = 0; i < vertexs.Length; i++)
{
if (i == vertexs.Length - 1) { index = -i; }
//找到相交的线段
if (point.z >= vertexs[i].z && point.z < vertexs[i + index].z || point.z < vertexs[i].z && point.z >= vertexs[i + index].z)
{
Vector3 vecNor = (vertexs[i + index] - vertexs[i]);
//处理直线方程为常数的情况
if (vecNor.x == 0.0f)
{
if (vertexs[i].x < point.x)
{
leftNum++;
}
else if (vertexs[i].x == point.x)
{ }
else
{
rightNum++;
}
}
else
{
vecNor = vecNor.normalized;
float k = vecNor.z / vecNor.x;
float b = vertexs[i].z - k * vertexs[i].x;
if ((point.z - b) / k < point.x)
{
leftNum++;
}
else if ((point.z - b) / k == point.x)
{ }
else
{
rightNum++;
}
}
}
}
if (leftNum % 2 != 0 || rightNum % 2 != 0)
{
return true;
}
return false;
}
#if UNITY_EDITOR
private Vector3[] GetCorners(float distance)
{
Vector3[] corners = new Vector3[4];
float halfFOV = (mainCamera.fieldOfView * 0.5f) * Mathf.Deg2Rad;
float aspect = mainCamera.aspect;
float halfHeight = distance * Mathf.Tan(halfFOV);
float halfWidth = halfHeight * aspect;
var tx = this.transform;
// UpperLeft
corners[0] = tx.position - (tx.right * halfWidth);
corners[0] += tx.up * halfHeight;
corners[0] += tx.forward * distance;
// UpperRight
corners[1] = tx.position + (tx.right * halfWidth);
corners[1] += tx.up * halfHeight;
corners[1] += tx.forward * distance;
// LowerLeft
corners[2] = tx.position - (tx.right * halfWidth);
corners[2] -= tx.up * halfHeight;
corners[2] += tx.forward * distance;
// LowerRight
corners[3] = tx.position + (tx.right * halfWidth);
corners[3] -= tx.up * halfHeight;
corners[3] += tx.forward * distance;
return corners;
}
#endif
}
#if UNITY_EDITOR
[CustomEditor(typeof(SceneCameraView))]
public class DCGSceneCameraViewEditor : Editor
{
private Vector3 lookAtPos;
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
var scview = this.target as DCG.SceneCameraView;
GUILayout.Space(5);
if (GUILayout.Button("跳转到moveTarget", GUILayout.Height(25)))
{
scview.lerpMove = true;
}
GUILayout.Space(5);
lookAtPos = EditorGUILayout.Vector3Field("LookAt坐标点", lookAtPos);
if (GUILayout.Button("LookAt", GUILayout.Height(25)))
{
scview.LookAt(lookAtPos); //new Vector3(122.4f, 5.3f, 132.2f)
}
}
}
#endif