上一章地址:UnityStandardAsset工程、源码分析_3_赛车游戏[玩家控制]_特效、声效
经过前几章的分析,我们已经大致地了解了车辆控制相关的脚本。现在还有最后一个与玩家体验息息相关的部分——摄像机。
Unity一共设计了三种类型的摄像机,通过左上角的摄像机按钮切换:
而在场景中的摄像机分布是这样的:
可见,这三个摄像机同时存在于场景中,而切换的方式是将其他两个不需要的摄像机设为非活动,而独开启需要的摄像机。用于切换的脚本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;
算法有点迷,有一些不必要的计算,但总体而言还是完成了这个脚本应当尽到的责任。
追踪摄像机分析完了,接着我们来分析自由摄像机。这个摄像机在不同平台上有不同的操作方式:
这是一种很常见的观察模式,以目标为中心,在一定半径的球面上旋转摄像机,摄像机的中心点始终在物体上,也就是不管你怎样旋转摄像机,它都会紧盯着对象。
我们来看看摄像机在场景中的结构:
可见自由摄像机的结构与追踪摄像机时十分相似的,都有一个锚点,摄像机围绕锚点旋转。
在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有没有对于这方面的说明吧。
下一章想到啥写啥吧。