状态模式与有限状态机
State Pattern & Finite State Machines
状态模式 (State Pattern) 把对象“依赖状态的行为”分散到各个状态类中,对象只持有指向当前状态的指针并把调用委托给它,状态切换就是换指针。它是实现有限状态机 (Finite State Machine, FSM) 的一种方式。FSM 能用很强的结构约束理清一团乱麻的标志位代码;状态组合爆炸、需要复用和需要“回到之前状态”时,分别用并发状态机、分层状态机和下推自动机来扩展。
意图:从一堆布尔值到状态机
平台游戏女主角:按 B 跳,不能空中连跳,于是加 isJumping_;下蹲,又加 isDucking_;在空中按下改成俯冲,再加一个标志……每改一次都冒出新 bug。拿纸画出“站立、跳跃、下蹲、俯冲”四个框和按键箭头,就得到了一个有限状态机。(Game Programming Patterns — state)
FSM 的要点:状态集合固定;同一时刻只处于一个状态;输入或事件依次送入;每个状态有若干转换,每个转换对应一种输入并指向一个状态;当前状态没有定义的输入就忽略。(Game Programming Patterns — state)
下图是书中女主角的状态机,箭头上的标签就是触发转换的输入。
stateDiagram-v2 [*] --> 站立 站立 --> 跳跃: 按 B 站立 --> 下蹲: 按下 下蹲 --> 站立: 松开下 跳跃 --> 俯冲: 按下 跳跃 --> 站立: 落地 俯冲 --> 站立: 落地
从图中可以直接读出约束:只有站立时能起跳,所以空中连跳和蹲着时姿势错乱这类 bug 在结构上就不可能出现。
机制:三种实现
- 枚举 + switch:一组同时只有一个为真的标志,就该换成一个枚举。先按状态 switch,再按输入分支,让同一状态的代码集中在一起。这样主角也不可能再处于无意义的非法状态组合。这是最简单的实现,很多场合够用。缺点是:状态专属的数据(例如蓄力时间
chargeTime_)只能挂在主对象上,增加一个状态要同时改好几个方法。(Game Programming Patterns — state) - GoF 状态模式:定义状态接口(
handleInput()、update()),每个状态一个类,状态专属数据随之移进状态类(chargeTime_进入DuckingState),主对象只持有state_指针并委托给它。作者在旁注里区分了三种“主对象委托给从属对象”的模式:策略 (Strategy) 的目的是把主类与它的一部分行为解耦;类型对象 的目的是让多个对象通过共享同一个类型对象表现相似;状态模式的目的是让主对象通过更换委托对象来改变行为。(Game Programming Patterns — state) - 状态对象从哪来:没有字段的状态可以做成静态实例(书中放在
HeroineState基类里,如HeroineState::jumping),多个状态机共用(即 享元模式);如果状态没有字段、又只有一个虚方法,还可以干脆把状态类换成普通函数,state_变成函数指针。有字段的状态(如带chargeTime_的下蹲,双人合作时两个主角各需一份)必须每次转换时新建,动态分配时还要考虑内存碎片,可借助 对象池。为避免在当前状态的方法里delete this,可以让handleInput()返回新状态,由主对象删除旧状态、换上新状态。(Game Programming Patterns — state)
进入/退出动作 (entry/exit action):精灵图切换应由“进入哪个状态”决定,而不是由“从哪个状态离开”决定。给状态加 enter(),无论从哪里转入都会执行,多个状态都会转入同一状态时,重复代码就集中到了一处;对称地还可以加 exit()。(Game Programming Patterns — state)
扩展:当 FSM 不够用
- 并发状态机:拿枪后,每个状态都要翻倍(站立、持枪站立……)而且组合会爆炸,原因是把“在做什么”和“拿着什么”塞进了一台机器。改用两台独立状态机(n 个“动作”状态和 m 个“装备”状态塞进一台机器需要 n × m 个状态,分开则只要 n + m 个),输入同时交给两者(更完整的系统应允许一台机器“消费”输入,以免两台都响应同一输入);两者偶有交互时,用简单的
if检查另一台的状态即可。(Game Programming Patterns — state) - 分层状态机 (Hierarchical State Machine):站立、行走、奔跑、滑行都是“按 B 跳、按下蹲”,就把这部分放进“在地面上”这个父状态,子状态不处理的输入向上冒泡,就像覆盖继承来的方法。用状态模式时可以直接用类继承实现,也可以用一个状态栈显式表示父状态链。(Game Programming Patterns — state)
- 下推自动机 (Pushdown Automaton):FSM 没有历史。开火结束后应当回到开火前的状态,于是用状态栈:开火时把“开火”状态压栈,结束时出栈,自动回到之前的状态。注意这个栈和分层状态机的栈含义完全不同。(Game Programming Patterns — state)
何时使用
满足以下条件时,FSM 是好的建模工具:实体的行为取决于内部状态;这个状态可以严格划分成数量较少的离散选项;实体随时间响应一系列输入或事件。游戏中除了 AI,还常用于输入处理、菜单导航、文本解析、网络协议等异步行为。(Game Programming Patterns — state)
取舍与坑
- FSM 最大的优点(强约束)也是最大的缺点:只有固定状态、一个当前状态和硬编码的转换,它甚至不是图灵完备的。复杂 AI 会撞上这个天花板,业界趋势是行为树和规划系统。(Game Programming Patterns — state)
- 新建状态对象要付出分配成本,能用静态状态就用静态状态。
- 状态机需要一串输入;若希望异步响应输入,可以给每台状态机配一个待处理输入队列(邮箱),多台状态机互发消息就相当于重新发明了 Actor 模型。(Game Programming Patterns — event-queue)
- 用 游戏循环与更新方法 逐帧更新时,代码必须存下“下一帧从哪里继续”的信息,状态机正好保存这类状态。(Game Programming Patterns — update-method)
代码草图(C++)
按书中示例整理:实例化状态 + 进入动作的版本,以及用类继承实现的分层状态机。
// 下蹲状态:状态专属数据 chargeTime_ 从 Heroine 移入状态类
class DuckingState : public HeroineState {
public:
DuckingState() : chargeTime_(0) {}
virtual HeroineState* handleInput(Heroine& heroine, Input input) {
if (input == RELEASE_DOWN) return new StandingState(); // 图形由站立状态的 enter() 负责
return NULL; // 保持当前状态
}
virtual void update(Heroine& heroine) {
chargeTime_++;
if (chargeTime_ > MAX_CHARGE) heroine.superBomb();
}
private:
int chargeTime_;
};
class StandingState : public HeroineState {
public:
virtual void enter(Heroine& heroine) { heroine.setGraphics(IMAGE_STAND); }
};
// 由主对象删除旧状态:等旧状态的方法返回后再删,避免 delete this
void Heroine::handleInput(Input input) {
HeroineState* state = state_->handleInput(*this, input);
if (state != NULL) {
delete state_;
state_ = state;
state_->enter(*this); // 进入动作
}
}
// 分层状态机:父状态处理“按 B 跳、按下蹲”,子状态不处理的输入交给父类
class OnGroundState : public HeroineState {
public:
virtual void handleInput(Heroine& heroine, Input input) {
if (input == PRESS_B) { /* 跳... */ }
else if (input == PRESS_DOWN) { /* 蹲... */ }
}
};
class DuckingState : public OnGroundState {
public:
virtual void handleInput(Heroine& heroine, Input input) {
if (input == RELEASE_DOWN) { /* 站起... */ }
else OnGroundState::handleInput(heroine, input); // 沿层级向上
}
};
书中各节示例的签名并不统一(静态状态版 handleInput 返回 void,实例化状态版返回新状态),上面两段分别沿用各自小节的写法。
UE 中的对应
- 分层状态机 → 行为树与 StateTree:StateTree 的官方定义就是“通用的分层状态机”,把行为树的选择器与状态机的状态和转换结合起来,从根开始逐层评估进入条件来选择状态;它也是 Mass 的行为层。该页的建议是:优先级抢占多的战斗 AI 用行为树,状态显式、分层或规模大的场景用 StateTree。这和本章结尾说复杂 AI 会转向行为树一致。
- 动画状态机与进入/离开动作 → 动画蓝图与动画系统:动画蓝图用状态机组织待机、走、跑、跳;状态可以在进入、离开、完全混合时发出骨架通知 (Skeleton Notify),对应本模式的进入/退出动作。
Max Transitions Per Frame设为 1 可以防止同一帧多个转换同时成立造成的竞争。
GEA 的视角:动画中的动作状态机(卷 II 13.10、13.12.1.2)
- GEA 把角色动作(站、走、跑、跳)建模为有限状态机,称为动作状态机 (ASM),它位于低层动画管线之上,几乎所有高层游戏代码都通过它驱动动画。与本页的通用 FSM 相比,ASM 的每个状态不是一段行为代码,而是一棵任意复杂的混合树,状态之间的转换通常要交叉淡入淡出(卷 II 13.10)。
- 并发状态机的动画版:GEA 用多个独立状态机分层(基础层、变化层、手势层)让身体不同部位同时做不同的事,各层输出姿势再混合,对应本页「扩展」中的并发状态机。
- 转换的数据驱动:过渡矩阵通常很稀疏;《荣誉勋章:血战太平洋》用通配符(
from="*" to="*")定义默认过渡再逐级细化;Naughty Dog 把过渡做成按名字请求的一等实体(如每种走路状态都有叫 "jump" 的过渡),使状态图可以改而 C++ 不用动(卷 II 13.10.4)。 - 逻辑与物理两层:GEA 还把游戏逻辑用的粗粒度"逻辑 ASM"与负责生物力学正确运动的"物理 ASM"分开,后者可以用 运动匹配 代替手工状态图(卷 II 13.12.1.2)。
- 详见 动画管线与动作状态机。
GEA 的视角:脚本状态机与高层游戏流程(卷 II 17.9.5.6、17.10)
GEA 第 17 章中状态机出现在两个层面:
- 状态驱动的对象模型:有的引擎把 FSM 内建进核心对象模型,更新函数和事件处理函数都放在状态里而不是对象本身;Naughty Dog 引擎里脚本本身就是状态机,事件送到脚本的当前状态,该状态有处理器就调用、否则忽略。Pawn 脚本语言原生支持状态和状态局部变量。即使语言没有专门支持,也可以约定一套 FSM 写法。详见 游戏脚本系统。
- 高层游戏流程:每个状态代表一个玩家目标或遭遇,完成后前进,失败则回到当前状态开头,命用完回主菜单。Naughty Dog 的任务图支持并行任务(一个任务分叉成多个并行任务,再汇合回主序列),GEA 指出这正是它不同于普通状态机之处,因为状态机一次通常只能处于一个状态;可与本页"扩展:当 FSM 不够用"中的并发状态机对比:后者是多台独立状态机分别表示正交的维度(动作、装备),任务图则是同一条流程在某处分叉、之后再汇合(对比归纳)。见 游戏世界与游戏对象。
出处
- Game Programming Patterns — state
- Game Programming Patterns — state(全文版)
- Robert Nystrom《Game Programming Patterns》(2014) state 章,gameprogrammingpatterns.com/state.html
- Game_Engine_Architecture_4th_Edtion_Volume_2_-_Jason_Gregory.pdf
- Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 13 章 13.10、13.12.1.2 节
- Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 17 章 17.9.5.6 节
- Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 17 章 17.10 节