개발/Unreal Engine / / 2022. 2. 6. 18:13

Learning Unreal 4 언리얼 공부일지 - AIController class와 Behaivor Tree

반응형

언리얼에는 AIController class가 있어 이를 이용해서 AI움직임을 구현할 수 있다.

 

// MyAIController.h

/.../

UCLASS()
class UNREALINTRODUCTION_API AMyAIController : public AAIController
{
	GENERATED_BODY()
	
public:
	AMyAIController();

	// Pawn에 인칭이 고정될 때 호출
	virtual void OnPossess(APawn* InPawn) override;
	// 인칭이 풀릴 때 호출
	virtual void OnUnPossess() override;

private:
	void RandomMove();
private:
	// deletgate의 타이머를 구분할 수 있는 핸들
	FTimerHandle TimerHandle;
};

AIController 헤더에 Pawn에 시점이 고정되는 순간과 풀리는 순간에 호출되는 Possess함수들을 선언한다. 

FTimerHandle을 이용해서 주기적으로 함수를 실행하는 Timer를 설정할 수 있는데, FTimerHandle을 찾아보면 동일한 delegate를 가진 timer를 구분할 수 있는 유일한 핸들이라고 하는데, 무슨 말인지 아직은 이해하지 못하겠다.

 

// MyAIController.cpp

#include "MyAIController.h"
#include "NavigationSystem.h" // UNavigationSystemV1을 위한 헤더
#include "Blueprint/AIBlueprintHelperLibrary.h"

/.../

void AMyAIController::OnPossess(APawn* InPawn)
{
	Super::OnPossess(InPawn);

	// RandomMove함수를 주기적으로 실행하는 Timer
	GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &AMyAIController::RandomMove, 3.f, true);
}

void AMyAIController::OnUnPossess() 
{
	Super::OnUnPossess();

	// TimerHandle에 설정된 함수 루틴이 clear
	GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
}

void AMyAIController::RandomMove()
{
	auto CurrentPawn = GetPawn();

	// Navigation system을 설정
	UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());
	if (NavSystem == nullptr)
		return;

	FNavLocation RandomLocation;
	// 반경 내 임의의 점 좌표를 RandomLocation에 저장
	if (NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, 500.f, RandomLocation))
	{
		// 이동함수
		UAIBlueprintHelperLibrary::SimpleMoveToLocation(this, RandomLocation);
	}
}

cpp파일에는 possess함수들에 Timer를 세팅하고 해제하는 로직과, Navigation System을 이용하여 갈 수 있는 임의의 위치로 이동하는 random move를 정의하였다.

navigation system은 엔진 버전마다 다르기도 하고 여러 버전이 있다는데, 나중에 찾아보는 것이 좋을 것 같다.

 

// MyCharacter.cpp

#include "MyAIController.h"

AMyCharacter::AMyCharacter()
{
	/.../
    
	// AMyAIController를 StaticClass로 지정
	AIControllerClass = AMyAIController::StaticClass();
	// possess되었을 때 AI가 실행되는 상황설정
	AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}

Character cpp파일의 생성자에서 AIController를 StaticClass로 지정하고, AI가 possess될 때 실행관련 설정을 진행한다.

 

 

// probjectName.Build.cs

public class UnrealIntroduction : ModuleRules
{
	public UnrealIntroduction(ReadOnlyTargetRules Target) : base(Target)
	{
		PublicDependencyModuleNames.AddRange(new string[] { ..., "NavigationSystem", "AIModule", "GameplayTasks" });
	}
}

build c#파일에 NavigationSystem, AIModule, GameplayTasks를 추가했다.

NavigationSystem 모듈은 Navigation System을 이용하기 위해, AIModule은 블랙보드 및 Behavior Tree를 이용하기 위해, GameplayTasks는 BTTaskNode를 사용하기 위한 모듈이다.

 

이제 엔진에서 NavMesh를 설치하기 위해 '볼륨 → 내비 메시 바운드 볼륨'을 끌어다 Scene에 넣는다.

 

네비 메시의 구역을 설정한 뒤 키보드 P키를 통해 Navigation이 적용되는 구간을 초록색 바운더리로 확인할 수 있다.

 


이번에는 AI의 Behavior Tree 기능에 대해서 알아보자.

엔진의 콘텐츠 브라우저에서 '인공지능 → 블랙보드'로 블랙보드를, '인공지능 → 비헤이비어 트리'로 Behavior Tree를 생성할 수 있다.

 

비헤이비어 트리는 위 그림처럼, 루트에서 시작해서 각 행동들을 노드로 연결할 수 있다. 

 

// MyAIController.h

UCLASS()
class UNREALINTRODUCTION_API AMyAIController : public AAIController
{
	/.../
    
	UPROPERTY()
	class UBehaviorTree* BehaviorTree;

	UPROPERTY()
	class UBlackboardData* BlackboardData;
};

AIController 헤더에 블랙보드와 비헤이비어 트이 프로퍼티를 추가하고, 

 

// MyAIController.cpp

// Behavior Tree 및 Blackboard 관련 헤더 추가
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
#include "BehaviorTree/BlackboardComponent.h"

AMyAIController::AMyAIController()
{
	// Behavior Tree 에셋 불러오기
	static ConstructorHelpers::FObjectFinder<UBehaviorTree> BT(TEXT("BehaviorTree'/Game/AI/BT_MyCharacter.BT_MyCharacter'"));
	if (BT.Succeeded()) {
		BehaviorTree = BT.Object;
	}
	// Blackboard 에셋 불러오기
	static ConstructorHelpers::FObjectFinder<UBlackboardData> BD(TEXT("BlackboardData'/Game/AI/BB_MyCharacter.BB_MyCharacter'"));
	if (BD.Succeeded()) {
		BlackboardData = BD.Object;
	}
}

void AMyAIController::OnPossess(APawn* InPawn)
{
	Super::OnPossess(InPawn);

	//GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &AMyAIController::RandomMove, 3.f, true);

	// Blackboard를 확인하고 Behavior Tree 실행
	if (UseBlackboard(BlackboardData, Blackboard)) {
		if (RunBehaviorTree(BehaviorTree)) {

		}
	}
}

위와같이 실행하는 코드를 작성하면 

 

이렇게 Behavior Tree에서만든 동작이 실행하는 것을 확인할 수 있다.

 

 

만약 특정 위치로 이동하고 싶은 상황이라고 해보자. 

그렇다면 먼저 Blackboard창에서 새키를 통해 vector key를 생성한다.

 

위 그림을 보면 MoveTo라는 요소를 추가하였고, MoveTo의 Blackboard Key가 위에서 만든 key로 설정되 있는 것을 볼 수 있다. 

 

방금 생성한 key를 이용하기 위해 Behavior Tree관련 코드를 작성해보자.

엔진에서 BTTaskNode를 상속받는 class를 생성하고 다음과 같이 코드를 작성하자.

// BTTask_FindPatrolPos.h

UCLASS()
class UNREALINTRODUCTION_API UBTTask_FindPatrolPos : public UBTTaskNode
{
	/.../

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

EBTNodeResult는 Behavior Tree에서 사용하는 데이터타입으로 보인다. Task를 실행한 결과에 대해 Type형식으로 반환하는 모양이다.

 

// BTTask_FindPatrolPos.cpp

#include "BTTask_FindPatrolPos.h"

// AIController.cpp의 헤더 추가
#include "MyAIController.h"
#include "NavigationSystem.h"
#include "Blueprint/AIBlueprintHelperLibrary.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
#include "BehaviorTree/BlackboardComponent.h"

UBTTask_FindPatrolPos::UBTTask_FindPatrolPos() 
{
	// Behavior Tree에서 Node로 표시할 이름을 정한다.
	NodeName = TEXT("FindPatrolPos");
}

EBTNodeResult::Type UBTTask_FindPatrolPos::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);

	// Pawn 가져오기
	auto CurrentPawn = OwnerComp.GetAIOwner()->GetPawn();
	if (CurrentPawn == nullptr) // 가져오기 실패의 경우 failed 반환
		return EBTNodeResult::Failed;

	UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());
	if (NavSystem == nullptr)
		return EBTNodeResult::Failed;

	FNavLocation RandomLocation;
	if (NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, 500.f, RandomLocation))
	{
		// blackboard에서 생성한 vector key에 random location을 value로 세팅
		OwnerComp.GetBlackboardComponent()->SetValueAsVector(FName(TEXT("PatrolPos")), RandomLocation.Location);
		return EBTNodeResult::Succeeded;
	}

	return EBTNodeResult::Failed;
}

생성자에서 엔진에서 Behavior Tree의 node로 표시할 이름을 정한다.

ExecuteTask함수에서 Pawn과 Navigation System을 가져오고 임의의 Location을 Blackboard key에 세팅하여 성공 실패 여부를 반환한다.

 

코드로 생성한 FindPatrolPos라는 Node를 2번째 Behavior로 추가하면, 

기다렸다가 → 진행할 위치를 찾고 → 해당위치로 이동하는 로직을 실행한다.

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유