본문 바로가기

언리얼C++게임개발/12.AI컨트롤러와 비헤이비어 트리

AIController와 내비게이션 시스템

● 플레이어가 조종하지 않지만 레벨에 배치돼 스스로 행동하는 캐릭터를 'NPC(Non Player Character)'라고 한다.

● 언리얼 엔진은 컴퓨터가 인공지능으로 NPC를 제어하도록 AI 컨트롤러를 제공한다. 폰은 플레이어 컨트롤러와 동일한 방식으로 AI 컨트롤러에 빙의될 수 있고, 빙의되면 AI가 시키는 대로 행동하게 된다.

● AI 컨트롤러를 생성하여 캐릭터에게 빙의하기 위해서는 AI Controller를 부모 클래스로 하는 AI 컨트롤러를 만들어줘야 한다.

● ABCharacter가 위에서 만든 ABAIController를 사용하도록 AIControllerClass(=AI 컨트롤러 클래스) 속성을 ABAIController의 클래스로 지정하고, AI 생성 옵션을 PlaceInWorldOrSpawned로 설정해 준다.

그러면 레벨에 배치하거나 새롭게 생성되는 캐릭터마다 ABAIController 액터가 생성된다. 즉, 플레이어가 조종하는 캐릭터를 제외한 모든 캐릭터는 ABAIController에 빙의된다.

C++로 AIControllerClass 속성(AI 컨트롤러 클래스)과 AutoPossessAI 속성(AI 자동 빙의) 설정 방법

ABCharacter.cpp

 

#include "ABAIController.h"

AABCharacter::AABCharacter()
{
    ...
        
    AIControllerClass = AABAIController::StaticClass();
    AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}

● NPC는 스스로 움직여야 하므로, 보조 장치가 필요하다. 이때 사용되는 것이 내비게이션 메시(Navigation Mesh)를 기반으로 한 길 찾기 시스템이다.

우선 내비게이션 메시로 NPC가 스스로 움직일 수 있는 범위를 만들어주어야 한다. 그러기 위해서 액터 배치 창에서 'NavMesh Bounds Volume'을 추가해 줘야 한다.

이 내비게이션 메시를 배치하고 'P'를 누르면 캐릭터가 돌아다닐 수 있는 구간을 녹색으로 표시해 준다.

● 돌아다닐 수 있는 영역을 만들어주고, 이를 활용하여 ABAIContoller에 빙의한 캐릭터에게 목적지를 알려주어 캐릭터가 목적지까지 스스로 움직이도록 명령을 주어야 한다.

그리고 AI 컨트롤러에 타이머를 설정하여 3초마다 폰에게 목적지로 이동하는 명령을 추가한다.

- GetRandomPointInNavigableRadius() 함수 : 내비게이션 시스템은 이동 가능한 목적지를 랜덤으로 가져오는 함수

- SimpleMoveToLocation() 함수 : 목적지로 폰을 이동시키는 함수

위 함수들을 이용하여 캐릭터가 자동으로 움직이도록 코드를 작성하면 헤더 파일은 다음과 같이 작성한다.

ABAIController.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "../ArenaBattle.h"
#include "AIController.h"
#include "ABAIController.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API AABAIController : public AAIController
{
	GENERATED_BODY()
public:
	AABAIController();
	virtual void OnPossess(APawn* InPawn) override;
	virtual void OnUnPossess() override;
protected:
	virtual void BeginPlay() override;

public:
	UPROPERTY()
	class AAIController* ai;
	UPROPERTY()
	class AABCharacter* me;
	
private:
	void OnRepeatTimer();

	FTimerHandle RepeatTimeHandle;
	float RepeatInterval;
	
};

이때, 책에서 Possess() / UnPossess() 함수를 사용하였는데, 언리얼 엔진 5에서는 OnPossess() / OnUnPossess() 함수로 변경하여 사용해야 한다. 사용방법은 똑같다. (final로 선언이 되어있어서 override가 안됨)

다음으로 cpp 파일에서 구현한 코드는 다음과 같다.

ABAIController.cpp에서 함수 구현

// Fill out your copyright notice in the Description page of Project Settings.


#include "ABAIController.h"
#include "ABCharacter.h"
#include <AIController.h>
#include <NavigationSystem.h>
#include <Blueprint/AIBlueprintHelperLibrary.h>
#include "Navigation/PathFollowingComponent.h"

AABAIController::AABAIController()
{
	RepeatInterval = 3.0f;
}

void AABAIController::OnPossess(APawn* InPawn)
{
	Super::OnPossess(InPawn);
	GetWorld()->GetTimerManager().SetTimer(RepeatTimeHandle, this, &AABAIController::OnRepeatTimer, RepeatInterval, true);
}

void AABAIController::OnUnPossess()
{
	Super::OnUnPossess();
	GetWorld()->GetTimerManager().ClearTimer(RepeatTimeHandle);
}

void AABAIController::BeginPlay()
{
	Super::BeginPlay();
	// 소유객체 가져오기
	me = Cast<AABCharacter>(GetOwner());
	// AAIController 할당하기
	//ai = Cast<AAIController>(me->GetController());
}

void AABAIController::OnRepeatTimer()
{
	auto CurrentPawn = GetPawn();
	ABCHECK(nullptr != CurrentPawn);

	UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());
	if (nullptr == NavSystem) return;
	FNavLocation nextLocation;
	if (NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, 500.0f, nextLocation))
	{
		UAIBlueprintHelperLibrary::SimpleMoveToLocation(this, nextLocation.Location);
		//ai->MoveToLocation(nextLocation.Location);
		ABLOG(Warning, TEXT("Next Location : %s"), *nextLocation.Location.ToString());
	}
}

여기서 OnPossess() 함수로 빙의가 되면, 빙의된 폰이 지정한 시간마다 반복하여 OnRepeatTimer() 함수를 실행하도록 한다.

+ OnUnPossess() 함수로 빙의가 끝나면, 타이머를 중지시켜, 함수가 반복 실행되는 것을 멈춘다.

ABAIController.cpp에서 함수 구현

 

여기서 GetRandomPointInNavigableRadius() 함수는 지정한 좌표(0, 0, 0)에서 반경 500cm 이내의 모든 좌표를 임의로 지정하고, 그 좌표를 NextLocation 변수에 담도록 하였다.

그리고 SimpleMoveToLocation() 함수를 사용하여 방금 지정한 임의의 좌표로 이동하도록 하였다.

그리고 내비게이션이 언리얼 엔진 5에서 변경된 것이 많다.

1. 언리얼 엔진 5에서는 NavigationSystem 모듈이 추가되어, 이를 헤더 파일로 추가해 줘야 한다.

#include "NavigationSystem.h"

2. NavigationSystem 클래스가 NavigationSystemV1으로 변경되어 UNavigationSystem이 아니라 UNavigationSystemV1으로 선언해야 한다.

UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());

3. SimpleMoveToLocation() 함수가 블루프린트 라이브러리로 이동하여 헤더 파일을 추가해 줘야 사용이 가능하다.

#include "Blueprint/AIBlueprintHelperLibrary.h" UAIBlueprintHelperLibrary::SimpleMoveToLocation(this, NextLocation.Location);

● 플레이해 보면 ABAIController가 생기는 것을 확인할 수 있다.

이때, ABAIController의 PathFollowingComponent가 부착되어 있어, AI Controller가 조종하는 폰이 길 찾기를 사용해 목적지까지 도달하는지 지속적으로 관리해 준다.

+ NavigationSystem을 사용하기 위해서 ArenaBattle.Build.cs에서 "UMG"를 추가했던 것처럼 "NavigationSystem"을 추가해 줘야 한다.

● AI 컨트롤러에 특정 로직을 부여하여 스스로 움직이는 NPC 행동을 제작할 수 있었다. 하지만 좀 더 복잡한 NPC 행동 패턴을 만들려면 체계적인 모델에서 설계하는 것이 바람직하다.

그래서 언리얼 엔진은 '비헤이비어 트리 모델'을 사용하여 AI 컨트롤러가 수행해야 하는 행동 패턴을 체계적으로 설계할 수 있다.

● 비헤이비어 트리는 NPC가 해야 할 행동을 분석하고, 우선순위가 높은 행동부터 NPC가 실행할 수 있도록 트리 구조로 설계하는 방법이다.

 

비헤이비어 트리를 제작하려면 '비헤이비어 트리''블랙 보드 애셋'을 생성해야 한다.

 
 

● 블랙 보드와 비헤이비어 트리의 역할은 다음과 같다.

- 비헤이비어 트리 : 블랙보드 데이터에 기반해 설계한 비헤이비어 트리의 정보를 지정한 애셋. 언리얼 에디터에서 비헤이비어 트리를 시각화해서 저장할 수 있도록 편집 기능을 제공한다.

- 블랙보드 : 인공지능의 판단에 사용하는 데이터 집합. NPC의 의사 결정은 블랙보드에 있는 데이터를 기반으로 진행된다.

● 만든 비헤이비어 트리에 들어가서 wait 태스크를 생성한다.

wait 태스크는 폰에게 지정한 ㅅ시간 동안 대기하라는 명령을 내린다.

※ 태스크는 독립적으로 실행될 수 없고, 반드시 컴포짓 노드를 거쳐 실행되어야 한다.

컴포짓 노드는 대표적으로 셀렉터(Selector)와 시퀀스(Sequence)가 있다.

연결된 태스크들이 False의 결과가 나올 때까지 왼쪽에서 오른쪽으로 태스크를 계속 실행하는 시퀀스 컴포짓을 사용해 보겠다.

● 방금 만든 블랙보드와 비헤이비어 트리 애셋을 이전에 만든 C++의 ABAIController에서 사용하려면(= 비헤이비어 트리 관련 기능을 사용하려면) ArenaBattle.Build.cs에 AIModule 모듈을 추가해 줘야 한다.

● 그리고 ABAIController에서 UBehaviorTree 클래스와 UBlackboardData 클래스로 애셋을 추가할 수 있다.

그리고 구현부에서 이를 사용하면

+ 구현부에서 사용하기 위해서 다음과 같은 헤더 파일을 추가해 줘야 한다.

#include "BehaviorTree/BehaviorTree.h" #include "BehaviorTree/BlackboardData.h"

OnPossess() 함수에서 UseBlackboard() 함수를 사용하여 블랙보드 컴포넌트에서 BBAsset을 사용할 수 있게 해주고,

BTAsset 비헤이비어 트리를 실행할 수 있도록 해준다.

+ 즉, 위 코드로 비헤이비어 트리 애셋과 같은 폴더에 위치한 블랙보드 애셋과 비헤이비어 트리가 연결되어 함께 동작할 수 있는 것이다.

+ 이때, UseBlackboard()의 매개변수로 BBAsset, bbComponent를 사용했다. 원래 책에서 두 번째 매개변수로 Blackboard가 사용되었는데, 이를 사용하면 타입 오류가 발생하여 37번째 줄처럼 변수 하나를 추가해서 Blackboard를 저장하여 사용해야 한다.

UBlackboardComponent* bbComponent = Blackboard;

● 게임을 플레이하고, 비헤이비어 트리를 확인해 보면 비헤이비어 트리의 로직 흐름을 확인할 수 있다.

● 다음으로, 블랙보드에 특정 유형 데이터를 저장하고, 이를 비헤이비어 트리가 활용하도록 구성할 수 있다.

NPC의 순찰 기능을 구현하기 위해서 2가지 데이터가 필요하다.

1. NPC가 생성됐을 때, 위치 값이 필요하다. 블랙보드에서 이를 보관하도록 Vector 타입으로 키를 생성한다.

2. 앞으로 NPC가 순찰할 위치 정보를 보관할 블랙보드 키도 필요하다. 이 키도 Vector 타입으로 생성한다.

● 비헤이비어 트리를 구동하기 전, AI 컨트롤러에서 블랙보드의 HomePos 키값을 지정하도록 C++에서 구현해야 한다.

ABAIController.cpp에서 HomePosKey 변수를 생성하고, 이 변수의 키 이름을 HomePos로 지정하고,

ABAIController.cpp의 OnPossess()에서 SetValueAsVector() 함수를 사용하여 값을 GetActorLocation()으로, 현재 캐릭터 위치의 좌표를 값으로 지정한다.

즉, 블랙보드에서 만들었던 Vector 타입으로 생성했던 키 HomePos에 현재 캐릭터의 위치 좌표가 값으로 들어가는 것이다. 그래서 플레이해 보면 비헤이비어 트리 에디터에서 HomePos 키값이 블랙보드에 잘 전달되는 것을 확인할 수 있다.

 

● 다음으로 PatrolPos는 앞으로 NPC가 순찰할 위치 정보를 보관하는 데이터로, 순찰할 때마다 값이 바뀌게 된다. 그러므로 태스크를 제작하여 비헤이비어 트리에서 블랙보드에 값을 쓰도록 설계해 주는 것이 좋다.

+ 이때, 오류가 발생하는데, 이 에러는 관련 모듈을 참조할 수 없어 생기는 오류이다. 그러므로, ArenaBattle.Build.cs에 "GameplayTasks" 모듈을 추가해 주고 생성해야 한다.

● 비헤이비어 트리는 태스크를 실행할 때, 태스크 클래스의 'ExecuteTask'라는 멤버 함수를 실행한다. ExecuteTask는 4개 중 하나의 값을 반환해야 한다.

- Aborted : 태스크 실행 중에 중단되었다. 결과적으로 실패를 의미.
- Failed : 태스크를 수행했지만 실패.
- Succeeded : 태스크를 성공적으로 수행.
- InProgress : 태스크를 계속 수행하고 있다. 태스크의 실행 결과는 향후 알려줄 예정이다.

ExecuteTask 함수의 실행 결과에 따라서 컴포짓 내에 있는 다음 태스크를 계속 수행할 것인지 중지할 것인지 결정되는 것이다. 현재 사용 중인 컴포짓은 자신에 속한 태스크를 실패할 때까지 계속 실행하려고 한다.

● 이를 코드로 작성하면 다음과 같다.

BTTask_FindPatrolPos.h

BTTask_FindPatrolPos.cpp

+ C++에서 클래스 이름이 BTTask_FindPatrolPos이지만, UI에서 표현할 때는 BTTask_ 접두사 부분은 자동으로 걸러진다.

● 새로운 FindPatrolPos 태스크가 만들어졌으면, Wait 태스크 오른쪽에 배치해 준다.

1. 배치한 후, FindPatrolPos 오른쪽에는 MoveTo 태스크를 추가하여 배치한다.

2. 이어서 시퀀스 컴포짓에 의해 Wait 태스크가 성공하면 FindPatrolPos 태스크를 수행하고,

3. FindPatrolPos 태스크가 성공하면 FindPatrolPos에서 설정한 블랙보드의 PatrolPos 키값을 참고하여 MoveTo 태스크가 실행된다.

이렇게 플레이를 하고, 비헤이비어 트리의 흐름을 보면 5초 뒤, 캐릭터가 랜덤한 위치를 찾아 그 좌표로 이동하는 것을 확인할 수 있다.

이동이 끝나면 다시 5초를 기다리고 랜덤 위치를 또 찾아 그 좌표로 이동한다.

앞에 있는 AIController에 빙의한 캐릭터가 이동하는 사진