中EN
精修课概念页

行为树:失败了,还有 Plan B

Behavior Tree Fundamentals

更新于 2026-10-19 约 14 分钟游戏AI行为树Behavior TreeSelectorSequence

一个 NPC 要进楼

设想一个 NPC 要进一栋楼。它先走到门口;门能直接开就开,锁着就用钥匙,没钥匙就砸;门实在进不去,就绕去窗户爬进来。

用状态机 (FSM) 写,你会得到「走向门」「开门」「解锁」「砸门」「走向窗」「爬窗」一串状态,以及每种失败之后该跳去哪里的一大把转换。再加一个「找钥匙」,转换又要改一遍。

行为树 (Behavior Tree, BT) 换了一种说法:把方案按优先级排好,前一个失败就自动试下一个。「失败了还有 Plan B」,正是行为树最擅长表达的东西。《游戏引擎架构》(GEA) 也把行为树列为角色 AI 最常见的决策方法。

树的形状:分支管流程,叶子干活

Chris Simpson 对行为树的描述很简洁:树的分支只是控制流程的工具,叶子才是真正的命令——走路、开门、检查条件。树可以很深,子树可以做成库反复使用,而按优先级排列备选分支,是行为树的强项。

每个节点被执行(叫做一次 tick)以后,向父节点返回三种结果之一:

  • Success:这件事这一拍已经办成了;
  • Failure:这件事办不成;
  • Running:还在办,比如还在寻路、还在走路,下一拍接着 tick。

第三种结果是关键。「走到门口」要花好几秒,有了 Running,它就是一个普通的叶子,不必像状态机那样拆成「出发」「途中」「到达」几个状态。

节点按孩子的数量分三类:

类型孩子作用
顺序 (Sequence)多个从左到右依次执行,任何一个失败就失败,全部成功才成功(相当于「与」)
选择 (Selector)多个从左到右依次尝试,任何一个成功就成功,全部失败才失败(相当于「或」,也就是按优先级回退)
装饰 (Decorator)一个改写孩子的结果或执行方式:取反 (Inverter)、强制成功 (Succeeder)、重复 (Repeater)、直到失败 (Repeat Until Fail)
叶子 (Leaf)没有游戏自己定义:动作、条件测试,或者调用另一棵子树

把进楼这件事画成树,就是下面这样:

flowchart TD
  R["Selector:进建筑"] --> D["Sequence:走门"]
  R --> W["Sequence:走窗"]
  D --> D1["走到门"]
  D --> D2["Selector:开门"]
  D2 --> O1["直接开"]
  D2 --> O2["解锁再开"]
  D2 --> O3["砸开"]
  W --> W1["走到窗"]
  W --> W2["爬进去"]

外层的 Selector 先试「走门」,整条门的方案失败了再试「走窗」;内层的 Selector 在三种开门手段之间回退。每个 Sequence 都是「先走到,再进去」。

逐拍走一遍

假设这扇门锁着,NPC 没有钥匙,门也结实得砸不开。下面一拍一拍地看这棵树怎么运转:

拍发生了什么根节点返回
1进建筑 → 走门 → 走到门:刚出发Running
2走到门:还在路上Running
3走到门:到了,Success。轮到「开门」:直接开 Failure(锁着),解锁再开 Failure(没钥匙),砸开 Failure(砸不开),于是开门 Failure,走门 Failure。进建筑转而去试「走窗」:走到窗,RunningRunning
4–5走到窗:还在路上Running
6走到窗:Success。爬进去:RunningRunning
7爬进去:Success,于是走窗 Success,进建筑 SuccessSuccess

第 3 拍是最热闹的一拍。下图就是这一拍结束时各个节点的状态:

? 进建筑→ 走门→ 走窗走到门? 开门走到窗爬进去直接开解锁再开砸开成功失败运行中还没轮到
第 3 拍结束时:开门的三种手段全部失败,失败一路向上冒到「走门」,外层 Selector 于是改走窗户。带 ? 的是 Selector,带 → 的是 Sequence。

动手:写出 Sequence 和 Selector

两种组合节点的核心只有十几行。下面是一个最小版本(C++ 风格的示意代码):

enum class Status { Success, Failure, Running };

struct Node {
  virtual ~Node() = default;
  virtual Status Tick(Blackboard& bb) = 0;   // 每一拍被调用一次
};

// Sequence:依次执行,任何一个失败就失败(与)
struct Sequence : Node {
  std::vector<Node*> children;
  size_t current = 0;                        // 记住上一拍停在哪个孩子
  Status Tick(Blackboard& bb) override {
    while (current < children.size()) {
      Status s = children[current]->Tick(bb);
      if (s == Status::Running) return Status::Running;              // 下一拍从这里接着来
      if (s == Status::Failure) { current = 0; return Status::Failure; }
      ++current;                                                     // 成功了,轮到下一个
    }
    current = 0;
    return Status::Success;
  }
};

// Selector:依次尝试,任何一个成功就成功(或,按优先级回退)
struct Selector : Node {
  std::vector<Node*> children;
  size_t current = 0;
  Status Tick(Blackboard& bb) override {
    while (current < children.size()) {
      Status s = children[current]->Tick(bb);
      if (s == Status::Running) return Status::Running;
      if (s == Status::Success) { current = 0; return Status::Success; }
      ++current;                                                     // 失败了,换下一个方案
    }
    current = 0;
    return Status::Failure;
  }
};

两个类几乎是镜像:Sequence 遇到 Failure 就停,Selector 遇到 Success 就停。对照上面的逐拍表格,在脑子里跑一遍第 3 拍:开门 这个 Selector 的 current 从 0 走到 3,三个孩子全部失败,于是它返回 Failure;走门 这个 Sequence 收到 Failure 立刻停下,也返回 Failure。

current 这个成员值得多看一眼。它让组合节点记住上一拍停在哪里,下一拍直接从那个 Running 的孩子接着 tick。Simpson 特别提到:朴素的实现每一拍都从根节点把整棵树走一遍,生产环境里应该缓存当前 Running 的节点。

节点之间怎么传数据

「走到门」得知道门在哪,「爬进去」得知道是哪扇窗。Simpson 给每个行为树实例配了一块数据上下文 (Data Context):一张「字符串键 → 任意值」的表。前面的节点把目标点写进 destination,后面的 Walk 叶子读出来去走;调用子树的叶子把同一块上下文传下去,子树就能当成模块复用。上下文里还可以放一个栈,让节点依次取出候选的门和窗来逐个尝试。

这块共享的表就是黑板 (Blackboard) 思想在行为树里的样子。虚幻引擎把黑板做成独立的资产,行为树的装饰器、服务和任务都通过黑板的键来通信。

Halo 2 的做法:优先级列表

Halo 2 的 AI 用的是一种行为有向无环图,其中最常用的决策方式叫优先级列表 (prioritized-list):按列表顺序找第一个「相关」的子行为去执行,而且更高优先级的行为可以在之后的某一拍把正在执行的打断。这在语义上很接近「Selector 加上持续抢占」。

为了让这棵树在行为很多时还能管得住,Damian Isla 还加了几样工具:

  • 相关性是二元的:一个行为要么相关、要么不相关,不用连续的浮点分数去比;
  • 冲动 (Impulse):游离在树外的触发器,指向一个完整的行为,用来在特定条件下翻转优先级、在树里跳转;
  • 行为标签:用位向量预先过滤掉一批条件明显不满足的行为。

虚幻引擎的行为树则走了另一条路:它是事件驱动的,不每帧遍历整棵树,而是监听事件(比如黑板键的值变了)再触发重新选择。条件写成装饰器,装饰器可以当观察者,按 Observer Aborts 的设置中止正在执行的低优先级任务——这正好解决了上一节说的「只缓存不重查」的问题。所以在虚幻引擎里看到的行为树,和这一课讲的「经典每拍 tick」的行为树在细节上会有差异。

在游戏里怎么用

取舍与常见的坑

小结与练习

  • 分支管流程、叶子干活;每个节点返回 Success、Failure 或 Running,Running 让一个动作可以跨好几拍;
  • Sequence 是「全部成功才成功」,Selector 是「一个成功就成功」,后者就是按优先级回退的 Plan B;
  • 缓存 Running 节点省计算,但要另外考虑高优先级怎么打断;
  • 节点之间靠数据上下文,也就是黑板来传数据。

练习:给「解锁再开」前面加一个「找钥匙」的子树——钥匙可能在桌上,也可能在守卫身上。想一想它该是 Sequence 还是 Selector,放进树里以后,第 3 拍的逐拍表格会怎么变。

  1. Behavior trees for AI: How they work(笔记) Chris Simpson, Behavior trees for AI: How they work (2014)
  2. GEA4 Vol II §18.2.3 Artificial Intelligence(摘录笔记) GEA4 §18.2.3(BT 作为常见方法)
  3. Handling Complexity in the Halo 2 AI(笔记) Isla Halo 2 behavior DAG / prioritized-list
  4. www.gamedeveloper.com/programming/behavior-trees-for-ai-how-they-work
  5. dev.epicgames.com/community/learning/tutorials/qzZ2/unreal-engine-behavior-tree-theory

相关页面

游戏开发游戏 AI 决策管线Game AI Sense-Decide-Act Pipeline游戏 AI(与生成式 DNN 不同)通常按 感知 → 决策 → 执行 分层:底层是寻路、感知与记忆;中层是角色控制(对接动画/物理);上层是目标与决策(行为树、效用、规划等)。GEA 强调「perception is reality」——玩家看不见的内部聪明不算数。游戏开发黑板架构Blackboard Architecture黑板 (Blackboard) 是多模块共享的「工作记忆」:感知写入、决策读取、任务更新键值,避免各子系统用私有状态互相耦合。行为树的 Data Context、GOAP 的 Working Memory、Halo 的 Props,都是同一思想的变体。游戏开发决策架构选型:FSM、行为树与效用Choosing FSM, Behavior Tree & Utility AIFSM 强在少量离散状态与硬转换;行为树强在优先级回退与模块化任务;效用强在连续权衡与数据驱动调参;GOAP 强在声明式动作组合与重规划。实作常分层混用,而不是四选一宗教战。游戏开发状态模式与有限状态机State Pattern & Finite State Machines状态模式 (State Pattern) 把对象“依赖状态的行为”分散到各个状态类中,对象只持有指向当前状态的指针并把调用委托给它,状态切换就是换指针。它是实现有限状态机 (Finite State Machine, FSM) 的一种方式。FSM 能用很强的结构约束理清一团乱麻的标志位代码;状态组合爆炸、需要复用和需要“回到之前状态”时,分别用并发状态机、分层状态机和下推自动机来扩展。游戏开发 · UE5行为树与 StateTreeBehavior Tree & StateTreeUE 的 AI 决策有两套工具:行为树 (Behavior Tree) + 黑板 (Blackboard) 是事件驱动的,条件写成装饰器 (Decorator),用 Simple Parallel、Services 和 Observer Aborts 代替传统并行节点;StateTree 是结合了行为树选择器和状态机转换的通用分层状态机,只在转换触发时按需选择状态,激活路径上的任务并发运行,通过属性绑定传递数据,也是 Mass 的行为层。优先级抢占多的战斗 AI 适合行为树,状态显式、分层或大规模的场景适合 StateTree。游戏开发效用 AIUtility AI效用 AI (Utility AI) 为每个候选行动/决策计算「有多想做」的分数,选最高分(或按分抽样)。Dave Mark 的 IAUS 将其做成纯数据驱动架构:考量 (Consideration) + 响应曲线打分,行为模块可拼装;相对硬编码 if/then,更不易卡死在单一行为。游戏开发目标导向行动规划 GOAPGoal-Oriented Action PlanningGOAP 把「如何满足目标」交给运行时规划:行动带前提与效果,用 A 在世界状态空间搜索最低成本动作序列。F.E.A.R. 用极薄 FSM(Goto/Animate)执行计划,换来目标与行动解耦、行为可叠加、失败可重规划。

被这些页面引用1 篇

输入关键词开始搜索

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