Unity: 一个简单的镜头移动/缩放管理类(只移动镜头方式)

用于场景中镜头的移动/缩放行为管理:

  1. 场景固定,移动和缩放的是镜头.
  2. 边界控制,是通过直接限定摄像机的移动范围来做的.
  3. 缩放是通过摄像机的高度变化来实现的.
  4. 进行定点缩放, editor模式下: 按住ctrl键设置缩放中心点,按住alt围绕此点进行缩放,即在该点在缩放过程中不会发生位置偏移.
  5. 需要在Hierarchy中添加EasyTouch
  6. 移动时会出现偏差,不能完全匹配手指(鼠标),通过移动地图的方式可以解决. 具体参考: 镜头移动/缩放管理(镜头固定,地图移动方式)

主要是通过摄像机距离目标的距离和透视值(FiledOfView)换算得到相关值,参考图(来自https://www.jianshu.com/p/148725feecfa):

image

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

转载请注明出处: https://www.jianshu.com/p/a46b1715b099

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,100评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,862评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,993评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,309评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,303评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,421评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,830评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,501评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,689评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,506评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,564评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,286评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,826评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,875评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,114评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,705评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,269评论 2 341