上一章地址: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
是主控腳本直接繼承於AbstractTargetFollower
;TargetFieldOfView
用於將視野拉近,也直接繼承於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有沒有對於這方面的說明吧。
下一章想到啥寫啥吧。