You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity C#里氏替换原则实践疑问:第三人称相机锚点控制类设计

第三人称相机SOLID实现疑问解析与优化方案

在练习用SOLID原则重构第三人称相机控制代码时,针对锚点控制部分的两个核心疑问,解析如下:


一、AnchorControlForThirdPersonCamera类的方法是否需要拆分?

当前类同时承担了锚点生命周期管理(创建/销毁)、锚点旋转计算(含输入读取、角度限制)、锚点位置计算三类职责,既违反了单一职责原则(SRP),也限制了*里氏替换原则(LSP)*的灵活性——如果后续需要替换旋转逻辑(比如手柄输入、固定角度锁死),当前类的高耦合会导致修改成本很高。

建议拆分职责,拆分为三个独立的类与对应接口:

  1. 锚点生命周期管理类:专注锚点的创建、销毁、实例持有,只保留与锚点生命周期相关的方法。
  2. 锚点旋转控制类:独立实现旋转逻辑,包含输入读取、旋转速度、角度限制等参数,后续可轻松替换为不同输入方式的实现。
  3. 锚点位置计算类:专注根据玩家位置与偏移量计算锚点位置,职责单一。

拆分后,每个类只负责一件事,且符合里氏替换原则——任何实现对应接口的子类都可以无缝替换原有实现,不会影响整体相机逻辑。


二、anchor变量应该放在哪个类?

建议保留在锚点生命周期管理类中(原AnchorControlForThirdPersonCamera或拆分后的AnchorLifecycleManager),原因如下:

  1. 职责归属清晰:锚点的创建、持有、销毁是锚点管理类的核心职责,由它持有anchor变量完全符合单一职责原则。
  2. 降低耦合:如果主相机类持有anchor,则需要承担锚点的生命周期管理逻辑,会增加主相机的职责,后续替换锚点实现时还需修改主相机代码,违反开闭原则(OCP)。
  3. 符合迪米特法则:主相机只需要通过接口获取锚点的Transform实例,无需关心锚点的创建、销毁细节,减少不必要的依赖。

另外,当前代码中Update()重复调用CreateAnchor()是冗余操作,CreateAnchor()已包含空判断,仅需在Awake()中调用一次即可。


优化后的代码示例

拆分后的核心接口

// 锚点生命周期管理接口
public interface IAnchorLifecycleManager
{
    Transform GetAnchor();
    void CreateAnchor();
    void OnDestroyAnchor();
}

// 锚点旋转控制接口
public interface IAnchorRotationControl
{
    void CalculateRotation(Transform anchor);
}

// 锚点位置计算接口
public interface IAnchorPositionCalculator
{
    void CalculatePosition(Transform anchor, Transform player);
}

锚点生命周期管理类

public class AnchorLifecycleManager : MonoBehaviour, IAnchorLifecycleManager
{
    [SerializeField] private string anchorName = "CameraAnchor";
    private Transform anchor;

    public Transform GetAnchor() => anchor;

    public void CreateAnchor()
    {
        if (!anchor)
        {
            anchor = new GameObject(anchorName).transform;
            anchor.hideFlags = HideFlags.HideInHierarchy;
        }
    }

    public void OnDestroyAnchor()
    {
        if (anchor) Destroy(anchor);
    }
}

鼠标驱动的锚点旋转控制类

public class MouseAnchorRotationControl : MonoBehaviour, IAnchorRotationControl
{
    [Header("Rotation Settings")]
    [SerializeField] private string inpMouseX = "Mouse X";
    [SerializeField] private string inpMouseY = "Mouse Y";
    [SerializeField] private float rotationSpeed = 100;
    [SerializeField, Range(1, 179)] private float minAxisY = 89;
    [SerializeField, Range(181, 359)] private float maxAxisY = 320;

    public void CalculateRotation(Transform anchor)
    {
        float axisMouseX = Input.GetAxis(inpMouseX);
        float axisMouseY = Input.GetAxis(inpMouseY);

        Vector3 anchorEuler = anchor.eulerAngles;
        anchorEuler.y += axisMouseX * rotationSpeed * Time.fixedDeltaTime;
        anchorEuler.x -= axisMouseY * rotationSpeed * Time.fixedDeltaTime;

        // 角度限制逻辑
        if (anchorEuler.x > minAxisY && anchorEuler.x < 180) 
            anchorEuler.x = minAxisY;
        if (anchorEuler.x < maxAxisY && anchorEuler.x > 180) 
            anchorEuler.x = maxAxisY;

        anchor.eulerAngles = anchorEuler;
    }
}

基于偏移量的锚点位置计算类

public class OffsetAnchorPositionCalculator : MonoBehaviour, IAnchorPositionCalculator
{
    [SerializeField] private Vector3 offset = new Vector3(0, 1, 0);

    public void CalculatePosition(Transform anchor, Transform player)
    {
        anchor.position = player.TransformPoint(offset);
    }
}

修改后的主相机类

public class ThirdPersonCamera : MonoBehaviour
{
    private IAnchorLifecycleManager _anchorLifecycle;
    private IAnchorRotationControl _anchorRotation;
    private IAnchorPositionCalculator _anchorPosition;
    
    private ICameraPositionRayForThirdPersonCamera _cameraPositionRay;
    private ICameraPositionForThirdPersonCamera _cameraPosition;
    private ICameraOrientationForThirdPersonCamera _cameraOrientation;

    [SerializeField] private Transform player;
    private RaycastHit hitForCameraPos = new RaycastHit();
    private Ray rayForCameraPos;

    private void Awake()
    {
        // 初始化各依赖,自动添加缺失的组件
        _anchorLifecycle = GetComponent<IAnchorLifecycleManager>() ?? gameObject.AddComponent<AnchorLifecycleManager>();
        _anchorRotation = GetComponent<IAnchorRotationControl>() ?? gameObject.AddComponent<MouseAnchorRotationControl>();
        _anchorPosition = GetComponent<IAnchorPositionCalculator>() ?? gameObject.AddComponent<OffsetAnchorPositionCalculator>();

        _cameraPositionRay = GetComponent<ICameraPositionRayForThirdPersonCamera>() ?? gameObject.AddComponent<CameraPositionRayForThirdPersonCamera>();
        _cameraPosition = GetComponent<ICameraPositionForThirdPersonCamera>() ?? gameObject.AddComponent<CameraPositionForThirdPersonCamera>();
        _cameraOrientation = GetComponent<ICameraOrientationForThirdPersonCamera>() ?? gameObject.AddComponent<CameraOrientationForThirdPersonCamera>();

        _anchorLifecycle.CreateAnchor();
    }

    void Start()
    {
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;

        Transform anchor = _anchorLifecycle.GetAnchor();
        _anchorRotation.CalculateRotation(anchor);
        rayForCameraPos = _cameraPositionRay.CreateRayForCameraPosition(anchor);
        hitForCameraPos = _cameraPosition.SpherecastForCalculateCameraPosition(rayForCameraPos);

        _anchorPosition.CalculatePosition(anchor, player);
        rayForCameraPos = _cameraPositionRay.CreateRayForCameraPosition(anchor);
        transform.position = _cameraPosition.GetCameraPosition(rayForCameraPos, hitForCameraPos);
        transform.rotation = _cameraOrientation.GetCameraLookAtTargetPoint(anchor, transform);
    }

    void Update()
    {
        Transform anchor = _anchorLifecycle.GetAnchor();
        _anchorRotation.CalculateRotation(anchor);
        rayForCameraPos = _cameraPositionRay.CreateRayForCameraPosition(anchor);
        hitForCameraPos = _cameraPosition.SpherecastForCalculateCameraPosition(rayForCameraPos);
    }

    void LateUpdate()
    {
        Transform anchor = _anchorLifecycle.GetAnchor();
        _anchorPosition.CalculatePosition(anchor, player);
        rayForCameraPos = _cameraPositionRay.CreateRayForCameraPosition(anchor);
        transform.position = _cameraPosition.GetCameraPosition(rayForCameraPos, hitForCameraPos);
        transform.rotation = _cameraOrientation.GetCameraLookAtTargetPoint(anchor, transform);
    }

    private void OnDestroy()
    {
        _anchorLifecycle.OnDestroyAnchor();
    }
}

内容的提问来源于stack exchange,提问作者Ary Guilherme Pires Caramez

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 19:50:17