普通FSM因无法表达嵌套行为导致状态爆炸,HFSM通过父子状态栈解决;需统一管理栈生命周期、逆序分发事件、禁止update中切换状态,并可选静态优化提升性能。

为什么普通FSM在游戏AI里很快就不够用
因为敌人行为会嵌套:巡逻状态里可能临时进入“蹲下掩体”子状态,而蹲下时又可能触发“投掷手雷”这一瞬时动作——这些不是并列状态,而是父子层级关系。用扁平FSM硬编码所有组合(比如 Patrol_While_Crouching、Chase_While_Reloading)会导致状态爆炸,维护成本直线上升。
层次有限状态机(HFSM)通过允许状态拥有子状态来解决这个问题。关键不是“能不能写”,而是“怎么组织才不掉进递归陷阱或事件丢失坑里”。
用 std::unique_ptr + 虚基类实现可嵌套的状态栈
别用裸指针管理状态生命周期,也别让每个状态自己持有子状态指针——那样容易循环引用或提前析构。标准做法是让状态机统一管理当前活跃状态栈,每个状态只负责定义自己的子状态创建逻辑和转移条件。
-
State是纯虚基类,含enter()、update(float dt)、exit()和handleEvent(const Event&) - 每个具体状态(如
PatrolState)在handleEvent中可返回std::unique_ptr<state></state>表示要推入的子状态,或返回nullptr表示不切换 - 状态机主循环不直接调用子状态的
update,而是从栈顶开始逐层调用——栈顶状态优先处理事件,只有它明确“不消费”时才往下传
示例片段:
立即学习“C++免费学习笔记(深入)”;
void HFSM::update(float dt) {
if (!m_stateStack.empty()) {
m_stateStack.back()->update(dt);
}
}
状态切换时如何避免“子状态残留”和“事件吞没”
这是最常出 bug 的地方:比如角色从 CrouchState 切到 DeadState,但 CrouchState 的子状态 ThrowGrenadeState 还在栈里;或者一个输入事件被父状态吃掉,子状态根本收不到。
- 切换新状态时,必须清空整个栈(
m_stateStack.clear()),再 push 新状态——除非你明确支持“保留部分子栈”的高级语义(极少需要) - 事件分发必须按栈逆序:先给栈顶(最内层)状态处理,它返回
true表示已消费,不再往下传;返回false才交给下一层 - 不要在
update()里做状态切换决策——所有转移逻辑必须收口到handleEvent(),否则帧间状态不一致
错误示范:PatrolState::update() 里直接 new ChaseState 并替换栈顶——这会让 ChaseState 的 enter() 在非预期时机执行,且无法响应刚发生的事件。
性能敏感场景下避免虚函数和动态内存开销
每帧几十个AI同时跑HFSM,虚函数调用和 std::unique_ptr 分配确实有成本。真到瓶颈时,可以换为基于索引的静态状态表 + 栈数组。
- 用
std::array<statedata></statedata>替代栈,其中StateData包含状态ID和局部数据(如蹲下时的掩体指针) - 状态逻辑用
switch(stateId)分发,避免虚函数表跳转 - 子状态创建改为查表:给每个状态配一个
subStateMap,key 是事件类型,value 是预分配的子状态ID
代价是灵活性下降——不能运行时构造任意子状态,但对游戏AI这种行为模式相对固定的场景,反而更可控。真正难的是调试时看不到运行中的状态对象,得靠日志打点补足。
层级越深,状态生命周期和事件流向就越难直观追踪。建议在 enter() 和 exit() 里加轻量日志,至少输出状态ID和栈深度。



















