各种Camera,总有一款适合你(二)

时间:2021-01-29 14:50:40

  在实际的项目开发中,一般需要程序抽象出一些在几何意义上有明确意义的参数,这样方便策划或美术在自己的机器上进行调试。

  下面是一个可变参的地下城摄像机的简单实现:

// 第三人称摄像机,平移和旋转会同时进行平滑
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;
}

}