GOAP:只告诉 AI 想要什么,让它自己排步骤
Goal-Oriented Action Planning
门被堵了,它跳了窗
在《F.E.A.R.》里,你把门堵住,冲过来的士兵会先踢门;踢不开,他就转身从窗户跳进来。没有哪个程序员写过「如果门被堵就去跳窗」这条规则——这串行动是 AI 在运行时自己规划出来的。
Jeff Orkin 在讲 F.E.A.R. 的 AI 时说过:单独实现找掩体、盲射、扔手雷都不难,难的是这些行为互相纠缠的时候,手工维护状态机里的转换。他的办法是把「何时切换、切到哪」从状态机里拿走,交给一个规划器。这个系统叫做目标导向行动规划 (Goal-Oriented Action Planning, GOAP)。
三个状态和一个计划
F.E.A.R. 角色的状态机只有三个状态:Goto(走到某处)、Animate(播一段动画)和 UseSmartObject(使用场景里的交互物体),后两者本质上都是播动画。状态机本身不再决定「下一步干什么」,只负责把规划器给的每一步执行出来。
flowchart LR G["目标:消灭敌人"] --> P["A* 规划器"] AS["行动集:攻击、装弹、去掩体……"] --> P WS["当前世界状态"] --> P P --> Plan["行动序列"] Plan --> FSM["执行:Goto / Animate"]
规划器的输入是目标、行动集和当前的世界状态,输出是一串行动;每个行动激活时,再去设置执行层状态机的状态和参数。
规划的骨架:目标、前提、效果
GOAP 的规划方式近似经典的 STRIPS 规划:
- 目标是一个期望的世界状态,比如「威胁已消除」;
- 行动有前提 (Precondition) 和效果 (Effect):前提满足才能执行,执行后效果会改写世界状态;
- 计划就是一条行动链,能把当前的世界状态一步步变成目标状态。
动手:拿枪,还是近战
用一个自己编的小例子把规划走一遍。世界状态只有三项:有没有枪、枪有没有上膛、敌人死没死。现在的状态是「没枪、没上膛、敌人活着」,目标是「敌人死亡」。士兵有四个行动:
| 行动 | 前提 | 效果 | 成本 |
|---|---|---|---|
| 捡枪 | 无 | 有枪 | 1 |
| 装弹 | 有枪 | 已上膛 | 1 |
| 开枪 | 已上膛 | 敌人死亡 | 1 |
| 近战 | 无 | 敌人死亡 | 5 |
把每一种世界状态看成图上的一个节点,每个能执行的行动看成一条边,边的长度就是行动的成本。规划,就是在这张图上找一条从当前状态走到「敌人死亡」的最短路:
flowchart LR S0["没枪 · 没上膛 · 敌人活着"] ==>|"捡枪 1"| S1["有枪 · 没上膛 · 敌人活着"] S1 ==>|"装弹 1"| S2["有枪 · 已上膛 · 敌人活着"] S2 ==>|"开枪 1"| G1["有枪 · 敌人死亡"] S0 -->|"近战 5"| G2["没枪 · 敌人死亡"] S1 -->|"近战 5"| G3["有枪 · 没上膛 · 敌人死亡"]
粗箭头那条路总成本是 3,直接近战是 5,所以规划器选了「捡枪 → 装弹 → 开枪」。这张图的节点是世界状态,不是地图上的位置;用来搜它的,正是寻路里那个 A*。
现在把行动集换掉,目标不变:
- 刺客的行动集里没有枪械类行动,只能近战——同一个目标,规划出完全不同的行为;
- 老鼠根本没有攻击行动,规划失败,于是退回去巡逻。
这就是 Orkin 说的第一个收益:目标和行动解耦。士兵、刺客、老鼠可以共用同一套目标,各自的行动集决定它们表现成什么样;后来加进游戏的无人机,也能用现成的目标和行动拼出来。
GOAP 对 STRIPS 做的四处实用化
学术上的 STRIPS 直接搬进游戏不够用,Orkin 做了四处改动:
| 改动 | 做法 | 为什么 |
|---|---|---|
| 行动有成本 | 每个行动带一个 cost,用 A* 搜成本最低的计划 | 同一个目标有多种做法时,能表达「更倾向哪种」 |
| 固定大小的世界状态 | 用一个固定长度的数组表示世界状态,不用 STRIPS 的增删列表 | 便于在规划时把前提和效果链式匹配起来 |
| 过程式前提 | 昂贵的检查(比如「有没有逃跑的路」)在规划时按需计算 | 不必把所有可能用到的事实都常驻在世界状态里 |
| 过程式效果 | 行动激活时,直接设置执行层状态机的状态和参数 | 把计划和执行连起来 |
叠加与重规划
第二个收益是分层叠加:开火、闪避、近战、找掩体、盲射、换掩体、喊话,每一项都是一个简单的行动,叠在一起就成了复杂的战术,不需要手画一张完整的转换图。
第三个收益是失败后重规划,也就是开头那一幕:
flowchart LR A["计划:走到门 → 踢门 → 冲进去"] --> B["踢门失败:门被堵住了"] B --> C["重新规划"] C --> D["新计划:走到窗 → 跳窗 → 近战"]
计划执行到一半发现走不通,就以当前的世界状态为起点重新规划。AI 看起来像是在「临场想办法」。
小队:命令加个体的判断
F.E.A.R. 的小队协调并不复杂:全局协调器按距离把附近的士兵重新分组,简单的小队行为发出「命令」,比如一起找掩体、一人压制其他人推进;每个士兵再按自己目标的优先级决定服不服从——逃命的优先级比前进高。
玩家眼里那些「包抄」「钳形攻势」,很多时候并不是专门的小队规划,而是每个士兵各自找掩体、各自寻路的结果凑出来的。小队喊话也是在行为决定之后才补上的,用来制造「他们听懂了命令」的错觉。要做更重的并行协调,Orkin 指向的后续方向是分层任务网络 (HTN)。
在游戏里怎么用
取舍与常见的坑
小结与练习
- GOAP 让行动带上前提、效果和成本,用 A* 在世界状态图上搜成本最低的计划;
- F.E.A.R. 的状态机只剩三个状态,「下一步干什么」交给规划器;
- 三大收益:目标和行动解耦、简单行为叠加出复杂战术、失败后重规划;
- 代价是世界状态的设计、行动的作者纪律和规划的 CPU。
练习:在拿枪的例子里再加两个行动:「捡手雷」(前提无,效果有手雷,成本 1)和「扔手雷」(前提有手雷,效果敌人死亡,成本 1)。画出新的状态图,算一算哪条计划最便宜。如果附近根本没有枪可捡,计划又会变成什么?
GOAP 把「如何满足目标」交给运行时规划:行动带前提与效果,用 A* 在世界状态空间搜索最低成本动作序列。F.E.A.R. 用极薄 FSM(Goto/Animate)执行计划,换来目标与行动解耦、行为可叠加、失败可重规划。
问题:组合复杂度
Orkin:单独实现「掩体 / 盲射 / 手雷」都不难;难的是 行为互相纠缠 时手工维护 FSM 转换。规划把转换逻辑从状态机挪走。(Three States and a Plan: The A.I. of F.E.A.R.(笔记);复杂度主题亦见 Isla)
STRIPS 骨架
- 目标:期望世界状态(如 ThreatEliminated)。
- 行动:前提满足才可执行;效果修改世界状态。
- 计划 = 行动链,使当前状态到达目标。
GOAP 相对学术 STRIPS 的四处实用化
- 每行动 cost,A* 搜最低成本计划(与导航 A* 同算法、异图)。
- 固定大小世界状态数组,代替 Add/Delete List。
- 过程式前提(贵的寻路检查按需做)。
- 过程式效果:激活时设置 FSM 状态/参数。
(Three States and a Plan: The A.I. of F.E.A.R.(笔记))
flowchart LR G["目标 KillEnemy"] --> P["A* 规划器"] AS["Action Set\nAttack / Reload / GotoCover…"] --> P WS["世界状态"] --> P P --> Plan["行动序列"] Plan --> FSM["Goto / Animate"]
三大收益(F.E.A.R. 案例)
- 同目标不同行动集:士兵开枪、刺客近战、老鼠无法攻击则规划失败回落巡逻。
- 叠加简单层得复杂战术,无需手写完整转换图。
- 重规划:门被挡 → 踢门失败 → 跳窗,动态解题。
小队层
个体 GOAP + 简单小队命令;「侧翼」常由导航与掩体选择涌现。更重并行协调可考虑 HTN(文中指向后续方向)。(同上)
取舍
- 适合:行动组合多、要声明式扩展、需运行时应变。
- 成本:世界状态设计、行动作者纪律、规划 CPU;调试「为何选出这串行动」需要工具。
- 不是银弹:执行层仍要可靠动画/导航;玩家感知仍靠站位与喊话(perception is reality)。
引擎对照:UE 主推 BT/StateTree 而非内置 GOAP;原理上仍可自研规划器挂到任务叶。见 行为树与 StateTree。
出处
- Three States and a Plan: The A.I. of F.E.A.R.(笔记) Jeff Orkin, Three States and a Plan (GDC 2006)
- alumni.media.mit.edu/~jorkin/gdc2006_orkin_jeff_fear.pdf
- alumni.media.mit.edu/~jorkin/goap.html
- Handling Complexity in the Halo 2 AI(笔记) Isla 2005(复杂度背景,Orkin 引用)