从头开始聊一下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()