状态模式(State Pattern)

💡 为什么要用状态模式?

假设你正在写玩家的控制脚本(PlayerController),玩家有四种状态:待机(Idle)、奔跑(Run)、跳跃(Jump)、攻击(Attack)

面向过程的新手的灾难写法就是在 Update 里写一个巨大的 switch-case

void Update()
{
    switch (currentState)
    {
        case State.Idle:
            // 监测按键切换到 Run 或 Jump...
            break;
        case State.Run:
            // 处理移动,监测按键...
            break;
        case State.Jump:
            // 处理重力,检测落地...
            break;
    }
}

当状态只有 3 个时还好。但如果你的游戏越来越复杂,增加了:受伤、死亡、攀爬、滑行、格挡、硬直……这个 Update 会膨胀到几千行,里面的 if-else 互相嵌套,改一个地方,整个角色直接动不了了。

状态模式的优雅解法:
每一种状态都独立写成一个类(比如 IdleState 类、JumpState 类)。每个类只负责自己在该状态下要做的事,以及满足什么条件时切换到下一个状态。


一、 Unity 状态机标准核心代码

在 Unity 中,一个标准的状态机由三部分组成:状态接口状态机状态切换器具体的状态类

1. 定义状态接口 (IState)

每个状态都应该有生命周期:进入时做什么、每帧刷新做什么、离开时做什么。

public interface IState
{
    void Enter();  // 进入状态时执行一次(初始化动画、音效等)
    void Update(); // 在 Unity 的 Update 中每帧调用(处理输入、物理等)
    void Exit();   // 离开状态时执行一次(清理工作)
}

2. 状态机控制器 (PlayerStateMachine)

负责管理当前是谁在运行,并提供“切换状态”的方法。

public class PlayerStateMachine
{
    // 当前正在生效的状态
    public IState CurrentState { get; private set; }

    // 初始化状态机
    public void Initialize(IState initialState)
    {
        CurrentState = initialState;
        CurrentState.Enter();
    }

    // 核心:切换状态
    public void ChangeState(IState newState)
    {
        CurrentState?.Exit();  // 1. 让老状态发表退场感言
        CurrentState = newState; // 2. 换上新状态
        CurrentState.Enter(); // 3. 让新状态发表开场白
    }

    // 供外部的 MonoBehaviour 每帧调用
    public void Update()
    {
        CurrentState?.Update();
    }
}

3. 编写具体的状态类(以 Idle 和 Run 为例)

为了让状态类能控制玩家,我们在构造函数中把 PlayerController 的引用传给它。

using UnityEngine;

// 状态 A:待机状态
public class PlayerIdleState : IState
{
    private PlayerController _player;

    public PlayerIdleState(PlayerController player)
    {
        _player = player;
    }

    public void Enter()
    {
        Debug.Log("进入【待机】状态:播放 Idle 动画");
    }

    public void Update()
    {
        // 在待机状态下,每帧检测有没有移动输入
        float moveInput = Input.GetAxisRaw("Horizontal");
        if (moveInput != 0)
        {
            // 满足条件,直接通过状态机切换到奔跑状态!
            _player.StateMachine.ChangeState(_player.RunState);
        }
    }

    public void Exit()
    {
        Debug.Log("离开【待机】状态");
    }
}

// 状态 B:奔跑状态
public class PlayerRunState : IState
{
    private PlayerController _player;

    public PlayerRunState(PlayerController player)
    {
        _player = player;
    }

    public void Enter()
    {
        Debug.Log("进入【奔跑】状态:播放 Run 动画");
    }

    public void Update()
    {
        float moveInput = Input.GetAxisRaw("Horizontal");
        
        // 执行移动逻辑
        _player.transform.Translate(Vector3.right * moveInput * _player.moveSpeed * Time.deltaTime);

        // 如果没有输入了,切回待机状态
        if (moveInput == 0)
        {
            _player.StateMachine.ChangeState(_player.IdleState);
        }
    }

    public void Exit()
    {
        Debug.Log("离开【奔跑】状态");
    }
}

4. 在 Unity 宿主中使用它 (PlayerController)

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [Header("Movement")]
    public float moveSpeed = 5f;

    // 1. 声明状态机
    public PlayerStateMachine StateMachine { get; private set; }

    // 2. 预先实例化好所有状态,防止在运行时频繁 new 产生垃圾
    public PlayerIdleState IdleState { get; private set; }
    public PlayerRunState RunState { get; private set; }

    private void Start()
    {
        StateMachine = new PlayerStateMachine();

        // 分配状态,并把自己(this)传进去
        IdleState = new PlayerIdleState(this);
        RunState = new PlayerRunState(this);

        // 让玩家默认进入待机状态
        StateMachine.Initialize(IdleState);
    }

    private void Update()
    {
        // 把 Unity 的生命周期泵入状态机中
        StateMachine.Update();
    }
}


🆚 终极解惑:策略模式 vs 状态模式

状态模式和策略模式的 UML 类图一模一样,它们都是肚子里装着一个接口,然后动态去替换实现类。

它们唯一的区别在于“意图”“谁来控制切换”:

  • 策略模式(Strategy): 外部客户端来决定用哪个策略。比如玩家在面板上主动选择装备手枪还是散弹枪,武器本身不会自己从手枪变成散弹枪。
  • 状态模式(State): 状态类内部自己决定什么时候切到下一个状态。比如 IdleState 发现有按键输入,就主动自我触发切到 RunState,客户端(PlayerController)全程不需要操心状态切换的条件。

💡 避坑指南

  • 不要在状态的 Update 里乱 new 对象: 状态类每帧都在跑,千万别在里面写 new Vector3() 或者 GetComponent,否则你的游戏很快就会因为 GC 导致卡顿。像我上面展示的那样,在 Start 里把状态全部 new 好缓存起来才是正道。
  • 配合 Animator 的哈希值: 在状态的 Enter 里播放动画时,用 Animator.StringToHash("Base Layer.Idle") 代替直接写字符串 "Idle",能帮你抠出更多的 CPU 性能。