中EN
概念页

状态模式与有限状态机

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 在结构上就不可能出现。

机制:三种实现

  1. 枚举 + switch:一组同时只有一个为真的标志,就该换成一个枚举。先按状态 switch,再按输入分支,让同一状态的代码集中在一起。这样主角也不可能再处于无意义的非法状态组合。这是最简单的实现,很多场合够用。缺点是:状态专属的数据(例如蓄力时间 chargeTime_)只能挂在主对象上,增加一个状态要同时改好几个方法。(Game Programming Patterns — state)
  2. GoF 状态模式:定义状态接口(handleInput()、update()),每个状态一个类,状态专属数据随之移进状态类(chargeTime_ 进入 DuckingState),主对象只持有 state_ 指针并委托给它。作者在旁注里区分了三种“主对象委托给从属对象”的模式:策略 (Strategy) 的目的是把主类与它的一部分行为解耦;类型对象 的目的是让多个对象通过共享同一个类型对象表现相似;状态模式的目的是让主对象通过更换委托对象来改变行为。(Game Programming Patterns — state)
  3. 状态对象从哪来:没有字段的状态可以做成静态实例(书中放在 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 不够用"中的并发状态机对比:后者是多台独立状态机分别表示正交的维度(动作、装备),任务图则是同一条流程在某处分叉、之后再汇合(对比归纳)。见 游戏世界与游戏对象。
  1. Game Programming Patterns — state
  2. Game Programming Patterns — state(全文版)
  3. Robert Nystrom《Game Programming Patterns》(2014) state 章,gameprogrammingpatterns.com/state.html
  4. Game_Engine_Architecture_4th_Edtion_Volume_2_-_Jason_Gregory.pdf
  5. Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 13 章 13.10、13.12.1.2 节
  6. Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 17 章 17.9.5.6 节
  7. Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 17 章 17.10 节

相关页面

游戏开发游戏循环与更新方法Game Loop & Update Method游戏循环 (Game Loop) 让游戏时间的推进与玩家输入和处理器速度脱钩:每一轮非阻塞地处理输入、更新状态、渲染,并跟踪真实时间来控制节奏;最常推荐的做法是“固定更新步长、可变渲染”。更新方法 (Update Method) 是它的搭档:世界维护一组对象,每个对象实现一个模拟“一帧”行为的 update(),每帧依次调用。两者和组件模式一起常被视为游戏引擎的核心。游戏开发享元模式Flyweight Pattern享元模式 (Flyweight Pattern) 把对象的数据拆成可共享的“内在状态”和每个实例独有的“外在状态”,让大量对象共用一份内在状态。森林里成千上万棵树共用一份网格和纹理,以及瓦片地图中每格只存一个指向共享地形对象的指针,都是它的典型应用。游戏开发事件队列Event Queue事件队列 (Event Queue) 用一个先进先出队列,把“发送消息或事件”和“处理它”在时间上解耦:发送方入队后立即返回,接收方在自己方便的时候再取出处理。它比观察者和命令模式多做的一件事是时间上的解耦,适合“推”与“拉”两端节奏不同的场合(例如音频请求),代价是全局可见、世界状态可能已经改变,以及可能出现隐蔽的反馈回环。游戏开发 · UE5行为树与 StateTreeBehavior Tree & StateTreeUE 的 AI 决策有两套工具:行为树 (Behavior Tree) + 黑板 (Blackboard) 是事件驱动的,条件写成装饰器 (Decorator),用 Simple Parallel、Services 和 Observer Aborts 代替传统并行节点;StateTree 是结合了行为树选择器和状态机转换的通用分层状态机,只在转换触发时按需选择状态,激活路径上的任务并发运行,通过属性绑定传递数据,也是 Mass 的行为层。优先级抢占多的战斗 AI 适合行为树,状态显式、分层或大规模的场景适合 StateTree。游戏开发 · UE5动画蓝图与动画系统Animation Blueprint & Animation SystemUE5 角色动画以动画蓝图 (Animation Blueprint) 为核心:它每帧为骨骼网格体 (Skeletal Mesh) 计算最终姿势。动画蓝图内部用状态机 (State Machine) 组织“何时播什么”,用混合空间 (Blend Space) 按速度、方向等参数连续混合,用蒙太奇 (Animation Montage) 在插槽 (Slot) 中插播攻击、交互等一次性动作,用 Control Rig 做程序化骨骼控制(如贴地)。算出的骨骼姿势最终交给蒙皮 (Skinning) 驱动顶点。综合游戏编程模式选型:解耦与性能Choosing Game Programming Patterns本页把《Game Programming Patterns》里的 19 个模式放到同一张取舍图上:大多数模式要么为了解耦(降低改代码时需要装进脑子里的知识量),要么为了性能(利用对数据和硬件的具体假设),而这两个目标经常互相拉扯。下面先讲全书的定位和 Nystrom 的架构观,再给出按问题选模式的路线和与 UE 的对照。游戏开发类型对象Type Object类型对象 (Type Object) 用“一个类的不同实例”来表示不同的逻辑类型:例如不为每种怪物写一个子类,而是一个 Monster 类加一个 Breed(种族)类,每个 Breed 实例描述一种怪物,怪物只持有指向自己 Breed 的引用。这样新增或修改种类只需要改数据,不必重新编译;代价是要自己管理这些类型对象,而且给每种类型定义不同的“行为”会变难。游戏开发引擎输入系统Game Engine HID System多数引擎不直接使用「原始」HID 输入,而是先加工(死区、滤波)使游戏内行为平滑直观,并在 HID 与游戏之间加至少一层间接(如按键映射表把原始按钮翻译成逻辑游戏动作)。GEA 列出引擎输入系统的典型需求并逐一讲解实现:死区、模拟信号滤波、按下/松开事件、组合键与序列手势、多设备多玩家、跨平台抽象、输入重映射、上下文相关控制与输入禁用。(卷 I 9.5–9.6 节)游戏开发 · UE5运动匹配Motion Matching运动匹配 (Motion Matching) 是 Pose Search 插件提供的动画蓝图节点,可以作为状态机和混合空间的动态替代:每次用 Schema 定义的通道(骨骼位置/速度、轨迹采样点等)构造查询,在数据库里的大量动画中找出代价最低的姿势,而不需要手工编写过渡和混合逻辑。增加动画数据就能提高保真度;代价是内存和查询开销会随通道、采样数和采样率增加。游戏开发动画管线与动作状态机Animation Pipeline & Action State Machines低层动画管线把片段和混合规格变成局部姿势、全局姿势和蒙皮矩阵调色板;其上的动作状态机 (ASM) 给游戏代码提供"按状态驱动动画"的接口。每个状态是任意复杂的片段混合,跟踪当前所有片段的方式有扁平加权平均和混合树两种;过渡用数据驱动的过渡矩阵描述,控制参数通过节点搜索、命名变量或控制结构体暴露给代码(GEA4 卷 II 13.9–13.10)。游戏开发游戏世界与游戏对象Game World & Game Objects游戏由玩法 (gameplay) 而不是技术定义;游戏机制 (game mechanics) 是支配游戏中各实体交互的规则,以及目标、成败标准、玩家能力、非玩家实体和整体流程(GEA 引用的论文称实现玩法的软件系统为游戏的 G 因子)。大多数游戏世界由静态元素和动态元素组成,划分为若干世界块,并由高层游戏流程组织玩家目标;动态元素通常实现为游戏对象 (GO),引擎提供的建模设施称为游戏对象模型,且分为工具侧和运行时两套(GEA4 卷 II 16、16.1–16.2)。游戏开发游戏脚本系统Game Scripting脚本语言是主要用来让用户控制和定制软件行为的编程语言;在游戏引擎中,它是高层、相对易用、能方便访问引擎大多数常用功能的语言,程序员和非程序员都能用它开发新游戏或给现有游戏做 mod。GEA 先区分数据定义语言和运行时脚本语言,梳理语言分类与游戏脚本的典型特征(解释执行、轻量、快速迭代、方便),介绍 QuakeC、UnrealScript、Lua、Python、Pawn,列出从"脚本回调"到"脚本驱动整个游戏"的六种架构,再以 Naughty Dog 的 DCScript 为例讲虚拟机与原生函数接口,最后是对象引用、事件收发、面向对象脚本、脚本状态机和协作式多线程脚本(GEA4 卷 II 17.游戏策划游戏机制Game Mechanics游戏机制是剥去美学、技术和故事之后剩下的交互与关系,是游戏的骨骼。Schell 承认没有公认分类(经济学"博弈论"只能处理过于简单的系统),给出一套实用的七类分法:空间 (Space)、时间 (Time)、对象/属性/状态 (Objects)、动作 (Actions)、规则 (Rules)、技能 (Skill)、机会 (Chance)。本页讲前六类;机会见 概率与期望值(第 3 版第 12 章;第 1 版为第 10 章,六类,没有"时间")。游戏策划复杂系统与涌现Complex Systems & EmergenceAdams & Dormans 第 3 章把玩法看作游戏这一复杂系统 (Complex System) 的涌现属性:涌现处于有序与混沌之间,只有系统运转起来才会出现,所以只能靠原型与测试。借助复杂性科学,作者提炼出促成涌现的三个结构特征——活跃且相互连接的部件、反馈回路、不同尺度上的行为——并引入 Fromm 的涌现分类,作为 Machinations 框架的出发点。游戏策划Machinations 图示语言Machinations DiagramsMachinations 是 Joris Dormans 设计的可视化语言与在线模拟工具,用于创建、记录、模拟和测试游戏的内部经济。其理论前提是:玩法最终由有形、无形与抽象资源在游戏系统中的流动决定。图由存放资源的节点 (Nodes)、搬运资源的实线资源连接 (Resource Connections)、以及用当前状态去修改别处的虚线状态连接 (State Connections) 组成;语法足够严格,可被计算机按时间步执行,接近一种专门表示游戏机制的可视化编程语言。本页按第 5 章整理全部基本元素与《吃豆人》建模示例,并给出本 wiki 用 Mermaid 改画 Machinations 图游戏策划模拟试玩与人工玩家Simulated Playtests & Artificial Players简单的赌博游戏(21 点、轮盘)可以直接算胜率;复杂而带随机的游戏必须玩很多次才知道是否平衡。Machinations 工具提供人工玩家 (Artificial Player, AP):用简单脚本代替人点击节点,配合快速运行与批量运行收集上千局统计。Adams & Dormans 用它做了两个完整案例:给两人版《大富翁》加入房屋与房产税(动态摩擦)来遏制动态引擎;给 Will Wright 的假想 RTS SimWar 调参,使"乌龟流"与"速攻流"从 929:68 调到 500:500。核心纪律:人工玩家要做成始终坚持一种策略的漫画式角色;调机制,不调人工玩家;一次只改一处并先大幅改动。游戏数学马尔可夫链与状态机概率Markov Chains & State-Machine Probability当一个随机过程可能无限进行下去(如"每次 80% 再加 1 点伤害")时,无法枚举所有结果;把它画成以概率转移的状态机,写成转移矩阵,再用列向量反复左乘矩阵(马尔可夫链 (Markov Chain)),就能得到任意回合后的状态分布、稳态、游戏时长的中位数/均值/众数;反向则可用递归把复杂局面化归为已知的简单局面。本页按 Schreiber 与 Romero 第 22 章整理(p.525–556),并核对了书中数字。游戏开发对象池Object Pool对象池 (Object Pool) 预先一次性分配一组可复用对象,需要时从池里取一个未使用的对象并初始化,用完标记为未使用,而不是逐个 new/delete。它能避免频繁分配的开销和堆碎片,常用于粒子、子弹、正在播放的声音等。代价是池大小要调校、同时活跃的数量有上限、复用的对象不会被自动清零,在有 GC 的环境里还可能让本该回收的对象一直活着。游戏开发决策架构选型:FSM、行为树与效用Choosing FSM, Behavior Tree & Utility AIFSM 强在少量离散状态与硬转换;行为树强在优先级回退与模块化任务;效用强在连续权衡与数据驱动调参;GOAP 强在声明式动作组合与重规划。实作常分层混用,而不是四选一宗教战。游戏开发行为树原理Behavior Tree Fundamentals行为树 (Behavior Tree, BT) 用层级节点控制决策流:叶子做动作或条件,Sequence / Selector 等组合节点决定子节点如何推进;节点返回 Success / Failure / Running,从而自然跨多帧。相对 FSM,BT 更易用优先级回退表达「失败了还有 Plan B」。游戏开发目标导向行动规划 GOAPGoal-Oriented Action PlanningGOAP 把「如何满足目标」交给运行时规划:行动带前提与效果,用 A 在世界状态空间搜索最低成本动作序列。F.E.A.R. 用极薄 FSM(Goto/Animate)执行计划,换来目标与行动解耦、行为可叠加、失败可重规划。游戏开发游戏 AI 决策管线Game AI Sense-Decide-Act Pipeline游戏 AI(与生成式 DNN 不同)通常按 感知 → 决策 → 执行 分层:底层是寻路、感知与记忆;中层是角色控制(对接动画/物理);上层是目标与决策(行为树、效用、规划等)。GEA 强调「perception is reality」——玩家看不见的内部聪明不算数。

被这些页面引用1 篇

输入关键词开始搜索

滚轮或双指缩放 · 拖动平移 · 双击放大 / 还原