很多這樣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的自主行爲。
【附】