UnityStandardAsset工程、源碼分析_4_賽車遊戲[玩家控制]_攝像機控制

上一章地址:UnityStandardAsset工程、源碼分析_3_賽車遊戲[玩家控制]_特效、聲效

經過前幾章的分析,我們已經大致地瞭解了車輛控制相關的腳本。現在還有最後一個與玩家體驗息息相關的部分——攝像機。
Unity一共設計了三種類型的攝像機,通過左上角的攝像機按鈕切換:

  • 跟蹤攝像機
    在這裏插入圖片描述
  • 自由攝像機
    在這裏插入圖片描述
  • 閉路電視(CCTV)攝像機
    在這裏插入圖片描述

而在場景中的攝像機分佈是這樣的:
在這裏插入圖片描述
可見,這三個攝像機同時存在於場景中,而切換的方式是將其他兩個不需要的攝像機設爲非活動,而獨開啓需要的攝像機。用於切換的腳本SimpleActivatorMenu掛載在Cameras上,有攝像機按鈕調用NextCamera方法:

namespace UnityStandardAssets.Utility
{
    public class SimpleActivatorMenu : MonoBehaviour
    {
        // An incredibly simple menu which, when given references
        // to gameobjects in the scene
        public Text camSwitchButton;
        public GameObject[] objects;


        private int m_CurrentActiveObject;


        private void OnEnable()
        {
            // active object starts from first in array
            m_CurrentActiveObject = 0;
            camSwitchButton.text = objects[m_CurrentActiveObject].name;
        }


        public void NextCamera()
        {
            // 循環切換下一個攝像機,其實用模3的方法更好
            int nextactiveobject = m_CurrentActiveObject + 1 >= objects.Length ? 0 : m_CurrentActiveObject + 1;

            // 將除了需要的以外的攝像機都設成非活動
            for (int i = 0; i < objects.Length; i++)
            {
                objects[i].SetActive(i == nextactiveobject);
            }

            m_CurrentActiveObject = nextactiveobject;
            camSwitchButton.text = objects[m_CurrentActiveObject].name;
        }
    }
}

看完了切換的腳本,接下來我們逐個分析攝像機的實現方法。


追蹤攝像機

在這裏插入圖片描述
在這裏插入圖片描述
這兩個腳本掛載在CarCameraRig上,AutoCam是主控腳本,ProtectCameraFromWallClip是一個輔助的腳本,用於使攝像機不被牆壁遮擋,也就是遇到牆壁時拉近距離。先來看看AutoCam

public class AutoCam : PivotBasedCameraRig

可見AutoCam是直接繼承於PivotBasedCameraRig類的,而繼承鏈爲MonoBehaviour->AbstractTargetFollower->PivotBasedCameraRig->AutoCam。我們從頂層AbstractTargetFollower開始分析:

namespace UnityStandardAssets.Cameras
{
    public abstract class AbstractTargetFollower : MonoBehaviour
    {
        // 三種更新方式 Update/FixedUpdate/LateUpdate
        public enum UpdateType // The available methods of updating are:
        {
            FixedUpdate, // Update in FixedUpdate (for tracking rigidbodies).
            LateUpdate, // Update in LateUpdate. (for tracking objects that are moved in Update)
            ManualUpdate, // user must call to update camera
        }

        [SerializeField] protected Transform m_Target;            // The target object to follow
        [SerializeField] private bool m_AutoTargetPlayer = true;  // Whether the rig should automatically target the player.
        [SerializeField] private UpdateType m_UpdateType;         // stores the selected update type

        protected Rigidbody targetRigidbody;


        protected virtual void Start()
        {
            // 如果啓用了了自動尋找玩家功能,就自動尋找Tag爲Player的物體作爲目標
            // if auto targeting is used, find the object tagged "Player"
            // any class inheriting from this should call base.Start() to perform this action!
            if (m_AutoTargetPlayer)
            {
                FindAndTargetPlayer();
            }

            if (m_Target == null) return;
            targetRigidbody = m_Target.GetComponent<Rigidbody>();
        }


        private void FixedUpdate()
        {
            // 在目標有剛體組件或者不是運動學模式時調用
            // we update from here if updatetype is set to Fixed, or in auto mode,
            // if the target has a rigidbody, and isn't kinematic.

            // 若啓用了自動尋找玩家功能,在目標爲null或是非活動時自動尋找玩家
            if (m_AutoTargetPlayer && (m_Target == null || !m_Target.gameObject.activeSelf))
            {
                FindAndTargetPlayer();
            }
            if (m_UpdateType == UpdateType.FixedUpdate)
            {
                FollowTarget(Time.deltaTime);
            }
        }


        private void LateUpdate()
        {
            // 在目標沒有剛體組件或是運動學模式時調用
            // we update from here if updatetype is set to Late, or in auto mode,
            // if the target does not have a rigidbody, or - does have a rigidbody but is set to kinematic.
            if (m_AutoTargetPlayer && (m_Target == null || !m_Target.gameObject.activeSelf))
            {
                FindAndTargetPlayer();
            }
            if (m_UpdateType == UpdateType.LateUpdate)
            {
                FollowTarget(Time.deltaTime);
            }
        }

        public void ManualUpdate()
        {
            // 同LateUpdate,但這不是Unity定義的消息,不知道什麼時候可以調用,或者只是寫錯了?應該是Update()
            // we update from here if updatetype is set to Late, or in auto mode,
            // if the target does not have a rigidbody, or - does have a rigidbody but is set to kinematic.
            if (m_AutoTargetPlayer && (m_Target == null || !m_Target.gameObject.activeSelf))
            {
                FindAndTargetPlayer();
            }
            if (m_UpdateType == UpdateType.ManualUpdate)
            {
                FollowTarget(Time.deltaTime);
            }
        }

        // 如何跟隨目標,交給子類重寫
        protected abstract void FollowTarget(float deltaTime);


        public void FindAndTargetPlayer()
        {
            // 尋找Tag爲Player的物體並設爲目標
            // auto target an object tagged player, if no target has been assigned
            var targetObj = GameObject.FindGameObjectWithTag("Player");
            if (targetObj)
            {
                SetTarget(targetObj.transform);
            }
        }

        // 設置目標
        public virtual void SetTarget(Transform newTransform)
        {
            m_Target = newTransform;
        }


        public Transform Target
        {
            get { return m_Target; }
        }
    }
}

AbstractTargetFollower作爲抽象基類,搭了一個大體的框架。提供三種FollowTarget的調用方式,以應對不同的情況,而FollowTarget交由子類重寫,以此衍生出了三種不同的攝像機跟隨方式,而追蹤攝像機就是其中的一種。


接着是AbstractTargetFollower的子類,也是AutoCam的父類PivotBasedCameraRig。值得一提的是,自由攝像機的控制腳本也直接繼承於PivotBasedCameraRig,場景中的組成結構也同追蹤攝像機一樣,擁有一個Pivot物體。所以PivotBase代表了基於錨點的攝像機:

namespace UnityStandardAssets.Cameras
{
    public abstract class PivotBasedCameraRig : AbstractTargetFollower
    {
        // 這個類沒幹太多的事情,僅僅是獲取錨點物體和攝像機
        // This script is designed to be placed on the root object of a camera rig,
        // comprising 3 gameobjects, each parented to the next:

		// 場景中的物體結構,CameraRig是腳本掛載的對象,Camera是真正的攝像機
        // 	Camera Rig
        // 		Pivot
        // 			Camera

        protected Transform m_Cam; // the transform of the camera
        protected Transform m_Pivot; // the point at which the camera pivots around
        protected Vector3 m_LastTargetPosition;


        protected virtual void Awake()
        {
            // find the camera in the object hierarchy
            m_Cam = GetComponentInChildren<Camera>().transform;
            m_Pivot = m_Cam.parent;
        }
    }
}

最後就是自由攝像機的重頭戲——AutoCam,類中最主要的部分就是被重寫的FollowTarget方法,我們逐條分析:

protected override void FollowTarget(float deltaTime)

首先進行了對時間流動和目標存在的判斷,時間不流動或者目標不存在的話,攝像機是不應移動的:

// 時間沒有流動,或者沒有目標的話直接返回
// if no target, or no time passed then we quit early, as there is nothing to do
if (!(deltaTime > 0) || m_Target == null)
{
    return;
}

接下來是變量的初始化,這個腳本提供了兩種追蹤模式,一種是攝像機面朝的方向是速度的方向(跟隨速度模式),另一種是車輛模型的z軸方向(跟隨模型模式)。接下來的工作會對這兩個變量進行修改,最後對攝像機的位置和旋轉狀態進行修改和賦值:

// 初始化變量
// initialise some vars, we'll be modifying these in a moment
var targetForward = m_Target.forward;
var targetUp = m_Target.up;

如果是跟隨速度模式:

if (m_FollowVelocity && Application.isPlaying)
{
    // 在跟隨速度模式下,只有目標的速度超過了給定閾值時,攝像機的旋轉才與速度的方向平齊
    // in follow velocity mode, the camera's rotation is aligned towards the object's velocity direction
    // but only if the object is traveling faster than a given threshold.

    if (targetRigidbody.velocity.magnitude > m_TargetVelocityLowerLimit)
    {
        // 速度足夠高了,所以我們使用目標的速度方向
        // velocity is high enough, so we'll use the target's velocty
        targetForward = targetRigidbody.velocity.normalized;
        targetUp = Vector3.up;
    }
    else
    {
        // 否則只使用車身朝向
        targetUp = Vector3.up;
    }
    // 平滑旋轉
    m_CurrentTurnAmount = Mathf.SmoothDamp(m_CurrentTurnAmount, 1, ref m_TurnSpeedVelocityChange, m_SmoothTurnTime);
}

如果是跟隨模型模式:

// 現在是跟隨旋轉模式,也就是攝像機的旋轉跟隨着物體的旋轉
// 這個部分允許當目標旋轉速度過快時,攝像機停止跟隨
// we're in 'follow rotation' mode, where the camera rig's rotation follows the object's rotation.

// This section allows the camera to stop following the target's rotation when the target is spinning too fast.
// eg when a car has been knocked into a spin. The camera will resume following the rotation
// of the target when the target's angular velocity slows below the threshold.

// 獲取y軸旋轉角
var currentFlatAngle = Mathf.Atan2(targetForward.x, targetForward.z)*Mathf.Rad2Deg;
if (m_SpinTurnLimit > 0)    // 如果有旋轉速度的限制
{
    // 根據上一幀的角度和這一幀的角度計算速度
    var targetSpinSpeed = Mathf.Abs(Mathf.DeltaAngle(m_LastFlatAngle, currentFlatAngle))/deltaTime;
    var desiredTurnAmount = Mathf.InverseLerp(m_SpinTurnLimit, m_SpinTurnLimit*0.75f, targetSpinSpeed);
    // 緩慢回覆,快速跟進
    var turnReactSpeed = (m_CurrentTurnAmount > desiredTurnAmount ? .1f : 1f);
    if (Application.isPlaying)
    {
        m_CurrentTurnAmount = Mathf.SmoothDamp(m_CurrentTurnAmount, desiredTurnAmount,
                                             ref m_TurnSpeedVelocityChange, turnReactSpeed);
    }
    else
    {
        // 編輯器模式的平滑移動無效
        // for editor mode, smoothdamp won't work because it uses deltaTime internally
        m_CurrentTurnAmount = desiredTurnAmount;
    }
}
else
{
    // 即刻轉向
    m_CurrentTurnAmount = 1;
}
m_LastFlatAngle = currentFlatAngle;

根據如上語句的計算,進行最後的處理:

// 相機超車目標位置平滑移動
// camera position moves towards target position:
transform.position = Vector3.Lerp(transform.position, m_Target.position, deltaTime*m_MoveSpeed);

// 攝像機的旋轉可以分爲兩部分,獨立於速度設置
// camera's rotation is split into two parts, which can have independend speed settings:
// rotating towards the target's forward direction (which encompasses its 'yaw' and 'pitch')
if (!m_FollowTilt)
{
    targetForward.y = 0;
    if (targetForward.sqrMagnitude < float.Epsilon)
    {
        targetForward = transform.forward;
    }
}
var rollRotation = Quaternion.LookRotation(targetForward, m_RollUp);

// and aligning with the target object's up direction (i.e. its 'roll')
m_RollUp = m_RollSpeed > 0 ? Vector3.Slerp(m_RollUp, targetUp, m_RollSpeed*deltaTime) : Vector3.up;
transform.rotation = Quaternion.Lerp(transform.rotation, rollRotation, m_TurnSpeed*m_CurrentTurnAmount*deltaTime);

經過一系列的步驟,攝像機的位置被調整完畢。不過還有個很容易發生的問題,如果攝像機被牆壁等物體遮擋了,怎麼辦?一般來說,正常的處理是將攝像機不斷地朝目標物體靠近,直到攝像機不被遮擋。掛載在CarCameraRig上的另一個腳本ProtectCameraFromWallClip以接近這個思路的方式解決了該問題。
這個類中定義了一個簡單的實現了IComparer的內部類,用於比較兩條射線接觸點距離起點的距離,方便之後的計算:

// comparer for check distances in ray cast hits
public class RayHitComparer : IComparer
{
    public int Compare(object x, object y)
    {
        return ((RaycastHit) x).distance.CompareTo(((RaycastHit) y).distance);
    }
}

初始化過程,無需多言:

private void Start()
{
    // 做一些初始化
    // find the camera in the object hierarchy
    m_Cam = GetComponentInChildren<Camera>().transform;
    m_Pivot = m_Cam.parent;
    m_OriginalDist = m_Cam.localPosition.magnitude;
    m_CurrentDist = m_OriginalDist;

    // 簡單的繼承了IComparer的類,用於比較兩個rayhit的距離
    // create a new RayHitComparer
    m_RayHitComparer = new RayHitComparer();
}

接下來是重點,由於是對於攝像機當前狀態的二次處理,需要放在LateUpdate中,避免被AutoCam的相關計算覆蓋掉:

// 先將距離設置爲Start()中獲取的原始距離
// initially set the target distance
float targetDist = m_OriginalDist;

// 射線的起點是錨點向前的一個球體中心
m_Ray.origin = m_Pivot.position + m_Pivot.forward*sphereCastRadius;
m_Ray.direction = -m_Pivot.forward;

// 在剛纔的球體進行碰撞檢測
// initial check to see if start of spherecast intersects anything
var cols = Physics.OverlapSphere(m_Ray.origin, sphereCastRadius);

bool initialIntersect = false;
bool hitSomething = false;

// 在所有碰撞的物體中尋找非trigger、不是player的物體,也就是尋找視野內是否有遮擋物
// loop through all the collisions to check if something we care about
for (int i = 0; i < cols.Length; i++)
{
    if ((!cols[i].isTrigger) &&
        !(cols[i].attachedRigidbody != null && cols[i].attachedRigidbody.CompareTag(dontClipTag)))
    {
        initialIntersect = true;
        break;
    }
}

// 如果有的話
// if there is a collision
if (initialIntersect)
{
    // 射線的起點前進一個球半徑的距離
    m_Ray.origin += m_Pivot.forward*sphereCastRadius;

    // 射線向前碰撞所有物體
    // do a raycast and gather all the intersections
    m_Hits = Physics.RaycastAll(m_Ray, m_OriginalDist - sphereCastRadius);
}
else
{
    // if there was no collision do a sphere cast to see if there were any other collisions
    m_Hits = Physics.SphereCastAll(m_Ray, sphereCastRadius, m_OriginalDist + sphereCastRadius);
}

// 尋找最近的接觸點,將攝像機移動到接觸點上
// sort the collisions by distance
Array.Sort(m_Hits, m_RayHitComparer);

// set the variable used for storing the closest to be as far as possible
float nearest = Mathf.Infinity;

// loop through all the collisions
for (int i = 0; i < m_Hits.Length; i++)
{
    // only deal with the collision if it was closer than the previous one, not a trigger, and not attached to a rigidbody tagged with the dontClipTag
    if (m_Hits[i].distance < nearest && (!m_Hits[i].collider.isTrigger) &&
        !(m_Hits[i].collider.attachedRigidbody != null &&
          m_Hits[i].collider.attachedRigidbody.CompareTag(dontClipTag)))
    {
        // change the nearest collision to latest
        nearest = m_Hits[i].distance;
        targetDist = -m_Pivot.InverseTransformPoint(m_Hits[i].point).z;
        hitSomething = true;
    }
}

// visualise the cam clip effect in the editor
if (hitSomething)
{
    Debug.DrawRay(m_Ray.origin, -m_Pivot.forward*(targetDist + sphereCastRadius), Color.red);
}

// 移動到適當位置
// hit something so move the camera to a better position
protecting = hitSomething;
m_CurrentDist = Mathf.SmoothDamp(m_CurrentDist, targetDist, ref m_MoveVelocity,
                               m_CurrentDist > targetDist ? clipMoveTime : returnTime);
m_CurrentDist = Mathf.Clamp(m_CurrentDist, closestDistance, m_OriginalDist);
m_Cam.localPosition = -Vector3.forward*m_CurrentDist;

算法有點迷,有一些不必要的計算,但總體而言還是完成了這個腳本應當盡到的責任。


自由攝像機

追蹤攝像機分析完了,接着我們來分析自由攝像機。這個攝像機在不同平臺上有不同的操作方式:

  • Standalone平臺,根據鼠標的移動來旋轉攝像機。
  • 移動平臺,通過滑動屏幕中間的白色區域來旋轉攝像機。

這是一種很常見的觀察模式,以目標爲中心,在一定半徑的球面上旋轉攝像機,攝像機的中心點始終在物體上,也就是不管你怎樣旋轉攝像機,它都會緊盯着對象。
我們來看看攝像機在場景中的結構:
在這裏插入圖片描述
可見自由攝像機的結構與追蹤攝像機時十分相似的,都有一個錨點,攝像機圍繞錨點旋轉。
在這裏插入圖片描述
FreeLookCameraRig上掛載的腳本也很類似,一個控制腳本FreeLookCam繼承於PivotBasedCameraRig,一個ProtectCameraFromWallClip避免攝像機被遮擋。而ProtectCameraFromWallClip我們之前已經分析過了,那麼我們現在來分析FreeLookCam
首先觀察腳本的初始化部分,它提供了是否隱藏鼠標的選項:

protected override void Awake()
{
    base.Awake();
    // 設置是否將鼠標鎖定在屏幕中間
    // Lock or unlock the cursor.
    Cursor.lockState = m_LockCursor ? CursorLockMode.Locked : CursorLockMode.None;
    // 鎖定了鼠標就不可見
    Cursor.visible = !m_LockCursor;
    // 記錄錨點的歐拉角、旋轉四元數
	m_PivotEulers = m_Pivot.rotation.eulerAngles;
	m_PivotTargetRot = m_Pivot.transform.localRotation;
	m_TransformTargetRot = transform.localRotation;
}

接着時重寫的FollowTarget方法:

protected override void FollowTarget(float deltaTime)
{
    if (m_Target == null) return;
    // Move the rig towards target position.
    transform.position = Vector3.Lerp(transform.position, m_Target.position, deltaTime*m_MoveSpeed);
}

可見重寫後的方法只是單純的讓錨點跟隨車輛移動,攝像機的旋轉方面則在接下來被Update調用的HandleRotationMovement方法中:

protected void Update()
{
    HandleRotationMovement();
    // 鎖定鼠標
    if (m_LockCursor && Input.GetMouseButtonUp(0))
    {
        Cursor.lockState = m_LockCursor ? CursorLockMode.Locked : CursorLockMode.None;
        Cursor.visible = !m_LockCursor;
    }
}
private void HandleRotationMovement()
{
    // 處理相機旋轉

    // 時間靜止則不能旋轉
	if(Time.timeScale < float.Epsilon)return;

    // 讀取輸入
    // Read the user input
    var x = CrossPlatformInputManager.GetAxis("Mouse X");
    var y = CrossPlatformInputManager.GetAxis("Mouse Y");

    // 根據x軸輸入調整視角的y軸旋轉
    // Adjust the look angle by an amount proportional to the turn speed and horizontal input.
    m_LookAngle += x*m_TurnSpeed;

    // 賦值
    // Rotate the rig (the root object) around Y axis only:
    m_TransformTargetRot = Quaternion.Euler(0f, m_LookAngle, 0f);

    if (m_VerticalAutoReturn)
    {
        // 對於傾斜輸入,我們需要根據使用鼠標還是觸摸輸入採取不同的行動
        // 在移動端上,垂直輸入可以直接映射爲傾斜值,所以它可以在觀察輸入釋放後自動彈回
        // 我們必須測試它是否超過最大值或是小於0,因爲我們想要它自動回到0,即便最大值和最小值不對稱
        // For tilt input, we need to behave differently depending on whether we're using mouse or touch input:
        // on mobile, vertical input is directly mapped to tilt value, so it springs back automatically when the look input is released
        // we have to test whether above or below zero because we want to auto-return to zero even if min and max are not symmetrical.
        m_TiltAngle = y > 0 ? Mathf.Lerp(0, -m_TiltMin, y) : Mathf.Lerp(0, m_TiltMax, -y);
    }
    else
    {
        // 在使用鼠標的平臺山,我們根據鼠標的y軸輸入和轉向速度調整當前角度
        // on platforms with a mouse, we adjust the current angle based on Y mouse input and turn speed
        m_TiltAngle -= y*m_TurnSpeed;
        // 保證角度在限制範圍內
        // and make sure the new value is within the tilt range
        m_TiltAngle = Mathf.Clamp(m_TiltAngle, -m_TiltMin, m_TiltMax);
    }

    // 賦值
    // Tilt input around X is applied to the pivot (the child of this object)
	m_PivotTargetRot = Quaternion.Euler(m_TiltAngle, m_PivotEulers.y , m_PivotEulers.z);
	
	// 平滑賦值
	if (m_TurnSmoothing > 0)
	{
		m_Pivot.localRotation = Quaternion.Slerp(m_Pivot.localRotation, m_PivotTargetRot, m_TurnSmoothing * Time.deltaTime);
		transform.localRotation = Quaternion.Slerp(transform.localRotation, m_TransformTargetRot, m_TurnSmoothing * Time.deltaTime);
	}
	else
	{
	    // 即時賦值
		m_Pivot.localRotation = m_PivotTargetRot;
		transform.localRotation = m_TransformTargetRot;
	}
}

方法讀取並使用輸入值來完成了旋轉,通過採用歐拉角的方式實現了對於旋轉角度限制。


閉路電視攝像機

最後是閉路電視攝像機,這個攝像機正如它的名字一樣,就是一個固定的監控攝像頭,只是不停地將攝像頭的中心對準了目標。
不似之前兩種攝像機有三層結構,這個攝像機只有一個物體,上面掛載了攝像機腳本和如下的控制腳本。LookatTarget是主控腳本直接繼承於AbstractTargetFollowerTargetFieldOfView用於將視野拉近,也直接繼承於AbstractTargetFollower,因爲車輛如果離攝像頭過遠,就會顯得太小了,所以需要一個獨立的腳本來將視野拉近。其實這個物體上掛載了兩個TargetFieldOfView,設置的參數也完全相同,不知道是什麼原因。並且在LookatTarget中沒有任何啓用TargetFieldOfView的語句,只能靠我們手動勾選腳本來將視野拉近。在這裏插入圖片描述
我們先來分析主控腳本LookatTarget

public class LookatTarget : AbstractTargetFollower
{
    // 一個簡單的腳本,讓一個物體看向另一個物體,但有着可選的旋轉限制
    // A simple script to make one object look at another,
    // but with optional constraints which operate relative to
    // this gameobject's initial rotation.

    // 只圍着X軸和Y軸旋轉
    // Only rotates around local X and Y.

    // 在本地座標下工作,所以如果這個物體是另一個移動的物體的子物體,他的本地旋轉限制依然能夠正常工作。
    // 就像在車內望向車窗外面,或者一艘移動的飛船上的有旋轉限制的炮塔
    // Works in local coordinates, so if this object is parented
    // to another moving gameobject, its local constraints will
    // operate correctly
    // (Think: looking out the side window of a car, or a gun turret
    // on a moving spaceship with a limited angular range)

    // 如果想要沒有限制的話,把旋轉距離設置得大於360度
    // to have no constraints on an axis, set the rotationRange greater than 360.

    [SerializeField] private Vector2 m_RotationRange;
    [SerializeField] private float m_FollowSpeed = 1;

    private Vector3 m_FollowAngles;
    private Quaternion m_OriginalRotation;

    protected Vector3 m_FollowVelocity;


    // 初始化
    // Use this for initialization
    protected override void Start()
    {
        base.Start();
        m_OriginalRotation = transform.localRotation;
    }

    // 重寫父類的方法,編寫跟隨邏輯
    protected override void FollowTarget(float deltaTime)
    {
        // 將旋轉初始化
        // we make initial calculations from the original local rotation
        transform.localRotation = m_OriginalRotation;

        // 先處理Y軸的旋轉
        // tackle rotation around Y first
        Vector3 localTarget = transform.InverseTransformPoint(m_Target.position);   // 將目標座標映射到本地座標
        float yAngle = Mathf.Atan2(localTarget.x, localTarget.z)*Mathf.Rad2Deg; // 得到y軸上的旋轉角度

        yAngle = Mathf.Clamp(yAngle, -m_RotationRange.y*0.5f, m_RotationRange.y*0.5f);  // 限制旋轉角度
        transform.localRotation = m_OriginalRotation*Quaternion.Euler(0, yAngle, 0);    // 賦值

        // 再處理X軸的旋轉
        // then recalculate new local target position for rotation around X
        localTarget = transform.InverseTransformPoint(m_Target.position);
        float xAngle = Mathf.Atan2(localTarget.y, localTarget.z)*Mathf.Rad2Deg;
        xAngle = Mathf.Clamp(xAngle, -m_RotationRange.x*0.5f, m_RotationRange.x*0.5f);  // 同y軸的計算方法
        // 根據目標角度增量來計算目標角度
        var targetAngles = new Vector3(m_FollowAngles.x + Mathf.DeltaAngle(m_FollowAngles.x, xAngle),
                                       m_FollowAngles.y + Mathf.DeltaAngle(m_FollowAngles.y, yAngle));

        // 平滑跟蹤
        // smoothly interpolate the current angles to the target angles
        m_FollowAngles = Vector3.SmoothDamp(m_FollowAngles, targetAngles, ref m_FollowVelocity, m_FollowSpeed);

        // 賦值
        // and update the gameobject itself
        transform.localRotation = m_OriginalRotation*Quaternion.Euler(-m_FollowAngles.x, m_FollowAngles.y, 0);
    }
}

根據Unity自己寫的註釋看來,這個LookatTarget不僅僅適用於攝像機的旋轉,也可以用於炮塔之類的需要有旋轉限制的物體。
接着是TargetFieldOfView

public class TargetFieldOfView : AbstractTargetFollower
{
    // 這個腳本用於與LookatTarget協同工作,簡而言之就是能夠放大視野,避免車輛開遠了以後圖像過小的問題
    // 不過沒有在LookatTarget中找到調用這個方法的地方,只能通過手動勾選腳本啓用
    // This script is primarily designed to be used with the "LookAtTarget" script to enable a
    // CCTV style camera looking at a target to also adjust its field of view (zoom) to fit the
    // target (so that it zooms in as the target becomes further away).
    // When used with a follow cam, it will automatically use the same target.

    [SerializeField] private float m_FovAdjustTime = 1;             // the time taken to adjust the current FOV to the desired target FOV amount.
    [SerializeField] private float m_ZoomAmountMultiplier = 2;      // a multiplier for the FOV amount. The default of 2 makes the field of view twice as wide as required to fit the target.
    [SerializeField] private bool m_IncludeEffectsInSize = false;   // changing this only takes effect on startup, or when new target is assigned.

    private float m_BoundSize;
    private float m_FovAdjustVelocity;
    private Camera m_Cam;
    private Transform m_LastTarget;

    // Use this for initialization
    protected override void Start()
    {
        base.Start();

        // 獲取最大的Bound
        m_BoundSize = MaxBoundsExtent(m_Target, m_IncludeEffectsInSize);

        // get a reference to the actual camera component:
        m_Cam = GetComponentInChildren<Camera>();
    }


    protected override void FollowTarget(float deltaTime)
    {
        // 根據最大bounds平滑計算視野
        // calculate the correct field of view to fit the bounds size at the current distance
        float dist = (m_Target.position - transform.position).magnitude;
        float requiredFOV = Mathf.Atan2(m_BoundSize, dist)*Mathf.Rad2Deg*m_ZoomAmountMultiplier;

        m_Cam.fieldOfView = Mathf.SmoothDamp(m_Cam.fieldOfView, requiredFOV, ref m_FovAdjustVelocity, m_FovAdjustTime);
    }

    // 設置目標
    public override void SetTarget(Transform newTransform)
    {
        base.SetTarget(newTransform);
        m_BoundSize = MaxBoundsExtent(newTransform, m_IncludeEffectsInSize);
    }


    public static float MaxBoundsExtent(Transform obj, bool includeEffects)
    {
        // 獲得目標最大的邊界並返回,表示攝像機的最大視野
        // 這裏設計了includeEffects參數用於表示是否包括特效,但未被使用
        // 所以這裏一律不包括粒子效果
        // get the maximum bounds extent of object, including all child renderers,
        // but excluding particles and trails, for FOV zooming effect.

        // 獲取對象的所有renderer
        var renderers = obj.GetComponentsInChildren<Renderer>();

        Bounds bounds = new Bounds();
        bool initBounds = false;

        // 遍歷所有的renderer,使bounds不斷生長,也就是取所有bounds中的最大值
        foreach (Renderer r in renderers)
        {
            // 不包括線渲染器和粒子渲染器
            if (!((r is TrailRenderer) || (r is ParticleSystemRenderer)))
            {
                if (!initBounds)
                {
                    initBounds = true;
                    bounds = r.bounds;  // 對於第一個遇到的bound就不生長了
                }
                else
                {
                    bounds.Encapsulate(r.bounds);   // 生長
                }
            }
        }
        // 選擇三個軸中最大的一個
        float max = Mathf.Max(bounds.extents.x, bounds.extents.y, bounds.extents.z);
        return max;
    }
}

在判斷最大視野範圍時,這裏採用了Bounds.Encapsulate方法,使得較的bounds不斷生長,較小的則沒有影響,直到得出最大範圍,值得學習。


總結

這幾個攝像機的組織結構很簡單,算法卻較爲複雜,有些地方我現在還不是很理解。一開始我還奇怪爲什麼不直接使用四元數進行旋轉的操作,非要轉到歐拉角再轉到四元數再進行旋轉,後來才發現是爲了角度限制的需要,再歐拉角下計算較四元數來說更加的方便。
不過算法這東西如果不給你用自然語言寫的完整說明,是很難看懂的,基本就是盲人摸象慢慢猜,有機會去找找Unity有沒有對於這方面的說明吧。
下一章想到啥寫啥吧。

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