들어가기전에..
현재 작업중인 프로젝트의 몬스터클래스와 AI컨트롤러 클래스를 예시로 설명합니다.
여러분에게는 이미 몬스터클래스가 있다고 가정합니다.
또한 에디터에 이미 NavMeshBoundsVolume이 셋팅이 되어있다고 가정합니다.
이 글에서 우리는 AI컨트롤러에 AI Perception을 붙이고 플레이어를 발견하기전까지 임의의 지점으로 계속 이동하는것을 구현합니다.
에디터에서 비헤이비어트리와 블랙보드를 추가합니다.
블랙보드는 AI의 기억저장소를 담당합니다.
우선은 블랙보드의 변수를 담을 클래스를 생성합니다. 여기에서는 빈 클래스를 사용했습니다.
[BlackBoardKeys.h]
#pragma once
#include "Runtime/Core/Public/UObject/NameTypes.h"
#include "Runtime/Core/Public/Containers/UnrealString.h"
namespace bb_keys
{
TCHAR const * const target_location = TEXT("TargetLocation");
TCHAR const * const can_see_player = TEXT("CanSeePlayer");
TCHAR const * const player_is_in_melee_range = TEXT("IsPlayerInMeRange");
}
저는 편의성을 위해 C++과 에디터의 블랙보드 변수를 연결시키려고 이렇게 작성했습니다.
cpp에는 따로 작성할 것이 없습니다.
먼저 AI Perception을 부착할 AIController 클래스를 생성합니다.
저는 이 클래스 이름을 MeleeEnemyAIController로 정했습니다.
[MeleeEnemyAIController.h]
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "Perception/AIPerceptionTypes.h"
#include "MeleeEnemyAIController.generated.h"
/**
*
*/
UCLASS()
class PORTFOLIOPROJECT_API AMeleeEnemyAIController : public AAIController
{
GENERATED_BODY()
public:
AMeleeEnemyAIController(FObjectInitializer const& object_initializer);
void BeginPlay() override;
void OnPossess(APawn* pawn) override;
class UBlackboardComponent* get_blackboard() const;
private:
UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category = "AI", meta = (AllowPrivateAccess = "true"))
class UBehaviorTreeComponent* behavior_tree_component;
UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category = "AI", meta = (AllowPrivateAccess = "true"))
class UBehaviorTree* btree;
class UBlackboardComponent* blackboard;
class UAISenseConfig_Sight* SightConfig;
public:
static const FName HomePosKey;
static const FName TargetLocation;
UFUNCTION()
void OnUpdated(TArray<AActor*> const & updated_actors);
UFUNCTION()
void OnTargetDetected(AActor* actor, FAIStimulus const Stimulus);
UFUNCTION()
void SetPerceptionSystem();
//AI Perception 변수
public:
UPROPERTY(EditAnywhere,BlueprintReadWrite)
float AISightRadius = 500.f;
UPROPERTY(EditAnywhere,BlueprintReadWrite)
float AILoseSightRadius = 50.f;
UPROPERTY(EditAnywhere,BlueprintReadWrite)
float AIFieldOfView = 90.f;
UPROPERTY(EditAnywhere,BlueprintReadWrite)
float AISightAge = 5.f;
UPROPERTY(EditAnywhere,BlueprintReadWrite)
float AILastSeenLocation = 900.f;
};
AIController에 붙일 BehaiviorTreeComponent와 BlackBoardComponent 클래스를 선언합니다.
SetPerceptionSystem()함수를 만들어 AI Perception 변수를 한번에 초기화 하려고 합니다.
[MeleeEnemyAIController.cpp]
#include "MeleeEnemyAIController.h"
#include "DoubleHitEnemy.h"
#include "UObject/ConstructorHelpers.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Perception/AISenseConfig_Sight.h"
#include "Perception/AIPerceptionStimuliSourceComponent.h"
#include "Perception/AIPerceptionComponent.h"
#include "BlackBoardKeys.h"
#include "Runtime/Engine/Classes/Kismet/GameplayStatics.h"
#include "Runtime/Engine/Classes/Engine/World.h"
#include "EngineGlobals.h"
#include "PortfolioProjectCharacter.h"
const FName AMeleeEnemyAIController::HomePosKey(TEXT("HomePos"));
const FName AMeleeEnemyAIController::TargetLocation(TEXT("TargetLocation"));
AMeleeEnemyAIController::AMeleeEnemyAIController(FObjectInitializer const& object_initializer)
{
//ConstructorHelpers로 에디터에 미리 만들어둔 비헤이비어트리를 지정
static ConstructorHelpers::FObjectFinder<UBehaviorTree> BTObject(TEXT("BehaviorTree'/Game/Movable/AI/BT_MeleeEnemy.BT_MeleeEnemy'"));
if (BTObject.Succeeded())
{
btree = BTObject.Object;
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("BT completed!"));
}
behavior_tree_component = object_initializer.CreateDefaultSubobject<UBehaviorTreeComponent>(this, TEXT("BehaviorComp"));
blackboard = object_initializer.CreateDefaultSubobject<UBlackboardComponent>(this, TEXT("BlackboardComp"));
//Perception초기화
SetPerceptionSystem();
}
void AMeleeEnemyAIController::BeginPlay()
{
Super::BeginPlay();
RunBehaviorTree(btree);
behavior_tree_component->StartTree(*btree);
}
void AMeleeEnemyAIController::OnPossess(APawn* pawn)
{
Super::OnPossess(pawn);
if (blackboard)
{
//비헤이비어트리에 있는 블랙보드로 초기화
blackboard->InitializeBlackboard(*btree->BlackboardAsset);
}
}
UBlackboardComponent* AMeleeEnemyAIController::get_blackboard() const
{
return blackboard;
}
void AMeleeEnemyAIController::OnUpdated(TArray<AActor*> const& updated_actors)
{
}
void AMeleeEnemyAIController::OnTargetDetected(AActor* actor, FAIStimulus const Stimulus)
{
if (auto const player = Cast<APortfolioProjectCharacter>(actor))
{
//성공적으로 감지하면 블랙보드에 true값을 넣어준다.
get_blackboard()->SetValueAsBool(bb_keys::can_see_player,Stimulus.WasSuccessfullySensed());
}
}
void AMeleeEnemyAIController::SetPerceptionSystem()
{
SightConfig = CreateOptionalDefaultSubobject<UAISenseConfig_Sight>(TEXT("Sight Config"));
SetPerceptionComponent(*CreateOptionalDefaultSubobject<UAIPerceptionComponent>(TEXT("AI Perception")));
SightConfig->SightRadius = AISightRadius;
SightConfig->LoseSightRadius = SightConfig->SightRadius+AILoseSightRadius;
SightConfig->PeripheralVisionAngleDegrees = AIFieldOfView;
SightConfig->SetMaxAge(AISightAge);
SightConfig->AutoSuccessRangeFromLastSeenLocation = AILastSeenLocation;
SightConfig->DetectionByAffiliation.bDetectEnemies = true;
SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
SightConfig->DetectionByAffiliation.bDetectFriendlies = true;
GetPerceptionComponent()->SetDominantSense(*SightConfig->GetSenseImplementation());
GetPerceptionComponent()->OnTargetPerceptionUpdated.AddDynamic(this,&AMeleeEnemyAIController::OnTargetDetected);
GetPerceptionComponent()->ConfigureSense(*SightConfig);
}
ContructorHelpers를 통해 에디터의 비헤이비어트리를 가리키고 그것을 btree변수에 초기화합니다.
이후 Onpossess()함수에서 btree의 blackboard 또한 초기화 합니다.
SetPerceptionSystem()함수를 작성하고 MeleeEnemyAIController생성자에서 초기화합니다.
알아둬야할 것은 OnTargetDetected함수입니다.
이부분에서 Player를 발견하면 SetValueAsBool함수를 통해 true로 바꿔줍니다.
만일 WasSuccessfullySensed()의 반환값이 false라면 false가 저장이 됩니다.
잠시 에디터로 돌아와서 블랙보드에 vector타입의 TargetLocation, bool타입의 CanSeePlayer 변수를 추가합니다.
추가한 이유는 간단합니다. AI는 TargetLocation변수에 플레이어의 위치를 저장해놨다가
그것을 읽고 해당하는 위치로 이동할 것입니다. 그리고 AIPerception을 통해 플레이어를 보았는지 확인하고 CanSeePlayer변수에 true 또는 false를 저장할 것입니다.
이제 우리는 AI의 Perception과 인지했을 때 그것을 저장하기 위한 BlackBoard(기억 저장소)와 변수를 만들었습니다. 그렇다면 남은 것은 Task 즉 행동입니다. 이것을 활용해서 몬스터가 플레이어를 발견하기 전까지 랜덤한 지점을 계속 이동하도록 하게 합니다.
BTTaskNode클래스를 생성합니다.
저는 이 클래스 이름을 FindPatrolPosTask로 정했습니다.
[FindPatrolPosTask.h]
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/Tasks/BTTask_BlackboardBase.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BehaviorTree/BehaviorTreeTypes.h"
#include "UObject/UObjectGlobals.h"
#include "FindPatrolPosTask.generated.h"
/**
*
*/
//Blueprintable로 바꿔줘야 에디터상에 BP로 뽑아낼 수 있습니다.
UCLASS(Blueprintable)
class PORTFOLIOPROJECT_API UFindPatrolPosTask : public UBTTaskNode
{
GENERATED_BODY()
public:
UFindPatrolPosTask(FObjectInitializer const& object_initializer);
EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory);
private:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Search", meta = (AllowPrivateAccess = "true"))
float search_radius = 1500.f;
};
ExecuteTask()함수에서 몬스터가 할 것을 실행합니다.
그리고 그 반환값은 성공 또는 실패가 됩니다. 이것은 개발자가 정하는 것입니다.
serch_radius변수를 두어 랜덤 이동 범위를 제한했습니다.
[FindPatrolPosTask.cpp]
#include "FindPatrolPosTask.h"
#include "MeleeEnemyAIController.h"
#include "NavigationSystem.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/Blackboard/BlackboardKeyType_Vector.h"
#include "BlackBoardKeys.h"
#include "BehaviorTree/BlackboardComponent.h"
UFindPatrolPosTask::UFindPatrolPosTask(FObjectInitializer const& object_initializer)
{
//에디터에 보여질 TaskNode의 이름
NodeName = TEXT("FindPatrolPosTask");
}
EBTNodeResult::Type UFindPatrolPosTask::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
auto Controller = Cast<AMeleeEnemyAIController>(OwnerComp.GetAIOwner());
auto Enemy = Controller->GetPawn();
if (nullptr == Enemy) {
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("Enemy Initialize faild!"));
return EBTNodeResult::Failed;
}
//현재 에디터에 설정된 navi mesh로 초기화
UNavigationSystemV1* const NavSystem = UNavigationSystemV1::GetCurrent(GetWorld());
if (nullptr == NavSystem) {
//navi mesh가 없다면 실패를 반환
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("No Enemy in Navi"));
return EBTNodeResult::Failed;
}
FVector const Origin = Enemy->GetActorLocation();
FNavLocation NextPatrol;
//NextPatrol변수에 임의의 location 데이터를 넣고 다시 TargetLocation키의 value에 값을 넣어준다.
if (NavSystem->GetRandomPointInNavigableRadius(Origin, search_radius, NextPatrol,nullptr))
{
Controller->get_blackboard()->SetValueAsVector(AMeleeEnemyAIController::TargetLocation, NextPatrol.Location);
//GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("NextPosSuccess!"));
}
//그 다음 이동할 곳을 확인하기 위한 디버그메시지
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, FString::Printf(TEXT("%s"),*NextPatrol.Location.ToString()));
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
return EBTNodeResult::Succeeded;
}
현재 설정된 search_radius는 1500.f입니다. AI는 이 범위 내에서 임의의 지점을 정하고
그것을 NextPatrol변수에 값을 넣습니다. 그리고 다시 블랙보드의 TargetLocation변수에 이 값을 저장합니다.
여기까지 실행이 되었다면 FinishLatentTask()함수에서 성공을 반환하고 Task를 종료시킵니다.
이제 우리는 비헤이비어 트리에서 노드를 작성하고
에디터 상에서 MoveTo를 사용하여 TargetLocation위치로 이동하도록 하면 됩니다.
방금까지 만든 FindPatrolPosTask클래스를 BP로 생성합니다.
이는 비헤이비어 트리에서 간편하게 사용하기 위함입니다.
에디터상의 비헤이비어 트리에서 빨간 박스부분을 작성합니다.
그리고 Move To를 클릭한 후 파란색 박스부분으로 설정합니다.
Can't See Player는 Decorator입니다.
Go To Random Location노드 위에
마우스 오른쪽클릭을 하여 데코레이터 추가를 누르고 Blackboard를 선택합니다.
이렇게 하면 Blackboard에서 설정한 변수값에 따라 Sequence실행을 정의하겠다는 것입니다.
Is Not Set으로 설정함에 따라 CanSeePlayer값이 false면 해당 노드를 실행합니다.
이제 에디터에서 몬스터클래스에 MeleeEnemyAIController를 설정해줍니다.
실행결과
'언리얼엔진 > UE4' 카테고리의 다른 글
UE4 C++ 비헤이비어 트리를 활용한 몬스터 AI 구현 (0) (0) | 2021.10.01 |
---|---|
UE4 C++ Datatable을 이용한 몬스터 정보 초기화 (0) | 2021.09.17 |