場景中旋轉攝像頭,被觀察的物體始終在界面的一側

前言

現在很多遊戲都會在主頁中顯示一個場景,然後玩家的人物會在場景當中。這個時候往往會需要一些旋轉功能,來方便玩家查看人物的各個角度。

一般的做法就是人物自身轉動,攝像機不動,這樣用戶無法用其他視角觀察到整個場景。另一種是人物不動,相機圍繞人物,或者某個點轉動,這種做法的缺點在於要麼人物在屏幕中央(看向人物),要麼會在屏幕左右亂跑(看向人物邊上某個點),會侷限UI的設計。

因此最好的方法便是可以讓人物在屏幕中的任意位置,同時旋轉起來的時候,轉動相機,同時人物相對屏幕的位置也保持不變,如圖:

     

 

思路

因爲相機看向的點(這裏我們叫他LookedAt點),永遠在屏幕的中央。假設我們的人物放在UI的左側,那麼我們要保證旋轉相機的時候,人物的點(這裏我們叫他Target點)在相機Camera看來始終在LookedAt點的左邊。

我們可以把LookedAt點,Target點,和Camera,三個點想象成一個三角形,然後這個三角形圍繞着Target點旋轉,也就是LookedAt點和Camera同時以相同速度(角度/每秒)圍繞Target點公轉。即可實現我們要的效果

視角俯視或仰視的話,只需修改Camera的高度,其他兩個點不動。

視角拉近或拉遠,則需要同比例修改LookedAt點到Target點的距離和Camera到Target點的距離,Target點不動,即可保持三角形的三個角的角度不變。

 

代碼實現

首先我們先搭建一個簡單的場景,如圖,Target點在LookAt點的左側

我們可以先通過一些參數來初始化Camera的位置,然後此時Camera,LookedAt和Target三個點形成的三角形即我們到時候要旋轉的樣式。

然後獲取變成和角度等,來計算旋轉後的偏移值等等。具體註釋見下面代碼:

using UnityEngine;

[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class CameraAndLookAtRotateWithTarget : MonoBehaviour
{
    [SerializeField] Transform m_target;//需要被觀察的點
    [SerializeField] Transform m_lookedAt;//攝像機lookat的點

    [SerializeField] bool m_isTargetLeft;//相機看到的,m_target是否在m_lookedAt的左邊

    [SerializeField] float m_defaultCameraAngleX = 180;//相機初始位置相對m_lookedAt.forward的水平角度偏移
    [SerializeField] float m_defaultCameraAngleY = 5;//相機初始位置相對法線爲m_lookedAt.up的平面的角度偏移,銳角
    [SerializeField] float m_defaultCameraDistance = 8;//相機初始位置相對m_lookedAt的距離
#if UNITY_EDITOR
    [SerializeField] bool m_isChangeDefaultValue = false;//修改默認值之後設爲true來重新設置攝像機初始位置,方便調試
#endif

    //相機上下的最大最小偏移角度
    [SerializeField] float m_cameraMaxAngleY = 60;
    [SerializeField] float m_cameraMinAngleY = 0;

    //相機距離m_target的最大最小距離
    [SerializeField] float m_cameraMaxDistance = 20;
    [SerializeField] float m_cameraMinDistance = 5;

    [SerializeField] float m_cameraCurrentDistance = 0;//顯示當前相機距離m_target的距離,方便調試

    Transform m_transform;

    const float ANGLE_CONVERTER = Mathf.PI / 180;//弧度,用於Mathf.Sin,Mathf.Cos的計算

    //m_target到m_lookedAt的距離,m_target到相機距離
    float m_targetToLookedAtDistance, m_targetToCameraDistance;

    //m_lookedAt和相機到m_target的連線,x爲相對m_target.forward的角度,y爲相對法線爲m_target.up的平面的角度
    float m_lookedAtAngleX, m_lookedAtAngleY, m_cameraAngleX, m_cameraAngleY;

    ///m_lookedAt和相機的位置相對m_target位置的偏移
    Vector3 m_lookedAtOffset, m_cameraOffset;

    //是否旋轉或拉近拉遠了
    bool m_isChange = false;

    //距離每次變化的比例
    float m_changeDistanceRatio = 0.05f;

    void Awake()
    {
        m_transform = transform;
    }

    void Start()
    {
        SetCameraDefaultPosition();
        GetInitialData();
    }

    //設置攝像機的初始位置
    void SetCameraDefaultPosition()
    {
        Vector3 offset = CalculateOffset(m_defaultCameraDistance, m_defaultCameraAngleX, m_defaultCameraAngleY);
        m_transform.position = m_lookedAt.position + offset;
        m_transform.LookAt(m_lookedAt);
    }

    void GetInitialData()
    {
        //獲取距離
        m_targetToLookedAtDistance = Vector3.Distance(m_lookedAt.position, m_target.position);
        m_targetToCameraDistance = Vector3.Distance(m_transform.position, m_target.position);

        //獲取角度
        Vector3 targetToLookedAt = m_lookedAt.position - m_target.position;
        m_lookedAtAngleX = Vector3.Angle(Vector3.ProjectOnPlane(targetToLookedAt, m_target.up), m_target.forward);
        m_lookedAtAngleY = Vector3.Angle(Vector3.ProjectOnPlane(targetToLookedAt, m_target.up), targetToLookedAt);
        //取銳角
        if ((m_isTargetLeft && targetToLookedAt.x < 0) || (!m_isTargetLeft && targetToLookedAt.x > 0))
        {
            m_lookedAtAngleY = 180 - m_lookedAtAngleY;
        }

        Vector3 targetToCamera = m_transform.position - m_target.position;
        m_cameraAngleX = Vector3.Angle(Vector3.ProjectOnPlane(targetToCamera, m_target.up), m_target.forward);
        m_cameraAngleY = Vector3.Angle(Vector3.ProjectOnPlane(targetToCamera, m_target.up), targetToCamera);
        if ((m_isTargetLeft && targetToCamera.x < 0) || (!m_isTargetLeft && targetToCamera.x > 0))
        {
            m_cameraAngleY = 180 - m_cameraAngleY;
        }
    }

#if UNITY_EDITOR
    void Update()
    {
        if (m_isChangeDefaultValue)
        {
            SetCameraDefaultPosition();
            m_isChangeDefaultValue = false;
        }
    }
#endif

    void LateUpdate()
    {
        if (m_isChange)
        {
            //更新位置
            CalculateLookedAtAndCameraOffset();
            m_isChange = false;

            m_lookedAt.position = m_target.position + m_lookedAtOffset;
            m_transform.position = m_target.position + m_cameraOffset;

            m_transform.LookAt(m_lookedAt);
        }
    }

    //根據角度計算偏移相機和m_lookedAt相對m_target的偏移
    void CalculateLookedAtAndCameraOffset()
    {
        m_lookedAtOffset = CalculateOffset(m_targetToLookedAtDistance, m_lookedAtAngleX, m_lookedAtAngleY);
        m_cameraOffset = CalculateOffset(m_targetToCameraDistance, m_cameraAngleX, m_cameraAngleY);
        if (!m_isTargetLeft)
        {
            m_lookedAtOffset.x = -m_lookedAtOffset.x;
            m_cameraOffset.x = -m_cameraOffset.x;
        }
    }

    //可以想象成在一個球面上運動,根據xy的角度和球的半徑,計算球面上的一個點相對球心的偏移
    //先計算出高度y,然後就是在一個圓面上去計算xz
    Vector3 CalculateOffset(float distance, float x, float y)
    {
        Vector3 offset;
        offset.y = distance * Mathf.Sin(y * ANGLE_CONVERTER);
        float newRadius = distance * Mathf.Cos(y * ANGLE_CONVERTER);
        offset.x = newRadius * Mathf.Sin(x * ANGLE_CONVERTER);
        offset.z = newRadius * Mathf.Cos(x * ANGLE_CONVERTER);
        return offset;
    }

    //水平方向同時修改m_lookedAtAngleX和m_cameraAngleX,即m_lookAt和相機一同繞着m_target公轉
    //垂直方向,只修改m_cameraAngleY,即只有相機會有高低變化
    public void Rotate(Vector2 v)
    {
        if (v.x != 0)
        {
            if (m_isTargetLeft)
            {
                m_lookedAtAngleX -= v.x;
                m_cameraAngleX -= v.x;
            }
            else
            {
                m_lookedAtAngleX += v.x;
                m_cameraAngleX += v.x;
            }
        }
        if (v.y != 0)
        {
            m_cameraAngleY += v.y;
            m_cameraAngleY = m_cameraAngleY > m_cameraMaxAngleY ? m_cameraMaxAngleY : m_cameraAngleY;
            m_cameraAngleY = m_cameraAngleY < m_cameraMinAngleY ? m_cameraMinAngleY : m_cameraAngleY;
        }

        m_isChange = true;
    }

    //同時縮放m_target到相機和m_target到m_lookedAt的距離
    public void ChangeDistance(float value)
    {
        if (value > 0)
        {
            if (m_targetToCameraDistance < m_cameraMaxDistance)
            {
                m_targetToCameraDistance += m_targetToCameraDistance * m_changeDistanceRatio;
                m_targetToLookedAtDistance += m_targetToLookedAtDistance * m_changeDistanceRatio;
            }
        }
        else
        {
            if (m_targetToCameraDistance > m_cameraMinDistance)
            {
                m_targetToCameraDistance -= m_targetToCameraDistance * m_changeDistanceRatio;
                m_targetToLookedAtDistance -= m_targetToLookedAtDistance * m_changeDistanceRatio;
            }
        }

        m_isChange = true;
    }
}

然後我們將這個組件掛載在場景的Camera上,同時寫個組件來控制UI來調用Camera的旋轉和距離縮放

using UnityEngine;
using UnityEngine.EventSystems;

public class UIRotate : MonoBehaviour, IBeginDragHandler, IDragHandler
{
    [SerializeField]CameraAndLookAtRotateWithTarget m_controller;
    Vector2 m_lastPosition;

    public void OnBeginDrag(PointerEventData eventData)
    {
        m_lastPosition = eventData.position;
    }

    public void OnDrag(PointerEventData eventData)
    {
        //屏幕拖動
        m_controller.Rotate(m_lastPosition - eventData.position);
        m_lastPosition = eventData.position;
    }

    void Update()
    {
        //鼠標滾輪
        if (Input.GetAxis("Mouse ScrollWheel") < 0)
        {
            m_controller.ChangeDistance(1);
        }
        if (Input.GetAxis("Mouse ScrollWheel") > 0)
        {
            m_controller.ChangeDistance(-1);
        }
    }
}

掛載在UIPanel上即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章