Unreal 4引擎中,使用BehaviorTree控制AI行爲的C++實現

        很多這樣AI的實現都是使用藍圖,尤其在國內網站上,Unreal C++的資料少之又少。本文講述如何用C++實現一個由BehaviorTree控制的AI,並提供源代碼供讀者參考。本文目標受衆是有一定Unreal開發基礎甚至Unreal C++開發基礎的開發人員。

        從結構上,此模塊可劃分爲AI、AIController和BehaviorTree三個部分。

一、AI

        根據實際需要,AI可以是Pawn類型或者Character類型。二者的主要區別在於,Character對物體運動功能的支持更好,自帶了CharacterMovementComponent等。筆者使用的是Pawn。代碼如下:
AGuide.h
#pragma once

#include "GameFramework/Pawn.h"
#include "Guide.generated.h"

UCLASS()
class TOOTH_API AGuide : public APawn
{
    GENERATED_BODY()

public:
    AGuide();

    virtual void BeginPlay() override;

    virtual void Tick(float DeltaSeconds) override;

    virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override;

    // ...

    UPROPERTY(EditAnywhere, Category = "BehaviorTree")
    class UBehaviorTree *BehaviorTree;
    
};
        其中,UPROPERTY宏表明BehaviorTree屬性可以在編輯器中編輯,如下圖所示。

二、AIController

        AIController是AI的大腦,負責控制AI的行爲。代碼中較爲重要的部分是初始化兩個Component,即BehaviorTreeComponent和BlackboardComponent,如下:
GuideController.h
#pragma once

#include "AIController.h"
#include "GuideController.generated.h"


UCLASS()
class TOOTH_API AGuideController : public AAIController
{
    GENERATED_BODY()
    
public:
    AGuideController(const class FObjectInitializer& ObjectInitializer);

    virtual void Possess(class APawn* InPawn) override;

    virtual void UnPossess() override;

    UFUNCTION(BlueprintCallable, Category = "Guide")
    void GuideMoveToActor(AActor* DestinationActor, AActor* TurnToActor, float RelativeDistance);

    UFUNCTION(BlueprintCallable, Category = "Guide")
    void GuideMoveToLocation(FVector DestinationLocation, AActor* TurnToActor, float RelativeDistance);

    // ...

    //重要
    UBehaviorTreeComponent* BehaviorTreeComponent;

    //重要
    UBlackboardComponent* BlackboardComponent;
    
    const FName MoveToActorKeyName = "MoveToActor";
    const FName DestinationActorKeyName = "DestinationActor";
    const FName MoveToLocationKeyName = "MoveToLocation";
    
    // ...


private:
    AActor* PreDestinationActor;

    AActor* PreTurnToActor;
};
GuideController.cpp
#include "Tooth.h"
#include "Guide.h"
#include "GuideController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "BehaviorTree/Blackboard/BlackboardKeyAllTypes.h"

AGuideController::AGuideController(const class FObjectInitializer& ObjectInitializer) 
    : Super(ObjectInitializer), PreDestinationActor(nullptr), PreTurnToActor(nullptr)
{
    BehaviorTreeComponent = ObjectInitializer.CreateDefaultSubobject<UBehaviorTreeComponent>(this, TEXT("BehaviorTree"));
    BlackboardComponent = ObjectInitializer.CreateDefaultSubobject<UBlackboardComponent>(this, TEXT("Blackboard"));
}


void AGuideController::Possess(APawn* InPawn)
{
    Super::Possess(InPawn);

    AGuide* Guide = Cast<AGuide>(InPawn);

    if (Guide)
    {
        if (Guide->BehaviorTree)
        {
            if (Guide->BehaviorTree->BlackboardAsset)
            {
		        //重要
                BlackboardComponent->InitializeBlackboard(*Guide->BehaviorTree->BlackboardAsset);
                BehaviorTreeComponent->StartTree(*Guide->BehaviorTree);
            }
            else
            {
                UE_LOG(GuideLog, Error, TEXT(LOG_HEADER"No blackboard is assigned to the guide's behavior tree."));
            }
        }
        else
        {
            UE_LOG(GuideLog, Error, TEXT(LOG_HEADER"No behavior tree is assigned to guide."));
        }
    }
    else
    {
        UE_LOG(GuideLog, Error, TEXT(LOG_HEADER"The pawn possessed is not an instance of AGuide."));
    }
}


void AGuideController::UnPossess()
{
    Super::UnPossess();
    BehaviorTreeComponent->StopTree();
}


void AGuideController::GuideMoveToActor(AActor* DestinationActor, AActor* TurnToActor, float RelativeDistance)
{
    if (BlackboardComponent)
    {
        BlackboardComponent->SetValueAsFloat(RelativeDistanceKeyName, RelativeDistance);
        if (!BlackboardComponent->GetValueAsBool(MoveToActorKeyName))
        {
            if (!PreDestinationActor || !DestinationActor->GetActorLabel().Equals(PreDestinationActor->GetActorLabel()))
            {
                PreDestinationActor = DestinationActor;
                BlackboardComponent->SetValueAsObject(DestinationActorKeyName, DestinationActor);
                BlackboardComponent->SetValueAsObject(TurnToActorKeyName, TurnToActor);
                BlackboardComponent->SetValueAsBool(MoveToActorKeyName, true);
            }
        }
    }
}


// ...

三、BehaviorTree

        BehaviorTree是AIController控制AI的一種方式。雖然BehaviorTree及Blackboard也可以用C++實現,但非常不推薦,因爲二者實際上是對AI行爲進行設計,設計人員會隨時根據需求更改,並且不存在性能問題(事實是,所有藍圖能做的事情C++都能做,但反過來不成立)。所以這裏我主要介紹BehaviorTree Task的實現。
        首先,我選用的父類是BTTask_BlueprintBase,基於此實現的Task就和藍圖實現的Task在使用方法上是相同的。代碼中關鍵的部分是實現BlackboardKeySelector,如下所示:
GuidePlayAudio.h
#pragma once

#include "BehaviorTree/Tasks/BTTask_BlueprintBase.h"
#include "GuidePlayAudio.generated.h"


UCLASS()
class TOOTH_API UGuidePlayAudio : public UBTTask_BlueprintBase
{
    GENERATED_BODY()

public:
    UGuidePlayAudio(const FObjectInitializer& ObjectInitializer);

    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComponent, uint8* NodeMemory) override;

    //重要
    FName GetSelectedAudioNameKey() const;

    FName GetSelectedPlayAudioKey() const;

    //重要
    UPROPERTY(EditAnywhere, Category = "Blackboard")
    struct FBlackboardKeySelector AudioNameKey;

    UPROPERTY(EditAnywhere, Category = "Blackboard")
    struct FBlackboardKeySelector PlayAudioKey;

private:
    UFUNCTION()
    void SetSoundAndPlay();

    class UAudioComponent* AudioComponent;

    const FString GuideAudioPath = "SoundWave'/Game/StarterContent/Audio/AUDIO_NAME.AUDIO_NAME'";

    class USoundBase* Sound;

    const float FadeOutDuration = 1;
};


FORCEINLINE FName UGuidePlayAudio::GetSelectedAudioNameKey() const
{
    return AudioNameKey.SelectedKeyName;
}

FORCEINLINE FName UGuidePlayAudio::GetSelectedPlayAudioKey() const
{
    return PlayAudioKey.SelectedKeyName;
}
GuidePlayAudio.cpp
#include "Tooth.h"
#include "Guide.h"
#include "GuideController.h"
#include "GuideUtils.h"
#include "Runtime/Engine/Classes/Sound/SoundBase.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "GuidePlayAudio.h"


UGuidePlayAudio::UGuidePlayAudio(const FObjectInitializer& ObjectInitializer)
{

}


EBTNodeResult::Type UGuidePlayAudio::ExecuteTask(UBehaviorTreeComponent& OwnerComponent, uint8* NodeMemory)
{
    UBlackboardComponent* Blackboard = OwnerComponent.GetBlackboardComponent();
    if (Blackboard)
    {
        AGuideController* GuideController = Cast<AGuideController>(OwnerComponent.GetAIOwner());
        AGuide* Guide = Cast<AGuide>(GuideController->GetPawn());
        AudioComponent = Guide->Audio;
        FString AudioName = Blackboard->GetValueAsString(GetSelectedAudioNameKey());
        Sound = FGuideUtils::LoadAssetReference<USoundBase>(GuideAudioPath.Replace(*FString("AUDIO_NAME"), *AudioName));
        if (AudioComponent->IsPlaying())
        {
            USoundBase* PresentSound = AudioComponent->Sound;
            if (!Sound->GetName().Equals(PresentSound->GetName()))
            {
                AudioComponent->FadeOut(FadeOutDuration, 0);
                AudioComponent->OnAudioFinished.AddDynamic(this, &UGuidePlayAudio::SetSoundAndPlay);
            }
        }
        else
        {
            SetSoundAndPlay();
        }
        Blackboard->SetValueAsBool(GetSelectedPlayAudioKey(), false);
        UE_LOG(GuideLog, Log, TEXT(LOG_HEADER"Guide play audio finished."));
        return EBTNodeResult::Succeeded;
    }
    else
    {
        UE_LOG(GuideLog, Error, TEXT(LOG_HEADER"Blackboard of the behavior tree is null."));
        return EBTNodeResult::Failed;
    }
}


void UGuidePlayAudio::SetSoundAndPlay()
{
    AudioComponent->SetSound(Sound);
    AudioComponent->FadeIn(0);
    AudioComponent->OnAudioFinished.Clear();
}
        編譯後,就會在BehaviorTree的藍圖中看到這個Task。

        所有代碼寫完並且編譯通過後,設計人員就可以在藍圖中設計BehaviorTree了。運行之前,要指定AI的BehaviorTree和AIController Class。至於具體怎麼操作AI,AIController已經給你留了許多接口,甚至你可以在BehaviorTree中設計AI的自主行爲。

【附】
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章