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
}

这里有几个关键点需要注意:

  1. 使用LateUpdate而不是Update,确保相机运动在角色移动之后执行
  2. 将控制模式定义为枚举类型,提高代码可读性
  3. 通过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和移动平台都表现良好。记得根据具体项目需求调整参数,比如在手机游戏中可能需要降低旋转灵敏度,或者增加触摸控制的支持。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐