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

行為樹Decorator節點說明

  • 用於決定節點能否執行/ 對節點的返回結果進行修改,Compositie節點和Task節點上都可以添加
    • 右鍵節點 -> Add Decorator

內置Decorator

  • Blackboard:判斷blackboard裡某Key的值的狀態
  • Check Gameplay Tags On Actor:判斷blackboard裡某個Actor類型Key的Gameplay Tags
  • Compare BBEntries:對blackboard裡的兩個Key進行比較
  • Composite:組合Decorator藍圖,可以自定義添加多個Decorator及And/Or/Not組合成一個複合條件Decorator
  • Conditional Loop:黑板的某個Key符合條件就一直循環
  • Cone Check:從Cone Origin開始,往Cone Direction建立一個半角為Cone Half Angle的視錐,判斷Observed是否在視錐範圍裡
  • Cooldown:進入節點後,會開始指定時間的計時,在計時結束前每次嘗試進入該節點,都會直接返回
  • Does Path Exist: 判斷黑板中的兩個Actor Key之間是否有可尋路徑
    • Path Query Type:
      • Navmesh Raycast 2D 最快,使用NavMesh網格的數據進行尋路
      • Hierarchical Query / Regular Pathfinding 都比較慢,使用UE的另一套的尋路算法進行尋路
//BTDecorator_DoesPathExist.cpp

bool UBTDecorator_DoesPathExist::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
    const UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
    if (BlackboardComp == NULL)
    {
        return false;
    }

    FVector PointA = FVector::ZeroVector;
    FVector PointB = FVector::ZeroVector;    
    const bool bHasPointA = BlackboardComp->GetLocationFromEntry(BlackboardKeyA.GetSelectedKeyID(), PointA);
    const bool bHasPointB = BlackboardComp->GetLocationFromEntry(BlackboardKeyB.GetSelectedKeyID(), PointB);
    
    bool bHasPath = false;

    const UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(OwnerComp.GetWorld());
    if (NavSys && bHasPointA && bHasPointB)
    {
        const AAIController* AIOwner = OwnerComp.GetAIOwner();
        const ANavigationData* NavData = AIOwner ? NavSys->GetNavDataForProps(AIOwner->GetNavAgentPropertiesRef(), AIOwner->GetNavAgentLocation()) : NULL;
        if (NavData)
        {
            FSharedConstNavQueryFilter QueryFilter = UNavigationQueryFilter::GetQueryFilter(*NavData, AIOwner, FilterClass);

            if (PathQueryType == EPathExistanceQueryType::NavmeshRaycast2D)
            {
#if WITH_RECAST
                const ARecastNavMesh* RecastNavMesh = Cast<const ARecastNavMesh>(NavData);
                bHasPath = RecastNavMesh && RecastNavMesh->IsSegmentOnNavmesh(PointA, PointB, QueryFilter);
#endif
            }
            else
            {
                EPathFindingMode::Type TestMode = (PathQueryType == EPathExistanceQueryType::HierarchicalQuery) ? EPathFindingMode::Hierarchical : EPathFindingMode::Regular;
                bHasPath = NavSys->TestPathSync(FPathFindingQuery(AIOwner, *NavData, PointA, PointB, QueryFilter), TestMode);
            }
        }
    }

    return bHasPath;
}

//NavigationSystem.cpp
bool UNavigationSystemV1::TestPathSync(FPathFindingQuery Query, EPathFindingMode::Type Mode, int32* NumVisitedNodes) const
{
    SCOPE_CYCLE_COUNTER(STAT_Navigation_PathfindingSync);
    CSV_SCOPED_TIMING_STAT(NavigationSystem, PathfindingSync);

    if (Query.NavData.IsValid() == false)
    {
        Query.NavData = GetDefaultNavDataInstance();
    }
    
    bool bExists = false;
    if (Query.NavData.IsValid())
    {
        if (Mode == EPathFindingMode::Hierarchical)
        {
            bExists = Query.NavData->TestHierarchicalPath(Query.NavAgentProperties, Query, NumVisitedNodes);
        }
        else
        {
            bExists = Query.NavData->TestPath(Query.NavAgentProperties, Query, NumVisitedNodes);
        }
    }

    return bExists;
}
  • Force Success: 節點返回值修改為true
  • Is At Location:判斷當前位置是否在blackboard的某個Vector key的指定半徑範圍內
    • Inverse Condition:判斷是否不在範圍內
    • 可以用來做一些攻撃/交互可行性的判斷
  • Is BBEntry Of Class:檢查blackboard裡的某個Object是否為指定類型
  • Keep in Cone:判斷某被觀察對象是否在某觀察者對象的視錐範圍內,半角可指定
  • Loop:循環次數,判斷某個節點的進入次數是否 < Num Loops
  • Set Tag Cooldown / Tag Cooldown:給指定Gameplay Tag設置冷卻時間,具體判定邏輯與Cooldown一樣
  • Time Limit:對節點的運行時間增加時間限制,節點運行超時後返回Failed,每次進入節點時計時器都會重置

自定義Decorator

  • 行為樹上方 -> New Decorator
  • Decorator藍圖提供了5式10種事件函數和1式2種的一般函數重載,最重要的就是這個一般函數 Perform Condition Check,其他都是一些時機事件。且帶AI後綴的才能拿到AI Controller,否則只能拿到Actor 
    • Receive Execution Finish(AI):依附的Task節點執行完成
    • Receive Execution Start(AI):依附的Task節點開始執行
    • Receive Observer Activated(AI) (Deactivated(AI))
      • Observer指的abort observer的observer
      • abort observer == self
        • observer為當前子樹
        • observer activated:當前執行保留在子樹時激活
        • deactivated: 當前執行離開子樹時激活
      • abort observer == Lower priority
        • observer為低優先級子樹
        • activated: 當前執行保留在低優先級子樹時激活
        • deactivated: 當前執行離開在低優先級子樹時激活
      • abort observer == both
        • observer為當前子樹及低優先級子樹
        • activated:當前執行保留在子樹時和在低優先級子樹時激活(待實驗)
        • deactivated:當前執行離開子樹和低優先級子樹時激活(待實驗)
    • Receive Tick(AI):當Observer被激活時,Receive Observer Activated被調用,Receive Tick事件才開始每幀被調用
    • Perform Condition Check(AI):核心重載,用來寫Decorator的具體判斷邏輯
      • 每Tick和BlackboardKey值改變時會被調用

//BTDecorator_BlueprintBase.cpp
void UBTDecorator_BlueprintBase::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    if (AIOwner != nullptr && (ReceiveTickImplementations & FBTNodeBPImplementationHelper::AISpecific))
    {
        ReceiveTickAI(AIOwner, AIOwner->GetPawn(), DeltaSeconds);
    }
    else if (ReceiveTickImplementations & FBTNodeBPImplementationHelper::Generic)
    {
        ReceiveTick(ActorOwner, DeltaSeconds);
    }
        
    // possible this got ticked due to the decorator being configured as an observer
    if (GetNeedsTickForConditionChecking())
    {
        RequestAbort(OwnerComp, EvaluateAbortType(OwnerComp));
    }
}

UBTDecorator_BlueprintBase::EAbortType  UBTDecorator_BlueprintBase::EvaluateAbortType(UBehaviorTreeComponent& OwnerComp) const
{
    if (PerformConditionCheckImplementations == 0)
    {
        return EAbortType::Unknown;
    }

    if (FlowAbortMode == EBTFlowAbortMode::None)
    {
        return EAbortType::NoAbort;
    }

    const bool bIsOnActiveBranch = OwnerComp.IsExecutingBranch(GetMyNode(), GetChildIndex());

    EAbortType AbortType = EAbortType::NoAbort;
    if (bIsOnActiveBranch)
    {
        if ((FlowAbortMode == EBTFlowAbortMode::Self || FlowAbortMode == EBTFlowAbortMode::Both) && CalculateRawConditionValue(OwnerComp, /*NodeMemory*/nullptr) == IsInversed())
        {
            AbortType = EAbortType::DeactivateBranch;
        }
    }
    else 
    {
        if ((FlowAbortMode == EBTFlowAbortMode::LowerPriority || FlowAbortMode == EBTFlowAbortMode::Both) && CalculateRawConditionValue(OwnerComp, /*NodeMemory*/nullptr) != IsInversed())
        {
            AbortType = EAbortType::ActivateBranch;
        }
    }

    return AbortType;
}

bool UBTDecorator_BlueprintBase::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
    bool CurrentCallResult = false;
    if (PerformConditionCheckImplementations != 0)
    {
        // can't use const functions with blueprints
        UBTDecorator_BlueprintBase* MyNode = (UBTDecorator_BlueprintBase*)this;

        if (AIOwner != nullptr && (PerformConditionCheckImplementations & FBTNodeBPImplementationHelper::AISpecific))
        {
            CurrentCallResult = MyNode->PerformConditionCheckAI(MyNode->AIOwner, MyNode->AIOwner->GetPawn());
        }
        else if (PerformConditionCheckImplementations & FBTNodeBPImplementationHelper::Generic)
        {
            CurrentCallResult = MyNode->PerformConditionCheck(MyNode->ActorOwner);
        }
    }

    return CurrentCallResult;
}

打斷設置

  • 當Decorator觀察的條件變動時,可以指定打斷自己/同級行為樹的邏輯 => Observer aborts
    • None
      • 不打斷,等待本次執行結束
    • Self
      • 一旦狀態更改且不滿足Decorator條件,當前子樹就會立即中止
    • LowerPriority
      • 一旦狀態更改且滿足Decorator條件,低優先級子樹就會立即中止,並執行跳轉到Decorator所在節點
    • Both
      • 一旦狀態更改且不滿足Decorator條件,子樹就會立即中止
      • 一旦狀態更改且滿足Decorator條件,低優先級子樹就會立即中止,並執行跳轉到Decorator所在節點