UE5.8 Behavior Tree、Blackboard 与 EQS:从一名站岗守卫的差劲表现说起

📑 目录

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:MoveToWaitRotate to face BB entryPlay AnimationRun 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 ConditionIs At LocationCone CheckTime LimitCooldown

给追击分支加一个 Blackboard Based Condition,Key 选 TargetActor,Observer aborts 选 Both,含义是:

  • 进入追击分支前,如果 TargetActor 为空,条件失败,跳过追击。
  • 追击过程中,如果 TargetActor 变为空(比如玩家隐身或死亡),条件失败,行为树会强制中断当前分支,重新从 Selector 顶部评估。

Observer aborts 有三个选项:NoneSelfLower 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 看到玩家时,把 TargetActorTargetLocation 都更新。
  • 玩家跑出视野后,Service 不再更新 TargetActor,但不清空 TargetLocation
  • Decorator 检测到 TargetActor 为空,退出追击分支。
  • 搜索分支读取 TargetLocation,移动到该位置,环顾四周。
  • 搜索结束后,清空 TargetLocation,返回巡逻。

如果追击分支一失败就把 TargetLocation 清空,搜索分支就找不到地方去了。这种时序问题在调试行为树时很常见。

2.3 黑板与任务之间的数据流

Task 通常通过 OwnerComp.GetBlackboardComponent() 获取黑板,然后用 SetValueAsObjectGetValueAsVector 等函数读写。蓝图 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,并提供 MoveToActorMoveToLocation 等移动接口。

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 感知常见的有两种做法:PawnSensingComponentAIPerceptionComponent。PawnSensing 轻量,有视线、听觉,适合简单 AI。AIPerception 功能更全,支持团队、遗忘、刺激源,也更容易扩展。

AIPerception 的配置步骤:

  • 在 AIController 里创建 UAIPerceptionComponent
  • 配置 SenseConfig,比如 AISenseConfig_SightAISenseConfig_Hearing
  • 绑定 OnPerceptionUpdatedOnTargetPerceptionUpdated 委托。
  • 在回调里把感知结果写进 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: GridPoints: Pathing GridActors of Class
  • Test:对每个候选打分或过滤。比如 Distance 测试距离,Dot 测试朝向,Trace 测试视线,Project 投影到导航网格。

一个找掩体的 Query 可能这样配:

  • Generator:Points: Grid,以玩家为中心,半径 10001000,步长 150150
  • Test 1:Distance 到玩家的距离,打分模式 Prefer Closer,权重 1.01.0
  • Test 2:Trace 从候选点看玩家,要求可见,作为 Filter(不可见直接淘汰)。
  • Test 3:Trace 从玩家看候选点,要求被遮挡(也就是候选点 behind cover),作为 Filter。
  • Test 4:PathLength 到候选点的导航路径长度,Prefer Shorter,权重 0.50.5

4.2 评分机制

每个 Test 会给候选点一个标准化分数,通常在 0011 之间。最终分数是所有 Test 分数按权重加权求和。设某个候选点在第 ii 个 Test 上的得分为 sis_i,权重为 wiw_i,则:

Stotal=iwisiiwiS_{total} = \frac{\sum_i w_i \cdot s_i}{\sum_i |w_i|}

这个公式让结果保持在 [0,1][0, 1] 区间。如果某个 Test 是 Filter 模式,分数只有 0 或 1,0 直接淘汰,1 不影响后续加权。设计师可以通过调整权重来改变 AI 的倾向:更看重距离的 Query 会让 AI 贴脸,更看重掩体的 Query 会让 AI 猥琐。

4.3 在行为树里调用 EQS

行为树里调用 EQS 有几种方式:

  • Run EQS Query Task:直接运行一个 Query,把最佳结果写进 Blackboard。
  • EQS Query Decorator:用 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 攻击位置查询

攻击位置和掩体位置不一定相同。攻击位置更关心:

  • 到目标的距离在 [dmin,dmax][d_{min}, d_{max}] 之间,比如手枪 50050015001500
  • 对目标有清晰视线。
  • 周围有其他掩体作为撤退路线。
  • 不要离队友太远(多人 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 的半径和密度直接决定候选点数量。半径 20002000、步长 100100 在平面上就是 400400 多个点,每个点跑三四个 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 功能,或者自己实现距离分档:

  • 001010 米:全速 Tick,感知 0.1 秒。
  • 10103030 米:感知 0.5 秒,EQS 1 秒一次。
  • 3030 米以上:停止行为树,只播放待机动画,玩家靠近再唤醒。

这个距离阈值不是死的,要看你项目 AI 密度和摄像机视角。第三人称过肩视角下,3030 米外的 AI 玩家根本看不清,省下来的性能很可观。

八、与 GAS 的联动

之前写 GAS 那篇文章时讲过,技能、状态、冷却、消耗都通过 Gameplay Ability、Gameplay Effect、Gameplay Tag 管理。Behavior Tree 和 GAS 结合,可以让 AI 的战术决策直接使用角色状态。

8.1 用 Gameplay Tag 驱动行为树分支

Blackboard 里加一个 EnumName Key 表示 AI 当前能否放技能,或者直接用 Tag 查询。行为树的 Decorator 可以调用 AIController 上的函数,检查 ASC 里有没有某个 Tag。

比如守卫有一个「冲锋」技能,冷却 8 秒。行为树的追击分支可以这样设计:

  • Sequence 1:如果 TargetActor 有效、冲锋技能可用、距离在 80080015001500 之间,执行冲锋 Ability。
  • Sequence 2:正常 MoveTo 攻击距离,平砍。

冲锋技能的可用性就交给 GAS 判断。行为树不需要知道冷却时间,只需要问 ASC:能不能激活 Ability.Charge

8.2 AI 施放 Ability 的 Task

写一个自定义 Task UBTTask_ActivateAbility,在 ExecuteTask 里调用 ASC 的 TryActivateAbilityByClassTryActivateAbilityByTag

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 血量低于 30%30\% 时,找掩体的权重提高;法力值不足时,不再选择需要施法的攻击位置。

自定义 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 做扎实,后面的多人战术、编队、包抄才有基础。

希望老陈那个原地打转的守卫,能让你少熬一个调试的晚上。