[C++/UE] UE5中AI開發的一點功能梳理(二) – 行為樹Composite類型節點說明

行為樹Composite節點說明

Selector

//BTComposite_Selector.cpp
/** 
 * Selector composite node.
 * Selector Nodes execute their children from left to right, and will stop executing its children when one of their children succeeds.
 * If a Selector's child succeeds, the Selector succeeds. If all the Selector's children fail, the Selector fails.
 */
int32 UBTComposite_Selector::GetNextChildHandler(FBehaviorTreeSearchData& SearchData, int32 PrevChild, EBTNodeResult::Type LastResult) const
{
    // success = quit
    int32 NextChildIdx = BTSpecialChild::ReturnToParent;

    if (PrevChild == BTSpecialChild::NotInitialized)
    {
        // newly activated: start from first
        NextChildIdx = 0;
    }
    else if (LastResult == EBTNodeResult::Failed && (PrevChild + 1) < GetChildrenNum())
    {
        // failed = choose next child
        NextChildIdx = PrevChild + 1;
    }

    return NextChildIdx;
}
  • 如Selector有節點A(BTTask_Patrol)跟節點B(BTTask_MoveRandomLocation),如果節點A返回Succeeded,則Selector會馬上返回Succeeded,不會執行節點B的行為
  • 換言之就是在第一個子節點返回Succeeded時,Selector也返回,否則會一直執行,直到所有節點都返回Failed時,自身(Selector)返回Failed

Sequence

//BTComposite_Sequence.cpp
/**
 * Sequence composite node.
 * Sequence Nodes execute their children from left to right, and will stop executing its children when one of their children fails.
 * If a child fails, then the Sequence fails. If all the Sequence's children succeed, then the Sequence succeeds.
 */
int32 UBTComposite_Sequence::GetNextChildHandler(FBehaviorTreeSearchData& SearchData, int32 PrevChild, EBTNodeResult::Type LastResult) const
{
    // failure = quit
    int32 NextChildIdx = BTSpecialChild::ReturnToParent;

    if (PrevChild == BTSpecialChild::NotInitialized)
    {
        // newly activated: start from first
        NextChildIdx = 0;
    }
    else if (LastResult == EBTNodeResult::Succeeded && (PrevChild + 1) < GetChildrenNum())
    {
        // success = choose next child
        NextChildIdx = PrevChild + 1;
    }

    return NextChildIdx;
}
  • 基本是Selector的相反
  • 如Sequence有上述三個節點,從左至右分別為(A、B、C),如果節點A返回Failed,則Sequence會馬上返回Failed,不會執行節點B及C的行為
  • 換言之就是在第一個子節點返回Failed時,Sequence也返回,否則會一直執行,直到所有節點都返回Succeeded時,自身(Sequence)返回Succeeded

Simple Parallel

  • 主任務必須為一個單獨的Task節點
  • 副任務則可以是一個Sequence或者其他組合節點
  • FinishMode
    • Immediate => 當Main Task執行完成,馬上打斷background tree並返回
    • Delayed => Main Task執行完成,等待background tree也執行完成才返回
//BTComposite_SimpleParallel.cpp
/**
 * Simple Parallel composite node.
 * Allows for running two children: one which must be a single task node (with optional decorators), and the other of which can be a complete subtree.
 */
int32 UBTComposite_SimpleParallel::GetNextChildHandler(FBehaviorTreeSearchData& SearchData, int32 PrevChild, EBTNodeResult::Type LastResult) const
{
    FBTParallelMemory* MyMemory = GetNodeMemory<FBTParallelMemory>(SearchData);
    int32 NextChildIdx = BTSpecialChild::ReturnToParent;

    if (PrevChild == BTSpecialChild::NotInitialized)
    {
        NextChildIdx = EBTParallelChild::MainTask;
        MyMemory->MainTaskResult = EBTNodeResult::Failed;
        MyMemory->bRepeatMainTask = false;
    }
    else if ((MyMemory->bMainTaskIsActive || MyMemory->bForceBackgroundTree) && !SearchData.OwnerComp.IsRestartPending())
    {
        // if main task is running - always pick background tree
        // unless search is from abort from background tree - return to parent (and break from search when node gets deactivated)
        NextChildIdx = EBTParallelChild::BackgroundTree;
        MyMemory->bForceBackgroundTree = false;
    }
    else if (MyMemory->bRepeatMainTask)
    {
        UE_VLOG(SearchData.OwnerComp.GetOwner(), LogBehaviorTree, Verbose, TEXT("Repeating main task of %s"), *UBehaviorTreeTypes::DescribeNodeHelper(this));
        NextChildIdx = EBTParallelChild::MainTask;
        MyMemory->bRepeatMainTask = false;
    }

    if ((PrevChild == NextChildIdx) && (MyMemory->LastSearchId == SearchData.SearchId))
    {
        // retrying the same branch again within the same search - possible infinite loop
        SearchData.bPostponeSearch = true;
    }

    MyMemory->LastSearchId = SearchData.SearchId;
    return NextChildIdx;
}

void UBTComposite_SimpleParallel::NotifyChildExecution(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, int32 ChildIdx, EBTNodeResult::Type& NodeResult) const
{
    FBTParallelMemory* MyMemory = (FBTParallelMemory*)NodeMemory;
    if (ChildIdx == EBTParallelChild::MainTask)
    {
        MyMemory->MainTaskResult = NodeResult;

        if (NodeResult == EBTNodeResult::InProgress)
        {
            EBTTaskStatus::Type Status = OwnerComp.GetTaskStatus(Children[EBTParallelChild::MainTask].ChildTask);
            if (Status == EBTTaskStatus::Active)
            {
                // can't register if task is currently being aborted (latent abort returns in progress)

                MyMemory->bMainTaskIsActive = true;
                MyMemory->bForceBackgroundTree = false;
                
                OwnerComp.RegisterParallelTask(Children[EBTParallelChild::MainTask].ChildTask);
                RequestDelayedExecution(OwnerComp, EBTNodeResult::Succeeded);
            }
        }
        else if (MyMemory->bMainTaskIsActive)
        {
            MyMemory->bMainTaskIsActive = false;
            
            // notify decorators on main task, ignore observers updates in FakeSearchData - they are not allowed by parallel composite
            FBehaviorTreeSearchData FakeSearchData(OwnerComp);
            NotifyDecoratorsOnDeactivation(FakeSearchData, ChildIdx, NodeResult, true /*bIsRequestInSameInstance*/);

            const int32 MyInstanceIdx = OwnerComp.FindInstanceContainingNode(this);

            // use check() here not IntCastChecked() as MyInstanceIdx can be INDEX_NONE!
            check(MyInstanceIdx < MAX_uint16);
            OwnerComp.UnregisterParallelTask(Children[EBTParallelChild::MainTask].ChildTask, static_cast<uint16>(MyInstanceIdx));
            if (NodeResult != EBTNodeResult::Aborted && !MyMemory->bRepeatMainTask)
            {
                // check if subtree should be aborted when task finished with success/failed result
                if (FinishMode == EBTParallelMode::AbortBackground)
                {
                    OwnerComp.RequestExecution((UBTCompositeNode*)this, MyInstanceIdx,
                        Children[EBTParallelChild::MainTask].ChildTask, EBTParallelChild::MainTask,
                        NodeResult);
                }
            }
        }
        else if (NodeResult == EBTNodeResult::Succeeded && FinishMode == EBTParallelMode::WaitForBackground)
        {
            // special case: if main task finished instantly, but composite is supposed to wait for background tree,
            // request single run of background tree anyway

            MyMemory->bForceBackgroundTree = true;

            RequestDelayedExecution(OwnerComp, EBTNodeResult::Succeeded);
        }
    }
}

void UBTComposite_SimpleParallel::NotifyNodeDeactivation(FBehaviorTreeSearchData& SearchData, EBTNodeResult::Type& NodeResult) const
{
    FBTParallelMemory* MyMemory = GetNodeMemory<FBTParallelMemory>(SearchData);
    const uint16 ActiveInstanceIdx = SearchData.OwnerComp.GetActiveInstanceIdx();

    // modify node result if main task has finished
    if (!MyMemory->bMainTaskIsActive)
    {
        NodeResult = MyMemory->MainTaskResult;
    }

    // remove main task
    if (Children.IsValidIndex(EBTParallelChild::MainTask))
    {
        SearchData.AddUniqueUpdate(FBehaviorTreeSearchUpdate(Children[EBTParallelChild::MainTask].ChildTask, ActiveInstanceIdx, EBTNodeUpdateMode::Remove));
    }
}
  • 首次進入Simple Parallel時,會選中Main Task節點並開始執行;而當Main Task開始執行後,Simple Parallel就會持續選中background tree的節點並執行
    • Main Task執行完成後,Simple Parallel節點會收到通知,然後Simple Parallel會判斷:
      • Main Task執行途中被打斷
        • 標記mainTaskIsActive(main task is running)為false
        • 把Main Task從并行執行列表中取消注冊
        • 如果Finish Mode為 AbortBackground(立即打斷),馬上發出一次執行Main Task節點的請求,完成後會重新嘗試GetNextChild,但是由於mainTaskIsActive和forceBackgroundTree都為false,因此整支節點直接返回
      • Main Task執行成功且Finish Mode設為Delayed(FinishMode == WaitForBackground)
        • 標記需要等forceBackgroundTree(是否延遲完成)為true,使Parallel在GetNextChildHandler時,即使Main Task已經執行完畢,仍然會獲取到Background Task作為下個執行的節點
      • 最後返回Main Task的返回結果
  • 目前官方更推薦使用Services節點取代Simple Parallel來做并行