总览
很多老项目已经有 Behavior Tree。看到 StateTree 后,第一反应通常是:要不要把旧 BT 全删了?先别急。更现实的做法是把 StateTree 当成“可复用的一段状态流程”,让 Behavior Tree 继续做外层调度,StateTree 接管某个分支里的细节。
一句话版本:BT 适合做“现在应该进哪个大行为”,StateTree 适合做“进入这个大行为以后,里面要按哪些状态一步一步运行”。
先看一个迁移场景
你的旧守卫 AI 可能长这样:
| Behavior Tree 分支 | 原来的做法 | 适合交给 StateTree 的部分 |
|---|---|---|
| Patrol | Sequence: MoveTo -> Wait -> FindNextPoint | Idle/Patrol 子状态循环 |
| Investigate | MoveTo LastSeenLocation -> Wait -> Scan | Search 状态流程 |
| Combat | RotateToTarget -> UseAbility -> Strafe -> Reposition | Combat 子状态机 |
| Flee | FindCover -> MoveTo -> Wait | 逃跑流程 |
不要一次迁移整棵树。先挑一个 BT 分支,比如 Combat。BT 判断“现在进入战斗”,StateTree 负责“战斗里瞄准、开火、换弹、找掩体、重新压上”。
混用时的运行边界
最重要的一条:同一时间只让一个系统控制移动。BT 的 MoveTo 和 StateTree 的 Move To Task 不要同时跑。否则你会看到 AI 一会儿被 BT 拉向巡逻点,一会儿被 StateTree 拉向玩家,调试器里每个系统都觉得自己没错。
推荐边界:
- Behavior Tree 负责黑板、感知大入口、团队已有 Decorator/Service。
- StateTree 负责一个局部行为的状态推进。
- Blackboard 只传少量入口数据,例如 TargetActor、LastSeenLocation、CombatStyle。
- StateTree 内部用 Parameters、Evaluator 和 Binding 组织细节数据。
- StateTree 完成后,返回 BT,让 BT 决定下一段大行为。
源码依据
UBTTask_RunStateTree 和 UBTTask_RunDynamicStateTree 位于 GameplayStateTree 插件。前者适合在 BT 节点里固定引用一个 StateTree,后者适合运行时从外部选择 StateTree。它们内部依赖 FStateTreeReference 和实例数据,让 Behavior Tree Task 可以启动并等待 StateTree 的运行结果。StateTree 资产侧仍然使用 StateTreeAIComponentSchema 这类 Schema 来保证 AIController/Pawn 上下文。
架构分析
把混用架构想成三层:
| 层级 | 谁负责 | 数据 |
|---|---|---|
| 感知与高层决策 | AIController、Perception、Behavior Tree | Blackboard: TargetActor、ThreatLevel |
| 局部状态流程 | StateTree | Parameters、Evaluator 输出、Task InstanceData |
| 具体执行系统 | MoveTo、GAS、Montage、EQS、SmartObject | GameplayTask、Ability、Animation |
StateTree 不应该直接取代所有系统。它更像一个流程编排层:状态切换归它,移动还是交给导航,技能还是交给 GAS,动画还是交给 AnimBP/UAF,感知还是交给 AI Perception。
使用案例
案例:旧 BT 有一个 Combat 分支,现在把战斗细节拆进 ST_GuardCombat。
编辑器步骤:
- 创建
ST_GuardCombat,Schema 选择 AI Component 相关 Schema。 - 加 Parameters:
TargetActor、PreferredDistance、LowHealthThreshold。 - 添加状态:
FaceTarget、ShootBurst、Reload、Reposition、ExitCombat。 - 在旧 BT 的 Combat Sequence 中添加
Run StateTreeTask。 - Task 里选择
ST_GuardCombat。 - 把 Blackboard 的
TargetActor绑定到 StateTree 参数。 - 设置 StateTree 完成后 BT 继续执行后续节点。
一个可落地的状态表:
| State | Task | Transition |
|---|---|---|
| FaceTarget | 面向 TargetActor | 完成 -> ShootBurst |
| ShootBurst | 激活射击 Ability 或播放射击逻辑 | 弹药为空 -> Reload,目标丢失 -> ExitCombat |
| Reload | 播放换弹/等待换弹完成 | 成功 -> FaceTarget |
| Reposition | Move To 掩体点或侧移点 | 到达 -> FaceTarget,失败 -> FaceTarget |
| ExitCombat | 清理战斗临时状态 | 成功 -> StateTree Succeeded |
代码演示
如果你不想在 BT 里直接写很多分支,可以用黑板只传入口数据。伪代码如下:
void AGuardAIController::EnterCombat(AActor* Target)
{
UBlackboardComponent* BB = GetBlackboardComponent();
BB->SetValueAsObject(TEXT("TargetActor"), Target);
BB->SetValueAsBool(TEXT("ShouldRunCombatStateTree"), true);
}
StateTree 里不要再四处读 Blackboard。把 TargetActor 作为参数或上下文输入接进来,后续 Task 只依赖 StateTree 内部的显式数据流。这样以后从 BT 迁到纯 StateTree 时,迁移成本会低很多。
项目落地
旧项目迁移路线建议:
- 第一周只迁移一个局部分支,比如 Search 或 Combat。
- 保留原 BT 作为总控,避免一次性重写 AI。
- 每个 StateTree 分支都定义清楚输入、输出和完成状态。
- 不在 StateTree 内部直接改一堆 Blackboard Key。
- 跑稳定后,再考虑把多个 BT 分支合并成更大的 StateTree。
团队规范可以写成一句话:BT 决定“要不要进入这段流程”,StateTree 决定“这段流程内部怎么走”。
常见坑
不要让 BT Service 每帧改 StateTree 正在用的目标,同时 StateTree Evaluator 又自己算目标。不要把 BT Decorator 和 StateTree Condition 写成两套相反规则。不要让 BT Abort 中断 StateTree 时忘记清理 MoveTo、Montage 或 GameplayTask。不要把 StateTree 的失败一律当作 BT 失败;有些失败只是“这段流程不可用,回 BT 选别的分支”。
源码路径索引
GameplayStateTreeModule/Public/BehaviorTree/Tasks/BTTask_RunStateTree.hGameplayStateTreeModule/Public/BehaviorTree/Tasks/BTTask_RunDynamicStateTree.hGameplayStateTreeModule/Public/BehaviorTree/GameplayStateTreeBTUtils.hStateTreeModule/Public/StateTreeReference.h