第 31 天我们学过有限状态机(FSM),它的痛点是:状态一多,_physics_process 里全是 if state == ... 的跳转蜘蛛网,加一个行为就要动到处。今天换一条更抗「长大」的路线——行为树(Behavior Tree, BT):把 AI 决策拆成一棵可组合的树,每个节点只负责一件小事,整棵树每帧从根「走」一遍决定敌人此刻该干嘛。这是《光环》《最后生还者》这一类商业游戏里的主流做法,也是独立游戏让敌人「看起来有心眼」最省力的方式。

今日速览

  • 行为树 = 一棵会「逐帧重跑」的决策树:每帧从根节点 Tick 一次,每个节点只回三种结果之一——SUCCESS(成功)、FAILURE(失败)、RUNNING(正在做,还没完)。
  • 两种组合节点撑起 90% 逻辑:Sequence(顺序)=「先 A 再 B,全成功才成功」,Selector(选择)=「先试 A,不行再试 B,成功一个就停」。前者像「AND」,后者像「OR」。
  • 落叶节点分两类:Action(动作,会真正改变游戏世界,可以返回 RUNNING 跨多帧)和 Condition(条件,只检查不改变,只回成功/失败)。
  • Godot 里用 Beehave 插件最省事:它是 node-based 的,直接在场景树里把行为树搭出来挂到敌人节点上,还带编辑器内运行时调试视图和性能监monitor,能实时看到「现在走到了哪个叶子」。Godot 4.5+ 请用 Beehave 2.10+。
  • 行为树不是越多越好:官方作者自己说,简单 AI 用行为树是「杀鸡用牛刀」,复杂 NPC/多敌人复用逻辑时才划算。

核心学习内容

1. 先记住「三个状态 + 两种组合」

行为树唯一的运行机制就是 Tick:每帧(或固定间隔)从根节点开始,父节点按规则「踢」子节点,子节点返回状态,父节点根据状态决定下一步。三个状态的含义必须刻进脑子:

  • SUCCESS:这件事做完了/成立。
  • FAILURE:这件事做不了/不成立。
  • RUNNING:这件事正在进行,下一帧接着做(追击敌人时帧帧都在 RUNNING)。

两种组合节点的语义:

节点别名规则像典型用途
Sequence顺序从左到右跑,遇到 FAILURE 就失败,全部 SUCCESS 才成功AND看见玩家 → 转过去 → 开火
Selector选择/回退从左到右跑,遇到 SUCCESS 就成功,全部 FAILURE 才失败OR能攻击就攻击,否则能追就追,否则巡逻

只要记住:Sequence 是「一路做到底」,Selector 是「挑第一个能成的」,一棵树的读法就通了大半。

2. 装饰器与「黑板」:让树会记忆、会节流

光有组合节点还不够,工程里真正好用的是两类东西:

  • 装饰器(Decorator):套在某棵子树上,改写它的返回或执行次数。常用的有 Inverter(取反)、Cooldown(冷却,避免敌人每帧狂发招)、Repeat/RepeatUntilFail(重复巡逻)、TimeLimit(限时)、以及「条件守卫」——只有当条件满足时才允许进这棵子树(例如「血量 > 30% 才允许正面硬刚」)。
  • 黑板(Blackboard):一块共享数据黑板,存 has_target、last_known_position、health_ratio 这类信息。条件节点负责「写」,动作节点负责「读」,节点之间不直接互相调用,于是子树可以整块搬到别的敌人身上复用。

举个实战结构,一个「会心眼」的敌人,用 Selector 从高优先级到低优先级排:

Selector(根)
├─ Sequence【受击逃跑】: 条件(血量<30%) → 动作(跑向盟友/掩体)
├─ Sequence【攻击】    : 条件(在攻击范围 & 冷却好) → 动作(播放攻击动画)
├─ Sequence【追击】    : 条件(看得见玩家) → 动作(沿导航追上去) 
├─ Sequence【警戒】    : 条件(有 last_known_position) → 动作(走过去搜查3秒) → 清空记忆
└─ 动作【巡逻】       : 在巡逻点之间来回走

这是行为树最舒服的地方:「逃跑」永远比「追击」优先,你只要把它放在 Selector 最左边就行,不用写一个巨大的优先级 if-else。要加新行为(比如「呼叫增援」),插一个子树进去,完全不碰其他分支。

3. 在 Godot 里落地(Beehave)

Beehave 的安装很短:从 Release 下载后,把 addons/beehave 放进项目的 /addons,在 项目设置 > 插件 里启用,再把 script_templates 放进项目即可。之后你会得到这几类核心节点:

  • BeehaveTree:根节点,负责 Tick 频率(可设每 N 帧一次以省性能)。
  • SequenceComposite / SelectorComposite:就是上面的顺序/选择。
  • ActionLeaf / ConditionLeaf:你写自定义脚本继承它们,实现 tick(actor, blackboard) 返回状态。
  • 各种装饰器:直接从节点库拖。

它的调试视图是关键生产力:运行游戏时能在编辑器里看到整棵树实时高亮「现在停在哪个节点」,敌人发呆时一眼就知道卡在哪。做 Steam 游戏时,AI 卡住是最难查的 bug 之一,这个视图能省掉大量 print。

自定义一个叶子的最小骨架(Action,返回 RUNNING 表示在执行):

extends ActionLeaf

func tick(actor: Node, blackboard: Blackboard) -> int:
    var target = blackboard.get_value("target")
    if target == null:
        return FAILURE
    actor.move_toward_node(target)          # 你自己的移动逻辑
    return SUCCESS if actor.reached(target) else RUNNING

最后一句叮嘱:行为树负责「决策」,移动/寻路交给 NavAgent(第 43 天学过 NavigationRegion2D + NavigationAgent2D),两者配合,敌人就既会想又会走。

🎯 DeepSeek 提示词

① 让 DeepSeek 把「敌人需求」翻译成一棵行为树

你是一位资深游戏 AI 设计师,精通行为树(Behavior Tree):Sequence=顺序(全成功才成功)、Selector=选择(成功一个即停)、Condition=只检查、Action=可改变世界并可返回 RUNNING。

【任务】把我的敌人需求翻译成一棵行为树,并标明每个节点的类型与返回值含义。
【我的敌人需求】一个近战小兵:血量低于 30% 会逃跑找掩体;玩家在攻击距离内且攻击冷却好了就打;看得见玩家就追;看不见但记得最后位置就过去搜查 3 秒然后放弃;否则在固定巡逻点之间巡逻。
【输出格式】
1. 用缩进文本画出整棵树(每行:节点类型(名称) : 该节点职责)。
2. 给出「优先级从高到低」的一句话解释。
3. 列出需要用到的黑板键(名字 + 类型 + 谁来写/谁来读)。
4. 指出哪几个子树可以抽成「可复用子树」以给其他敌人类复用。
【约束】只用 Sequence / Selector / Condition / Action 和必要的装饰器;不要引入无关概念;输出用中文。

② 让 DeepSeek 写 Beehave 的自定义 ActionLeaf / ConditionLeaf 脚本

你是 Godot 4 + GDScript 专家,熟悉 Beehave 行为树插件(Godot 4.5+ 用 Beehave 2.10+)。Beehave 的叶子脚本继承 ActionLeaf 或 ConditionLeaf,实现 tick(actor, blackboard) -> int,返回 Beehave 的 SUCCESS/FAILURE/RUNNING。

【任务】为下面这棵行为树里缺失的叶子,生成完整、可直接使用的 GDScript 脚本。
【树结构】(在此粘贴你上面得到的行为树)
【我要的叶子】1) 条件:血量低于阈值 2) 动作:跑向最近掩体 3) 动作:沿导航追击玩家 4) 动作:在巡逻点之间巡逻。
【输出格式】每个叶子一个代码块,含:文件名、extends 谁、tick 完整实现、用到的黑板键、以及「在场景里怎么挂到 BeehaveTree 下」的一句话说明。
【约束】Godot 4 稳定版 API;只用 Node2D/CharacterBody2D/NavigationAgent2D 等内置节点;代码带中文注释;不要在 tick 里做耗时寻路初始化;用 blackboard.get_value / set_value 读写共享数据。

③ 让 DeepSeek 当「行为树排错员」,诊断敌人为什么发呆/抽搐

你是行为树调试专家。行为树每帧从根 Tick,节点返回 SUCCESS/FAILURE/RUNNING;常见 bug 是「某个 Condition 永远为 FAILURE 导致整条 Selector 落空」或「某个 Action 忘了返回 RUNNING 导致帧帧重来、动画抽搐」。

【任务】根据我描述的「敌人坏行为」+ 树结构 + 运行时日志,定位根因并给出最小修复方案。
【敌人坏行为】(例:敌人看到玩家却在原地不停切朝向 / 一直站着不动 / 一靠近就立刻后退)
【我的树结构】(在此粘贴)
【运行时日志】(粘贴 Beehave 调试视图里观察到的每帧停留节点 / 或你的 print 日志)
【输出格式】
1. 最可能的 1–3 个根因,按可能性排序,各写明「树上哪个节点出问题、为什么」。
2. 每个根因给一个最小修改(改条件阈值 / 改返回状态 / 调整 Selector 顺序 / 加 Cooldown 装饰器)。
3. 给一段「改造后的树片段」。
【约束】只基于我给的信息推断,不要泛泛讲行为树理论;无法确定时明确列出还需要我提供哪条信息。

学习心得

做一款能上 Steam 的游戏,敌人「会不会想」往往直接决定玩家会不会觉得这游戏「有内容」。FSM 能撑起前期,但一旦敌人种类变多、行为变复杂,行为树的模块化 + 可复用 + 可视化调试会帮你省下大量维护时间——这恰好是个人开发者最缺的资源。

结合「用 DeepSeek 做 Steam 游戏」的目标,AI 与行为树是绝配:行为树本身就是结构化、可被描述的(树 + 状态 + 条件),DeepSeek 极擅长把「自然语言里的敌人性格」翻译成节点树,再逐叶子生成脚本。上面三条提示词构成闭环——①设计树 → ②生成叶子 → ③排错,正好覆盖从 0 到能跑的全过程。

明日关注

  • 把今天三条提示词跑一遍,产出一个 Enemy_BT 场景挂到现有 Godot 项目的敌人上,接上第 43 天的 NavigationAgent2D。
  • 给行为树加一个黑板存 target / last_known_position,让「警戒搜查」子树能记住玩家的最后位置。
  • 装好 Beehave 插件后,用它的运行时调试视图抓一次敌人发呆,验证「一眼定位卡点」是否真省时间。

本文由自动化学习流水线每日生成,坚持即学即记。