决策架构选型:同一个守卫,四种写法
Choosing FSM, Behavior Tree & Utility AI
同一个守卫
前面几课分别讲了行为树、效用 AI 和 GOAP。这一课把它们放在一起比:拿同一个守卫,用四种架构各写一遍。
守卫的需求很普通:
- 平时巡逻;
- 看见玩家就开枪,子弹打光要装弹;
- 玩家跑了,就去他最后出现的地方搜一搜,搜不到再回去巡逻;
- 血量低了,不管在干什么,都先找掩体。
下面的四张草图是自己编的示意,只为看清每种架构的「形状」,不是哪个游戏的真实实现。
写法一:有限状态机
状态机 (FSM) 的写法最直接:列出状态,再画出状态之间的转换。
flowchart LR P["巡逻"] -->|"看见玩家"| F["开枪"] F -->|"子弹打光"| R["装弹"] R -->|"装好了"| F F -->|"丢了目标"| S["搜索"] S -->|"又看见了"| F S -->|"搜不到"| P P -->|"血量低"| C["找掩体"] F -->|"血量低"| C R -->|"血量低"| C S -->|"血量低"| C
状态少的时候它非常清楚。麻烦在于「血量低就找掩体」这种对所有状态都成立的规则,得在每个状态上各画一条边。再加一个维度更糟:守卫可能拿着枪,也可能空着手。把「在做什么」和「拿着什么」塞进同一台状态机,n 个动作状态乘 m 个装备状态,就是 n × m 个状态。
《游戏编程模式》(GPP) 给了三种扩展办法:
| 扩展 | 解决什么 | 守卫的例子 |
|---|---|---|
| 并发状态机 | 把正交的维度拆成两台机器,n × m 变成 n + m | 「动作」一台、「装备」一台,偶尔用 if 互相查一下 |
| 分层状态机 | 共用的转换放进父状态,子状态不处理的输入往上冒 | 开枪、装弹、搜索都放进父状态「警戒中」,「血量低 → 找掩体」只写一次 |
| 下推自动机 | 用栈记住历史,做完一件事回到之前的状态 | 装弹时把「装弹」压栈,装完出栈,自动回到装弹前在干的事 |
GPP 的结论是:状态机最大的优点——强约束——也是它的上限。AI 再复杂下去,通常会转向行为树和规划。
写法二:行为树
行为树把守卫的需求写成一张优先级列表,排在前面的先试:
flowchart TD R["Selector:守卫"] --> A["Sequence:保命"] R --> B["Sequence:战斗"] R --> D["Sequence:搜索"] R --> E["巡逻"] A --> A1["血量低?"] A --> A2["去掩体"] B --> B1["看得见玩家?"] B --> B2["Selector:开火"] B2 --> B3["开枪(没子弹就失败)"] B2 --> B4["装弹"] D --> D1["有上次看见的位置?"] D --> D2["去那里搜"]
「血量低就找掩体」不再需要到处画边:它是排在最前面的分支,每次都先检查。没子弹时开枪失败,Selector 自然退到装弹。这正是行为树的强项:优先级回退和模块化的子树。弱点是它基本是反应式的,不会往前推演一长串行动。
写法三:效用 AI
效用 AI 不排顺序,而是给每个行动打分,选最高的:
| 行动 | 分数怎么算(几项考量相乘) |
|---|---|
| 开枪 | 看得见玩家 × 距离合不合适 × 还有没有子弹 |
| 装弹 | 子弹越少越想装 |
| 找掩体 | 血越少越想躲 × 附近有没有掩体 |
| 搜索 | 上次看见玩家的时间越近,越想去搜 |
| 巡逻 | 一条恒定的低分基线 |
守卫会显得在「权衡」:血不多但玩家就在眼前、掩体又很远时,它可能选择继续开枪。代价是硬优先级很难表达——如果设计要求「血量低时无论如何先躲」,靠调几个分数去凑,迟早会在某个边角情况下被别的高分行动压过去。Damian Isla 在 Halo 2 上就批评过这种纯浮点竞争的做法。
写法四:GOAP
GOAP 只声明目标和行动,计划交给规划器去搜:
| 行动 | 前提 | 效果 |
|---|---|---|
| 搜索 | 无 | 看得见玩家 |
| 装弹 | 有备用弹匣 | 已上膛 |
| 开枪 | 已上膛、看得见玩家 | 威胁消除 |
| 去掩体 | 附近有掩体 | 在掩体里 |
目标「威胁消除」、当前状态「没上膛、看不见玩家」时,规划器会排出「搜索 → 装弹 → 开枪」这样的序列;中途被堵住了,就从当前状态重新规划。执行层仍是一个很薄的状态机。它擅长行动组合多、需要应变的情况,代价是世界状态的设计和规划的 CPU。
对照表
| 状态机 | 行为树 | 效用 AI | GOAP | |
|---|---|---|---|---|
| 表达 | 状态 + 转换 | 树 + 成功 / 失败 / 运行中 | 分数竞争 | 前提 / 效果 + 搜索 |
| 怎么扩展 | 加状态、加转换 | 加分支、加子树 | 加行动和曲线 | 加行动、加目标 |
| 失败了怎么办 | 手写转换 | Selector 自然回退 | 改选次优分 | 重新规划 |
| 典型痛点 | 组合爆炸 | 长期规划弱 | 硬优先级难调 | 世界状态设计和 CPU |
怎么选
先看问题像什么:
flowchart TB
Q{"问题像什么?"} --> A["离散的阶段不多:状态机 / StateTree"]
Q --> B["有优先级、失败要回退:行为树"]
Q --> C["连续权衡、要调参:效用 AI"]
Q --> D["行动组合多、要重新规划:GOAP"]- 角色动作、UI、动画层:状态少、转换清楚,用状态机或分层状态机(虚幻引擎里是 StateTree)。
- 战术任务、需要 Plan B:用行为树。虚幻引擎里,优先级抢占明显的战斗场景也倾向事件驱动的行为树。
- 选项多、要「权衡感」、设计师要调参:用效用 AI,但记住 Isla 对纯浮点优先级的警告。
- 行动组合爆炸、要声明式扩展和失败重试:用 GOAP 或别的规划器,执行层仍可以是一个很薄的状态机。
混着用
实际项目很少四选一,更常见的是分层混用:
- 行为树选高层,效用选叶子战术:树负责「巡逻 / 战斗 / 撤退」这样的阶段,进到战斗分支后,用效用分数挑具体打法。
- 效用或 GOAP 出意图,状态机落地:Orkin 的 F.E.A.R. 就是计划 → Goto / Animate。
- Halo 2 的做法:行为有向无环图加优先级列表,相当于行为树的 Selector;再用订单和风格让设计师导控。
在游戏里怎么用
取舍与常见的坑
小结与练习
- 状态机擅长少量离散状态和硬转换;行为树擅长优先级回退和模块化;效用 AI 擅长连续权衡和数据驱动;GOAP 擅长声明式组合和重新规划;
- 状态机的组合爆炸可以用并发状态机、分层状态机、下推自动机缓解,但它的上限来自强约束本身;
- 选型先看问题的形状,实际项目常常分层混用。
练习:给守卫加一条新需求——「听到枪声就去查看」。分别想一想:在状态机里要加几条边?行为树里这个分支放在哪个位置?效用 AI 要加什么考量?GOAP 要加什么行动或目标?哪种改动最小?
FSM 强在少量离散状态与硬转换;行为树强在优先级回退与模块化任务;效用强在连续权衡与数据驱动调参;GOAP 强在声明式动作组合与重规划。实作常分层混用,而不是四选一宗教战。
对照表
| FSM | 行为树 | 效用 AI | GOAP | |
|---|---|---|---|---|
| 表达 | 状态+边 | 树+Success/Fail/Running | 分数竞争 | 前提/效果+搜索 |
| 扩展方式 | 加状态/边 | 加分支/子树 | 加决策与曲线 | 加行动/目标 |
| 失败处理 | 手写转换 | Selector 回退自然 | 改选次优分 | 重规划 |
| 典型痛点 | 组合爆炸 | 长期规划弱 | 硬优先级难调 | 世界状态与 CPU |
| 出处锚点 | GPP State;GEA 亦指复杂 AI 趋向 BT/规划 | Simpson;GEA | Mark IAUS | Orkin F.E.A.R. |
GPP 明确:FSM 最大优点(强约束)也是上限,复杂 AI 常走向行为树与规划。(Game Programming Patterns — state)
怎么选(启发式)
- 角色动作/UI/动画层:状态少、转换清晰 → FSM / 分层 FSM(见 状态模式与有限状态机、UE StateTree)。
- 战术任务、要 Plan B:BT(行为树原理)。UE 战斗抢占场景官方亦偏事件驱动 BT(行为树与 StateTree)。
- 选项多、要「权衡感」、设计师调参 → Utility(效用 AI);注意 Isla 对纯 float 优先级的警告。
- 行动组合爆炸、要声明式扩展与失败重试 → GOAP/规划(目标导向行动规划 GOAP);执行层仍可薄 FSM。
混用模式(常见)
- BT 选高层,效用选叶子战术。
- 效用/GOAP 出意图,FSM/动画 ASM 落地(Orkin:计划 → Goto/Animate)。
- Halo 式:行为 DAG + prioritized-list ≈ BT Selector;用 style/order 做设计师导控(Handling Complexity in the Halo 2 AI(笔记))。
flowchart TB
Q{"问题像什么?"} --> A["离散阶段少\n→ FSM / StateTree"]
Q --> B["优先级任务+回退\n→ Behavior Tree"]
Q --> C["连续权衡+调参\n→ Utility"]
Q --> D["动作组合+重规划\n→ GOAP"]跨引擎
原理页保持引擎无关;UE 落地见 BT/StateTree 与感知导航页,Godot 无统一官方 BT(可用插件),决策架构仍按上表选。
出处
- Game Programming Patterns — state Robert Nystrom《Game Programming Patterns》State 章
- Behavior trees for AI: How they work(笔记) Chris Simpson BT
- Infinite Axis Utility System / Utility AI(笔记) Dave Mark IAUS
- Three States and a Plan: The A.I. of F.E.A.R.(笔记) Orkin GOAP
- Handling Complexity in the Halo 2 AI(笔记) Isla Halo 2
- GEA4 Vol II §18.2.3 Artificial Intelligence(摘录笔记) GEA4 §18.2.3
- Behavior Tree Overview / StateTree Overview UE 选型语境