본문 바로가기

언리얼 엔진(Unreal Engine)/UE5

[UE5] 비헤이비어 트리(Behavior Tree)

 

이 글은 언리얼엔진 5.0.3 버전을 기준으로 작성되었습니다. 다른 버전과는 차이점이 존재할 수 있습니다.

 

 

게임에서 인공지능은 여러 가지 역할을 맡습니다. 때로는 플레이어에게 도움을 주는 아군의 역할을 맡기도 하고, 때로는 플레이어를 공격하는 적군의 역할을 맡기도 합니다. 아군 AI와 적군 AI는 실제 플레이어가 플레이하는 것처럼 '똑똑한' 인공지능이 되어야 합니다. HP가 다 떨어져 가는데 바보같이 계속 맞기만 하거나, MP가 다 떨어졌는데 계속 스킬을 시전 하려는 인공지능이라면 사람들은 '멍청한' 인공지능이라고 평하면서 결국에는 게임성을 떨어뜨리게 됩니다.

 

이런 멍청한 AI를 피하고 똑똑한 AI를 만들기 위해서 여러가지 방법들이 많은데, 언리얼 엔진에서는 비헤이비어 트리(Behavior Tree)와 블랙보드(Blackboard)라는 것을 지원해줍니다. 이 두 가지에 대해서 알아보겠습니다.

 

비헤이비어 트리

이번 글에서는 간단하게, 아래와 같은 AI를 만들어보겠습니다.

 

1. 무작위 이동을 한다.

2. 도착 지점에 도달했을 때, 너무 멀리 갔는지 테스트한다.

3. 너무 멀리 갔다면 초기 위치로 돌아가고, 그렇지 않으면 다시 무작위 이동을 한다.

 

블랙보드(Blackboard)

블랙보드는 AI의 기억공간이라고 생각하시면 됩니다. 비헤이비어 트리가 결정을 하기 위해 사용할 key값들이 저장되는 곳입니다.

 

블랙보드 예시

블랙보드를 새로 하나 만들어 보겠습니다. 우클릭 후 인공지능 - 블랙보드를 선택해 블랙보드를 생성합니다.

 

 

블랙보드의 이름은 저는 BB_Test로 지정했습니다. 블랙보드는 AI의 기억공간이라고 했습니다. AI가 무작위 이동을 하기로 했으니, 무작위 이동으로 도착할 위치에 대한 정보를 저장해야합니다. 그러기 위해서, 위치를 저장해 줄 키를 하나 만들어줘야 합니다.

 

만들었던 블랙보드를 열어줍니다. 새 키를 눌러 타입벡터로 지정해주고, 이름을 MovePoint라고 지정하겠습니다.

 

이어서 너무 멀리 갔는지를 체크할 키도 선언하겠습니다. 이번에는 타입을 부울로 해서 신규 생성합니다. 이름은 IsOverRun으로 지정하겠습니다.

 

최종적으로는 아래와 같이 키가 만들어집니다.

 

 

비헤이비어 트리(Behavior Tree)

블랙보드가 키에 정보를 저장해서 기억하고 있다면, 비헤이비어 트리는 블랙보드의 키에 기억된 정보를 이용해서 행동을 실행합니다. 일단 비헤이비어 트리를 만들어 보겠습니다. 인공지능 - 비헤이비어 트리를 눌러서 비헤이비어 트리를 하나 만듭니다. 저는 이름을 BT_Test로 지정했습니다.

 

 

비헤이비어 트리의 창을 열어봅니다. 비헤이비어 트리 창에는 루트노드가 하나 있고, 루트 노드가 클릭된 상태로 디테일 패널에는블랙보드 에셋을 지정할 수 있는 창이 나타납니다.

 

 

루트는 비헤이비어 트리의 시작점입니다. 오로지 하나만 존재하며 항상 루트 노드에서 트리가 시작됩니다. 루트는 데코레이터 노드나 서비스 노드와는 붙일 수 없고, 단 하나의 노드에만 연결할 수 있습니다.

 

루트를 제외한 노드로는 위에서 언급한 데코레이터(Decorator) 노드, 서비스(Service) 노드 외에도 컴포짓(Composite) 노드, 태스크(Task) 노드가 있습니다.

 

컴포짓 노드

컴포짓 노드는 분기 노드가 실행되는 방식을 정의하는 노드입니다. 종류는 셀렉터(Selector), 시퀀스(Sequence), 심플 패러렐(Simple Parallel)가 있습니다.

 

Selector

 

Selector 노드

왼쪽에서 오른쪽 자손 순으로 차례대로 실행합니다. 이때, 하나의 자손 노드가 실행에 성공하면 다른 자손 노드는 실행되지 않습니다. 하나의 자손이라도 성공하면 셀렉터는 성공을, 모두가 실패하면 셀렉터는 실패로 처리됩니다.

 

Sequence

 

Sequence 노드

왼쪽에서 오른쪽 자손 순으로 차례대로 하나씩 실행합니다. 모든 자손 노드가 실행에 성공해야 시퀀스는 성공으로, 자손 노드 중 하나라도 실행에 실패하면 실행이 중단되고 시퀀스도 실패로 처리됩니다.

 

Simple Parallel

 

Simple Parallel 노드

태스크 노드 하나를 지정해서 이를 메인 태스크로 잡습니다. 메인 태스크와 아래 트리들이 동시에 실행됩니다. 메인 태스크의 실행이 끝나면 하위 트리들의 실행을 멈출지(Immediate), 하위 트리들의 실행이 끝날 때까지 기다릴지(Delayed)를 지정할 수 있습니다.

 

데코레이터

 

데코레이터 노드(파란색 노드)

일종의 조건문 역할을 합니다. 컴포짓 노드나 태스크 노드에 붙여서 해당 노드를 실행할지의 여부를 결정합니다. 여러 종류의 데코레이터를 기본 제공하지만, 새 데코레이터를 직접 만들어 추가할 수 있습니다.

 

 

서비스

 

서비스 노드(초록색 노드)

컴포짓 노드나 태스크 노드에 붙여서 주로 블랙보드의 내용을 확인하거나 업데이트에 사용하는 노드입니다. 데코레이터와 마찬가지로 새 서비스를 직접 만들어 추가할 수 있습니다.

 

 

태스크

 

태스크 노드(보라색 노드)

AI의 행동, 블랙보드 값 변경 등 여러 작업이 실행되는 노드입니다. 기본적인 태스크를 몇 가지 지원하고, 태스크 또한 새 태스크를 직접 만들어 추가할 수 있습니다.

 

 

 

다시 예제로 돌아와서, 트리를 구성할 시간입니다. 먼저, 비헤이비어 트리의 블랙보드 에셋을 미리 만들어 두었던 BB_Test로 지정해줍니다.

 

이제 트리와 블랙보드가 연결되어 블랙보드의 키를 이용할 수 있는 상태가 되었습니다. 이어서 루트 노드의 연결 부분을 끌어와 셀렉터 노드를 하나 만들어 줍니다. 이 셀렉터에서는 위의 2번 과정 중 일부분인, "너무 멀리 갔는지"를 결정합니다.

 

이제 셀렉터에서 왼쪽과 오른쪽으로 각각 끌어와서 시퀀스 노드를 배치합니다. 왼쪽에는 너무 멀리 가지 않았으면 무작위 이동을, 오른쪽에는 너무 멀리 갔으면 원래 지점으로의 복귀를 지정할 것입니다.

 

무작위 이동을 지정해보겠습니다. 무작위 이동에 대한 태스크들을 왼쪽 시퀀스에 연결해주겠습니다.

 

새 태스크를 눌러서 이름을 BTTask_BlueprintBase라고 지정해서 저장해줍니다. 그러면 아래와 같이 새 태스크를 누르면 만들어둔 태스크가 나타납니다. 이제, 새 태스크를 또 만들 일이 생길 때 마다 만들어둔 BTTask_BlueprintBase를 눌러서 생성하겠습니다.

 

다시 새 태스크를 만들어줍니다. 이번에 만들 태스크는 랜덤 이동을 위해, 랜덤으로 이동할 위치를 블랙보드에 설정할 것입니다. 이름은 BTTask_RandomPoint로 지정하겠습니다.

 

이제부터 BTTask_RandomPoint의 이벤트 그래프에서 작업합니다. 우클릭해서 Receive Execute AI 이벤트를 배치합니다. 해당 이벤트는 비헤이비어 노드에서 태스크가 실행되었을 때를 감지해 작동합니다.

 

다음으로 GetRandomLocationNavigableRadius를 실행핀으로 연결합니다.

 

Get Random Location Navigable Radius는 Origin의 위치에서 Radius 만큼의 반경 이내에서 이동 가능한(=네비 빌드가 된) 무작위 위치를 반환해주고, 성공 여부를 Return Value를 통해 반환합니다.

Origin은 AI 폰의 위치로, Radius는 500으로 지정하겠습니다.

 

다음은 랜덤 위치를 블랙보드의 MovePoint에 저장할 차례입니다. Random Location의 출력핀에서 끌어와서 Set Blackboard Value as Vector 함수와 연결합니다. 블랙보드에 값을 저장할 때에는 항상 Set Blackboard Value as ...으로 지정합니다.

입력 핀의 Key를 끌어와 변수로 승격해주고 인스턴스 편집가능에 체크해줍니다. 최종적으로 아래와 같이 만들어집니다.

 

태스크가 정상적으로 작동이 되었음을 알려서 정상적으로 종료 되었음을 알려줘야합니다. 이것을 해주는 함수가 Finish Execute입니다.

 

Success가 true면 성공을, false면 실패를 반환합니다. 이 태스크의 경우에는 무작위 위치를 받아오지 못했으면 false를, 성공했으면 true를 반환하면 됩니다. 그러니, Success 입력 핀을 앞의 GetRandomLocationNavigableRadius의 Return Value와 연결합니다.

 

최종적으로 아래와 같이 만들어집니다.

 

잘 보이지 않는다면 클릭해서 확대해주세요.

태스크를 만들었으니, 왼쪽 시퀀스에 태스크를 연결해줍니다. 그리고, 해당 태스크를 눌러 디테일 패널에서 Key를 Move Point로 지정합니다.

 

랜덤 위치 생성 태스크 옆에는 Move To 태스크를 연결해줍니다. Move To 태스크는 이름에서 알 수 있듯이, 특정 위치로 이동하는 태스크입니다.

 

Move To 태스크를 눌러서 디테일 패널을 엽니다. 그리고, 블랙보드 - 블랙보드 키MovePoint로 지정합니다. 이제 AI 폰은 랜덤 위치로 지정된 MovePoint로 이동을 할 것입니다.

 

Move To 태스크의 옆에 Wait 태스크를 추가한 후, 디테일 패널에서 2초로 지정해줍니다.

 

왼쪽 시퀀스는 아래와 같이 구성됩니다.

 

 

초기 위치로의 복귀를 만들겠습니다. 초기 위치는 간단하게 0,0,0 으로 지정하겠습니다.

 

우선, AI 폰이 너무 멀리 갔는지부터 판별해야합니다. Selector에서 현재 너무 멀리 갔는지를 확인하고, 다음 실행할 시퀀스를 결정해야합니다. 그렇기 때문에, Selector에서 너무 멀리 갔는지를 확인하는 서비스를 먼저 추가해줘야합니다. 새 태스크를 만들때와 마찬가지로 새 서비스를 만들어줍니다. 이름은 BTService_IsOverrun으로 하겠습니다.

 

이제부터 BTService_IsOverrun에서 작업합니다.

 

이벤트 그래프에서 Receive Activation AI 이벤트를 배치합니다. 해당 노드가 활성화 되는 것을 감지하면 이벤트가 실행됩니다. 이번에는 Selector가 활성화 되면 바로 실행되는 서비스입니다. 출력핀은 태스크에서 사용한 Receive Execute AI와 같습니다.

 

변수를 하나 추가해줍니다. 이름은 InitLocation, 초기값은 0,0,0으로 합니다. 아래와 같이 이벤트를 구성해줍니다.

 

현재 AI 폰과 InitLocation과의 거리가 1000이상 차이나면 Overrun이 되었다고 판별하는 것입니다.

 

이렇게 만든 서비스를, Selector 노드에 붙일것입니다. Selector 노드우클릭 - 서비스 추가 - BTService Is Overrun을 누릅니다. 그러면 아래와 같이 서비스와 Selector가 서로 붙습니다.

 

이제 Selector가 Sequence중 하나를 선택하게 해줘야합니다. 이럴 때 사용하는 것이 데코레이터 노드입니다. 위에서 데코레이터 노드는 일종의 조건문 역할을 한다고 했습니다. 즉, 시퀀스에 Overrun 상태인지를 체크해주어서 Selector가 어떤 Sequence를 실행할지 결정하게 해줍니다.

 

왼쪽 Sequence우클릭해서 데코레이터 추가 - Blackboard를 누릅니다.

 

그러면 Sequence 노드에 데코레이터가 붙습니다. 데코레이터 노드를 눌러서 디테일 패널에서 블랙보드 - 블랙보드 키를 IsOverrun으로, 키 쿼리를 Is Not Set으로 설정합니다. 왼쪽 Sequence는 Overrun 상태가 아니니, 랜덤 위치로 계속 이동해야합니다.

 

오른쪽 Sequence는 반대로 Is Set으로 해줍니다. 그리고, 아래와 같이 오른쪽 Sequence를 완성해줍니다.

Wait의 값은 2초입니다.

 

 

완성된 트리의 모습은 다음과 같습니다.

 

 

이제 AIController에서 비헤이비어 트리를 실행하도록 만들어주면 끝입니다. (AI컨트롤러 제작은 생략하겠습니다.)

 

AI 폰을 하나 지정해 의도한 대로 잘 작동하는지 테스트를 해봅니다. 무작위 위치로 이동을 반복하다가, 너무 멀리 갔을 때 0,0,0으로 돌아오면 성공입니다.

 

 

 


 

 

틀린 점이나, 수정해야할 점, 보충이 필요한 부분이 있다면 댓글로 알려주세요!