关于 unity-state-machines
面向 Unity 开发者的状态与行为系统决策技能,帮助为游戏流程、AI 和 UI 导航选择合适的状态模式。涵盖 FSM、HFSM、行为树与状态栈,并强调可测试的纯 C# 状态逻辑。
主要功能
- 01比较 Animator FSM、代码 FSM、HFSM、行为树和状态栈
- 02提供基于状态数量与转换类型的选型建议
- 03说明类状态机与层级状态机的架构
- 04涵盖行为树及栈式状态机实现模式
- 05介绍无需场景的状态逻辑测试
适用场景
- 01为角色动画以外的游戏逻辑选择状态系统
- 02构建含分组子状态的战斗状态机
- 03设计可中断、按优先级决策的复杂 AI
- 04实现可返回上一界面的 UI 导航
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nice-wolf-studio/ unity-claude-skills --skill unity-state-machines安装后在 Claude Code 中输入/unity-state-machines确认已加载。
附带文件references/state-system-scaffolds.md
详细介绍
状态与行为系统 —— 决策模式
前置技能:
unity-game-architecture(MonoBehaviour 与纯 C#、组件组合)、unity-animation(Animator FSM、StateMachineBehaviour)、unity-scripting(MonoBehaviour 生命周期)
这些模式针对最常见的状态管理问题:Claude 编写临时的 if/else 链或结构混乱的巨型 switch 语句,导致状态逻辑在超过 3-4 个状态后便难以维护。
模式:状态系统选择
适用场景:实现 AI 行为、游戏流程、角色状态或 UI 导航
决策:
- Animator FSM —— 状态与动画关联。适用于角色移动、攻击连招、死亡动画。可视化图表对设计师友好。完整内容请参阅
unity-animation—— 不要仅为驱动动画而构建代码 FSM。 - 代码 FSM(基于 enum 或类) —— 适用于游戏流程(MainMenu/Playing/Paused/GameOver)、技能系统、回合阶段。完全可测试,且无 Animator 开销。当状态具有复杂逻辑而非仅仅切换动画时使用。
- 分层 FSM(HFSM) —— 状态中包含子状态。战斗包含 {Melee、Ranged、Blocking}。当扁平 FSM 中相关状态之间的转换过多时使用。
- 行为树(BT) —— 适用于需要优先级、可中断序列和并行行为的复杂 AI。当 FSM 超过 8-10 个状态,且转换爆炸导致图表难以阅读时使用。
- 基于栈的状态机(下推式) —— 适用于 UI 屏幕、暂停菜单、模态对话框。状态通过压入/弹出切换,先前状态会保留并恢复。
How many states? What kind of transitions?
|
+-- 2-3 states, simple toggling?
| --> if/else or bool flags (don't over-engineer)
|
+-- 4-8 states, clear transitions?
| --> Code FSM (enum or IState)
|
+-- States group naturally into clusters?
| --> HFSM (Locomotion > {Idle,Walk,Run}, Combat > {Melee,Ranged})
|
+-- AI with priorities, interrupts, parallel tasks?
| --> Behavior Tree
|
+-- Need to return to previous state (back button, unpause)?
| --> Stack-Based State Machine
|
+-- States tied to animations?
--> Animator FSM (use unity-animation skill)
注意:Animator FSM 即使没有播放任何动画,也会产生逐帧评估开销。不要将 Animator 用于纯逻辑状态机(游戏流程、回合系统)。应改用代码 FSM。
模式:基于类的 FSM 架构
适用场景:构建包含 3 个以上状态的代码 FSM
决策:
- Enum + switch —— 状态数不超过约 5 个,且转换逻辑很简单时使用。保持简单。
- IState 接口 + 字典 —— 状态数为 5 个或以上,且每个状态都有复杂的 Enter/Tick/Exit 逻辑时使用。状态应为纯 C# 类(可在 MonoBehaviour 之外进行测试)。
框架(Enum FSM —— 简单):
public enum GameState { MainMenu, Playing, Paused, GameOver }
public class GameFlowManager : MonoBehaviour
{
private GameState _state = GameState.MainMenu;
public void ChangeState(GameState newState)
{
ExitState(_state);
_state = newState;
EnterState(_state);
}
void Update()
{
switch (_state)
{
case GameState.Playing:
// game logic
break;
case GameState.Paused:
// pause logic
break;
}
}
void EnterState(GameState state) { /* ... */ }
void ExitState(GameState state) { /* ... */ }
}
框架(IState FSM —— 可扩展):
// See references/state-system-scaffolds.md for complete implementation
public interface IState
{
void Enter();
void Tick(float deltaTime);
void Exit();
}
public class StateMachine
{
private IState _currentState;
public void ChangeState(IState newState)
{
_currentState?.Exit();
_currentState = newState;
_currentState?.Enter();
}
public void Tick(float deltaTime) => _currentState?.Tick(deltaTime);
}
注意:状态应为纯 C# 类,而不是 MonoBehaviour。所属的 MonoBehaviour 通过构造函数或接口将共享上下文对象(transform、rigidbody、引用)传递给状态。这样既能让状态易于测试,也能避免空 MonoBehaviour 的性能开销。
模式:分层 FSM(HFSM)
适用场景:状态自然地归属于某些超状态(Locomotion 包含 Idle/Walk/Run/Sprint)
决策:
- 嵌套 StateMachine —— 超状态包含自己的 StateMachine。划分清晰,最适合两层深度的层级结构。
- 使用守卫条件扁平化 —— 如果层级会达到 3 层或以上,HFSM 的复杂度可能超过行为树。可以考虑切换到 BT。
框架:
// A super-state that contains a child state machine
public class HierarchicalState : IState
{
private readonly StateMachine _subMachine;
private readonly IState _defaultSubState;
public HierarchicalState(StateMachine subMachine, IState defaultSubState)
{
_subMachine = subMachine;
_defaultSubState = defaultSubState;
}
public void Enter() => _subMachine.ChangeState(_defaultSubState);
public void Tick(float deltaTime) => _subMachine.Tick(deltaTime);
public void Exit() => _subMachine.ChangeState(null); // Exit current sub-state
}
// Usage:
// var idleState = new IdleState(context);
// var walkState = new WalkState(context);
// var locomotionSubMachine = new StateMachine();
// var locomotionState = new HierarchicalState(locomotionSubMachine, idleState);
// mainMachine.ChangeState(locomotionState);
注意:超状态的 Exit 必须传递到当前激活的子状态。退出顺序:sub-state.Exit() -> super-state.Exit()。两个层级都可以存在转换:子状态转换(Idle -> Walk)仍在超状态内部,而超状态转换(Locomotion -> Combat)会退出整个层级结构。
模式:行为树架构
适用场景:AI 需要基于优先级的决策,并支持可中断序列
决策:
- 自定义精简 BT —— 节点少于 20 个,且需要完全控制、不依赖外部库时使用。构建
Selector、Sequence、ActionLeaf。 - 第三方 BT 库(NodeCanvas、Behavior Designer、Fluid BT)—— 支持可视化编辑,对设计师友好,提供 50 多种节点类型。当由非程序员设计 AI 时,依赖此类库是值得的。
框架(精简 BT):
// See references/state-system-scaffolds.md for complete implementation
public enum BTStatus { Success, Failure, Running }
public abstract class BTNode
{
public abstract BTStatus Tick(Blackboard bb);
}
// Selector: tries children in order, succeeds on first success
// Sequence: runs children in order, fails on first failure
// ActionLeaf: executes a single action
注意:行为树每帧都会从根节点开始 tick。为提升性能,可以缓存“运行中”的节点,并从该节点恢复执行,而不是重新评估整棵树。大型 BT(100 多个节点)应使用 Blackboard 共享数据,而非闭包。Blackboard 本质上只是一个带有类型化访问器的 Dictionary<string, object>。
模式:基于栈的状态机
适用场景:UI 导航、暂停菜单、模态对话框、可撤销的状态转换
决策:
- 栈机器——需要返回先前状态时使用。例如:游戏进行中 -> 暂停 -> 游戏进行中。游戏进行中 -> 背包 -> 制作 -> 背包 -> 游戏进行中。
- 扁平 FSM——转换不具有栈式特征时使用(永远不会“返回先前所在的位置”)。
脚手架:
public class StateStack
{
private readonly Stack<IState> _stack = new();
public IState Current => _stack.Count > 0 ? _stack.Peek() : null;
public void Push(IState state)
{
Current?.Pause(); // Pause current (not Exit -- it stays on the stack)
_stack.Push(state);
state.Enter();
}
public void Pop()
{
if (_stack.Count == 0) return;
var popped = _stack.Pop();
popped.Exit();
Current?.Resume(); // Resume the state underneath
}
public void Tick(float deltaTime) => Current?.Tick(deltaTime);
}
// Extended state interface for stack machines
public interface IStackableState : IState
{
void Pause(); // Called when a new state is pushed on top
void Resume(); // Called when the state above is popped
}
注意事项:如果忘记弹出状态,栈会无限增长。请设置最大深度(例如 10),并记录警告。清空栈时(例如返回主菜单),按顺序弹出所有状态,以便每个状态都调用 Exit。切勿将同一个状态实例压入栈两次——应创建新实例,或使用标记防止重复压入。
模式:测试状态机
适用场景:为 FSM 或 BT 逻辑编写测试
决策:状态是纯 C# 类 -> 使用 NUnit Edit Mode 测试。无需 MonoBehaviour、Play Mode 或场景。
脚手架:
// State that returns a transition signal
public class IdleState : IState
{
private readonly EnemyContext _ctx;
public bool ShouldTransitionToChase { get; private set; }
public IdleState(EnemyContext ctx) => _ctx = ctx;
public void Enter() => ShouldTransitionToChase = false;
public void Tick(float deltaTime)
{
if (_ctx.DistanceToPlayer < _ctx.DetectionRange)
ShouldTransitionToChase = true;
}
public void Exit() { }
}
// Test (Edit Mode, no Unity runtime needed)
[Test]
public void IdleState_TransitionsToChase_WhenPlayerInRange()
{
var ctx = new EnemyContext { DistanceToPlayer = 5f, DetectionRange = 10f };
var idle = new IdleState(ctx);
idle.Enter();
idle.Tick(0.016f);
Assert.IsTrue(idle.ShouldTransitionToChase);
}
[Test]
public void IdleState_StaysIdle_WhenPlayerOutOfRange()
{
var ctx = new EnemyContext { DistanceToPlayer = 15f, DetectionRange = 10f };
var idle = new IdleState(ctx);
idle.Enter();
idle.Tick(0.016f);
Assert.IsFalse(idle.ShouldTransitionToChase);
}
注意事项:如果状态依赖 Time.deltaTime,请将 float deltaTime 作为参数注入 Tick()。如果状态需要使用 Unity API(如 Physics.Raycast),请通过接口封装这些 API,以便测试提供存根。另请参阅:unity-testing,了解 Test Framework 的设置。
对比表
| 特性 | 枚举 FSM | IState FSM | HFSM | 行为树 | 栈机器 |
|---|---|---|---|---|---|
| 复杂度 | 低 | 中 | 中高 | 高 | 中 |
| 最适合 | 2-5 个简单状态 | 5-15 个状态 | 分组状态 | 复杂 AI | UI 导航 |
| 可测试性 | 测试 switch | 逐状态测试 | 逐状态 + 层级测试 | 逐节点测试 | 压栈/弹栈测试 |
| 设计师友好 | 否 | 否 | 否 | 使用可视化编辑器时 | 否 |
| 转换管理 | 在 switch 中手动管理 | 字典或信号 | 按层级转换 | 基于优先级 | 压栈/弹栈 |
| 内存 | 最少 | 每个状态一个实例 | 嵌套机器 | 内存中保存完整树 | 栈深度 |
| 性能 | O(1) switch | O(1) 委托 | 每次 tick 为 O(depth) | 每次 tick 为 O(tree) | O(1) |
| 应避免的情况 | 状态数 >5 | 与动画绑定的状态 | 层级数 >2 | 状态数 <8 | 非栈式转换 |
相关技能
- unity-animation——Animator FSM、StateMachineBehaviour、混合树(状态驱动动画时使用)
- unity-game-architecture——MonoBehaviour 与纯 C# 的区别(状态应使用纯 C#)、Service Locator
- unity-ai-navigation——NavMeshAgent 与 FSM/BT 集成,实现 AI 移动
- unity-testing——使用 NUnit Edit Mode 测试状态逻辑