UE5.8 Behavior Tree、Blackboard 与 EQS:从一名站岗守卫的差劲表现说起
引子:守卫像根木头
老陈负责的关卡是个废弃城堡外围,玩法不复杂:玩家从西侧城墙缺口潜入,躲过两名巡逻守卫,拿到中庭宝箱就算赢。第一次联调时,美术把场景氛围拉满,月光、火把、风声都有。两名守卫也穿着重甲,提着长戟,看上去挺唬人。
玩家蹲着从草丛摸过去,第一名守卫正好背对玩家,一切顺利。第二名守卫在塔楼下绕圈,玩家等他转身,轻步跟上。就在玩家即将穿过最后一道门时,守卫突然原地旋转一百八十度,直勾勾盯着玩家,却一动不动。玩家愣了两秒,伸手挥了一剑,守卫才慢悠悠抬起胳膊,嘴里喊着「发现入侵者」,脚下却继续原地踏步。
老陈看着屏幕叹气。这个守卫身上挂了三段蓝图:一段 Tick 检测玩家距离,一段 Timeline 控制巡逻路径,一段分支判断要不要追击。三段蓝图各自为政,距离到了却没人告诉移动逻辑该往哪走,攻击触发时巡逻 Timeline 还在跑。整个 AI 像一台三台发动机各转各的车。
UE5.8 里解决这种问题的标准做法就是 Behavior Tree + Blackboard + EQS。这篇文章围绕这名守卫的一天,把这套 AI 框架拆开讲清楚。
flowchart TD
subgraph actor ["守卫 Actor"]
pawn["Character + PawnSensing"]
aiMove["AI MoveTo"]
end
subgraph brain ["AI 大脑"]
aic["AIController"]
bb["Blackboard Component"]
bt["Behavior Tree Component"]
end
subgraph data ["数据资产"]
bbAsset["Blackboard Data Asset"]
btAsset["Behavior Tree Asset"]
end
subgraph query ["环境查询"]
eqs["EQS Query"]
env["场景几何 + 导航网格"]
end
aic --> bb
aic --> bt
bb --> bbAsset
bt --> btAsset
bt --> eqs
eqs --> env
pawn --> aic
aiMove --> pawn一、Behavior Tree:把行为切成节点
Behavior Tree(行为树)不是某种神秘算法,它只是一棵决策树,每帧从上到下、从左到右跑一遍,决定 AI 现在该干什么。和传统状态机不同,行为树里没有「状态」这个概念,只有「当前正在执行的节点路径」。节点跑完会返回三种结果之一:Success、Failure、Running。Running 表示这帧没做完,下帧继续。
UE5.8 的行为树编辑器在 Content Browser 里右键新建 Blueprint 资产即可打开。根节点下面挂两类控制节点:Composite 和 Task。Decorator 和 Service 挂在 Composite 或 Task 上做条件判断和持续更新。
1.1 Selector:有一个成功就停
Selector(选择器)节点的图标是问号。它从左到右执行子节点,只要有一个子节点返回 Success,整个 Selector 就返回 Success,后面的兄弟节点不再执行。如果所有子节点都失败,Selector 才返回 Failure。
守卫的顶层结构通常就是一个 Selector:
- 子节点 A:如果看到玩家,追击/攻击。
- 子节点 B:如果听到异响,去调查。
- 子节点 C:正常巡逻。
当 A 成功时,B 和 C 根本不会跑。玩家就在面前,守卫没理由再去关心远处的脚步声。把最紧迫的行为放在最左边,按紧急程度往下排,这种写法非常适合战斗 AI。
1.2 Sequence:全部成功才算成功
Sequence(序列)节点的图标是右箭头。它从左到右执行子节点,所有子节点都必须返回 Success,整个 Sequence 才算 Success。只要有一个子节点失败,Sequence 立刻返回 Failure,后续节点跳过。
一个「追击并攻击玩家」的 Sequence 可能是这样:
- 面向目标(Success)。
- 移动到攻击距离内(Running 若干帧,最终 Success)。
- 播放攻击动画(Running 若干帧,最终 Success)。
- 重置攻击冷却(Success)。
如果目标太远,移动节点会返回 Running,Sequence 会停在第二步,等移动完成再继续。如果目标突然跳崖摔死,移动节点可能失败,整个 Sequence 失败,Selector 会重新选择其他分支。
1.3 Task:真正干事的地方
Task 是行为树的叶子节点,负责具体动作。UE5.8 内置了常用 Task:MoveTo、Wait、Rotate to face BB entry、Play Animation、Run Behavior Tree。你也可以在 C++ 或蓝图里继承 UBTTask_BlueprintBase 写自定义 Task。
一个自定义 Task 的典型结构是:
UCLASS()
class MYGAME_API UBTTask_AttackTarget : public UBTTask_BlueprintBase
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AI")
FBlackboardKeySelector TargetKey;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AI")
float AttackRange = 200.0f;
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};Task 的 ExecuteTask 只在节点被激活时调用一次。如果动作一帧做不完,需要返回 EBTNodeResult::InProgress,并在完成时调用 FinishLatentTask 通知行为树。
1.4 Decorator:条件判断
Decorator 挂在 Composite 或 Task 上,决定这个分支能不能进、进了之后要不要中途退出。常用 Decorator 有 Blackboard Based Condition、Is At Location、Cone Check、Time Limit、Cooldown。
给追击分支加一个 Blackboard Based Condition,Key 选 TargetActor,Observer aborts 选 Both,含义是:
- 进入追击分支前,如果 TargetActor 为空,条件失败,跳过追击。
- 追击过程中,如果 TargetActor 变为空(比如玩家隐身或死亡),条件失败,行为树会强制中断当前分支,重新从 Selector 顶部评估。
Observer aborts 有三个选项:None、Self、Lower Priority。Self 只检查本分支的条件,Lower Priority 只影响优先级更低的兄弟分支,Both 同时生效。追击分支上通常选 Both,这样玩家一消失在视野里,守卫会立刻停止追击。
1.5 Service:持续更新黑板
Service 不是条件也不是动作,它像一段寄生在分支上的 Tick。只要这个分支正在执行,Service 就会按设定间隔被调用,用来更新 Blackboard 里的数据。
守卫的眼睛不能只看一次。给他挂一个 Update Target Service,间隔 0.5 秒:
- 扫描周围 Actor,找玩家。
- 如果看到,把玩家引用写进 Blackboard 的
TargetActor。 - 如果看不到但之前看到过,启动一个「丢失目标倒计时」,倒计时结束后清空
TargetActor。
Service 的间隔不要太短。0.5 秒对大多数 AI 足够,因为玩家移动速度相对固定,没必要每帧做视线检测。0.1 秒和 0.5 秒在手感上差别不大,性能开销却差五倍。
flowchart TD
Root["Root Selector"] --> A{Decorator: 看到玩家?}
Root --> B{Decorator: 听到异响?}
Root --> C["巡逻"]
A -->|Yes| A1["Sequence: 追击"]
A1 --> A1a["面向目标"]
A1 --> A1b["MoveTo 攻击距离"]
A1 --> A1c["播放攻击动画"]
B -->|Yes| B1["Sequence: 调查"]
B1 --> B1a["MoveTo 异响位置"]
B1 --> B1b["等待 2 秒"]
C --> C1["MoveTo 巡逻点 1"]
C --> C2["MoveTo 巡逻点 2"]
C --> C3["MoveTo 巡逻点 3"]
style A fill:#ffcccc
style B fill:#ffffcc
style C fill:#ccffcc二、Blackboard:AI 的记忆抽屉
Blackboard 是一个 Key-Value 存储,专门给 AI 记东西。它解决了一个很实际的问题:AI 的感知、决策、执行三个模块需要共享数据,但这些模块写在不同类里,互相引用很烦。Blackboard 提供一个中立的内存区域,谁都可以读、谁都可以写。
2.1 Key 类型与命名
UE5.8 的 Blackboard 支持 Bool、Int、Float、Vector、Rotator、Object、Class、Enum、Native Enum、String、Name 等类型。设计 Key 时有个口诀:Actor 用 Object,位置用 Vector,阶段用 Enum。
守卫的 Blackboard 可以这样设计:
TargetActor(Object):当前追击目标。TargetLocation(Vector):最后看到目标的位置,或 EQS 选出来的最佳位置。InvestigateLocation(Vector):听到异响想去看看的位置。PatrolIndex(Int):当前巡逻点索引。AIState(Enum):Patrol / Alert / Chase / Search / Return。LastSeenTime(Float):最后一次看到目标的游戏时间。
类型一旦定好就别乱改。把本可以用 Vector 表示的位置存成 Object,会造成不必要的 Cast 和空指针检查。Enum 比 Bool 更适合表达状态,因为状态往往不止两个。
2.2 记忆的生命周期
Blackboard 里的数据不是永远有效的。有些数据只在当前帧有意义,比如 EQS 查询结果;有些数据需要跨分支保留,比如最后看到玩家的位置。设计时要考虑清楚谁写、谁读、什么时候清空。
以「丢失目标后去最后看到位置搜索」为例:
- Service 看到玩家时,把
TargetActor和TargetLocation都更新。 - 玩家跑出视野后,Service 不再更新
TargetActor,但不清空TargetLocation。 - Decorator 检测到
TargetActor为空,退出追击分支。 - 搜索分支读取
TargetLocation,移动到该位置,环顾四周。 - 搜索结束后,清空
TargetLocation,返回巡逻。
如果追击分支一失败就把 TargetLocation 清空,搜索分支就找不到地方去了。这种时序问题在调试行为树时很常见。
2.3 黑板与任务之间的数据流
Task 通常通过 OwnerComp.GetBlackboardComponent() 获取黑板,然后用 SetValueAsObject、GetValueAsVector 等函数读写。蓝图 Task 里可以直接暴露 FBlackboardKeySelector 类型的变量,在编辑器里选择要操作的 Key。
一个读取目标位置的自定义 Task 示例:
EBTNodeResult::Type UBTTask_MoveToAttackPosition::ExecuteTask(
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
if (!BB) return EBTNodeResult::Failed;
AActor* Target = Cast<AActor>(BB->GetValueAsObject(TargetKey.SelectedKeyName));
if (!Target) return EBTNodeResult::Failed;
FVector BestPos = BB->GetValueAsVector(AttackPositionKey.SelectedKeyName);
if (BestPos.IsNearlyZero())
{
BestPos = Target->GetActorLocation() + FVector(200.0f, 0.0f, 0.0f);
}
AAIC_Guard* AIC = Cast<AAIC_Guard>(OwnerComp.GetAIOwner());
if (AIC)
{
AIC->MoveToLocation(BestPos, 50.0f);
}
return EBTNodeResult::InProgress;
}这个例子同时展示了黑板读写和延迟任务的写法。返回 InProgress 后,Task 会在移动完成时调用 FinishLatentTask。
三、AIController:把大脑装上身体
Behavior Tree 自己不会跑,它需要一个宿主。AIController 就是这个宿主。AIController 负责 Possess Pawn,初始化 Blackboard,启动 Behavior Tree,并提供 MoveToActor、MoveToLocation 等移动接口。
3.1 绑定流程
在 C++ 里,一个最简化的 AIController 看起来是这样的:
AAIC_Guard::AAIC_Guard(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
BlackboardComp = CreateDefaultSubobject<UBlackboardComponent>(TEXT("BlackboardComp"));
BehaviorTreeComp = CreateDefaultSubobject<UBehaviorTreeComponent>(TEXT("BehaviorTreeComp"));
}
void AAIC_Guard::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (GuardBehaviorTree && BlackboardComp && BehaviorTreeComp)
{
UseBlackboard(*GuardBehaviorTree->BlackboardAsset, BlackboardComp);
BlackboardComp->SetValueAsVector(TEXT("HomeLocation"), InPawn->GetActorLocation());
RunBehaviorTree(GuardBehaviorTree);
}
}UseBlackboard 会把 Blackboard 资产和组件绑定,RunBehaviorTree 启动行为树。HomeLocation 这种只在 Possess 时初始化一次的数据,适合放在这里写进黑板。
3.2 感知组件的选择
UE5.8 里给 AI 感知常见的有两种做法:PawnSensingComponent 和 AIPerceptionComponent。PawnSensing 轻量,有视线、听觉,适合简单 AI。AIPerception 功能更全,支持团队、遗忘、刺激源,也更容易扩展。
AIPerception 的配置步骤:
- 在 AIController 里创建
UAIPerceptionComponent。 - 配置 SenseConfig,比如
AISenseConfig_Sight、AISenseConfig_Hearing。 - 绑定
OnPerceptionUpdated或OnTargetPerceptionUpdated委托。 - 在回调里把感知结果写进 Blackboard。
void AAIC_Guard::OnPerceptionUpdated(const TArray<AActor*>& UpdatedActors)
{
for (AActor* Actor : UpdatedActors)
{
FActorPerceptionBlueprintInfo Info;
PerceptionComp->GetActorsPerception(Actor, Info);
for (const FAIStimulus& Stimulus : Info.LastSensedStimuli)
{
if (Stimulus.WasSuccessfullySensed())
{
if (Actor->ActorHasTag(FName("Player")))
{
BlackboardComp->SetValueAsObject(TEXT("TargetActor"), Actor);
BlackboardComp->SetValueAsVector(TEXT("TargetLocation"), Actor->GetActorLocation());
}
}
else
{
// 玩家离开视野,可以启动丢失目标倒计时
BlackboardComp->SetValueAsFloat(TEXT("LastSeenTime"), GetWorld()->GetTimeSeconds());
}
}
}
}这段代码不长,但要注意 WasSuccessfullySensed() 返回 true 表示检测到,false 表示刺激消失。很多新手只处理检测到的情况,导致玩家跑出视野后 AI 还在追空气。
四、EQS:让 AI 学会挑地方
EQS(Environment Query System,环境查询系统)是 UE5.8 里给 AI 做空间决策的工具。它的核心问题是:场景里有很多候选点,AI 该选哪个?
守卫追击玩家时,不能傻乎乎直线冲过去。玩家可能站在开阔地,守卫需要找一个既能开枪打到玩家、自己又不容易被打中的位置。这个「好位置」就是 EQS 要算的。
4.1 Query、Generator、Test
EQS 由三部分组成:
- Query:一个可复用的查询资产,定义「我要什么」。
- Generator:生成候选点或 Actor。常用
Points: Grid、Points: Pathing Grid、Actors of Class。 - Test:对每个候选打分或过滤。比如
Distance测试距离,Dot测试朝向,Trace测试视线,Project投影到导航网格。
一个找掩体的 Query 可能这样配:
- Generator:
Points: Grid,以玩家为中心,半径 ,步长 。 - Test 1:
Distance到玩家的距离,打分模式 Prefer Closer,权重 。 - Test 2:
Trace从候选点看玩家,要求可见,作为 Filter(不可见直接淘汰)。 - Test 3:
Trace从玩家看候选点,要求被遮挡(也就是候选点 behind cover),作为 Filter。 - Test 4:
PathLength到候选点的导航路径长度,Prefer Shorter,权重 。
4.2 评分机制
每个 Test 会给候选点一个标准化分数,通常在 到 之间。最终分数是所有 Test 分数按权重加权求和。设某个候选点在第 个 Test 上的得分为 ,权重为 ,则:
这个公式让结果保持在 区间。如果某个 Test 是 Filter 模式,分数只有 0 或 1,0 直接淘汰,1 不影响后续加权。设计师可以通过调整权重来改变 AI 的倾向:更看重距离的 Query 会让 AI 贴脸,更看重掩体的 Query 会让 AI 猥琐。
4.3 在行为树里调用 EQS
行为树里调用 EQS 有几种方式:
Run EQS QueryTask:直接运行一个 Query,把最佳结果写进 Blackboard。EQS QueryDecorator:用 Query 结果做条件判断,比如「有没有可用的掩体」。- 在 Service 里手动调用
RequestQuery。
守卫的掩体选择通常放在追击分支里。追击 Sequence 的第一步不是直接 MoveTo 玩家,而是先 Run EQS Query 找一个攻击位置,把结果写进 Blackboard 的 AttackPosition,第二步 MoveTo AttackPosition。这样守卫就不会无脑直线冲锋。
graph LR
A["玩家位置"] --> B["Grid Generator"]
B --> C1["候选点 A"]
B --> C2["候选点 B"]
B --> C3["候选点 C"]
C1 --> D1["距离测试"]
C2 --> D2["距离测试"]
C3 --> D3["距离测试"]
D1 --> E1["视线测试"]
D2 --> E2["视线测试"]
D3 --> E3["视线测试"]
E1 --> F1["掩体遮挡测试"]
E2 --> F2["掩体遮挡测试"]
E3 --> F3["掩体遮挡测试"]
F1 --> G1["路径长度测试"]
F2 --> G2["路径长度测试"]
F3 --> G3["路径长度测试"]
G1 --> H{加权求和}
G2 --> H
G3 --> H
H --> I["最佳掩体位置"]五、选择最佳掩体与攻击位置
EQS 真正好用的场景不是「算一个点」,而是「让 AI 在不同战术目标之间切换」。
5.1 掩体查询的 Trick
找掩体时,Generator 通常不以 AI 自己为中心,而以目标玩家为中心。为什么?因为掩体的价值是相对玩家而言的。玩家东侧有一堵墙,AI 要躲就应该躲到墙后面靠近玩家的一侧。以玩家为中心画网格,更容易找到这种「玩家看不到但 AI 能反击」的位置。
另一个 Trick 是用 Points: Pathing Grid 代替普通 Grid。Pathing Grid 只生成导航网格上的点,避免选到天花板上或 inaccessible 区域。搭配 Project Test 可以把点投影到地面,确保 AI 真的能走到。
5.2 攻击位置查询
攻击位置和掩体位置不一定相同。攻击位置更关心:
- 到目标的距离在 之间,比如手枪 到 。
- 对目标有清晰视线。
- 周围有其他掩体作为撤退路线。
- 不要离队友太远(多人 AI 时避免扎堆)。
这些条件可以写成多个 Test。距离用 Distance 的 Clamp 模式,视线用 Trace,撤退路线可以再用一个 EQS Query 在 Service 里预计算。队友距离通过 Actor of Class Generator 找到其他 AI,然后用 Distance Test 打分 Prefer Farther,权重放低一点。
5.3 用 Context 传递参考点
EQS 里的 Context 决定 Generator 和 Test 以谁为参考。常用 Context 有 Querier(执行查询的 AI)、PlayerContext(玩家)、BlackboardKey(黑板上的某个位置)。
Context 让同一个 Query 可以被不同 AI 复用。一个 Query 资产写好后,所有守卫都能用它找掩体,只是参考点不同。如果你想让 Query 以玩家为中心,就新建一个继承 UEnvQueryContext_Player 的 Context 类。
sequenceDiagram
autonumber
participant G as "守卫"
participant BT as "Behavior Tree"
participant BB as Blackboard
participant EQS as "EQS Query"
participant W as "场景几何"
Note over G,W: 巡逻阶段
BT->>G: MoveTo 巡逻点
G-->>W: 正常巡逻
Note over G,W: 发现玩家
W->>G: 视线检测命中玩家
G->>BB: TargetActor = Player
G->>BB: AIState = Chase
Note over G,W: 追击并选掩体
BT->>EQS: Run Query: FindCover
EQS->>W: 生成候选点
W-->>EQS: 返回距离/视线/遮挡数据
EQS->>EQS: 加权评分
EQS->>BB: AttackPosition = 最佳点
BT->>G: MoveTo AttackPosition
Note over G,W: 玩家跑出视野
G->>BB: TargetActor = None
G->>BB: LastKnownPosition 保留
BT->>G: MoveTo LastKnownPosition 搜索
Note over G,W: 搜索无果返回
BT->>BB: AIState = Return
BT->>G: MoveTo HomeLocation六、行为树调试
行为树这东西,写的时候很爽,调的时候想哭。好在 UE5.8 给了不少工具。
6.1 游戏内 AI 调试
选中 AI,按 ' 键(单引号)打开 AI 调试视图。屏幕上会显示当前行为树正在执行的节点、黑板 Key 的值、感知状态、导航路径。按 NumPad 0 切换 AI 调试类别,比如 BehaviorTree、EQS、Perception。
调试时重点关注两个东西:
- 当前激活的节点路径是不是预期。
- Blackboard 里的关键 Key 值对不对。
如果守卫看到玩家却不追击,先看 Blackboard 里 TargetActor 有没有被感知组件写进去。如果没有,问题在感知配置或回调。如果有,再看 Behavior Tree 里 Decorator 的条件是不是写反了。
6.2 EQS 可视化
EQS 的结果可以在编辑器里直接看。打开 Window -> Environment Query System -> EQS Testing Pawn,拖一个进场景,选择要测试的 Query,运行 PIE 就能看到候选点评分。高分点用绿色,低分点用红色,淘汰点不显示。
我调试掩体 Query 时常见的问题:
- Trace 碰撞通道没选对,所有点都判为不可见。
- Generator 中心或半径设错,Query 范围根本没覆盖到掩体。
- 权重设得太极端,某个 Test 把其他 Test 完全压没。
6.3 日志与断点
AIController 和 Task 里加日志是基本功。注意区分 Authority,AI 逻辑通常只在服务端跑,客户端没有行为树。UE_LOG(LogTemp, Log, TEXT("Guard sees player %s"), *Target->GetName()); 这种日志最好加上时间戳和 AI 名字,方便多人 AI 时区分。
断点可以打在 Task 的 ExecuteTask、Service 的 TickNode、AIController 的感知回调里。行为树节点切换频繁,全断下来会很卡,建议先通过日志缩小范围,再在关键节点下断。
七、性能优化
AI 性能问题通常不是 Behavior Tree 本身慢,而是感知、EQS、移动、动画挤在一起。UE5.8 项目里我见过的几个典型坑:
7.1 控制感知频率
AIPerception 默认更新间隔是 0.0,意思是一有刺激就更新。几十名 AI 同时感知玩家时,这个开销会上去。可以在 Project Settings -> AI System 里调 PerceptionSystemDominantSenseUpdateInterval,或者给单个 Sense 配置 Implementation->UpdateInterval。
Service 的 Interval 和 Random Deviation 也要用。Interval 0.5 秒,Random Deviation 0.1 秒,能让多个 AI 的扫描分散开,避免同帧卡顿。
7.2 限制 EQS 查询规模
Grid Generator 的半径和密度直接决定候选点数量。半径 、步长 在平面上就是 多个点,每个点跑三四个 Test,再乘以 AI 数量,帧时间很容易爆炸。
优化方法:
- 先粗筛再细查。先用大半径低密度的 Query 找方向,再在小范围高密度 Query 找精确位置。
- 把 Generator 中心放在预期方向,而不是 AI 脚下。
- 用
Points: Pathing Grid代替普通 Grid,减少不可达点。 - 减少 Filter Test 数量,优先用 Score Test。
7.3 行为树的重入与缓存
Behavior Tree 每帧都会从根节点重新评估,但 Running 节点会保持状态。不要让根节点下面所有分支都参与复杂计算。顶层 Selector 应该只有少数几个 Decorator,尽快确定走哪条大分支。
另一个常被忽略的是 Blackboard Key 的 Observer。某个 Key 被大量 Decorator 监听时,值一变会触发很多重新评估。如果某个 Key 更新特别频繁(比如玩家位置每帧都变),考虑把它拆成「偶尔更新的目标引用」和「实时读取的位置」两部分。
7.4 LOD 与 AI Tick 间隔
远处的 AI 不需要每帧都那么聪明。UE5.8 可以用 AI Behavior Kit 里的 LOD 功能,或者自己实现距离分档:
- 到 米:全速 Tick,感知 0.1 秒。
- 到 米:感知 0.5 秒,EQS 1 秒一次。
- 米以上:停止行为树,只播放待机动画,玩家靠近再唤醒。
这个距离阈值不是死的,要看你项目 AI 密度和摄像机视角。第三人称过肩视角下, 米外的 AI 玩家根本看不清,省下来的性能很可观。
八、与 GAS 的联动
之前写 GAS 那篇文章时讲过,技能、状态、冷却、消耗都通过 Gameplay Ability、Gameplay Effect、Gameplay Tag 管理。Behavior Tree 和 GAS 结合,可以让 AI 的战术决策直接使用角色状态。
8.1 用 Gameplay Tag 驱动行为树分支
Blackboard 里加一个 Enum 或 Name Key 表示 AI 当前能否放技能,或者直接用 Tag 查询。行为树的 Decorator 可以调用 AIController 上的函数,检查 ASC 里有没有某个 Tag。
比如守卫有一个「冲锋」技能,冷却 8 秒。行为树的追击分支可以这样设计:
- Sequence 1:如果 TargetActor 有效、冲锋技能可用、距离在 到 之间,执行冲锋 Ability。
- Sequence 2:正常 MoveTo 攻击距离,平砍。
冲锋技能的可用性就交给 GAS 判断。行为树不需要知道冷却时间,只需要问 ASC:能不能激活 Ability.Charge?
8.2 AI 施放 Ability 的 Task
写一个自定义 Task UBTTask_ActivateAbility,在 ExecuteTask 里调用 ASC 的 TryActivateAbilityByClass 或 TryActivateAbilityByTag。
EBTNodeResult::Type UBTTask_ActivateAbility::ExecuteTask(
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
AAIC_Guard* AIC = Cast<AAIC_Guard>(OwnerComp.GetAIOwner());
if (!AIC || !AIC->AbilitySystemComponent) return EBTNodeResult::Failed;
FGameplayTagContainer Tags;
Tags.AddTag(AbilityTag);
bool bActivated = AIC->AbilitySystemComponent->TryActivateAbilitiesByTag(Tags);
return bActivated ? EBTNodeResult::Succeeded : EBTNodeResult::Failed;
}这个 Task 返回 Success 只表示 Ability 被成功触发,不表示 Ability 已经执行完。如果 Ability 有吟唱时间,可以在 GAS 里给 Ability 配置 Activation Owned Tags,行为树用 Decorator 监听这个 Tag 是否存在,等 Tag 移除再继续。
8.3 状态同步
AI 被眩晕、被定身、被沉默时,行为树应该做出相应反应。GAS 给 AI 加上 State.Debuff.Stun 后,行为树可以有一个高优先级 Selector 分支处理眩晕:停止移动、播放眩晕动画、等待 Tag 移除。
这个分支要放在所有战斗分支之上,并且 Decorator 用 Blackboard 或 Tag 检查。不要直接在行为树里复制 GAS 的状态逻辑,而是通过 Tag 解耦。这样眩晕可能来自技能、陷阱、剧情演出,行为树都不需要改。
8.4 用 Attribute 影响战术选择
EQS 的 Test 默认只能算距离、角度、视线,但你可以写自定义 Test 读取 GAS Attribute。比如 AI 血量低于 时,找掩体的权重提高;法力值不足时,不再选择需要施法的攻击位置。
自定义 EQS Test 继承 UEnvQueryTest,重写 RunTest。在里面拿到 Querier,再拿到它的 ASC 和 Attribute Set,就能读任何 Attribute。这种 Test 的计算量比普通 Test 大,建议只在关键 AI 上用,并且不要每帧都查。
结尾:守卫终于像个活人了
老陈把守卫改成 Behavior Tree + Blackboard + EQS 之后,表现完全变了。巡逻时会左右看,听到声音会停顿,看到玩家会大喊并追过去,追半路玩家绕进掩体,守卫会停下来找角度,而不是头铁直线冲。玩家被追急了爬上矮墙,守卫也会绕到最近的台阶上去,而不是对着墙发呆。
Behavior Tree 不是让 AI 变聪明的魔法,它只是一套把「感知、决策、执行」解耦的结构。Blackboard 是记忆,EQS 是空间推理,AIController 是大脑和身体的接口。把这几块搭好,剩下的就是调参数、写 Task、试手感。
这篇文章围绕守卫的巡逻、发现、追击、返回展开,聊了 Behavior Tree 的节点、Blackboard 设计、AIController 绑定、EQS 查询、掩体选择、调试、性能优化,以及与 GAS 的联动。真正落到项目里,你还会遇到群体 AI 协作、动态难度、叙事脚本控制等更复杂的情况。先把单个 AI 做扎实,后面的多人战术、编队、包抄才有基础。
希望老陈那个原地打转的守卫,能让你少熬一个调试的晚上。