很多这样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的自主行为。
【附】