想象你是一家深夜大排档的主厨。客人报出“麻辣小龙虾、清炒时蔬、两碗米饭”的一瞬间,你的大脑已经自动推演出整套后厨流程:冰箱里还有没有虾?蔬菜洗了吗?米饭要现在就上锅吗?你不需要老板递来一张写着“先做A再做B再做C”的死板工序表——因为客人每次点的菜都不一样,冰箱里的存货也随时在变。你真正做的事情是:盯着最终目标(一顿完整的晚餐),调动脑海里所有可用的工序(洗、切、炒、蒸、摆盘),快速推演出一条最顺、最省事的行动链,然后动手。这个“看着目标、倒推步骤、随机应变”的决策方式,正是游戏AI领域中大名鼎鼎的GOAP(Goal-Oriented Action Planning,目标导向行动规划)。传统行为树像一份贴在墙上的固定菜谱,而GOAP更像是给了NPC一颗“主厨大脑”——只要告诉它最终想做什么,它自己能从所有可用动作中搜索出最佳行动序列。本文将以这条“主厨故事”为主线,带你从零看懂GOAP的原理、算法、工程实现与实战优化。
掌握GOAP,意味着你能为游戏NPC设计出“自主思考”的决策层:从固定脚本走向目标驱动,这是AI从业者拉开职业竞争力的核心技能之一。
目录
可视化演示
演示AI NPC的目标导向行动规划GOAP:从世界状态出发,搜索满足目标的最优动作序列并执行,实现智能化的行为决策。
本节配有交互动效(进入视口后自动加载,约 3KB)。
状态与目标 世界状态 目标 goal→best NPC从状态出发向目标规划
基本概念:后厨的“底料”
想象你是一家夜宵档大排档的主厨。客人坐下张口就是一个组合:“一份麻辣小龙虾,一盘清炒时蔬,两碗米饭。”你并不慌——因为你知道冰箱里有什么,你知道店里所有能做的工序,你知道客人要的最终结果,而报出菜名的一瞬间,你已经开始在脑子里盘算先烧水还是先切菜。这段话里藏着GOAP的全部底座:世界状态、动作集合、目标、规划器。
用GOAP的行话来说:世界状态(World State)是NPC当前对环境的完整认知快照,比如“小龙虾已解冻 = true”“蔬菜已购买 = true”;动作集合(Actions)是NPC能执行的所有原子行为,洗、切、炒、蒸都算一个动作,每个动作都带着它自己的前置条件和效果;目标(Goal)是一个需要满足的条件集合,比如“晚餐完成 = {菜已炒、饭已熟、饮料已倒}”;而规划器(Planner),就是那位负责在脑内快速推演出工序组合的“主厨”。四个部件缺一不可,共同构成一套完整的决策系统。
从形式化角度看,一个动作能否在当前世界状态下执行,取决于前置条件是否被世界状态包含:
当某个动作执行后,世界状态会发生更新——这就像你完成了“洗菜”这一步,后厨的食材清单上就多了“菜已洗好”这一项:
这四个要素构成了GOAP的全部“底料”。行为树像贴在墙上的固定菜谱,而GOAP更像主厨脑中的条件反射式配菜能力——同一个动作,今天和昨天组合的方式可能完全不同,这正是目标导向的精髓:不以流程为中心,而以结果为中心。
- 消耗集consume(a) 表示动作会消耗掉的条件,比如炒菜用掉了“生肉”。
- 差集\setminus 表示从世界状态中扣掉这些被消耗的条件。
- 效果集effect(a) 表示动作产生的新条件,如“菜已炒”“晚餐完成”。
- 新状态\cup 把消耗后的状态与效果集合并,得到执行后的世界状态。
规划原理:倒着看的菜谱
主厨看菜谱从来不是顺着读,而是从顾客要的最终成品倒着看:要做红烧肉 → 肉必须切好 → 肉必须解冻 → 肉必须从冰箱拿出来。GOAP的规划器也是这个思路——在动作空间中从目标状态出发,逆向搜索能产生目标状态的动作,再把动作的前置条件当作新的子目标继续回溯,直到与当前世界状态成功衔接。这个“倒着看菜谱”的过程,就是GOAP最核心的反向推导。
规划器的具体行为是:把目标状态压入待考察队列,然后问“哪个动作的效果能带来这个状态?”比如目标是“晚餐完成 = {菜已炒、饭已熟}”,你可能先找到“摆盘”动作的效果是“晚餐完成”,那么为了让“摆盘”可执行,需要“菜已炒”和“饭已熟”已在状态中,于是这两个条件成为新的子目标继续回溯,直到所有悬置的条件都被当前世界状态满足。每当找到一个可用动作,就把它的前置条件挂到新的待解决列表上;找不到则回退换一条路径。这个“状态到状态”的推演,最终会拉出一条完整的动作序列。
一组动作串联起来,就构成一个动作链,公式上可以写为:
倒着读菜谱的好处是:你永远只关注“还差什么”,而不是机械地按顺序做。如果中途发现客人不要米饭了(目标变化),倒推流程立即收缩,不会像固定菜谱那样一路做到头。这就是GOAP在决策灵活性上的第一层优势。
- 初始状态S0 等于当前世界状态 S_start,也就是后厨现在的真实存货。
- 状态递推第 i 个动作 a_i 作用在第 i-1 个状态上,产生第 i 个状态。
- 动作链a_1, a_2, ……, a_n 是从动作池里挑选出来并按顺序排列的动作序列。
- 目标满足最终状态 Sn 包含目标状态 G 的所有条件,即 G 是 Sn 的子集。
关键算法:A*与启发式搜厨艺
夜宵档高峰时段,主厨手上有两条备选方案:一条是“先做小龙虾再炒菜”,另一条是“先炒菜再做小龙虾”。两条都能完成,但前者因为小龙虾要早下锅才入味,总等待时间更短。主厨怎么在几秒内拍板?靠的不是瞎猜,而是“已完成工序耗时 + 预估还要多久”的心算。这个心算过程,正是A*搜索在GOAP中的核心评估逻辑。
GOAP规划器维护一个开放列表(open list),里面放着待扩展的状态节点;每次从开放列表中挑一个“最有希望”的节点扩展,直到找到一条覆盖目标状态的计划。所谓“最有希望”,由评估函数决定:
启发式函数就像主厨的经验直觉——“小龙虾至少还要炖二十分钟”这种脑内预估,让主厨不必真的把每条菜谱都试一遍,就能快速排除明显费时的路径。启发越准,搜索越快;启发过强甚至可能让计划偏离最优解,这是工程中要小心权衡的。
- 真实成本g(n) 是从初始世界状态到达当前搜索节点 n 付出的真实代价,类似做完前几道菜的累计耗时。
- 启发估算h(n) 启发式函数估算从 n 到目标状态还差多少代价,例如“未满足的前置条件个数”。
- 评估总和f(n) 是两者加和,代表一条完整候选计划的预计总成本,f 越小优先级越高。
- 挑选扩展A*每轮从待扩展队列中挑选 f 值最小的节点做状态转移,直到找到覆盖目标状态的计划。
引擎实现:把后厨装进类
要让这位“主厨”真正上岗,不能只靠口头描述,你得给他一份标准化的排班表:这个动作要什么前置、会产出什么效果、花费多少成本,都要写进系统。换句话说,GOAP落到工程上,就是三个核心组件:Action(动作)、Planner(规划器)、执行器(Agent/Brain)。
Action类是基础,一个动作至少要包含四个字段:name(名字)、preconditions(前置条件集合)、effects(效果集合)、cost(成本权重)。以“炒菜”为例:前置条件是 {“菜已切”},效果是 {“菜已炒”},成本可以是 2。Planner类则负责把动作池变成搜索空间,它维护开放列表和已访问表,用启发式评估决定下一步往哪走,最终返回一个有序的动作队列。执行器每帧从计划队列中取出第一个动作,驱动对应的行为表现;计划执行完或失败时,再让 Planner 重新规划。三个组件各司其职,就像后厨的排班表、主厨的脑子、帮厨的手。
规划与执行的循环可以抽象为分段函数,描述计划队列的状态切换:
这张“排班表”一旦落地,后厨即使换了个新主厨(新的NPC),只要动作表和执行循环不变,他依然能根据目标自个儿拽出流程来。这就是GOAP在工程实现上的复用价值——决策逻辑与具体行为解耦,新增一个动作就像往菜单上加一道菜,主厨的“脑回路”不用重写。
- 生成计划当计划为空时,用当前世界状态和目标调用规划器生成一份新计划。
- 执行队首当计划非空时,执行队首动作 a1,其余动作保留在队列中等待。
- 循环切换执行完成后回到循环起点,再次检查计划是否为空,决定继续执行还是重新规划。
性能优化:高峰时段的出餐效率
深夜档口同时来了三桌客人,主厨当然不会每三十秒就把整本菜谱重新推演一遍——他只在客人改单、或某道菜做砸了的时候才重新心算。GOAP性能优化的核心也是同一个原则:别动不动就重新规划。规划本身是有代价的,搜索空间的大小决定了代价的上限。
先看搜索空间。动作池数量 b 和计划长度 d 决定了搜索树规模:
工程里常用的三板斧:其一,控制规划频率——不每帧规划,只在目标切换或计划失败时触发重新规划;其二,缓存复用——对相同目标和相似状态,直接复用上次规划结果,或对规划结果做哈希缓存;其三,剪枝与动作设计——把不满足前置条件的动作在搜索前就剔除,同时精简约动作池的冗余项。用主厨的话说就是:不要每三十秒问一遍“我要做什么菜”,只有客人改单才重新配菜;上次配好的流程先记在小黑板上;备料台上有十几样菜,但明显缺主料的菜谱根本不往脑子里过。
性能优化的终极目标,是让主厨在高峰时段依然“秒回”——大脑的搜索过程快到来不及察觉,NPC的行为看起来才足够自然流畅。这也是GOAP能否从学术Demo走向商业游戏的关键门槛。
- 分支因子b 是从任意状态出发可尝试的动作数量,对应后厨里“现在能动手的工序数”。
- 计划深度d 是计划中动作的个数,对应从备料到上菜的总工序长度。
- 指数膨胀搜索空间以 b 的 d 次方速度增长,每一步都让候选组合爆炸性扩大。
实战策略:成熟后厨的管理智慧
真正的大厨不只自己会炒菜,还懂得怎么安排帮厨、如何在紧急情况下切换菜单。实战中的GOAP也同理——目标管理、失败重规划、与行为树的融合,这三个问题决定了一套GOAP系统能不能从Demo走进真实游戏。
目标管理:NPC往往同时存在多个目标,比如“生存(生命值 > 0)”“攻击敌人”“拾取弹药”。GOAP需要为每个目标打优先级分,并动态选出当前最重要的目标:
失败重规划:在执行过程中,如果某个动作的前置条件突然失效(比如要炒的菜被别的NPC拿走了),GOAP的做法是把剩余计划全部废弃,重新调用规划器生成新计划。这和主厨发现“小龙虾卖完了”立即改推荐香辣蟹的思路一致——不纠结于原计划,快速响应现实变化。行为树融合:成熟的游戏项目中,GOAP通常只负责“决定要做什么”(高层决策),而“具体怎么做”交给行为树或动画状态机。比如GOAP规划出“拾取弹药”,行为树再负责“走向弹药箱 → 播放拾取动画”的低层技能分解。两者不是替代关系,而是上下游协作的关系。
米其林后厨永远有一个“敢拍板换菜”的主厨,也有一套稳固的执行班组。GOAP就是那个拍板的主厨,行为树是熟练的帮厨团队——各司其职,才撑得起一场流畅的NPC表演。
- 目标集合Goals 是NPC所有潜在目标的集合,如“进食”“休息”“攻击”。
- 优先级评分Priority(g) 根据世界状态动态计算目标 g 的迫切程度,比如“生命值越低,治疗目标优先级越高”。
- 挑选最大值argmax 选出优先级最高的目标,当前所有规划都围绕它展开。
AI 应用场景
开放世界NPC日常行为编排
阶段:决策规划层
解决的问题:传统手工状态机按固定流程执行,NPC每天过得高度雷同,玩家很快审美疲劳。GOAP让NPC按自身需求动态组织一天:上午工作赚钱、下午饿了购物、晚上疲惫回家休息,行为自然多变且符合逻辑。
具体案例:在《看门狗:军团》类开放世界城市中,市民NPC可通过GOAP从“睡觉”“上班”“去酒吧”“回家做饭”等多个目标中按优先级选出当前目标,并从动作池里动态组合出行动链,配合行为树完成走路、开门等低层动作。
FPS/TPS士兵AI的战术决策
阶段:实时决策层
解决的问题:行为树写死“见人开枪、没子弹就蹲”会让士兵AI可预测且呆板;GOAP能以“消灭敌人”为目标动态组合“找掩体 → 换弹 → 侧翼包抄 → 开火”等动作,让每一场交火的战术都略有不同。
具体案例:在《求生之路》类合作射击游戏中,特感AI使用GOAP规划“抓住落单玩家”的行动链,根据战场地形、玩家位置和队友状态实时调整突袭计划,给玩家带来“被聪明对手盯上”的压迫感。
模拟经营游戏的生存需求管理
阶段:长期决策与目标管理
解决的问题:模拟人生类游戏NPC需要同时管理饥饿、清洁、社交等多类需求。GOAP的动态目标评分让NPC在“该吃饭还是该睡觉”之间自然摇摆,行为不再像脚本一样固定轮播。
具体案例:在一款模拟定居点游戏中,居民NPC每天根据体力、饥饿度、外界威胁动态切换目标并规划对应行动序列,从伐木、做饭到逃命,都通过同一套动作池灵活组合,玩家能看到“先填饱肚子再干活”的个体生存逻辑。
Python 演示
from heapq import heappush, heappopfrom itertools import count class Action: def __init__(self, name, preconditions, effects, cost=1): self.name = name self.preconditions = set(preconditions) self.effects = set(effects) self.cost = cost def can_execute(self, state): return self.preconditions.issubset(state) def apply(self, state): new_state = set(state) new_state.update(self.effects) return new_state def __repr__(self): return self.name class GoapPlanner: def __init__(self, actions): self.actions = actions def plan(self, start, goal): counter = count() start_key = tuple(sorted(start)) heap = [(0, next(counter), start_key, [])] visited = set() while heap: cost, _, state_key, path = heappop(heap) if state_key in visited: continue visited.add(state_key) state = set(state_key) if goal.issubset(state): return path, cost for action in self.actions: if action.can_execute(state): next_state = action.apply(state) next_key = tuple(sorted(next_state)) heappush(heap, (cost + action.cost, next(counter), next_key, path + [action])) return None, 0 def main(): actions = [ Action("洗菜", {"菜已买"}, {"菜已洗"}), Action("切菜", {"菜已洗"}, {"菜已切"}), Action("炒菜", {"菜已切"}, {"菜已炒"}, cost=2), Action("买肉", set(), {"肉已买"}, cost=3), Action("炖肉", {"肉已买"}, {"肉已熟"}, cost=4), Action("蒸饭", set(), {"饭已熟"}, cost=5), Action("摆盘", {"菜已炒", "肉已熟", "饭已熟"}, {"晚餐完成"}), ] start = {"菜已买"} goal = {"晚餐完成"} planner = GoapPlanner(actions) plan, total_cost = planner.plan(start, goal) if plan: print("找到计划: " + " -> ".join(action.name for action in plan)) print("总成本: " + str(total_cost)) else: print("无法达成目标") if __name__ == "__main__": main()
self.preconditions = set(preconditions)将前置条件转成集合,方便后续子集判断。action.can_execute(state)检查前置条件是否全部被当前世界状态包含。new_state.update(self.effects)将动作的效果集合并入新状态,模拟“做完这道菜桌上多了一盘菜”。start_key = tuple(sorted(start))将状态排序转成元组,作为状态去重的哈希键。if goal.issubset(state):检查当前状态是否已覆盖目标的所有条件,达成即返回计划。heappush(heap, (cost + action.cost, next(counter), next_key, path + [action]))将新状态连同累计成本和行动路径压入优先队列,等待下一轮扩展。
常见误区
误区用了GOAP就不该用行为树,行为树是过时的技术,GOAP是更先进的全套替代方案。
正解两者是不同粒度的决策工具。GOAP擅长“高层目标 → 行动计划”,行为树擅长“计划 → 具体技能分解”。工业界主流做法是混合:GOAP生成计划,行为树执行低层技能。就像主厨负责排菜,帮厨按固定步骤备料,缺一不可。
为何常见:多数教程为了讲清各自特性,喜欢对比两者优缺点,很容易让新手误以为是“只能二选一”的对立关系。
误区环境随时在变,我的NPC必须每帧都重新规划,才能对玩家行为做出及时响应。
正解GOAP不在每帧重规划,否则NPC会陷入“思考瘫痪”,性能也扛不住。正确做法是事件驱动:只有在目标切换、计划失败或关键状态突变的时刻才重规划。就像主厨不会每三十秒把整本菜谱重推一遍。
为何常见:初学者把“响应快”误解为“计算频率高”,忽略了规划器搜索本身的开销和事件触发的设计方式。
误区给NPC的动作越多,它能做的事情就越多,看起来就越聪明、越丰富。
正解动作池过大会让搜索空间按 O(b^d) 指数爆炸,规划耗时飙升,NPC反而显得“反应迟钝”。应该通过前置条件剪枝、动作归类控制分支因子,让动作数量“够用就好”。
为何常见:“更多动作 = 更多玩法”是直觉判断,但没考虑组合搜索的计算量瓶颈,本质上是把创意问题当成了数量问题。
误区GOAP是“自动规划路径”的算法,跟A*寻路是同一类东西,都是让角色从一个点走到另一个点。
正解GOAP解决的是决策层“接下来做什么”,寻路解决的是导航层“怎么走到那个位置”。GOAP规划出一条任务序列后,每个行动的具体移动仍由NavMesh寻路完成。就像菜单决定“做什么菜”,地图决定“去哪个菜市场”。
为何常见:GOAP和寻路都叫“planning“,中文翻译里都带”规划”二字,术语层面的混淆顺理成章。
误区GOAP里的世界状态只能用“开门/关门”“有钥匙/没钥匙”这种非黑即白的布尔量表达。
正解经典教程爱用布尔值是为了简洁,但工程实战常扩展为数值状态,如“弹药数量 = 12”,动作可以要求“弹药 > 5”。这样表达能力更强,能模拟更细腻的决策。
为何常见:教材示例为了降低理解门槛,几乎全用布尔状态,读者很容易把“示范简化”当成“唯一标准”。
面试题
Q1. GOAP中的“目标”在“主厨做菜”比喻里相当于什么?
- A.一道固定菜谱
- B.一顿晚餐完成态← 正确答案
- C.一口炒菜铁锅
- D.一盒新鲜食材
答案:B。目标是期望达到的世界状态集合,对应“一顿晚餐完成”这个终局状态,而不是菜谱(动作序列)、工具或食材。选项B精确对应GOAP中 Goal 的定义。
Q2. 下列哪项不是GOAP中动作(Action)的组成部分?
- A.前置条件设置
- B.动作效果描述
- C.执行成本权重
- D.目标连续编号← 正确答案
答案:D。动作包含前置条件(preconditions)、效果(effects)和成本(cost),用于判断可执行性、状态更新和方案择优。“目标编号”属于目标管理范畴,不是动作自身的组成部分。
Q3. GOAP规划的典型搜索方向是什么?
- A.从起始状态向目标状态正向搜索
- B.从目标状态向起始状态反向搜索← 正确答案
- C.沿动作表顺序依次尝试
- D.随机选择状态进行跳跃
答案:B。GOAP的核心特征是“倒着看菜谱”——从目标出发,找能产生目标状态的动作,再把动作的前置条件作为新子目标继续回溯,直到与当前状态衔接。选项A是BFS等前向搜索,选项C和D都不符合GOAP的反向规划思想。
Q4. 动作的前置条件相当于做菜流程中的什么?
- A.洗菜切菜必须按固定顺序← 正确答案
- B.菜品价格需要提前确定
- C.厨房营业时间有严格限制
- D.顾客座位需要提前安排
答案:A。前置条件描述动作执行前必须为真的状态,类似“要先洗菜才能切菜”的工序依赖。选项A最贴近;B、C、D都与状态依赖无关。
Q5. 相比行为树,GOAP的优势包括哪些?
- A.动作之间可以灵活自由组合← 正确答案
- B.完全不需要设计执行成本
- C.能够根据目标动态编排流程← 正确答案
- D.天然支持并行执行多个任务
答案:AC。GOAP通过搜索动态组合动作序列,动作复用性强,且随目标变化重新编排计划(A、C正确)。B错误——GOAP需要成本来评估方案优劣;D错误——GOAP本质是串行计划,并行任务通常靠外部机制处理。
Q6. GOAP规划中采用启发式函数 h(n) 的主要目的是什么?
- A.减少搜索过程中的内存占用
- B.估算当前状态到目标的距离← 正确答案
- C.确保每次都能得到相同计划
- D.直接降低每个动作的执行成本
答案:B。h(n) 是启发式函数,用于估算当前节点到目标的剩余代价,让A*优先扩展“看起来最接近目标”的节点,从而加速搜索。它不改变动作成本,也不直接减少内存,更不能保证唯一计划。
Q7. 在GOAP中,动作的“效果”可能产生哪些影响?
- A.直接新增一个世界状态条件← 正确答案
- B.修改其他动作的配置成本
- C.删除更新已有的世界状态← 正确答案
- D.修改当前目标的优先级分数
答案:AC。动作效果通过状态转移公式 S' = (S \ 消耗) ∪ 效果来更新世界状态,既能新增条件也能更新/删除旧条件(A、C正确)。效果不会直接修改其他动作的cost,也不会直接改目标优先级。
Q8. 关于GOAP与行为树的关系,正确说法是?
- A.GOAP可以完全取代行为树设计
- B.GOAP管高层决策行为树管技能← 正确答案
- C.行为树的算法比GOAP更加复杂
- D.两种技术在项目中完全不能共存
答案:B。工程中主流的混合架构是GOAP做高层目标规划,行为树做低层技能执行(如移动、动画)。选项A过于绝对,C说法不准确,D与工业实践相悖。
Q9. 当NPC发现当前计划不再可行时,GOAP会怎么做?
- A.继续按照原始计划执行下去
- B.重新调用规划器生成新计划← 正确答案
- C.随机挑选下一个可用的动作
- D.停止执行AI逻辑并原地待机
答案:B。计划失败是GOAP重规划的触发条件之一。规划器会基于当前的“世界状态”和原目标重新搜索,生成新计划。继续执行(A)会陷入无效循环,随机动作(C)和待机(D)都不符合目标导向原则。
Q10. 为了提升GOAP运行时性能,通常采用哪些策略?
- A.缓存上一次的规划结果← 正确答案
- B.每一帧都强制重新执行规划
- C.限制动作池中动作数量规模← 正确答案
- D.让所有NPC共享同一份计划
答案:AC。缓存规划结果可避免相同目标和相似状态下的重复搜索;精简动作池能降低分支因子 b,从而压缩搜索空间(A、C正确)。B会增加无谓开销,是性能反面教材;D会让NPC行为同质化,丧失个体差异。