0x0c.状态模式
状态模式(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 性能。