各种Camera,总有一款适合你(二)
在实际的项目开发中,一般需要程序抽象出一些在几何意义上有明确意义的参数,这样方便策划或美术在自己的机器上进行调试。
下面是一个可变参的地下城摄像机的简单实现:
// 第三人称摄像机,平移和旋转会同时进行平滑 public class ThirdPersonalCamera : MonoBehaviour { /// Camera Control Params public GameObject Target = null; public float Distance = 10f; public float RotateX = 0f; // pitch 俯仰 public float RotateY = 0f; // yaw 偏航 public float SmoothTime = 0.01f; // 平滑时间,默认为1s public float ScrollWheelSpeed = 1000f; // 中轴调整速度 public Vector3 TargetOffset = Vector3.zero; // 目标偏移量 private Vector3 Velocity = Vector3.zero; // 平滑初速度 private Vector3 Offset = Vector3.zero; // 根据上面三个变量计算出 private const float MinDistance = 5f; // 镜头最近距离 private const float MaxDistance = 100f; // 镜头最远距离 void Start() { if (Target == null) return; } public void Shake() { StartCoroutine("ShakeCoroutine"); } void LateUpdate() { if (Target == null) return; UpdateDistance(); float radX = Mathf.Deg2Rad * RotateX; float radY = Mathf.Deg2Rad * RotateY; Offset.x = Distance * Mathf.Cos(radX) * Mathf.Cos(radY); Offset.z = Distance * Mathf.Cos(radX) * Mathf.Sin(radY); Offset.y = Distance * Mathf.Sin(radX); Vector3 targetPos = Target.transform.position + Offset + TargetOffset; transform.position = Vector3.SmoothDamp(transform.position, targetPos, ref Velocity, SmoothTime); transform.LookAt(Target.transform.position + TargetOffset); } private void UpdateDistance() { float horizontal = Input.GetAxis("Mouse ScrollWheel") * ScrollWheelSpeed * Time.deltaTime; Distance -= horizontal; } }
可以看到抽象出的控制参数主要包括:摄像机分别绕X轴和Y轴的旋转、摄像机距离角色的距离、以及用来控制平滑时间的参数。
注意,上面这段代码的实现中,position和rotation是同时进行平滑的,下面来看另外一种实现:
using UnityEngine; using System.Collections; [ExecuteInEditMode] public class MyDungeonCamera : MonoBehaviour { /// [摄像机控制参数] public GameObject Target = null; public float RotateX = 0f; // pitch 俯仰 public float RotateY = 0f; // yaw 偏航 public float Distance = 10f; // 摄像机远近 public float MoveSmoothTime = 0.3f; // 位置平滑时间,默认为1s public float RotateSmoothTime = 0.3f; // 旋转平滑时间 public float ScrollWheelSpeed = 1000f; // 中轴调整速度 public Vector3 TargetOffset = Vector3.zero; // 目标偏移量 private Vector3 Velocity = Vector3.zero; // 平滑初速度 private Vector3 Offset = Vector3.zero; // 根据上面三个变量计算出 private const float MinDistance = 5f; // 镜头最近距离 private const float MaxDistance = 100f; // 镜头最远距离 private Quaternion tmpRotation = Quaternion.identity; private Vector3 tmpPosition = Vector3.zero; private float RotateDamping { get { if (RotateSmoothTime <= 0f) RotateSmoothTime = 0.001f; return 1f / RotateSmoothTime; } } void LateUpdate() { if (Target == null) return; tmpRotation = transform.rotation; tmpPosition = transform.position; UpdateDistance(); UpdateRotation(); UpdatePosition(); transform.rotation = tmpRotation; transform.position = tmpPosition; } private void UpdateRotation() { if (!NeedRotate()) return; Quaternion wantedRotation = Quaternion.Euler(RotateX, RotateY, 0f); // 旋转采用球形插值 tmpRotation = Quaternion.Slerp(tmpRotation, wantedRotation, Time.deltaTime * RotateDamping); } private void UpdatePosition() { // 如果有旋转插值,则位置根据旋转变换;否则,位置自己进行插值过渡 if (!NeedRotate()) { Offset = Quaternion.Euler(RotateX, RotateY, 0f) * Vector3.forward * Distance; Vector3 wantedPos = Target.transform.position - Offset + TargetOffset; // 位置采用平滑阻尼过渡 tmpPosition = Vector3.SmoothDamp(tmpPosition, wantedPos, ref Velocity, MoveSmoothTime); } else { Offset = tmpRotation * Vector3.forward * Distance; tmpPosition = Target.transform.position - Offset + TargetOffset; } } private void UpdateDistance() { float horizontal = Input.GetAxis("Mouse ScrollWheel") * ScrollWheelSpeed * Time.deltaTime; Distance -= horizontal; Distance = Mathf.Clamp(Distance, MinDistance, MaxDistance); } private bool NeedRotate() { Vector3 eulerAngles = transform.rotation.eulerAngles; return !(FloatEqual(eulerAngles.x, RotateX) && FloatEqual(eulerAngles.y, RotateY)); } public static bool FloatEqual(float value1, float value2) { float ret = value1 - value2; return ret > -0.0005f && ret < 0.0005f; } }
这个实现中,有几点需要注意的:
(1)rotation使用四元素球形插值,这样保证每次旋转的角速度是恒定的,不过两次旋转各自的角速度不相等,这里可以改进;
(2)position使用Vector3.SmoothDamp进行平滑阻尼过渡效果会比较好;
(3)浮点数相等判断,不要直接用等号哟;
(4)当同时有rotation和position时,以rotation为主;
(5)是否需要旋转的判断,只有需要的时候才走旋转逻辑,这样可以减轻Update的压力。
摄像机振动脚本:
public class ShakeCamera : MonoBehaviour { public float ShakeTime = 1f; public float ShakeStrength = 0.2f; private Vector3 ShakeOffset = Vector3.zero; private Vector3 preShakeOffset = Vector3.zero; void LateUpdate() { transform.position = transform.position - preShakeOffset + ShakeOffset; } public void Shake() { StopCoroutine("ShakeCoroutine"); StartCoroutine("ShakeCoroutine"); } public void Shake(float time) { ShakeTime = time; Shake(); } IEnumerator ShakeCoroutine() { float endTime = Time.time + ShakeTime; while (Time.time < endTime) { ShakeOffset = Random.insideUnitSphere * ShakeStrength; yield return null; } ShakeOffset = Vector3.zero; } }