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有没有对于这方面的说明吧。
下一章想到啥写啥吧。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值