虚幻引擎的行为树不同于传统行为树,它新增了Decorator和Service两个功能组件,而且不再是每次都从根节点开始Tick。要搞懂它们内在执行逻辑需要从源码入手,从开始执行到搜索节点,再到各种节点的声明周期回调。
开始行为树
开始时主要做两件事情:
- 构建Blackboard组件
- 构建BehaviorTree组件
1
2
3
4
5
6
7
8bool AAIController::RunBehaviorTree(UBehaviorTree* BTAsset)
{
UseBlackboard(BTAsset->BlackboardAsset, BlackboardComp);
BTComp = NewObject<UBehaviorTreeComponent>(this, TEXT("BTComponent"));
BTComp->RegisterComponent();
BrainComponent = BTComp;
BTComp->StartTree(*BTAsset, EBTExecutionMode::Looped);StartTree
开始,调几层后,走到下面这里,BehaviorTreeManager
后面再讲,然后核心是PushInstance
。 1
2
3
4
5
6
7
8void UBehaviorTreeComponent::ProcessPendingInitialize()
{
UBehaviorTreeManager* BTManager = UBehaviorTreeManager::GetCurrent(GetWorld());
if (BTManager)
{
BTManager->AddActiveComponent(*this);
}
const bool bPushed = PushInstance(*TreeStartInfo.Asset);PushInstance
那么怪?因为这个函数不只是用来在开始行为树的时候跑的,而是在一棵树里执行另一棵子树的时候也会跑到。每棵树执行的时候都是一个Instance,而同一时间只能执行一棵树,一棵树执行完又会返回上一棵树。执行一棵新树就像往栈新增元素一样,所以就称之为PushInstance
。
检查
加载资源之前,主要有两个检查,一个是规定Blackboard必须是同一种资源。另一个是要检查父节点。
如上面所说,执行子树的时候也会跑进来,所以要检查一下执行子树的父节点是否允许执行子树。调用父节点的CanPushSubtree
。 1
2
3
4
5
6
7
8
9
10
11
12
13
14bool UBehaviorTreeComponent::PushInstance(UBehaviorTree& TreeAsset)
{
if (TreeAsset.BlackboardAsset && BlackboardComp && !BlackboardComp->IsCompatibleWith(TreeAsset.BlackboardAsset))
{
return false;
}
const UBTNode* ActiveNode = GetActiveNode();
const UBTCompositeNode* ActiveParent = ActiveNode ? ActiveNode->GetParentNode() : NULL;
const bool bIsAllowed = ActiveParent->CanPushSubtree(*this, ParentMemory, ChildIdx);
if (!bIsAllowed)
{
return false;
}CanPushSubtree
默认返回true,只有并行节点SimpleParallel
进行了重写,规定其主任务不能够执行子树。
加载资源
UBehaviorTreeManager
就是专门用来加载资源的。之所以专门搞一个全局Manager,就是为了能缓存资源的template,以便于下次加载直接读template。所谓的template就是名为FBehaviorTreeTemplateInfo
的结构。 1
2
3
4
5
6bool UBehaviorTreeComponent::PushInstance(UBehaviorTree& TreeAsset)
{
UBTCompositeNode* RootNode = NULL;
uint16 InstanceMemorySize = 0;
UBehaviorTreeManager* BTManager = UBehaviorTreeManager::GetCurrent(GetWorld());
const bool bLoaded = BTManager->LoadTree(TreeAsset, RootNode, InstanceMemorySize);RootNode
。
从下面的代码可以看出,RootNode
其实是从template里面拿出来的,说明它作为一个单例对象来使用。其实也意味着所有Node其实都是单例对象的。但作为单例,它又怎么存数据呢,这个后面会讲到。 1
2
3
4
5
6
7
8bool UBehaviorTreeManager::LoadTree(UBehaviorTree& Asset, UBTCompositeNode*& Root, uint16& InstanceMemorySize)
{
FBehaviorTreeTemplateInfo TemplateInfo;
TemplateInfo.Asset = &Asset;
TemplateInfo.Template = Cast<UBTCompositeNode>(StaticDuplicateObject(Asset.RootNode, this));
LoadedTemplates.Add(TemplateInfo);
Root = TemplateInfo.Template;
静态初始化
LoadTree
里面还有些逻辑是用来做初始化的,主要干几件事情:
- InitializeNodeHelper 构造Service、Decorator等对象
- InitializeExecutionOrder 预先算好每个节点的下一个节点是谁
- InitializeNode 存几个信息变量
代码有点多,其实不需要全部搞懂,就简单的认为是对所有节点进行预计算和初始化的。之所以叫静态初始化是它对静态资源做初始化,每一种行为树资源只要第一次加载时做一次即可。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15bool UBehaviorTreeManager::LoadTree(UBehaviorTree& Asset, UBTCompositeNode*& Root, uint16& InstanceMemorySize)
{
InitializeNodeHelper(NULL, TemplateInfo.Template, 0, ExecutionIndex, InitList, Asset, this);
for (int32 Index = 0; Index < InitList.Num() - 1; Index++)
{
InitList[Index].Node->InitializeExecutionOrder(InitList[Index + 1].Node);
}
InitList.Sort(FNodeInitializationData::FMemorySort());
for (int32 Index = 0; Index < InitList.Num(); Index++)
{
InitList[Index].Node->InitializeNode(InitList[Index].ParentNode, InitList[Index].ExecutionIndex, InitList[Index].SpecialDataSize + MemoryOffset, InitList[Index].TreeDepth);
MemoryOffset += InitList[Index].DataSize; // 每个节点的自定义内存偏移,后面讲
}
节点的自定义内存结构
前面说过,节点默认是单例的。意味着你往单例里面存储的东西会一直留着,比如说有三个角色的行为树都用了同一种节点,那么这种节点里面的类成员属性是所有角色共享的。所以虚幻引擎提供了一个自定义结构体的方式,让你能够给每个角色都存点自己的东西。
注意,如果你用的是蓝图节点,那么不需要担心单例问题,因为暴露到蓝图里的类通过bCreateNodeInstance
属性强行不使用单例。 1
2
3UBTService_BlueprintBase::UBTService_BlueprintBase(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
bCreateNodeInstance = true;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29FSTRUCT()
struct FMyTaskMemory
{
GENERATED_BODY()
UPROPERTY()
AMyCharacter* ControlledCharactor = nullptr;
}
UCLASS()
class UBTMyTask
{
GENERATAED_BODY()
virtual uint16 GetInstanceMemorySize() const override
{
return sizeof(FMyTaskMemory);
}
virtual void InitializeMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTMemoryInit::Type InitType) const override
{
FMyTaskMemory* MemberMemory = CastInstanceNodeMemory<FMyTaskMemory>(NodeMemory);
MemberMemory->ControlledCharactor = StaticCast<AMyCharacter>(OwnerComp->GetOwner()->GetControlledPawn());
}
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override
{
FMyTaskMemory* MemberMemory = CastInstanceNodeMemory<FMyTaskMemory>(NodeMemory);
}
}NodeMemory
是一个指针,它将指向一块GetInstanceMemorySize()
返回的大小的内存块。至于内存块是什么时候构建的,存到哪里,下一节说明。
实例初始化
不同Controller加载同一种行为树,除了静态部分外,肯定有动态的部分。PushInstance
会构造一个FBehaviorTreeInstance
结构。记录一些基本信息。然后调用Initialize
。 1
2
3
4
5
6
7FBehaviorTreeInstance NewInstance;
NewInstance.InstanceIdIndex = UpdateInstanceId(&TreeAsset, ActiveNode, InstanceStack.Num() - 1);
NewInstance.RootNode = RootNode;
NewInstance.ActiveNode = NULL;
NewInstance.ActiveNodeType = EBTActiveNode::Composite;
NewInstance.Initialize(*this, *RootNode, NodeInstanceIndex, bFirstTime ? EBTMemoryInit::Initialize : EBTMemoryInit::RestoreSubtree);Initialize
里面会调每个节点的初始化函数。具体有哪些功能后面在讲。
然后就是构建树实例的中每个节点的自定义内存块,其中InstanceMemorySize
就是LoadTree
返回的那个,表示这棵树所有内存块的大小总和。 1
2
3
4
5
6
7
8// initialize memory and node instances
FBehaviorTreeInstanceId& InstanceInfo = KnownInstances[NewInstance.InstanceIdIndex];
const bool bFirstTime = (InstanceInfo.InstanceMemory.Num() != InstanceMemorySize);
if (bFirstTime)
{
InstanceInfo.InstanceMemory.AddZeroed(InstanceMemorySize);
InstanceInfo.RootNode = RootNode;
}InstanceInfo
指的是树的实例信息。
这个内存块还有个有意思的点是,它是一棵树的所有节点内存块是连续的,因为它是一次性申请的。而当想找到属于它自己节点的内存块时,则是通过UBTNode::MemoryOffset
属性进行偏移的。 1
2
3
4
5template<typename T>
T* UBTNode::GetNodeMemory(FBehaviorTreeInstance& BTInstance) const
{
return (T*)(BTInstance.GetInstanceMemory().GetData() + MemoryOffset);
}
栈
因为你可能会递归执行多棵行为树,比如树A执行树B,树B再执行树C,树C执行完又回到树B。就像一个栈,所以就用InstanceStack
存起来。 1
2InstanceStack.Push(NewInstance);
ActiveInstanceIdx = InstanceStack.Num() - 1;
执行Service
从这一步开始,就是开始执行树的逻辑了。首先是执行当前节点的所有Service。NotifyParentActivation
会启动Service的Tick。 1
2
3
4
5
6
7
8
9
10for (int32 ServiceIndex = 0; ServiceIndex < RootNode->Services.Num(); ServiceIndex++)
{
UBTService* ServiceNode = RootNode->Services[ServiceIndex];
uint8* NodeMemory = (uint8*)ServiceNode->GetNodeMemory<uint8>(InstanceStack[ActiveInstanceIdx]);
ServiceNode->NotifyParentActivation(SearchData);
InstanceStack[ActiveInstanceIdx].AddToActiveAuxNodes(ServiceNode);
ServiceNode->WrappedOnBecomeRelevant(*this, NodeMemory);
}1
FBehaviorTreeDelegates::OnTreeStarted.Broadcast(*this, TreeAsset);
发起执行任务节点的请求
RequestExecution
找到该子树的当前需要执行的任务节点,然后发起请求。注意我的用词,是发起请求,而不是正式执行。 1
RequestExecution(RootNode, ActiveInstanceIdx, RootNode, 0, EBTNodeResult::InProgress);
这个函数有三个重载,其实最终还是会调到第一个去, 1
2
3
4
5void RequestExecution(const UBTCompositeNode* RequestedOn, int32 InstanceIdx,
const UBTNode* RequestedBy, int32 RequestedByChildIndex,
EBTNodeResult::Type ContinueWithResult, bool bStoreForDebugger = true);
void RequestExecution(const UBTDecorator* RequestedBy) { check(RequestedBy); RequestBranchEvaluation(*RequestedBy); }
void RequestExecution(EBTNodeResult::Type ContinueWithResult) { RequestBranchEvaluation(ContinueWithResult); }
这个函数参数很多,且理解参数的含义是重要的。 1
2
3
4
5
6
7void UBehaviorTreeComponent::RequestExecution(
const UBTCompositeNode* RequestedOn,
int32 InstanceIdx,
const UBTNode* RequestedBy,
int32 RequestedByChildIndex,
EBTNodeResult::Type ContinueWithResult,
bool bStoreForDebugger)
- RequestedOn 要求传入一个组合节点。因为任务节点肯定是在组合节点下的,告诉代码你要找的是哪个组合节点下的任务节点。
- InstanceIdx 树在
InstanceStack
中的下标。 - RequestedBy 可以传入一个
UBTNode
,表示发起者。可能是组合节点或者修饰器(Decorator)。 - RequestedByChildIndex 传入发起者是第几个子节点。
- ContinueWithResult 表示任务节点执行完后,应该怎么办。看源码的话会发现它的作用很窄,可以先不深究。
- bStoreForDebugger 用来debug的,也可以先不管。
这个函数逻辑很长。总结起来就是给名为ExecutionRequest
的成员属性填充信息。 1
2
3
4class AIMODULE_API UBehaviorTreeComponent : public UBrainComponent
{
/** execution request, search will be performed when current task finish execution/aborting */
FBTNodeExecutionInfo ExecutionRequest;FBTNodeExecutionInfo
结构如下, 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17struct FBTNodeExecutionInfo
{
/** index of first task allowed to be executed */
FBTNodeIndex SearchStart;
/** index of last task allowed to be executed */
FBTNodeIndex SearchEnd;
/** node to be executed */
const UBTCompositeNode* ExecuteNode;
/** subtree index */
uint16 ExecuteInstanceIdx;
/** result used for resuming execution */
TEnumAsByte<EBTNodeResult::Type> ContinueWithResult;
/** if set, tree will try to execute next child of composite instead of forcing branch containing SearchStart */
uint8 bTryNextChild : 1;
/** if set, request was not instigated by finishing task/initialization but is a restart (e.g. decorator) */
uint8 bIsRestart : 1;
};bIsRestart
看起来有点难理解,但其实没有用到,可以先不管。ContinueWithResult
也有点难理解,它是分了多个使用场景的,后面再细讲。
SearchStart
和SearchEnd
在不同的调用情况下传值是不同的。它规定了一个范围。一般情况是不需要范围的,因为组合节点本身的规则已经足够你找到下一个节点了。
而组合节点本身无法知道的规则,就是Decorator的Observer Aborts
了,它可以在设置终止自身或低优先级节点,所以需要通过范围来限制。
Tick
执行任务节点是在Tick里面做的,但Tick里面也做了很多事情,所以先讲讲Tick。
尽管是Tick,但也不是每帧都执行的,它有自己的执行时间间隔,没到时间的时候就之间return掉。 1
2
3
4
5
6
7
8
9
10
11void UBehaviorTreeComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
NextTickDeltaTime -= DeltaTime;
if (NextTickDeltaTime > 0.0f)
{
AccumulatedTickDeltaTime += DeltaTime;
ScheduleNextTick(NextTickDeltaTime);
return;
}
DeltaTime += AccumulatedTickDeltaTime;
AccumulatedTickDeltaTime = 0.0f;
执行辅助节点的Tick
Unreal把装饰节点(Decorator)和服务节点(Service)定义为辅助节点(Auxiliary)。TickComponent
每次到时间后都会把所有子树的所有辅助节点的Tick逻辑。 1
2
3
4
5
6
7
8
9
10
11
12
13void UBehaviorTreeComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
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);
});
}AuxNode.WrappedTickNode
执行逻辑就会执行到蓝图里面的UBTService_BlueprintBase::ReceiveTick
事件。
这个执行阶段需要领会,这意味着辅助节点的Tick是在处理请求之前完成的。把握这一点将有助于解决实际开发中遇到的时序问题。
处理请求
然后就开始执行ProcessExecutionRequest
来处理请求。 1
2
3void UBehaviorTreeComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
ProcessExecutionRequest();
备份
处理请求过程中,有一个名为SearchData
的结构体成员属性,会存储搜索过程中的一些信息。搜索过程中会改变其中的字段,如果搜索失败,则需要回滚这些字段,所以使用了几个Rollback
开头的字段来备份。 1
2
3
4
5
6
7void UBehaviorTreeComponent::ProcessExecutionRequest()
{
SearchData.RollbackInstanceIdx = ActiveInstanceIdx;
SearchData.RollbackDeactivatedBranchStart = SearchData.DeactivatedBranchStart;
SearchData.RollbackDeactivatedBranchEnd = SearchData.DeactivatedBranchEnd;
CopyInstanceMemoryToPersistent();CopyInstanceMemoryToPersistent
则是对节点的自定义内存数据进行备份。
反激活
正式搜索前,还会有一个反激活逻辑,将调用DeactivateUpTo
函数。当我们要找下一个任务节点时,说明当前节点即将执行完毕。而所谓反激活就是指当前节点执行完毕之后需要做一些事情,所以有了这个反激活的阶段。 1
2
3
4
5void UBehaviorTreeComponent::ProcessExecutionRequest()
{
if (InstanceStack[ActiveInstanceIdx].ActiveNode != ExecutionRequest.ExecuteNode)
{
const bool bDeactivated = DeactivateUpTo(ExecutionRequest.ExecuteNode, ExecutionRequest.ExecuteInstanceIdx, NodeResult, LastDeactivatedChildIndex);DeactivateUpTo
该函数内部会向上进行父节点链路遍历,每个父节点都调用它的UBTCompositeNode::OnChildDeactivation
函数。函数本身的功能是这样,但实际上根据传入参数,调用起来的结果就只是反激活了当前活跃的任务节点。
OnChildDeactivation
其内部又会找到其指定子节点,然后触发子节点的Service、本身、以及Decorator的反激活逻辑。
如果该子节点是一个Task节点,则会先将其附带的Service存起来,然后在应用新的任务节点的时候才执行。至于为什么要特别进行这种设计,个人感觉是Epic自己的需求导致的,旁人没有遇到这样的需求,很难解释为什么要这么设计。
当执行反激活时,会调用到蓝图的相关事件。 对于Service是ReceiveDeactivation
事件,对于Decorator是ReceiveExecutionFinish
事件。
目前我还没用过这个反激活功能,也没想到有什么使用场景,所以暂不能举出实际的使用例子。
决定搜索范围
虽然我们的FBTNodeExecutionInfo
已经计算出了SearchStart
和SearchEnd
。但其bTryNextChild
将决定我们能否按照这个范围来搜索。
看看bTryNextChild
这个属性是怎么赋值的, 1
2const bool bSwitchToHigherPriority = (ContinueWithResult == EBTNodeResult::Aborted);
ExecutionRequest.bTryNextChild = !bSwitchToHigherPriority;1
2
3
4
5
6
7
8
9
10
11
12void UBehaviorTreeComponent::ProcessExecutionRequest()
{
if (!ExecutionRequest.bTryNextChild)
{
SearchData.SearchStart = ExecutionRequest.SearchStart;
SearchData.SearchEnd = ExecutionRequest.SearchEnd;
}
else
{
SearchData.SearchStart = FBTNodeIndex();
SearchData.SearchEnd = FBTNodeIndex();
}ContinueWithResult
被设置为Aborted
,意味着当前节点是被中断的,我们的搜索范围可能是受到限制的。否则,就是搜索没有被限制,可以单纯按照组合节点本身的规则去搜索。
搜索
TestNode
表示当前搜索到的节点,NextTask
表示目标任务节点,不为NULL说明搜索成功了。其中BTSpecialChild::ReturnToParent
表示找不到子节点,需要回到父节点找的意思。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47void UBehaviorTreeComponent::ProcessExecutionRequest()
{
while (TestNode && NextTask == NULL)
{
const int32 ChildBranchIdx = TestNode->FindChildToExecute(SearchData, NodeResult);
const UBTNode* StoreNode = TestNode;
if (SearchData.bPostponeSearch)
{
TestNode = NULL;
bIsSearchValid = false;
}
else if (ChildBranchIdx == BTSpecialChild::ReturnToParent)
{
const UBTCompositeNode* ChildNode = TestNode;
TestNode = TestNode->GetParentNode();
// does it want to move up the tree?
if (TestNode == NULL)
{
// special case for leaving instance: deactivate root manually
ChildNode->OnNodeDeactivation(SearchData, NodeResult);
// don't remove top instance from stack, so it could be looped
if (ActiveInstanceIdx > 0)
{
SearchData.PendingNotifies.Add(FBehaviorTreeSearchUpdateNotify(ActiveInstanceIdx, NodeResult));
// and leave subtree
ActiveInstanceIdx--;
}
}
if (TestNode)
{
TestNode->OnChildDeactivation(SearchData, *ChildNode, NodeResult, ActiveInstanceIdx == ExecutionRequest.ExecuteInstanceIdx);
}
}
else if (TestNode->Children.IsValidIndex(ChildBranchIdx))
{
// was new task found?
NextTask = TestNode->Children[ChildBranchIdx].ChildTask;
// or it wants to move down the tree?
TestNode = TestNode->Children[ChildBranchIdx].ChildComposite;
}
}FindChildToExecute
里调用了GetNextChild
来获取下一个子节点,其中调用了组合节点的GetNextChildHandler
,这是一个虚函数,Sequence和Selector都在其中实现了自己的逻辑,如果你要自定义组合节点,也可以去实现它。 1
2
3int32 UBTCompositeNode::FindChildToExecute(FBehaviorTreeSearchData& SearchData, EBTNodeResult::Type& LastResult) const
{
int32 ChildIdx = GetNextChild(SearchData, NodeMemory->CurrentChild, LastResult);1
2
3int32 UBTCompositeNode::GetNextChild(FBehaviorTreeSearchData& SearchData, int32 LastChildIdx, EBTNodeResult::Type LastResult) const
{
NextChildIndex = GetNextChildHandler(SearchData, LastChildIdx, LastResult);
搜索过程中的装饰器检查和回调触发
搜索过程中会对每个节点进行装饰器检查,即DoDecoratorsAllowExecution
。会调用UBTDecorator::WrappedCanExecute
,然后是CalculateRawConditionValue
,它是一个虚函数,自定义的装饰器可以重写它。 1
2
3
4
5
6
7
8
9
10
11
12int32 UBTCompositeNode::FindChildToExecute(FBehaviorTreeSearchData& SearchData, EBTNodeResult::Type& LastResult) const
{
while (Children.IsValidIndex(ChildIdx) && !SearchData.bPostponeSearch)
{
if (DoDecoratorsAllowExecution(SearchData.OwnerComp, SearchData.OwnerComp.ActiveInstanceIdx, ChildIdx))
{
OnChildActivation(SearchData, ChildIdx);
RetIdx = ChildIdx;
break;
}
ChildIdx = GetNextChild(SearchData, ChildIdx, LastResult);
}UBTDecorator_BlueprintBase
对其进行了重写并调用了事件PerformConditionCheckAI
和PerformConditionCheck
。带AI后缀的是当Controller是一个AIController时执行的。
检查成功后会调用OnChildActivation
,它会调用子节点的所有装饰器的OnNodeActivation
。如果本身是组合节点(Composite),还会调其 OnNodeActivation
。
组合节点的OnNodeActivation
内部会调用所有Service的NotifyParentActivation
,然后其内部又会产生调用UBTService::OnSearchStart
。说明它是是一个搜索过程中触发的回调,蓝图类会进一步调用事件ReceiveSearchStart
。
校验NextTask
主要有两个校验:
- 检查NextTask的优先级是否大于
SearchEnd
,否则就不能继续执行。SearchEnd
无限制时它是一个超大值,任何节点的优先级都大于它。 - 检查NextTask是否开启了
IgnoreRestartSelf
,这个配置在蓝图里面就能找到,表示如果当前节点是正在激活的,就不再执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14if (NextTask)
{
const FBTNodeIndex NextTaskIdx(ActiveInstanceIdx, NextTask->GetExecutionIndex());
bIsSearchValid = NextTaskIdx.TakesPriorityOver(ExecutionRequest.SearchEnd);
if (bIsSearchValid && NextTask->ShouldIgnoreRestartSelf())
{
const bool bIsTaskRunning = InstanceStack[ActiveInstanceIdx].HasActiveNode(NextTaskIdx.ExecutionIndex);
if (bIsTaskRunning)
{
bIsSearchValid = false;
}
}
}
处理搜索结果
失败时,会调用RollbackSearchChanges
回滚SearchData的相关修改。 成功时,会终止当前活跃Task,然后把数据存到PendingExecution
,然后调用ProcessPendingExecution
对其进行处理。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18if (!bIsSearchValid || SearchData.bPostponeSearch)
{
RollbackSearchChanges();
}
if (bIsSearchValid)
{
if (InstanceStack.Last().ActiveNodeType == EBTActiveNode::ActiveTask)
{
AbortCurrentTask();
}
if (!PendingExecution.IsLocked())
{
PendingExecution.NextTask = NextTask;
PendingExecution.bOutOfNodes = (NextTask == NULL);
}
}
ProcessPendingExecution();
调用缓存的回调
但一个新任务节点被激活,自然旧任务节点应该反激活,前面提到,很多反激活函数其实都存到这个阶段去调用了。 1
2
3void UBehaviorTreeComponent::ProcessPendingExecution()
{
ApplySearchData(SavedInfo.NextTask);1
2
3
4
5
6
7
8
9
10void UBehaviorTreeComponent::ApplySearchData(UBTNode* NewActiveNode)
{
for (int32 Idx = 0; Idx < SearchData.PendingNotifies.Num(); Idx++)
{
const FBehaviorTreeSearchUpdateNotify& NotifyInfo = SearchData.PendingNotifies[Idx];
if (InstanceStack.IsValidIndex(NotifyInfo.InstanceIndex))
{
InstanceStack[NotifyInfo.InstanceIndex].DeactivationNotify.ExecuteIfBound(*this, NotifyInfo.NodeResult);
}
}PendingUpdates
里面存下来的回调。第三个参数为bPostUpdate
,表明这种回调还分了次序,哪些先调用,哪些后调用。可能给是后调用的会依赖先调用的计算结果。 1
2
3
4void UBehaviorTreeComponent::ApplySearchData(UBTNode* NewActiveNode)
{
ApplySearchUpdates(SearchData.PendingUpdates, NewNodeExecutionIndex);
ApplySearchUpdates(SearchData.PendingUpdates, NewNodeExecutionIndex, true);Remove
和Add
两种情况。标记为Remove
的时候会调用其辅助节点的WrappedOnCeaseRelevant
,Add
的时候会调用WrappedOnBecomeRelevant
。 1
2
3
4
5
6
7
8
9
10
11
12
13void UBehaviorTreeComponent::ApplySearchUpdates(const TArray<FBehaviorTreeSearchUpdate>& UpdateList, int32 NewNodeExecutionIndex, bool bPostUpdate)
{
uint8* NodeMemory = (uint8*)UpdateNode->GetNodeMemory<uint8>(UpdateInstance);
if (UpdateInfo.Mode == EBTNodeUpdateMode::Remove)
{
UpdateInstance.RemoveFromActiveAuxNodes(UpdateInfo.AuxNode);
UpdateInfo.AuxNode->WrappedOnCeaseRelevant(*this, NodeMemory);
}
else
{
UpdateInstance.AddToActiveAuxNodes(UpdateInfo.AuxNode);
UpdateInfo.AuxNode->WrappedOnBecomeRelevant(*this, NodeMemory);
}
执行任务
1
2
3void UBehaviorTreeComponent::ProcessPendingExecution()
{
ExecuteTask(SavedInfo.NextTask);WrappedOnBecomeRelevant
和WrappedTickNode
。
然后真正执行任务的地方是下面这里, 1
2
3
4void UBehaviorTreeComponent::ExecuteTask(UBTTaskNode* TaskNode)
{
TaskResult = TaskNode->WrappedExecuteTask(*this, NodeMemory);
OnTaskFinished(TaskNode, TaskResult);1
EBTNodeResult::Type UBTTaskNode::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
UBTTask_BlueprintBase::ReceiveExecute
。
函数需要返回一个结果。不管什么结果都会调用OnTaskFinished
,当结果为InProgress的时候就不会做什么事情。当有了非InProgress的结果之后再用新结果去调用即可。可参考蓝图类的写法UBTTask_BlueprintBase::FinishExecute
。
流程总结
Unreal的行为树,从它的Tick可知,它的主流程就是,要么就是在请求执行下一个任务节点,要么就是在等待请求。
所谓的请求执行下一个任务节点,就是从一个任务节点行走至另一个任务节点。行走过程中,就会调用适时调用节点的生命周期函数。
激活与反激活
知道整个流程后,我们再来整理一下激活的逻辑。激活与反激活是一个重要概念,它发生在组合节点、Service和Decorator身上。
假设当前搜索到一个组合节点上时,搜索指针会逐个子节点尝试,当一个子节点的装饰器检查通过后,说明我们应该移动到该子节点上,就会调用组合节点的OnChildActivation
并传入目标子节点,不管该子节点是什么类型,都会先激活其装饰器NotifyDecoratorsOnActivation
,会调用装饰器的WrappedOnNodeActivation
,同时也会缓存住装饰器;如果该子节点是一个组合节点,还会调用其OnNodeActivation
,该函数会缓存其身上所有Service,注意是先缓存住,调用的地方就是调用缓存的回调章节里的阶段。
对于反激活,有两个执行的地方。
一个是搜索之前,反激活掉当前任务节点,具体见反激活章节。
另一个就是在搜索过程中,当决定要从该节点返回其父节点时,就会调用该节点的反激活逻辑。同理,通过组合节点的OnChildDeactivation
来反激活目标子节点。之所以从组合节点这层调用是因为要读取组合节点本身的配置。反激活时,如果该节点是任务节点,首先缓存该节点的素有Service;其次如果该节点是组合节点,会调用其OnNodeDeactivation
,也是先缓存住所有Service;然后不管是什么类型,再调用该子节点的所有装饰器的反激活函数,同时缓存住。
Service和Decorator的调用的先后顺序和激活刚好是反过来的。Decorator的反激活既有马上执行的部分,也有缓存的部分。而Service则是先缓存住。
在执行任务节点前,会把缓存的都拿出来执行,激活对应的函数是OnBecomeRelevant
,反激活对应的函数是OnCeaseRelevant
。
Service的回调总结
ReceiveActivation
和ReceiveDeactivation
分别对应激活和反激活。
ReceiveSearch
在搜索时调用。搜索过程中的装饰器检查和回调触发章节中提到过。
而ReceiveTick
则是在WrappedTickNode
函数中被调用,不只是在Tick的时候调用,还会在部分时机手动调用,具体全局搜索该函数即可。
Decorator的回调总结
PerformConditionCheck
是在搜索过程中经过组合节点时被调用的。搜索过程中的装饰器检查和回调触发章节中提到过。
ReceiveExecutionStart
和ReceiveExecutionFinish
分别在对应激活和反激活,搜索过程中马上执行时调用。
ReceiveObserverActivated
和ReceiveObserverDeactivated
分别在对应激活和反激活,执行缓存时调用。
ReceiveTick
则也可搜索WrappedTickNode
查找调用的地方。
Comments