本文主要是介绍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 scenepublic Text camSwitchButton;public GameObject[] objects;private int m_CurrentActiveObject;private void OnEnable(){// active object starts from first in arraym_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/LateUpdatepublic 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 typeprotected 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 assignedvar 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// Cameraprotected Transform m_Cam; // the transform of the cameraprotected Transform m_Pivot; // the point at which the camera pivots aroundprotected Vector3 m_LastTargetPosition;protected virtual void Awake(){// find the camera in the object hierarchym_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 veloctytargetForward = 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 internallym_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 hierarchym_Cam = GetComponentInChildren<Camera>().transform;m_Pivot = m_Cam.parent;m_OriginalDist = m_Cam.localPosition.magnitude;m_CurrentDist = m_OriginalDist;// 简单的继承了IComparer的类,用于比较两个rayhit的距离// create a new RayHitComparerm_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 intersectionsm_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 collisionsm_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 dontClipTagif (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 latestnearest = 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 inputvar 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 speedm_TiltAngle -= y*m_TurnSpeed;// 保证角度在限制范围内// and make sure the new value is within the tilt rangem_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 initializationprotected override void Start(){base.Start();m_OriginalRotation = transform.localRotation;}// 重写父类的方法,编写跟随逻辑protected override void FollowTarget(float deltaTime){// 将旋转初始化// we make initial calculations from the original local rotationtransform.localRotation = m_OriginalRotation;// 先处理Y轴的旋转// tackle rotation around Y firstVector3 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 XlocalTarget = 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 anglesm_FollowAngles = Vector3.SmoothDamp(m_FollowAngles, targetAngles, ref m_FollowVelocity, m_FollowSpeed);// 赋值// and update the gameobject itselftransform.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 initializationprotected override void Start(){base.Start();// 获取最大的Boundm_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 distancefloat 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.// 获取对象的所有renderervar 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有没有对于这方面的说明吧。
下一章想到啥写啥吧。
这篇关于UnityStandardAsset工程、源码分析_4_赛车游戏[玩家控制]_摄像机控制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!