一种简单的C++AI Task实现

经过考虑,决定使用C++写AI类以提升性能。
但对于C++的AI Task类没有经验,也没有一些优秀的样例学习参考,于是想到参考引擎内的实现。

参考蓝图类UBTTask_BlueprintBase的实现方式,可以新建C++类并复制内容。

注意,此处应当注释或删除FBTNodeBPImplementationHelper::AISpecific,此为常量会影响运行。

复制之后,可以将此作为C++的AI基类。在子类的构造函数中启用相应的变量,即可启用对应的函数回调。

下面是个子类例子(基类代码放在最后面):

//.CPP File
//构造器中启用ReceiveExecuteAI、Tick的回调:
UBTTask_Follow::UBTTask_Follow(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
    ReceiveExecuteImplementations = 1;
    ReceiveTickImplementations = 1;
    bNotifyTick =1;
}

//实现回调:
void UBTTask_Follow::ReceiveExecuteAI(AAIController* OwnerController, APawn* ControlledPawn)
{
}

void UBTTask_Follow::ReceiveTickAI(AAIController* OwnerController, APawn* ControlledPawn, float DeltaSeconds)
{
}
//.h File
UCLASS()
class PROJECTSPARK_API UBTTask_Follow : public UTaskNodeBase
{
    GENERATED_BODY()
    UBTTask_Follow(const FObjectInitializer& ObjectInitializer);

    public:

    protected:
    virtual void ReceiveExecuteAI(AAIController* OwnerController, APawn* ControlledPawn) override;
    virtual void ReceiveTickAI(AAIController* OwnerController, APawn* ControlledPawn, float DeltaSeconds) override;
};

下面是基类实现:

//.h File
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "BehaviorTree/BTTaskNode.h"
#include "TaskNodeBase.generated.h"

UCLASS()
class PROJECTSPARK_API UTaskNodeBase : public UBTTaskNode
{
    GENERATED_BODY()
    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
    virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;

    virtual FString GetStaticDescription() const override;
    virtual void DescribeRuntimeValues(const UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTDescriptionVerbosity::Type Verbosity, TArray<FString>& Values) const override;
    virtual void OnInstanceDestroyed(UBehaviorTreeComponent& OwnerComp) override;
    virtual void OnTaskFinished(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTNodeResult::Type TaskResult) override;
    virtual void InitializeFromAsset(UBehaviorTree& Asset) override;

    virtual void SetOwner(AActor* ActorOwner) override;

#if WITH_EDITOR
    virtual bool UsesBlueprint() const override;
#endif

public:
    UTaskNodeBase(const FObjectInitializer& ObjectInitializer);

protected:
    /** Cached AIController owner of BehaviorTreeComponent. */
    UPROPERTY(Transient)
    TObjectPtr<AAIController> AIOwner;

    /** Cached actor owner of BehaviorTreeComponent. */
    UPROPERTY(Transient)
    TObjectPtr<AActor> ActorOwner;

    /** If any of the Tick functions is implemented, how often should they be ticked.
     *  Values < 0 mean 'every tick'. */
    UPROPERTY(EditAnywhere, Category = Task)
    FIntervalCountdown TickInterval;

    /** temporary variable for ReceiveExecute(Abort)-FinishExecute(Abort) chain */
    mutable TEnumAsByte<EBTNodeResult::Type> CurrentCallResult;

    /** properties that should be copied */
    TArray<FProperty*> PropertyData;

#if WITH_EDITORONLY_DATA
    UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Description)
    FString CustomDescription;
#endif // WITH_EDITORONLY_DATA

    /** show detailed information about properties */
    UPROPERTY(EditInstanceOnly, Category=Description)
    uint32 bShowPropertyDetails : 1;

    /** set if ReceiveTick is implemented by blueprint */
    uint32 ReceiveTickImplementations : 2;

    /** set if ReceiveExecute is implemented by blueprint */
    uint32 ReceiveExecuteImplementations : 2;

    /** set if ReceiveAbort is implemented by blueprint */
    uint32 ReceiveAbortImplementations : 2;

    /** set when task enters Aborting state */
    uint32 bIsAborting : 1;

    /** if set, execution is inside blueprint's ReceiveExecute(Abort) event
      * FinishExecute(Abort) function should store their result in CurrentCallResult variable */
    mutable uint32 bStoreFinishResult : 1;

    /** entry point, task will stay active until FinishExecute is called.
     *  @Note that if both generic and AI event versions are implemented only the more 
     *  suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
    UFUNCTION(BlueprintImplementableEvent)
    void ReceiveExecute(AActor* OwnerActor);

    /** if blueprint graph contains this event, task will stay active until FinishAbort is called
     *  @Note that if both generic and AI event versions are implemented only the more
     *  suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
    UFUNCTION(BlueprintImplementableEvent)
    void ReceiveAbort(AActor* OwnerActor);

    /** tick function
     *  @Note that if both generic and AI event versions are implemented only the more
     *  suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
    UFUNCTION(BlueprintImplementableEvent)
    void ReceiveTick(AActor* OwnerActor, float DeltaSeconds);

    /** Alternative AI version of ReceiveExecute
    *   @see ReceiveExecute for more details
     *  @Note that if both generic and AI event versions are implemented only the more
     *  suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
    virtual void ReceiveExecuteAI(AAIController* OwnerController, APawn* ControlledPawn);

    /** Alternative AI version of ReceiveAbort
     *  @see ReceiveAbort for more details
     *  @Note that if both generic and AI event versions are implemented only the more
     *  suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
    UFUNCTION(BlueprintImplementableEvent, Category = AI)
    void ReceiveAbortAI(AAIController* OwnerController, APawn* ControlledPawn);

    /** Alternative AI version of tick function.
     *  @see ReceiveTick for more details
     *  @Note that if both generic and AI event versions are implemented only the more
     *  suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
    virtual void ReceiveTickAI(AAIController* OwnerController, APawn* ControlledPawn, float DeltaSeconds);

    /** finishes task execution with Success or Fail result */
    UFUNCTION(BlueprintCallable, Category="AI|BehaviorTree")
    void FinishExecute(bool bSuccess);

    /** aborts task execution */
    UFUNCTION(BlueprintCallable, Category="AI|BehaviorTree")
    void FinishAbort();

    /** task execution will be finished (with result 'Success') after receiving specified message */
    UFUNCTION(BlueprintCallable, Category="AI|BehaviorTree")
    void SetFinishOnMessage(FName MessageName);

    /** task execution will be finished (with result 'Success') after receiving specified message with indicated ID */
    UFUNCTION(BlueprintCallable, Category="AI|BehaviorTree")
    void SetFinishOnMessageWithId(FName MessageName, int32 RequestID = -1);

    /** check if task is currently being executed */
    UFUNCTION(BlueprintCallable, Category="AI|BehaviorTree")
    bool IsTaskExecuting() const;

    /** check if task is currently being aborted */
    UFUNCTION(BlueprintCallable, Category = "AI|BehaviorTree")
    bool IsTaskAborting() const;

    /** ticks this task */
    virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};
//.cpp File 
// Fill out your copyright notice in the Description page of Project Settings.

#include "AIModule/TaskNodeBase.h"
#include "AIController.h"
#include "BehaviorTree/BTFunctionLibrary.h"
#include "BlueprintNodeHelpers.h"
#include "BehaviorTree/BehaviorTree.h"

UTaskNodeBase::UTaskNodeBase(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
    UClass* StopAtClass = UTaskNodeBase::StaticClass();
    ReceiveTickImplementations = FBTNodeBPImplementationHelper::CheckEventImplementationVersion(TEXT("ReceiveTick"), TEXT("ReceiveTickAI"), *this, *StopAtClass);
    ReceiveExecuteImplementations = FBTNodeBPImplementationHelper::CheckEventImplementationVersion(TEXT("ReceiveExecute"), TEXT("ReceiveExecuteAI"), *this, *StopAtClass);
    ReceiveAbortImplementations = FBTNodeBPImplementationHelper::CheckEventImplementationVersion(TEXT("ReceiveAbort"), TEXT("ReceiveAbortAI"), *this, *StopAtClass);

    bNotifyTick = ReceiveTickImplementations != FBTNodeBPImplementationHelper::NoImplementation;
    bNotifyTaskFinished = true;
    bShowPropertyDetails = true;
    bIsAborting = false;

    // all blueprint based nodes must create instances
    bCreateNodeInstance = true;

    if (HasAnyFlags(RF_ClassDefaultObject))
    {
        BlueprintNodeHelpers::CollectPropertyData(this, StopAtClass, PropertyData);
    }
}

void UTaskNodeBase::InitializeFromAsset(UBehaviorTree& Asset)
{
    Super::InitializeFromAsset(Asset);
    if (Asset.BlackboardAsset)
    {
        BlueprintNodeHelpers::ResolveBlackboardSelectors(*this, *StaticClass(), *Asset.BlackboardAsset);
    }
}

void UTaskNodeBase::SetOwner(AActor* InActorOwner) 
{ 
    ActorOwner = InActorOwner;
    AIOwner = Cast<AAIController>(InActorOwner);
}

EBTNodeResult::Type UTaskNodeBase::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    // fail when task doesn't react to execution (start or tick)
    CurrentCallResult = (ReceiveExecuteImplementations != 0 || ReceiveTickImplementations != 0) ? EBTNodeResult::InProgress : EBTNodeResult::Failed;
    bIsAborting = false;

    if (ReceiveExecuteImplementations != FBTNodeBPImplementationHelper::NoImplementation)
    {
        bStoreFinishResult = true;

        if (AIOwner != nullptr && (ReceiveExecuteImplementations/*& FBTNodeBPImplementationHelper::AISpecific*/))
        {
            ReceiveExecuteAI(AIOwner, AIOwner->GetPawn());
        }
        else if (ReceiveExecuteImplementations & FBTNodeBPImplementationHelper::Generic)
        {
            ReceiveExecute(ActorOwner);
        }

        bStoreFinishResult = false;
    }

    return CurrentCallResult;
}

EBTNodeResult::Type UTaskNodeBase::AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    // force dropping all pending latent actions associated with this blueprint
    // we can't have those resuming activity when node is/was aborted
    BlueprintNodeHelpers::AbortLatentActions(OwnerComp, *this);

    CurrentCallResult = ReceiveAbortImplementations != 0 ? EBTNodeResult::InProgress : EBTNodeResult::Aborted;
    bIsAborting = true;

    if (ReceiveAbortImplementations != FBTNodeBPImplementationHelper::NoImplementation)
    {
        bStoreFinishResult = true;

        if (AIOwner != nullptr && (ReceiveAbortImplementations & FBTNodeBPImplementationHelper::AISpecific))
        {
            ReceiveAbortAI(AIOwner, AIOwner->GetPawn());
        }
        else if (ReceiveAbortImplementations & FBTNodeBPImplementationHelper::Generic)
        {
            ReceiveAbort(ActorOwner);
        }

        bStoreFinishResult = false;
    }

    return CurrentCallResult;
}

void UTaskNodeBase::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    if (TickInterval.Tick(DeltaSeconds))
    {
        DeltaSeconds = TickInterval.GetElapsedTimeWithFallback(DeltaSeconds);

        if (AIOwner != nullptr && (ReceiveTickImplementations /*& FBTNodeBPImplementationHelper::AISpecific*/))
        {
            ReceiveTickAI(AIOwner, AIOwner->GetPawn(), DeltaSeconds);
        }
        else if (ReceiveTickImplementations & FBTNodeBPImplementationHelper::Generic)
        {
            ReceiveTick(ActorOwner, DeltaSeconds);
        }

        TickInterval.Reset();
    }
}

void UTaskNodeBase::OnTaskFinished(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTNodeResult::Type TaskResult)
{
    Super::OnTaskFinished(OwnerComp, NodeMemory, TaskResult);

    TickInterval.Set(0); // so that we tick as soon as enabled back

    if (TaskResult != EBTNodeResult::InProgress)
    {
        BlueprintNodeHelpers::AbortLatentActions(OwnerComp, *this);
    }
}

void UTaskNodeBase::FinishExecute(bool bSuccess)
{
    UBehaviorTreeComponent* OwnerComp = Cast<UBehaviorTreeComponent>(GetOuter());
    EBTNodeResult::Type NodeResult(bSuccess ? EBTNodeResult::Succeeded : EBTNodeResult::Failed);

    if (bStoreFinishResult)
    {
        CurrentCallResult = NodeResult;
    }
    else if (OwnerComp && !bIsAborting)
    {
        FinishLatentTask(*OwnerComp, NodeResult);
    }
}

void UTaskNodeBase::FinishAbort()
{
    UBehaviorTreeComponent* OwnerComp = Cast<UBehaviorTreeComponent>(GetOuter());
    EBTNodeResult::Type NodeResult(EBTNodeResult::Aborted);

    if (bStoreFinishResult)
    {
        CurrentCallResult = NodeResult;
    }
    else if (OwnerComp && bIsAborting)
    {
        FinishLatentAbort(*OwnerComp);
    }
}

bool UTaskNodeBase::IsTaskExecuting() const
{
    UBehaviorTreeComponent* OwnerComp = Cast<UBehaviorTreeComponent>(GetOuter());
    EBTTaskStatus::Type TaskStatus = OwnerComp->GetTaskStatus(this);

    return (TaskStatus == EBTTaskStatus::Active);
}

bool UTaskNodeBase::IsTaskAborting() const
{
    // use already cached data
    return bIsAborting;
}

void UTaskNodeBase::SetFinishOnMessage(FName MessageName)
{
    UBehaviorTreeComponent* OwnerComp = Cast<UBehaviorTreeComponent>(GetOuter());
    if (OwnerComp)
    {
        OwnerComp->RegisterMessageObserver(this, MessageName);
    }
}

void UTaskNodeBase::SetFinishOnMessageWithId(FName MessageName, int32 RequestID)
{
    UBehaviorTreeComponent* OwnerComp = Cast<UBehaviorTreeComponent>(GetOuter());
    if (OwnerComp)
    {
        OwnerComp->RegisterMessageObserver(this, MessageName, RequestID);
    }
}

FString UTaskNodeBase::GetStaticDescription() const
{
    FString ReturnDesc =
#if WITH_EDITORONLY_DATA
        CustomDescription.Len() ? CustomDescription :
#endif // WITH_EDITORONLY_DATA
        Super::GetStaticDescription();

    UTaskNodeBase* CDO = (UTaskNodeBase*)(GetClass()->GetDefaultObject());
    if (bShowPropertyDetails && CDO)
    {
        UClass* StopAtClass = UTaskNodeBase::StaticClass();
        FString PropertyDesc = BlueprintNodeHelpers::CollectPropertyDescription(this, StopAtClass, CDO->PropertyData);
        if (PropertyDesc.Len())
        {
            ReturnDesc += TEXT(":\n\n");
            ReturnDesc += PropertyDesc;
        }
    }

    return ReturnDesc;
}

void UTaskNodeBase::DescribeRuntimeValues(const UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTDescriptionVerbosity::Type Verbosity, TArray<FString>& Values) const
{
    UTaskNodeBase* CDO = (UTaskNodeBase*)(GetClass()->GetDefaultObject());
    if (CDO && CDO->PropertyData.Num())
    {
        BlueprintNodeHelpers::DescribeRuntimeValues(this, CDO->PropertyData, Values);
    }
}

void UTaskNodeBase::OnInstanceDestroyed(UBehaviorTreeComponent& OwnerComp)
{
    // force dropping all pending latent actions associated with this blueprint
    BlueprintNodeHelpers::AbortLatentActions(OwnerComp, *this);
}

#if WITH_EDITOR

bool UTaskNodeBase::UsesBlueprint() const
{
    return true;
}

#endif // WITH_EDITOR

void UTaskNodeBase::ReceiveExecuteAI(AAIController* OwnerController, APawn* ControlledPawn)
{
}

void UTaskNodeBase::ReceiveTickAI(AAIController* OwnerController, APawn* ControlledPawn, float DeltaSeconds)
{
}