Unity Avatar Camera Controller 从零构建:第一人称与第三人称视角的平滑切换与避障实战
1. 为什么需要Avatar相机控制器?
在开发角色扮演游戏或第一人称射击游戏时,相机控制往往是决定玩家体验的关键因素之一。想象一下,当你操控角色在复杂场景中穿行时,如果相机突然卡在墙角或者切换视角时出现剧烈抖动,这种体验有多糟糕。我自己在早期项目中就遇到过这样的问题——一个精心设计的关卡,因为相机控制不流畅,硬生生把玩家逼成了"3D眩晕症患者"。
好的相机系统应该像专业的摄影师一样,既能捕捉最佳视角,又不会让观众察觉到它的存在。Unity自带的相机组件虽然基础功能完善,但要实现专业级的角色跟随、视角切换和动态避障,还需要我们自己动手搭建一套控制系统。这就是为什么我们需要从零构建Avatar Camera Controller。
2. 基础框架搭建
2.1 创建基本脚本结构
我们先从创建一个基础的C#脚本开始。我习惯把这类控制器命名为"AvatarCameraController",你可以根据自己的项目规范调整命名。
using UnityEngine;
public class AvatarCameraController : MonoBehaviour
{
[Header("References")]
public Transform avatar; // 要跟随的角色
[Header("Control Mode")]
public ControlMode controlMode = ControlMode.ThirdPerson;
public KeyCode modeChangeKey = KeyCode.V;
private float rotX; // 水平旋转值
private float rotY; // 垂直旋转值
private void LateUpdate()
{
HandleModeSwitch();
UpdateTargetRotation();
UpdateTargetPosition();
}
}
public enum ControlMode
{
FirstPerson,
ThirdPerson
}
这里有几个关键点需要注意:
- 使用
LateUpdate而不是Update,确保相机运动在角色移动之后执行 - 将控制模式定义为枚举类型,提高代码可读性
- 通过Header属性组织Inspector面板的显示
2.2 配置基本参数
在Inspector面板中,我们需要暴露一些基础参数供设计师调整:
[Header("Rotation Settings")]
public bool forwardAlignWithAvatar = true;
public float horizontalSensitivity = 5f;
public float verticalSensitivity = 5f;
public float rotYMinLimit = -60f;
public float rotYMaxLimit = 80f;
public float rotationLerpTime = 0.1f;
[Header("Position Settings")]
public float height = 1.7f;
public float distance = 3f;
public float minDistanceLimit = 1f;
public float maxDistanceLimit = 5f;
public float fpmDistance = 0.3f; // 第一人称时的固定距离
这些参数控制着相机的基本行为:
- 灵敏度设置影响鼠标移动对视角的控制程度
- 旋转限制防止相机出现不自然的翻转
- 距离参数控制第三人称视角的远近
3. 实现视角旋转控制
3.1 处理鼠标输入
视角旋转的核心是获取鼠标输入并转换为旋转角度。这里我推荐使用右键按住旋转的方式,这是很多3A游戏的标准操作:
private void UpdateTargetRotation()
{
if (Input.GetMouseButton(1)) // 右键按下时
{
float horizontal = forwardAlignWithAvatar ? 0f :
Input.GetAxis("Mouse X") * horizontalSensitivity;
float vertical = Input.GetAxis("Mouse Y") * verticalSensitivity;
rotX += horizontal;
rotY = Mathf.Clamp(rotY - vertical, rotYMinLimit, rotYMaxLimit);
}
Quaternion targetRotation = Quaternion.Euler(rotY, rotX, 0f);
float lerpPct = 1f - Mathf.Exp(Mathf.Log(1f - 0.99f) / rotationLerpTime * Time.deltaTime);
transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, lerpPct);
}
这里使用了指数衰减的插值方法,相比普通的Lerp能提供更平滑的过渡效果。我在多个项目实测中发现,这种方法在快速转动视角时也能保持流畅。
3.2 第一人称的特殊处理
第一人称模式下,我们通常希望角色的朝向与视角保持一致:
[Header("First Person Settings")]
public bool ctrlAvatarRotWhenFPMode = true;
private void UpdateTargetRotation()
{
// ...之前的旋转代码...
if (controlMode == ControlMode.FirstPerson && ctrlAvatarRotWhenFPMode)
{
Vector3 euler = avatar.rotation.eulerAngles;
euler.y = transform.rotation.eulerAngles.y;
avatar.rotation = Quaternion.Euler(euler);
}
}
这个功能可以通过开关控制,因为有些游戏设计可能希望保持角色朝向独立于视角。
4. 相机位置控制与避障
4.1 基础位置计算
相机位置由多个因素决定:与角色的距离、高度差、旋转角度等。我们先实现基础的位置计算:
private void UpdateTargetPosition()
{
float targetDistance = controlMode == ControlMode.ThirdPerson ?
Mathf.Clamp(distance, minDistanceLimit, maxDistanceLimit) : fpmDistance;
Vector3 targetPosition = avatar.position + Vector3.up * height;
targetPosition -= transform.rotation * Vector3.forward * targetDistance;
transform.position = targetPosition;
}
第三人称模式下,我们还需要支持鼠标滚轮调整距离:
[Header("Scroll Settings")]
public float scrollSensitivity = 10f;
public bool invertScrollDirection = false;
private void UpdateTargetPosition()
{
if (controlMode == ControlMode.ThirdPerson)
{
float scroll = Input.GetAxis("Mouse ScrollWheel") * scrollSensitivity *
(invertScrollDirection ? -1f : 1f);
distance = Mathf.Clamp(distance - scroll, minDistanceLimit, maxDistanceLimit);
}
// ...其余位置计算代码...
}
4.2 实现球形射线避障
为了避免相机穿墙,我们需要实现避障检测。SphereCast比普通的Raycast更适合这个场景:
[Header("Obstacle Avoidance")]
public LayerMask obstacleLayer;
public float avoidanceRadius = 0.2f;
private Vector3 ObstacleAvoidance(Vector3 targetPos)
{
Vector3 origin = avatar.position + Vector3.up * height;
Vector3 direction = targetPos - origin;
float maxDistance = direction.magnitude;
if (Physics.SphereCast(origin, avoidanceRadius, direction.normalized,
out RaycastHit hit, maxDistance, obstacleLayer))
{
return origin + direction.normalized * (hit.distance - avoidanceRadius);
}
return targetPos;
}
private void UpdateTargetPosition()
{
// ...之前的位置计算代码...
if (controlMode == ControlMode.ThirdPerson)
{
targetPosition = ObstacleAvoidance(targetPosition);
}
transform.position = targetPosition;
}
这个避障算法会在相机与角色之间检测碰撞,并自动调整相机位置。我建议将场景中所有可能成为障碍物的对象放在专门的Layer中,这样可以提高检测效率。
5. 视角平滑切换
5.1 模式切换逻辑
视角切换不仅仅是改变几个参数那么简单。我们需要处理过渡过程中的各种细节:
private void HandleModeSwitch()
{
if (Input.GetKeyDown(modeChangeKey))
{
controlMode = controlMode == ControlMode.FirstPerson ?
ControlMode.ThirdPerson : ControlMode.FirstPerson;
// 切换时重置旋转,避免视角突变
if (controlMode == ControlMode.FirstPerson)
{
rotY = 0f;
}
}
}
5.2 过渡动画优化
为了让切换更自然,我们可以添加过渡动画:
[Header("Transition Settings")]
public float transitionDuration = 0.5f;
private float transitionTimer;
private bool isTransitioning;
private ControlMode targetMode;
private void HandleModeSwitch()
{
if (Input.GetKeyDown(modeChangeKey) && !isTransitioning)
{
targetMode = controlMode == ControlMode.FirstPerson ?
ControlMode.ThirdPerson : ControlMode.FirstPerson;
isTransitioning = true;
transitionTimer = 0f;
}
if (isTransitioning)
{
transitionTimer += Time.deltaTime;
float pct = Mathf.SmoothStep(0f, 1f, transitionTimer / transitionDuration);
// 在这里处理过渡期间的参数插值
// ...
if (transitionTimer >= transitionDuration)
{
controlMode = targetMode;
isTransitioning = false;
}
}
}
在实际项目中,我发现使用SmoothStep插值比线性插值效果更好,能减少切换时的机械感。
6. 高级功能扩展
6.1 相机震动效果
为增强打击感,我们可以添加简单的相机震动:
private Vector3 shakeOffset;
private float shakeDuration;
private float shakeMagnitude;
public void TriggerShake(float duration, float magnitude)
{
shakeDuration = duration;
shakeMagnitude = magnitude;
}
private void UpdateShake()
{
if (shakeDuration > 0)
{
shakeOffset = Random.insideUnitSphere * shakeMagnitude;
shakeDuration -= Time.deltaTime;
}
else
{
shakeOffset = Vector3.zero;
}
transform.position += shakeOffset;
}
6.2 动态FOV调整
在快速移动或瞄准时调整视野范围能增强沉浸感:
private Camera cam;
private float defaultFOV;
private void Start()
{
cam = GetComponent<Camera>();
defaultFOV = cam.fieldOfView;
}
public void SetFOV(float targetFOV, float transitionTime)
{
StartCoroutine(FOVTransition(targetFOV, transitionTime));
}
private IEnumerator FOVTransition(float targetFOV, float time)
{
float startFOV = cam.fieldOfView;
float timer = 0f;
while (timer < time)
{
cam.fieldOfView = Mathf.Lerp(startFOV, targetFOV, timer / time);
timer += Time.deltaTime;
yield return null;
}
cam.fieldOfView = targetFOV;
}
7. 性能优化技巧
7.1 减少不必要的计算
在Update中进行的计算要尽可能高效。例如,只有当参数发生变化时才重新计算某些值:
private float lastDistance;
private float lastHeight;
private void UpdateTargetPosition()
{
bool needsRecalculate = !Mathf.Approximately(distance, lastDistance) ||
!Mathf.Approximately(height, lastHeight);
if (needsRecalculate)
{
// 执行计算
lastDistance = distance;
lastHeight = height;
}
}
7.2 使用对象池处理碰撞检测
如果场景中有大量动态障碍物,可以考虑使用对象池管理SphereCast的RaycastHit对象:
private RaycastHit[] hitBuffer = new RaycastHit[1];
private Vector3 ObstacleAvoidance(Vector3 targetPos)
{
Vector3 origin = avatar.position + Vector3.up * height;
Vector3 direction = targetPos - origin;
int hits = Physics.SphereCastNonAlloc(origin, avoidanceRadius, direction.normalized,
hitBuffer, direction.magnitude, obstacleLayer);
if (hits > 0)
{
return origin + direction.normalized * (hitBuffer[0].distance - avoidanceRadius);
}
return targetPos;
}
这套相机控制器经过多个项目验证,在PC和移动平台都表现良好。记得根据具体项目需求调整参数,比如在手机游戏中可能需要降低旋转灵敏度,或者增加触摸控制的支持。
更多推荐
所有评论(0)