赞
踩
AIController
继承自AController
,AController
控制着AActor
的行为,AIController
新增了UBrainComponent
组件。
UBehaviorTreeComponent
就继承自UBrainComponent
组件。
运行行为树的函数AAIController::RunBehaviorTree()最终就会调用UBehaviorTreeComponent::StartTree()函数,这个函数负责传入一个行为树资源文件,并负责初始化和运行该行为树,然后储存行为树节点的数据。
bool AAIController::RunBehaviorTree(UBehaviorTree* BTAsset) { /** * 如果BTAsset和BlackboardAsset都有值执行下面的逻辑 */ if(success) { 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(); } // make sure BrainComponent points at the newly created BT component BrainComponent = BTComp; check(BTComp != NULL); BTComp->StartTree(*BTAsset, EBTExecutionMode::Looped); } return bSuccess; }
UBehaviorTreeManager
继承自UObject
,充当着管理行为树资源加载的角色。
/** 使用单例获取,一个World一个UBehaviorTreeManager **/
UBehaviorTreeManager* BTManager = UBehaviorTreeManager::GetCurrent(GetWorld());
if (BTManager)
{
BTManager->AddActiveComponent(*this);
}
/** 亦可以使用WorldContextObject获取 **/
static UBehaviorTreeManager* GetCurrent(UObject* WorldContextObject);
UBlackboardComponent
继承自UActorComponent
,提供了很多对黑板值的操作。
UFUNCTION(BlueprintCallable, Category="AI|Components|Blackboard")
UObject* GetValueAsObject(const FName& KeyName) const;
UFUNCTION(BlueprintCallable, Category="AI|Components|Blackboard")
uint8 GetValueAsEnum(const FName& KeyName) const;
void UBlackboardComponent::SetValueAsEnum(const FName& KeyName, uint8 EnumValue)
{
// 根据KeyName获取ID
const FBlackboard::FKey KeyID = GetKeyID(KeyName);
// 通过ID设置值
SetValue<UBlackboardKeyType_Enum>(KeyID, EnumValue);
}
UBTNode:行为树节点的基
UBTTask:任务节点
UBTAuxiliaryNode:附在任务节点上的子节点,就是Decorator和Service
UBTService:服务节点
UBTDecorator:装饰器判断节点
在C++默认中,在被实例化的同一个行为树资源中,同一个类型的任务,是共用同一个实例的,也就是说其中一个更新会把其余的全部都覆盖掉。
虽然不同的资源都是用的一个实例,但也是有办法解决的,我们可以手动进行内存的深拷贝。
节点的内存是在UBehaviorTreeComponent::ExecuteTask()中拿到的。
uint8* NodeMemory = (uint8*)(TaskNode->GetNodeMemory<uint8>(ActiveInstance));
TaskResult = TaskNode->WrappedExecuteTask(*this, NodeMemory);
template<typename T>
T* UBTNode::GetNodeMemory(FBehaviorTreeInstance& BTInstance) const
{
return (T*)(BTInstance.GetInstanceMemory().GetData() + MemoryOffset);
}
行为树的启动:UBehaviorTreeComponent::OnTreeStarted
行为树的结束:UBehaviorTreeInstance::DeactivationNotify
行为树的预处理、实例化和运行树在UBehaviorTreeComponent
中执行,而树的加载工作全在UBehaviorTreeManager
中完成。
一棵行为树的执行流程如下
在准备加载行为树之前,UBehaviorTreeComponent
会做三个检查:
(1)检查父树与子树的所用黑板资源是否一致。
(2)检查是否能获取到全局的UBehaviorTreeManager
。
(3)检查父节点是否允许运行子树,唯一的情况是SimpleParallel的第一个点检如果是RunBehavior那么不允许被执行。
bool UBTComposite_SimpleParallel::CanPushSubtree(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, int32 ChildIdx) const
{
return (ChildIdx != EBTParallelChild::MainTask);
}
被加载完成的树模板会被缓存到数组LoadedTemplates
中,树模板是一个简单的结构体,包含三个内容:
(1)树资源
(2)树的根节点
(3)树实例的总内存大小
for (int32 TemplateIndex = 0; TemplateIndex < LoadedTemplates.Num(); TemplateIndex++)
{
FBehaviorTreeTemplateInfo& TemplateInfo = LoadedTemplates[TemplateIndex];
// 检查资源是否被缓存过,若被缓存则直接从缓存数组中读取
if (TemplateInfo.Asset == &Asset)
{
Root = TemplateInfo.Template;
InstanceMemorySize = TemplateInfo.InstanceMemorySize;
return true;
}
}
我们不希望在原有的树资源上做修改,因此我们需要深拷贝一份来做修改,这里使用StaticDuplicateObject()函数进行深拷贝。
FBehaviorTreeTemplateInfo TemplateInfo;
TemplateInfo.Asset = &Asset;
TemplateInfo.Template = Cast<UBTCompositeNode>(StaticDuplicateObject(Asset.RootNode, this));
接下来对每个节点的初始化信息来进行处理,使用InitializeNodeHelper函数,计算出每个节点的父节点、执行顺序和所需内存大小。
TArray<FNodeInitializationData> InitializeNodeHelper(NULL, TemplateInfo.Template, 0, ExecutionIndex, InitList, Asset, this);
执行顺序:行为树通过树的深度遍历来进行执行顺序的确认
最终的执行顺序是:父节点本身 → 父节点的service → 子节点的decorator → 子节点的service → 子节点本身
注意:FNodeInitializationData
不会遍历到本身的decorator,它只会遍历子节点的decorator,会将本身的decorator直接转换到RunBehavior节点上运行,因此单独的行为树的第一个节点的decorator不会有任何作用。
把计算出来的每个节点的内存大小,用内存大小值对InitList
进行排序,把内存较小的值排在一起。
uint16 MemoryOffset = 0;
/** 先排序,再进行遍历 **/
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;
}
至此,树的加载全部完成,所有的初始化数据通过TemplateInfo进行返回,在UBehaviorTreeComponent
继续完成初始化和实例化。
在UBehaviorTreeComponent::PushInstance中:
当树被加载完后,会构建一个FBehaviorTreeInstance
存储在InstanceStack
中,之后会被多次调用,再使用FBehaviorTreeInstance
的信息构建出一个FBehaviorTreeInstanceId
,在KnownInstances
通过Id保存和查用。
然后进行内存的初始化和数组的填充。
FBehaviorTreeInstanceId& InstanceInfo = KnownInstances[NewInstance.InstanceIdIndex];
int32 NodeInstanceIndex = InstanceInfo.FirstNodeInstance;
const bool bFirstTime = (InstanceInfo.InstanceMemory.Num() != InstanceMemorySize);
if (bFirstTime)
{
InstanceInfo.InstanceMemory.AddZeroed(InstanceMemorySize);
InstanceInfo.RootNode = RootNode;
}
NewInstance.SetInstanceMemory(InstanceInfo.InstanceMemory);
NewInstance.Initialize(*this, *RootNode, NodeInstanceIndex, bFirstTime ? EBTMemoryInit::Initialize : EBTMemoryInit::RestoreSubtree);
InstanceStack.Push(NewInstance);
ActiveInstanceIdx = InstanceStack.Num() - 1;
执行请求的发起是通过UBehaviorTreeComponent::RequestExecution()实现的。
bSwitchToHigherPriority
:是否切换到更高优先级
const bool bSwitchToHigherPriority = (ContinueWithResult == EBTNodeResult::Aborted);
可以看出,只要结果是Aborted时,我们才需要切换到更高优先级。
namespace EBTNodeResult
{
enum Type
{
Succeeded, // finished as success
Failed, // finished as failure
Aborted, // finished aborting = failure
InProgress, // not finished yet
};
}
我们在实际使用中也发现AbortMode = self不会影响Selector的执行,而AbortMode = LowerPriority,则Selector右侧的值都不会执行,因为只有当节点被打断并且AbortMode是LowerPriority的时候,才会返回Aborted。
在FBTNodeExecutionInfo
中通过一个起始点和一个结束点来判断。
FBTNodeIndex ExecutionIdx;
ExecutionIdx.InstanceIndex = InstanceIdx;
ExecutionIdx.ExecutionIndex = RequestedBy->GetExecutionIndex();
uint16 LastExecutionIndex = MAX_uint16;
const FBTNodeIndex SearchEnd(InstanceIdx, LastExecutionIndex);
/** 起点为请求发起者的ExecutionIndex **/
ExecutionRequest.SearchStart = ExecutionIdx;
对于UBehaviorTreeComponent
来说,有一个NextTickDeltaTime
,当每次TickComponent
被调用时,NextTickDeltaTime
会被减少,当NextTickDeltaTime
值小于等于0时,才会真正执行Tick的逻辑。
在RequestExecution()函数中,会调用ScheduleExecutionUpdate()函数,会把NextTickDeltaTime
值更新为0,即下一帧就会执行Tick。
根节点的执行请求:
(1)RequestedOn:Root节点
(2)RequestedBy:Root节点
(3)InstanceIndex:该树的InstanceId
一个节点在一个节点结束后执行,会调用UbehaviorTreeComponent::OnTaskFinished()函数,来调用RequestExecution()来请求下一个节点。
我们只需要确定传入参数的值:
(1)RequestedOn:正在执行节点的父节点(CompositeNode)
(2)RequestedBy:正在执行的节点
(3)InstanceIndex:取InstanceStack.Num() - 1,即栈顶的那棵活跃的树
(4)RequestedByChildIndex:-1
(5)ContinueWithResult:LastResult(成功还是失败)
Decorator的Abort请求:
(1)RequestedOn:含该Decorator节点的父节点(CompositeNode)
(2)RequestedBy:Decorator本身
(3)InstanceIdx:该树的InstanceId
(4)RequestedByChildIndex:含该Decorator节点的ChildIndex
(5)ContinueResult:Failed或者Aborted
/** 如果AbortMode是Both则会进行转化 **/
if (AbortMode == EBTFlowAbortMode::Both)
{
const bool bIsExecutingChildNodes = IsExecutingBranch(RequestedBy, RequestedBy->GetChildIndex());
AbortMode = bIsExecutingChildNodes ? EBTFlowAbortMode::Self : EBTFlowAbortMode::LowerPriority;
}
EBTNodeResult::Type ContinueResult = (AbortMode == EBTFlowAbortMode::Self) ? EBTNodeResult::Failed : EBTNodeResult::Aborted;
如果ContinueResult
为Aborted,则bSwitchToHigherPriority
为true,那么会造成以下影响:
(1)这会影响搜索范围,SearchStart
为上一个父节点下,最先被执行的节点,SearchEnd
为与该节点平级的下一个节点中最先执行的子节点。
(2)确保RequestedOn的可执行性,保证父节点的Decorator通过并且所有祖先均可执行。
处理请求的函数是ProcessExecutionRequest,在TickComponent()中被调用。
当请求发起后bRequestedFlowUpdate
会设置为true
if (bRequestedFlowUpdate)
{
ProcessExecutionRequest();
bDoneSomething = true;
// Since hierarchy might changed in the ProcessExecutionRequest, we need to go through all the active auxiliary nodes again to fetch new next DeltaTime
bActiveAuxiliaryNodeDTDirty = true;
NextNeededDeltaTime = FLT_MAX;
}
步骤如下:
(1)数据备份
(2)使用UBehaviorTreeComponent::DeactivateUpTo函数将while遍历当前活跃节点的所有祖宗的OnChildDeactivation(),通常是调用父节点的OnChildDeactivation()函数。
OnChildDeactivation()的作用如下:
SearchData.AddUniqueUpdate(FBehaviorTreeSearchUpdate(ChildInfo.ChildTask->Services[ServiceIndex], SearchData.OwnerComp.GetActiveInstanceIdx(), EBTNodeUpdateMode::Remove));
将往SearchData
中添加一个FBehaviorTreeSearchUpdate
结构体,这里传入EBTNodeUpdateMode::Remove
,即在UBehaviorTreeComponent::ApplySearchData()时会直接调用辅助节点的OnCeaseRelevant(),最后更新SearchData
的字段。
if (NewDeactivatedBranchStart.TakesPriorityOver(SearchData.DeactivatedBranchStart))
{
SearchData.DeactivatedBranchStart = NewDeactivatedBranchStart;
}
SearchData.DeactivatedBranchEnd = NewDeactivatedBranchEnd;
找到父节点后,通过优先级进行判断设置bTryNextChild
,并决定要搜索的范围。
const bool bSwitchToHigherPriority = (ContinueWithResult == EBTNodeResult::Aborted);
ExecutionRequest.bTryNextChild = !bSwitchToHigherPriority;
接下来进行搜索,搜索过程中存储两个变量:
(1)TestNode
:代测试的节点
(2)TaskNode
:目标节点,当不为nullptr时,即代表搜索成功
TestNode相当于取当前活跃的父节点中使用TestNode->FindChildToExecute(),去查找下一个子节点,当为-1时即为没找到,不然一直向下寻找,找到可执行的任务节点。
我们想要service提供一个数据时,通常会使用ReceiveActivationAI和SearchStartAI,区别在与ReceiveActivationAI只有当其依附的节点或其子节点变为运行时,才会执行,所以若使用这个,在下一个节点有Decorator判断时,可能会永远不执行。而SearchStartAI只要其依附的节点在搜索路径上,那么就会被执行。
注意:
(1)需要Service获取数据时,要把它依附在你要用该数据的节点上,别放在Composite上。
(2)尽量避免两个Service共用一个黑板值。
使用ProcessPendingExecution()函数,步骤如下:
(1)使用UBehaviorTreeComponent::ApplySearchData(),其中进行了两次ApplySearchUpdates()函数,主要逻辑是对SearchData.PendingUpdates
进行遍历,当Mode为Remove时,执行生命周期函数UBTAuxilaryNode::OnCeaseRelevant();Mode是Add时,用UBTAuxilaryNode::OnBecomeRelevant()
注意:之所以要进行两次的原因是有时候需要依赖第一轮处理后的信息。
(2)执行所有服务节点的OnBecomeRelevant()。
(3)调用UBTTask::WrappedExecuteTask()即UBTTask::ExecuteTask(),将会返回一个枚举值(Aborted、Processing、Succeeded、Failed)。
(4)不管返回值为什么,最后执行UBehaviorTreeComponent::OnTaskFinished(),当然这个时候任务可能还在执行中,要判断任务的结束,使用TaskResult != EBTNodeResult::InProgress。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。