LOADING

加载过慢请开启缓存 浏览器默认开启

Unity 常用面向对象设计模式系列(8):状态模式(State)


一、模式概述

意图
允许对象在内部状态改变时改变其行为,看起来好像改变了它的类。

1.1 核心角色

  • IState<T>(State 接口)
    定义状态通用方法:EnterExitUpdate
  • ConcreteState
    各具体状态类,实现接口并封装状态专属逻辑。
  • StateMachine<TContext>(Context)
    持有当前状态,负责状态切换;为状态提供共享数据上下文。
  • Client(使用者)
    StateMachine 持有在 MonoBehaviour 中,在 Update() 中驱动当前状态。

1.2 UML 类图

StatePattern

二、通用 StateMachine 实现

/// <summary>
///状态接口,T 是 Context 类型
///</summary>
public interface IState<T> {
    void Enter(T ctx);
    void Exit(T ctx);
    void Update(T ctx);
}

/// <summary>
///通用状态机
///</summary>
public class StateMachine<T> {
    private T           _context;
    private IState<T>  _current;

    public void Initialize(T context, IState<T> initialState) {
        _context = context;
        _current = initialState;
        _current.Enter(_context);
    }

    public void ChangeState(IState<T> nextState) {
        if (_current != null) _current.Exit(_context);
        _current = nextState;
        _current.Enter(_context);
    }

    public void Update() {
        _current?.Update(_context);
    }

    public IState<T> CurrentState => _current;
}
  • Initialize:在 Awake()Start() 时设定初始状态;
  • ChangeState:统一管理 Exit→切换→Enter 流程;
  • Update:在 MonoBehaviour 的 Update() 中调用。

三、Unity 实战:角色状态机

3.1 角色 Context

public class Character : MonoBehaviour {
    [HideInInspector] public StateMachine<Character> StateMachine;
    public float walkSpeed = 2f;
    public float runSpeed  = 5f;
    public float jumpForce = 7f;

    private Rigidbody _rb;
    void Awake() {
        _rb = GetComponent<Rigidbody>();
        StateMachine = new StateMachine<Character>();
    }

    void Start() {
        StateMachine.Initialize(this, new IdleState());
    }

    void Update() {
        StateMachine.Update();
    }
}

3.2 具体状态:Idle、Run、Jump

public class IdleState : IState<Character> {
    public void Enter(Character ctx) {
        ctx.GetComponent<Animator>().Play("Idle");
    }
    public void Exit(Character ctx) { }
    public void Update(Character ctx) {
        var h = Input.GetAxis("Horizontal");
        var v = Input.GetAxis("Vertical");
        if (new Vector2(h, v).sqrMagnitude > 0.01f) {
            ctx.StateMachine.ChangeState(new RunState());
        } else if (Input.GetButtonDown("Jump")) {
            ctx.StateMachine.ChangeState(new JumpState());
        }
    }
}

public class RunState : IState<Character> {
    public void Enter(Character ctx) {
        ctx.GetComponent<Animator>().Play("Run");
    }
    public void Exit(Character ctx) { }
    public void Update(Character ctx) {
        var dir = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")).normalized;
        ctx.transform.Translate(dir * ctx.runSpeed * Time.deltaTime, Space.World);
        if (dir.sqrMagnitude < 0.01f) {
            ctx.StateMachine.ChangeState(new IdleState());
        } else if (Input.GetButtonDown("Jump")) {
            ctx.StateMachine.ChangeState(new JumpState());
        }
    }
}

public class JumpState : IState<Character> {
    private bool _started;
    public void Enter(Character ctx) {
        ctx.GetComponent<Animator>().Play("Jump");
        ctx._rb.AddForce(Vector3.up * ctx.jumpForce, ForceMode.VelocityChange);
        _started = true;
    }
    public void Exit(Character ctx) { }
    public void Update(Character ctx) {
        // 跳跃阶段结束:检测落地
        if (_started && ctx._rb.velocity.y <= 0 && ctx.IsGrounded()) {
            ctx.StateMachine.ChangeState(new IdleState());
        }
    }
}

提示IsGrounded() 可用射线检测或碰撞回调实现。


四、高级话题与扩展

4.1 状态复用与单例状态

如果状态无内部数据,可将状态实例设为单例,避免频繁 new

public class IdleState : IState<Character> {
    public static readonly IdleState Instance = new IdleState();
    private IdleState() { }
    // … 实现 Enter/Exit/Update
}
...
ctx.StateMachine.ChangeState(IdleState.Instance);

4.2 基于 ScriptableObject 的状态

利用 SO 资产定义状态,便于策划在 Inspector 编辑动画剪辑、参数等:

public abstract class SOState : ScriptableObject, IState<Character> {
    public abstract void Enter(Character ctx);
    public abstract void Exit(Character ctx);
    public abstract void Update(Character ctx);
}

4.3 层级状态机(Hierarchical State Machine)

  • 父状态:定义公共 Enter/Exit;
  • 子状态:在父状态内部切换,支持复用行为。
  • 实现:StateMachine 嵌套或在状态内部持有子 StateMachine

4.4 状态超时与触发器

  • Enter 记录时间戳 t0
  • Update 中若 Time.time - t0 > duration,自动切换;
  • 用枚举或事件触发全局状态变更。

五、性能与调试

  1. GC 控制

    • 尽量重用状态实例,避免每帧 new
  2. Profiler

    • 确认 StateMachine.Update() 调用开销;
  3. 调试可视化

    • 在 Inspector 中显示当前状态:
    [ReadOnly] public string currentStateName;
    void Update() {
        currentStateName = StateMachine.CurrentState.GetType().Name;
    }
    
  4. 日志

    • ChangeState 中打印日志,快速定位错误切换。

六、注意事项

场景 陷阱 建议
状态爆炸 状态类数量过多,且相似逻辑未复用 抽象公共逻辑到基类或组件,使用组合+策略减少状态数量
状态切换逻辑分散 条件判断散落在多个状态,实现复杂且难以维护 统一在 Context 或 状态机配置表(ScriptableObject)中管理
频繁 new/GC 每次切换都 new 状态对象,导致 GC 压力 使用单例状态或状态池,重用状态实例
Update 调用冗余 在无状态或状态不需 Update 时仍调用,浪费运算 在状态机中支持 HasUpdate 标志,跳过无需更新的状态
多线程/协程冲突 在协程或异步中切换状态时,主线程 Update 也会访问状态机 确保所有状态切换和 Update 均在主线程执行

七、小结

  1. 职责单一:每个状态只封装自身行为,Enter/Exit/Update 清晰分离;
  2. 状态实例重用:无数据状态可用单例,带数据状态可用池化;
  3. 配置驱动:ScriptableObject 状态+Inspector 组合,方便无代码扩展;
  4. 层次化设计:用父子状态或子状态机复用公共逻辑;
  5. 可视化与日志:在运行时实时查看当前状态与切换日志,提升调试效率。