news 2026/9/24 22:42:14

行为树运行

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
行为树运行

从头开始聊一下AI行为树的运行。

一.bool AAIController::RunBehaviorTree

bool AAIController::RunBehaviorTree(UBehaviorTree* BTAsset) { // @todo: find BrainComponent and see if it's BehaviorTreeComponent // Also check if BTAsset requires BlackBoardComponent, and if so // check if BB type is accepted by BTAsset. // Spawn BehaviorTreeComponent if none present. // Spawn BlackBoardComponent if none present, but fail if one is present but is not of compatible class if (BTAsset == NULL) { UE_VLOG(this, LogBehaviorTree, Warning, TEXT("RunBehaviorTree: Unable to run NULL behavior tree")); return false; } bool bSuccess = true; // see if need a blackboard component at all UBlackboardComponent* BlackboardComp = Blackboard; if (BTAsset->BlackboardAsset && (Blackboard == nullptr || Blackboard->IsCompatibleWith(BTAsset->BlackboardAsset) == false)) { bSuccess = UseBlackboard(BTAsset->BlackboardAsset, BlackboardComp); } if (bSuccess) { UBehaviorTreeComponent* BTComp = Cast<UBehaviorTreeComponent>(BrainComponent); if (BTComp == NULL) { UE_VLOG(this, LogBehaviorTree, Log, TEXT("RunBehaviorTree: spawning BehaviorTreeComponent..")); BTComp = NewObject<UBehaviorTreeComponent>(this, TEXT("BTComponent")); BTComp->RegisterComponent(); REDIRECT_OBJECT_TO_VLOG(BTComp, this); } // make sure BrainComponent points at the newly created BT component BrainComponent = BTComp; check(BTComp != NULL); BTComp->StartTree(*BTAsset, EBTExecutionMode::Looped); } return bSuccess; }

这个方法会传进来一个行为树资源BTAsset。挑重点说一下。

1.初始化UBlackboardComponent黑板组件

用BTAsset中的UBlackboardData与AIController中的UBlackboardData做校验,然后进行黑板组件的初始化操作。

Blackboard->IsCompatibleWith(BTAsset->BlackboardAsset)用来判断当前AIController中持有的UBlackboardData与传入的新的UBlackboardData是否兼容。

这里的BlackboardData(黑板数据),指的是一个模板结构,里面存了父黑板数据结构,键数组,和自己的黑板数据结构,键数组,至于所有的值是保存在BlackboardComponent中的。这是因为对于BlackboardData来说,它是一个模版,里面的数据是共享的,所有能拿到这个BlackboardData的对象都可以去使用这里面的数据,也就是黑板的键,也就是黑板中有哪些变量,至于具体的值,每个AIController上的BlackBoardComponent都会存储各自的。运行时,UBlackboardComponent使用这个模板来初始化自己存储的 Key 和类型表,分配运行时 Key 存储空间,每个 AI 的黑板组件再单独持有 Key 实际值。

判断兼容的条件是:

(1)当前黑板数据(Blackboard)及其继承链(父黑板数据)有和BTAsset->BlackboardAsset(BTAsset->BlackboardAsset类型是UBlackboardData*)一样的(也就是地址一样,指向了同一块内存空间),说明:新的黑板数据就是当前的黑板数据或者是当前黑板数据的父类,因为父类中的东西子类都有,所以是兼容的。

(2)新的黑板数据中的keys数组地址与当前的黑板数据中的keys数组地址一致,也可以判断是兼容的。

注:如果不是兼容的或者当前BlackboardComponent为nullptr,则调用UseBlackboard()方法初始化BlackboardComponent和BlackboardComponent上的BlackboardData。

首先FindComponentByClass通过类去查找身上有没有BlackboardComponent组件,如果有就接着用,如果没有就像BehaviorTreeComponent一样New一个出来,然后注册一下组件。

初始化BlackboardComponent,然后是初始化BlackboardData,主要是为BlackboardComponent中的ValueMemory和ValueOffset开辟内存空间,并进行赋值。

取值时,必须通过 KeyType 判断,它定义了每个键的类型,因此也决定了你从 ValueMemory 中拿的是值,还是指针。

2.初始化UBehaviorTreeComponent行为树组件

当初始化BlackboardComponent后,再接着初始化BehaviorTreeComponent。

AIController中保存着一个BrainComponent,它是BehaviorTreeComponent的父类。

当转化的BehaviorTreeComponent是NULL,则创建一个新的BehaviorTreeComponent,注册一下这个新的BehaviorTreeComponent。

3.最后通过BehaviorTreeComponent开启这个行为树

一个参数是新的行为树资源

一个参数是执行模式EBTExecutionMode,EBTExecutionMode中有两个枚举,SingleRun(执行一次),Looped(循环执行)

二.UBehaviorTreeComponent::StartTree

void UBehaviorTreeComponent::StartTree(UBehaviorTree& Asset, EBTExecutionMode::Type ExecuteMode /*= EBTExecutionMode::Looped*/) { // clear instance stack, start should always run new tree from root UBehaviorTree* CurrentRoot = GetRootTree(); if (CurrentRoot == &Asset && TreeHasBeenStarted()) { UE_VLOG(GetOwner(), LogBehaviorTree, Log, TEXT("Skipping behavior start request - it's already running")); return; } else if (CurrentRoot) { UE_VLOG(GetOwner(), LogBehaviorTree, Log, TEXT("Abandoning behavior %s to start new one (%s)"), *GetNameSafe(CurrentRoot), *Asset.GetName()); } StopTree(EBTStopMode::Safe); TreeStartInfo.Asset = &Asset; TreeStartInfo.ExecuteMode = ExecuteMode; TreeStartInfo.bPendingInitialize = true; ProcessPendingInitialize(); }

这里补充一些知识点。

1.InstanceStack和KnownInstances

UBehaviorTreeComponent中有TArray<FBehaviorTreeInstance> InstanceStack和TArray<FBehaviorTreeInstanceId> KnownInstances。

首次StartTree时,ProcessPendingInitialize()方法会清空并释放InstanceStack和KnownInstances。

然后将这颗新行为树实例放入InstanceStack中,运行树的过程中如果要运行子树,则将子树压入栈中,子树就在栈顶,子树执行完后,将子树pop出,主树又重回栈顶。

这样每次栈顶都是正在激活运行的树实例。

而KnownInstances的作用有些实例池的意思,用于缓存所有曾经加载的行为树实例,用于避免重复创建相同结构的行为树实例,节省内存与初始化开销。如果要加载一个行为树(或子树),先检查KnownInstances,如果已有可用实例(相同行为树 Asset),则直接重用。如果KnownInstances中没有,再new一个新的。

2.EBTStopMode

EBTStopMode是停止模式,其中有两个枚举

(1)安全停止(Safe):会调用 OnTaskFinished(对于 Task)或 OnCeaseRelevant(对于 Service和Decorator)。如果当前任务正在运行,它会收到 EBTNodeResult::Aborted,并有机会响应。

(2)强制停止(Forced):用于紧急情况,例如销毁 Actor、强制重启行为树等。不保证节点会收到任何结束通知。直接清理数据并中断任务。

三.UBehaviorTreeComponent::ProcessPendingInitialize

void UBehaviorTreeComponent::ProcessPendingInitialize() { if ((SuspendedBranchActions & EBTBranchAction::ProcessPendingInitialize) != EBTBranchAction::None) { UE_VLOG(GetOwner(), LogBehaviorTree, Verbose, TEXT("ProcessPendingInitialize(%s) queued up"), *GetNameSafe(TreeStartInfo.Asset)); PendingBranchActionRequests.Emplace(nullptr, EBTBranchAction::ProcessPendingInitialize); return; } StopTree(EBTStopMode::Safe); if (bWaitingForLatentAborts) { return; } // finish cleanup RemoveAllInstances(); bLoopExecution = (TreeStartInfo.ExecuteMode == EBTExecutionMode::Looped); bIsRunning = true; #if USE_BEHAVIORTREE_DEBUGGER DebuggerSteps.Reset(); #endif UBehaviorTreeManager* BTManager = UBehaviorTreeManager::GetCurrent(GetWorld()); if (BTManager) { BTManager->AddActiveComponent(*this); } // push new instance const bool bPushed = PushInstance(*TreeStartInfo.Asset); TreeStartInfo.bPendingInitialize = false; }

1.BranchAction

(1)介绍BranchAction

首先这种BranchAction的判断在行为树逻辑中常会出现

if ((SuspendedBranchActions & EBTBranchAction::ProcessPendingInitialize) != EBTBranchAction::None)

首先看一下BranchAction这个枚举

enum class EBTBranchAction : uint16 { None = 0x0, DecoratorEvaluate = 0x1, DecoratorActivate_IfNotExecuting = 0x2, DecoratorActivate_EvenIfExecuting = 0x4, DecoratorActivate = DecoratorActivate_IfNotExecuting | DecoratorActivate_EvenIfExecuting, DecoratorDeactivate = 0x8, UnregisterAuxNodes = 0x10, StopTree_Safe = 0x20, StopTree_Forced = 0x40, ActiveNodeEvaluate = 0x80, SubTreeEvaluate = 0x100, ProcessPendingInitialize = 0x200, Cleanup = 0x400, UninitializeComponent = 0x800, StopTree = StopTree_Safe | StopTree_Forced, Changing_Topology_Actions = UnregisterAuxNodes | StopTree | ProcessPendingInitialize | Cleanup | UninitializeComponent, All = DecoratorEvaluate | DecoratorActivate_IfNotExecuting | DecoratorActivate_EvenIfExecuting | DecoratorDeactivate | Changing_Topology_Actions | ActiveNodeEvaluate | SubTreeEvaluate, };

这是一个位掩码结构,而在UBehaviorTreeComponent中存有一个SuspendedBranchActions变量,代表当前存在的BranchAction,用SuspendedBranchActions与EBTBranchAction::ProcessPendingInitialize做&运算,如果结果不是0,那么就代表SuspendedBranchActions中存在EBTBranchAction::ProcessPendingInitialize这一BranchAction。

在这里,如果当前挂起了对初始化的请求(行为树正在中途被打断等),那么不要立刻初始化,而是把它加入 PendingBranchActionRequests 队列,等稍后再处理。

比如正在 SubTreeEvaluate,而这时又请求初始化新树,就必须挂起这个初始化。

(2).SuspendedBranchActions的注册与PendingBranchActionRequests的处理

注:全局搜索了下BranchAction的引用。首先:

void UBehaviorTreeComponent::SuspendBranchActions(EBTBranchAction BranchActions) { UE_VLOG(GetOwner(), LogBehaviorTree, VeryVerbose, TEXT("Suspending branch actions.")); checkf(SuspendedBranchActions == EBTBranchAction::None, TEXT("This logic does not support re-entrance")); SuspendedBranchActions = BranchActions; }

上面这个方法是给SuspendedBranchActions变量赋值的。

void UBehaviorTreeComponent::ResumeBranchActions() { UE_VLOG(GetOwner(), LogBehaviorTree, VeryVerbose, TEXT("Resuming branch actions.")); checkf(SuspendedBranchActions != EBTBranchAction::None, TEXT("Expecting SuspendBranchActions() be called before calling resume")); SuspendedBranchActions = EBTBranchAction::None; // Flushing any pending branch actions while (PendingBranchActionRequests.Num() > 0) { TArray<FBranchActionInfo> PendingBranchActionRequestsToProcess(MoveTemp(PendingBranchActionRequests)); PendingBranchActionRequests.Reset(); for (const FBranchActionInfo& Info : PendingBranchActionRequestsToProcess) { switch (Info.Action) { case EBTBranchAction::DecoratorEvaluate: { const UBTDecorator* RequestedBy = CastChecked<UBTDecorator>(Info.Node); // Since we have been queued up, decorator might have been removed from active nodes, need to make sure it is still there. if (!IsAuxNodeActive(RequestedBy)) { UE_VLOG(GetOwner(), LogBehaviorTree, Verbose, TEXT("Request deactivation skipped because decorator(%s) is not active anymore"), *UBehaviorTreeTypes::DescribeNodeHelper(RequestedBy)); break; } EvaluateBranch(*RequestedBy); break; } case EBTBranchAction::DecoratorActivate_IfNotExecuting: case EBTBranchAction::DecoratorActivate_EvenIfExecuting: { const UBTDecorator* RequestedBy = CastChecked<UBTDecorator>(Info.Node); // Since we have been queued up, decorator might have been removed from active nodes, need to make sure it is still there. if (!IsAuxNodeActive(RequestedBy)) { UE_VLOG(GetOwner(), LogBehaviorTree, Verbose, TEXT("Request deactivation skipped because decorator(%s) is not active anymore"), *UBehaviorTreeTypes::DescribeNodeHelper(RequestedBy)); break; } ActivateBranch(*RequestedBy, Info.Action == EBTBranchAction::DecoratorActivate_EvenIfExecuting /*bForceRequestEvenIfExecuting*/); break; } case EBTBranchAction::DecoratorDeactivate: { const UBTDecorator* RequestedBy = CastChecked<UBTDecorator>(Info.Node); // Since we have been queued up, decorator might have been removed from active nodes, need to make sure it is still there. if (!IsAuxNodeActive(RequestedBy)) { UE_VLOG(GetOwner(), LogBehaviorTree, Verbose, TEXT("Request deactivation skipped because decorator(%s) is not active anymore"), *UBehaviorTreeTypes::DescribeNodeHelper(RequestedBy)); break; } DeactivateBranch(*RequestedBy); break; } case EBTBranchAction::UnregisterAuxNodes: { const UBTCompositeNode* BranchRoot = CastChecked<UBTCompositeNode>(Info.Node); UnregisterAuxNodesInBranch(BranchRoot, true/*bApplyImmediately*/); break; } case EBTBranchAction::StopTree_Safe: case EBTBranchAction::StopTree_Forced: { StopTree(Info.Action == EBTBranchAction::StopTree_Forced ? EBTStopMode::Forced : EBTStopMode::Safe); break; } case EBTBranchAction::ActiveNodeEvaluate: { const UBTNode* ActiveNode = GetActiveNode(); if (ActiveNode != Info.Node) { UE_VLOG(GetOwner(), LogBehaviorTree, Verbose, TEXT("Request evaluation skipped because node(%s) is not active anymore"), *UBehaviorTreeTypes::DescribeNodeHelper(ActiveNode), *UEnum::GetValueAsString(Info.ContinueWithResult)); break; } EvaluateBranch(Info.ContinueWithResult); break; } case EBTBranchAction::SubTreeEvaluate: { const UBTCompositeNode* BranchRoot = CastChecked<UBTCompositeNode>(Info.Node); const UBTNode* RootNode = InstanceStack.Num() ? InstanceStack[ActiveInstanceIdx].RootNode : nullptr; if (RootNode != BranchRoot) { UE_VLOG(GetOwner(), LogBehaviorTree, Verbose, TEXT("Sub tree evaluation skipped because node(%s) is not active anymore"), *UBehaviorTreeTypes::DescribeNodeHelper(BranchRoot)); break; } RequestExecution(BranchRoot, ActiveInstanceIdx, BranchRoot, 0, EBTNodeResult::InProgress); break; } case EBTBranchAction::ProcessPendingInitialize: { ProcessPendingInitialize(); break; } case EBTBranchAction::Cleanup: { Cleanup(); break; } case EBTBranchAction::UninitializeComponent: { // We do not call UninitializeComponent here because only part of the method was queued up and we cannot delay it nor call it twice. // All other actions in the method were performed synchronously. RemoveAllInstances(); break; } } } } }

上面这个方法是将SuspendedBranchActions置空,并且对挂起的BranchAction(PendingBranchActionRequests 队列中存储的BranchAction)做处理,完成在挂起前没做的逻辑。

(3).SuspendedBranchActions注册与PendingBranchActionRequests处理的地方

这两个方法一般是成对出现的。

**活跃辅助节点的Tick前后
struct FBTSuspendBranchActionsScoped { FBTSuspendBranchActionsScoped(UBehaviorTreeComponent& InBTComp, EBTBranchAction BranchActions = EBTBranchAction::All) : BTComp(InBTComp) { BTComp.SuspendBranchActions(BranchActions); } ~FBTSuspendBranchActionsScoped() { BTComp.ResumeBranchActions(); } UBehaviorTreeComponent& BTComp; };

这是 Unreal Engine 行为树 (UBehaviorTreeComponent) 内部使用的:RAII(Resource Acquisition Is Initialization)封装结构

用于在作用域内自动:

  • 挂起(Suspend)行为树分支操作

  • 作用域结束时自动恢复(Resume)行为树分支操作

而使用这个封装结构的地方有几处,我们重点说一下在BehaviorTreeComponent中的Tick方法中:

{ FBTSuspendBranchActionsScoped ScopedSuspend(*this, EBTBranchAction::Changing_Topology_Actions); for (int32 InstanceIndex = 0; InstanceIndex < InstanceStack.Num(); InstanceIndex++) { FBehaviorTreeInstance& InstanceInfo = InstanceStack[InstanceIndex]; InstanceInfo.ExecuteOnEachAuxNode([&InstanceInfo, this, &bDoneSomething, DeltaTime, &NextNeededDeltaTime](const UBTAuxiliaryNode& AuxNode) { uint8* NodeMemory = AuxNode.GetNodeMemory<uint8>(InstanceInfo); SCOPE_CYCLE_UOBJECT(AuxNode, &AuxNode); bDoneSomething |= AuxNode.WrappedTickNode(*this, NodeMemory, DeltaTime, NextNeededDeltaTime); }); } }

在这个作用域中,主要是遍历当前主树下所有的激活的辅助节点的Tick方法,这也说明行为树上辅助节点的Tick方法都是在BehaviorTree中的TickComponent中统一调用的。

注:这里遍历了InstanceStack,是因为InstanceStack中压入的行为树,都是在StartTree后用到的行为树,前面说过,首次StartTree时,ProcessPendingInitialize()方法会清空并释放InstanceStack和KnownInstances。所以InstanceStack中的结构有点像,主树,子树,子子树这种。

在活跃的辅助节点跑tick逻辑时,不能有其他BranchAction进行,所以在作用域开始,SuspendedBranchActions中被注册了BranchAction,作用域结束再去处理PendingBranchActionRequests。

**ProcessPendingExecution中
void UBehaviorTreeComponent::ProcessPendingExecution() { // can't continue if current task is still aborting if (bWaitingForLatentAborts || !PendingExecution.IsSet()) { return; } FBTPendingExecutionInfo SavedInfo = PendingExecution; PendingExecution = FBTPendingExecutionInfo(); // collect all aux nodes that have lower priority than new task // occurs when normal execution is forced to revisit lower priority nodes (e.g. loop decorator) const FBTNodeIndex NextTaskIdx = SavedInfo.NextTask ? FBTNodeIndex(ActiveInstanceIdx, SavedInfo.NextTask->GetExecutionIndex()) : FBTNodeIndex(0, 0); UnregisterAuxNodesUpTo(NextTaskIdx); // Suspending any all branch actions as it is impossible for decorators to have the right answer if they are in an executing branch or not. SuspendBranchActions(EBTBranchAction::All); // change aux nodes ApplySearchData(SavedInfo.NextTask); // make sure that we don't have any additional instances on stack if (InstanceStack.Num() > (ActiveInstanceIdx + 1)) { for (int32 InstanceIndex = ActiveInstanceIdx + 1; InstanceIndex < InstanceStack.Num(); InstanceIndex++) { InstanceStack[InstanceIndex].Cleanup(*this, EBTMemoryClear::StoreSubtree); } InstanceStack.SetNum(ActiveInstanceIdx + 1); } // execute next task / notify out of nodes // validate active instance as well, execution can be delayed AND can have AbortCurrentTask call before using instance index if (SavedInfo.NextTask && InstanceStack.IsValidIndex(ActiveInstanceIdx)) { // ResumeBranchActions() is done inside ExecuteTask after the active task is set but before we execute the task. ExecuteTask(SavedInfo.NextTask); } else { ResumeBranchActions(); OnTreeFinished(); } }

在ProcessPendingExecution()中调用ApplySearchData(SavedInfo.NextTask)前会执行下

SuspendBranchActions(EBTBranchAction::All);将EBTBranchAction::All注册进SuspendedBranchActions。

最后,如果不满足这个条件:

SavedInfo.NextTask && InstanceStack.IsValidIndex(ActiveInstanceIdx)

则ResumeBranchActions(),并OnTreeFinished()。

如果满足,则调用ExecuteTask()执行这个NextTask,在ExecuteTask()的最后做ResumeBranchActions()操作。

2.RemoveAllInstances()

执行RemoveAllInstances(),清除 InstanceStack和KnowInstances、释放内存、节点状态等。行为树在开始前必须有一张干净的“执行栈”。

3.将该行为树组件添加为活跃组件

UBehaviorTreeManager* BTManager = UBehaviorTreeManager::GetCurrent(GetWorld()); if (BTManager) { //将当前 BehaviorTreeComponent 注册为活跃组件。行为树管理器用于全局调度行为树,比如定期 Tick 等。 BTManager->AddActiveComponent(*this); }

4.PushInstance

const bool bPushed = PushInstance(*TreeStartInfo.Asset);
push new instance,将行为树主资源(UBehaviorTree)压入执行栈中,创建第一个 FBehaviorTreeInstance。执行 PushInstance() 会构造根节点、分配内存、启动根服务等。
PushInstance() 是真正初始化根节点、启动服务、执行 Task 的地方。
这个函数和 StartTree() 一起构成了行为树从创建到运行的核心启动流程。

5.总结

这几个阶段主要做了:

1.AAIController::RunBehaviorTreeAAIController::RunBehaviorTree():

初始化BlackBoardComponent黑板组件

初始化BehaviorTreeComponent行为树组件

注册这个BehaviorTreeComponent

执行BehaviorTreeComponent中的StartTree()

2.UBehaviorTreeComponent::StartTree():

当前激活的树引用的就是Asset这个传入的行为树资源,并且正在运行着,则return。

StopTree

执行ProcessPendingInitialize()

3.UBehaviorTreeComponent::ProcessPendingInitialize()

校验EBTBranchAction::ProcessPendingInitialize

清除 InstanceStack和KnownInstances、释放内存、节点状态等。

通过UBehaviorTreeManager将当前 BehaviorTreeComponent 注册为活跃组件。

执行PushInstance()

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 22:41:42

BackstopJS 终极指南:如何实现跨浏览器视觉回归测试

BackstopJS 终极指南&#xff1a;如何实现跨浏览器视觉回归测试 【免费下载链接】BackstopJS Catch CSS curve balls. 项目地址: https://gitcode.com/gh_mirrors/ba/BackstopJS BackstopJS 是一款强大的视觉回归测试工具&#xff0c;专为 Web 应用设计&#xff0c;能够…

作者头像 李华
网站建设 2026/9/24 6:33:14

终极指南:如何使用Elasticsearch-SQL实现智能加权平均聚合计算

终极指南&#xff1a;如何使用Elasticsearch-SQL实现智能加权平均聚合计算 【免费下载链接】elasticsearch-sql Use SQL to query Elasticsearch 项目地址: https://gitcode.com/gh_mirrors/el/elasticsearch-sql 在数据分析领域&#xff0c;简单平均值往往无法反映数据…

作者头像 李华
网站建设 2026/9/11 16:14:08

7个关键请求分发策略:PSR-7 HTTP消息在负载均衡中的完整指南

7个关键请求分发策略&#xff1a;PSR-7 HTTP消息在负载均衡中的完整指南 【免费下载链接】http-message The purpose of this PSR is to provide a set of common interfaces for HTTP messages as described in RFC 7230 and RFC 7231 项目地址: https://gitcode.com/gh_mir…

作者头像 李华
网站建设 2026/9/11 14:46:12

Docker4Drupal常见问题排查:从容器启动失败到数据持久化全解决

Docker4Drupal常见问题排查&#xff1a;从容器启动失败到数据持久化全解决 【免费下载链接】docker4drupal Docker-based Drupal stack 项目地址: https://gitcode.com/gh_mirrors/do/docker4drupal Docker4Drupal作为基于Docker的Drupal开发环境&#xff0c;极大简化了…

作者头像 李华