UE的AI基础(3)行为树执行流程

1. 核心类

1.1 UBehaviorTreeComponent

AIController继承自AControllerAController控制着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;
}

1.2 UBehaviorTreeManager

UBehaviorTreeManager继承自UObject,充当着管理行为树资源加载的角色。

/** 使用单例获取,一个World一个UBehaviorTreeManager **/
UBehaviorTreeManager* BTManager = UBehaviorTreeManager::GetCurrent(GetWorld());
if (BTManager)
{
    
    
    BTManager->AddActiveComponent(*this);
}
/** 亦可以使用WorldContextObject获取 **/
static UBehaviorTreeManager* GetCurrent(UObject* WorldContextObject);

1.3 UBlackboardComponent

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);
}

2. 行为树的节点

UBTNode:行为树节点的基
UBTTask:任务节点
UBTAuxiliaryNode:附在任务节点上的子节点,就是Decorator和Service
UBTService:服务节点
UBTDecorator:装饰器判断节点

3. 行为树的单例

在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);
}

4. 一棵行为树的执行流程

行为树的启动UBehaviorTreeComponent::OnTreeStarted
行为树的结束UBehaviorTreeInstance::DeactivationNotify

行为树的预处理、实例化和运行树在UBehaviorTreeComponent中执行,而树的加载工作全在UBehaviorTreeManager中完成。

一棵行为树的执行流程如下

在这里插入图片描述

4.1 预检查

在准备加载行为树之前,UBehaviorTreeComponent会做三个检查
(1)检查父树与子树的所用黑板资源是否一致。
(2)检查是否能获取到全局的UBehaviorTreeManager
(3)检查父节点是否允许运行子树,唯一的情况是SimpleParallel的第一个点检如果是RunBehavior那么不允许被执行。

bool UBTComposite_SimpleParallel::CanPushSubtree(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, int32 ChildIdx) const
{
    
    
    return (ChildIdx != EBTParallelChild::MainTask);
}

4.2 缓存机制

被加载完成的树模板会被缓存到数组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;
    }
}

4.3 初始化

我们不希望在原有的树资源上做修改,因此我们需要深拷贝一份来做修改,这里使用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不会有任何作用。

4.4 计算内存

把计算出来的每个节点的内存大小,用内存大小值对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继续完成初始化和实例化。

4.5 实例化

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;

5. 行为树节点的请求

5.1 发起执行请求

执行请求的发起是通过UBehaviorTreeComponent::RequestExecution()实现的。

5.2 关键标志位

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。

5.3 请求的范围

FBTNodeExecutionInfo中通过一个起始点和一个结束点来判断。

FBTNodeIndex ExecutionIdx;
ExecutionIdx.InstanceIndex = InstanceIdx;
ExecutionIdx.ExecutionIndex = RequestedBy->GetExecutionIndex();

uint16 LastExecutionIndex = MAX_uint16;
const FBTNodeIndex SearchEnd(InstanceIdx, LastExecutionIndex);
/** 起点为请求发起者的ExecutionIndex **/
ExecutionRequest.SearchStart = ExecutionIdx;

5.4 请求Tick的执行

对于UBehaviorTreeComponent来说,有一个NextTickDeltaTime,当每次TickComponent被调用时,NextTickDeltaTime会被减少,当NextTickDeltaTime值小于等于0时,才会真正执行Tick的逻辑。
RequestExecution()函数中,会调用ScheduleExecutionUpdate()函数,会把NextTickDeltaTime值更新为0,即下一帧就会执行Tick。

5.5 执行下一个节点

根节点的执行请求:
(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;

5.6 Abort请求的影响

如果ContinueResult为Aborted,则bSwitchToHigherPriority为true,那么会造成以下影响:
(1)这会影响搜索范围,SearchStart为上一个父节点下,最先被执行的节点,SearchEnd为与该节点平级的下一个节点中最先执行的子节点。
(2)确保RequestedOn的可执行性,保证父节点的Decorator通过并且所有祖先均可执行。

6. 行为树节点的处理请求

6.1 处理搜索的函数

处理请求的函数是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;
}

6.2 搜索的前期准备

步骤如下:
(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;

6.3 搜索的过程

找到父节点后,通过优先级进行判断设置bTryNextChild,并决定要搜索的范围。

const bool bSwitchToHigherPriority = (ContinueWithResult == EBTNodeResult::Aborted);
ExecutionRequest.bTryNextChild = !bSwitchToHigherPriority;

接下来进行搜索,搜索过程中存储两个变量:
(1)TestNode:代测试的节点
(2)TaskNode:目标节点,当不为nullptr时,即代表搜索成功
TestNode相当于取当前活跃的父节点中使用TestNode->FindChildToExecute(),去查找下一个子节点,当为-1时即为没找到,不然一直向下寻找,找到可执行的任务节点。

6.4 使用service的问题

我们想要service提供一个数据时,通常会使用ReceiveActivationAISearchStartAI,区别在与ReceiveActivationAI只有当其依附的节点或其子节点变为运行时,才会执行,所以若使用这个,在下一个节点有Decorator判断时,可能会永远不执行。而SearchStartAI只要其依附的节点在搜索路径上,那么就会被执行。

注意:
(1)需要Service获取数据时,要把它依附在你要用该数据的节点上,别放在Composite上。
(2)尽量避免两个Service共用一个黑板值。

6.5 任务的执行

使用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

猜你喜欢

转载自blog.csdn.net/qq_45617648/article/details/131150497