AI와 행동 트리
AI Controller와 Navigation Mesh를 준비하고 Blackboard·Task·Decorator로 NPC 행동 트리를 설계합니다.
경비병 NPC가 평소에는 순찰하다가, 플레이어를 보면 쫓아가고, 가까워지면 공격하고, 체력이 줄면 도망친다고 해 봅시다. 이 판단을 Tick 안의 if와 else로 모두 쓰면, 상태가 늘어날 때마다 조건이 얽혀 어떤 상황에서 무엇을 하는지 알아보기 어려워집니다. 언리얼은 이런 판단을 행동 트리(Behavior Tree)로 계층적으로 나눠 그리고, 실행 중에 어느 가지가 돌고 있는지 눈으로 확인할 수 있게 해 줍니다.
에디터 확장과 커스텀 툴에 이어 이 글에서는 언리얼의 AI 구조를 살펴봅니다. AI 컨트롤러와 내비게이션 메시를 준비하는 방법, 행동 트리를 이루는 노드와 블랙보드, 선택자와 시퀀스가 자식의 결과를 어떻게 처리하는지, 그리고 행동 트리를 설계하고 디버깅하는 방법을 정리합니다.
게임 AI는 무엇을 하는가
게임 AI는 플레이어가 아닌 캐릭터(Non-Player Character, NPC)가 플레이어의 행동에 반응하고 게임 세계 안에서 지능적인 것처럼 보이는 결정을 내리도록 제어하는 시스템입니다. 여기서 지능은 사람의 지능과는 다르며, 미리 정한 규칙, 알고리즘, 데이터를 바탕으로 목표를 이루는 능력을 말합니다. AI는 환경을 분석하고, 목표를 정하고, 행동을 계획하는 과정을 거쳐 결정을 내립니다.
언리얼 엔진은 AI를 만드는 여러 기능을 내장하고 있으며, 그 중심에는 AI 컨트롤러(AI Controller), 행동 트리, 블랙보드(Blackboard)가 있습니다.
AI 컨트롤러
이 글에서 다루는 폰 기반 AI는 그 폰을 빙의한 AI 컨트롤러가 조종합니다. 모든 AI 구현에 컨트롤러가 꼭 필요한 것은 아니지만, 폰을 조종하는 AI라면 보통 이 구조를 씁니다. AI 컨트롤러는 플레이어의 PlayerController처럼, 그 캐릭터의 인지(perception), 의사 결정, 행동 실행 로직을 맡습니다.
캐릭터의 블루프린트나 C++ 클래스에서 AI Controller Class 속성으로 그 캐릭터가 쓸 AI 컨트롤러를 정합니다. 레벨에 배치하는지, 실행 중에 스폰하는지에 맞게 Auto Possess AI 설정을 정하고, 빙의가 끝난 시점에 행동 트리를 실행합니다.
AI 컨트롤러가 하는 일은 세 가지로 볼 수 있습니다. AIPerceptionComponent로 시각, 청각, 피해 감지 같은 방식으로 게임 세계를 인지하고, MoveToActor나 MoveToLocation으로 내비게이션 메시를 따라 캐릭터를 움직이며, 행동 트리를 실행해 캐릭터의 복잡한 행동을 제어합니다.
내비게이션 메시
내비게이션 메시(Navigation Mesh)는 AI 캐릭터가 월드 안에서 움직일 수 있는 영역을 정하는 시스템입니다. 레벨에 Nav Mesh Bounds Volume을 두면 언리얼이 그 범위 안의 내비게이션 메시를 자동으로 만듭니다. 에디터에서 P 키를 누르면 만들어진 내비게이션 메시가 기본적으로 초록색으로 보입니다.
내비게이션 메시 덕분에 AI는 복잡한 환경에서도 장애물을 피해 목표 지점까지 갈 수 있습니다. 내비게이션 메시를 쓰는 지상 이동은 만들어진 내비게이션 데이터와 서로 이어진 영역 안에서 길을 찾습니다. 비행이나 직접 이동 같은 다른 이동 방식까지 내비게이션 메시에 묶이는 것은 아닙니다.
행동 트리를 이루는 노드
행동 트리는 AI의 의사 결정을 계층적이고 유연하게 구성하는 트리 구조의 시스템으로, 복잡한 AI 행동을 시각적으로 설계하고 디버깅하기에 효과적입니다.
트리의 시작점은 루트(Root)이며 하나만 있습니다. 그 아래에서 자식 노드들의 실행 흐름을 제어하는 노드를 컴포지트(Composite)라고 합니다. 선택자(Selector)는 자식을 왼쪽부터 차례로 시도해 하나가 성공할 때까지 이어 가며, 하나라도 성공하면 자신도 성공하고 모든 자식이 실패하면 자신도 실패합니다. "싸울 수 있으면 싸우고, 아니면 도망쳐라"처럼 여러 선택지 가운데 되는 것을 고를 때 씁니다. 시퀀스(Sequence)는 자식을 왼쪽부터 차례로 실행해 모두 성공해야 자신도 성공하고, 하나라도 실패하면 실패합니다. "목표를 찾고, 경로를 계산하고, 이동한 다음, 공격해라"처럼 단계를 차례로 밟을 때 씁니다. 두 노드 모두 자식이 아직 실행 중(InProgress)이면 그 완료를 기다리며 다음 자식으로 바로 넘어가지 않습니다.
단순 병렬(Simple Parallel)은 주 작업(Main Task) 하나와 보조 서브트리를 함께 진행하는 컴포지트로, 완료 모드에 따라 주 작업이 끝났을 때 보조 작업을 어떻게 끝낼지 정합니다. 이름과 달리 CPU의 멀티스레드 실행을 뜻하지는 않습니다.
데코레이터(Decorator)는 자식 노드를 실행할지 정하는 조건을 붙입니다. 블랙보드 값으로 조건을 검사하는 블랙보드 데코레이터, 행동의 재사용 대기 시간을 정하는 Cooldown, 결과를 성공으로 바꾸는 Force Success 같은 것이 있습니다. 플레이어와의 거리를 비교하려면 제공되는 거리 검사나 직접 만든 데코레이터를 쓰며, 특정 이름의 노드가 늘 내장되어 있다고 가정하지 않습니다. Force Success는 실패 경로를 가려 버릴 수 있으므로 쓰는 목적을 분명히 합니다.
서비스(Service)는 연결된 가지가 활성화된 동안 정한 주기마다 실행되어 블랙보드 값을 갱신하거나 환경을 살핍니다. Interval로 실행 간격을 정하는데, 예를 들어 0.5초마다 플레이어의 위치를 확인해 블랙보드에 기록하는 식입니다.
태스크(Task)는 AI가 실제로 수행하는 가장 작은 단위의 행동입니다. 특정 위치나 액터로 이동하는 Move To, 일정 시간 기다리는 Wait, 애니메이션을 재생하는 Play Animation, 환경 쿼리 시스템(Environment Query System, EQS)을 실행해 알맞은 위치를 찾는 Run EQS Query가 있고, 총 발사나 스킬 사용처럼 게임에 맞는 행동은 블루프린트나 C++로 직접 만듭니다.
블랙보드
블랙보드는 행동 트리와 함께 쓰는 AI의 데이터 저장소입니다. AI 컨트롤러가 인지하거나 계산한 플레이어 위치, 목표 액터, 현재 행동 상태 같은 정보를 저장하고, 행동 트리의 각 노드가 이 정보를 읽어 결정을 내립니다.
블랙보드는 UBlackboardData 에셋으로 정의하며, 이름(FName)으로 구별하는 키와 Vector, Object, Bool 같은 타입의 값으로 이루어집니다. 행동 트리의 데코레이터, 서비스, 태스크는 모두 블랙보드 키를 참조해 값을 읽거나 씁니다. 이렇게 AI의 로직과 데이터를 나누면 재사용하기 쉽고 읽기도 좋아집니다.
선택자와 시퀀스가 자식을 처리하는 방법
같은 자식 순서라도 중단 조건이 다릅니다
독립 Selector 사본의 A=Failure와 Sequence 사본의 A=Success를 선택하면 둘 다 B로 진행한다. B가 Running이면 기다리고 완료 결과가 Composite 종료를 정한다. 구체 AI 실행 기록이 아닌 두 트리 규칙의 독립 비교이며 다른 A 결과는 위 관계도와 표에 보존한다.
UNREAL C++ · CH11-3
같은 자식 순서라도 중단 조건이 다릅니다
자식 A 다음 B를 평가하는 단순 비교입니다. Selector는 성공할 선택지를 찾고, Sequence는 모든 단계를 성공시킵니다. 실행 중인 자식은 완료를 기다립니다.
Selector · 성공을 찾습니다
- A가 Failure
- B를 시도합니다. 모두 실패하면 Selector도 Failure입니다.
- A가 Success
- 여기서 Success로 끝납니다. 이 선택에서는 B를 실행하지 않습니다.
- A가 InProgress
- A의 완료를 기다립니다. 곧바로 B로 넘어가지 않습니다.
Sequence · 모두 성공해야 합니다
- A가 Failure
- 여기서 Failure로 끝납니다. 이 실행에서는 B를 실행하지 않습니다.
- A가 Success
- B로 진행합니다. 모두 성공해야 Sequence도 Success입니다.
- A가 InProgress
- A의 완료를 기다립니다. 곧바로 B로 넘어가지 않습니다.
실제 트리에서는 조건과 중단 정책도 함께 봅니다
이 그림은 두 Composite의 기본 결과 처리를 비교합니다. 자식 A·B는 구체적인 공격/이동 구현이나 실행 기록이 아닙니다.
Decorator의 Observer Aborts 등은 진행 중인 분기를 바꿀 수 있습니다. 비동기 Task는 완료를 알리고 Abort 시 요청·타이머·델리게이트를 정리합니다. Simple Parallel은 별도 Composite이며 이 그림은 CPU 병렬 실행을 뜻하지 않습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 본문·주석 | 자식 A 다음 B를 평가하는 단순 비교입니다. Selector는 성공할 선택지를 찾고, Sequence는 모든 단계의 성공이 필요합니다. 실행 중인 자식은 완료를 기다립니다. |
| 관계·조건 설명 | 위 Selector에서 자식 A와 자식 B로 각각 향하는 소속 관계 두 개다. 아래 배치는 A 다음 B의 순서를 유지하며 A에서 B로 실행되는 화살표가 아니다. 실제 다음 자식 실행 여부는 아래 Success/Failure/InProgress 정의로 결정한다. |
| 관계·조건 설명 | 위 Sequence에서 자식 A와 자식 B로 각각 향하는 소속 관계 두 개다. 아래 배치는 A 다음 B의 순서를 유지하며 A에서 B로 실행되는 화살표가 아니다. 실제 다음 자식 실행 여부는 아래 Success/Failure/InProgress 정의로 결정한다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| A 완료 독립 Selector는 A=Failure, Sequence는 A=Success인 선택 사례에서 각각 B로 진행한다. |
|
|
| B 진행 B가 Running이면 현재 Child를 기다린다. |
|
|
| 완료 결과 B의 성공·실패에 따라 Composite를 진행/종료한다. |
|
|
같은 자식 순서라도 중단 조건이 다릅니다
독립 Selector 사본의 A=Failure와 Sequence 사본의 A=Success를 선택하면 둘 다 B로 진행한다. B가 Running이면 기다리고 완료 결과가 Composite 종료를 정한다. 구체 AI 실행 기록이 아닌 두 트리 규칙의 독립 비교이며 다른 A 결과는 위 관계도와 표에 보존한다.
UNREAL C++ · CH11-3
같은 자식 순서라도 중단 조건이 다릅니다
자식 A 다음 B를 평가하는 단순 비교입니다. Selector는 성공할 선택지를 찾고, Sequence는 모든 단계를 성공시킵니다. 실행 중인 자식은 완료를 기다립니다.
Selector · 성공을 찾습니다
- A가 Failure
- B를 시도합니다. 모두 실패하면 Selector도 Failure입니다.
- A가 Success
- 여기서 Success로 끝납니다. 이 선택에서는 B를 실행하지 않습니다.
- A가 InProgress
- A의 완료를 기다립니다. 곧바로 B로 넘어가지 않습니다.
Sequence · 모두 성공해야 합니다
- A가 Failure
- 여기서 Failure로 끝납니다. 이 실행에서는 B를 실행하지 않습니다.
- A가 Success
- B로 진행합니다. 모두 성공해야 Sequence도 Success입니다.
- A가 InProgress
- A의 완료를 기다립니다. 곧바로 B로 넘어가지 않습니다.
실제 트리에서는 조건과 중단 정책도 함께 봅니다
이 그림은 두 Composite의 기본 결과 처리를 비교합니다. 자식 A·B는 구체적인 공격/이동 구현이나 실행 기록이 아닙니다.
Decorator의 Observer Aborts 등은 진행 중인 분기를 바꿀 수 있습니다. 비동기 Task는 완료를 알리고 Abort 시 요청·타이머·델리게이트를 정리합니다. Simple Parallel은 별도 Composite이며 이 그림은 CPU 병렬 실행을 뜻하지 않습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 본문·주석 | 자식 A 다음 B를 평가하는 단순 비교입니다. Selector는 성공할 선택지를 찾고, Sequence는 모든 단계의 성공이 필요합니다. 실행 중인 자식은 완료를 기다립니다. |
| 관계·조건 설명 | 위 Selector에서 자식 A와 자식 B로 각각 향하는 소속 관계 두 개다. 아래 배치는 A 다음 B의 순서를 유지하며 A에서 B로 실행되는 화살표가 아니다. 실제 다음 자식 실행 여부는 아래 Success/Failure/InProgress 정의로 결정한다. |
| 관계·조건 설명 | 위 Sequence에서 자식 A와 자식 B로 각각 향하는 소속 관계 두 개다. 아래 배치는 A 다음 B의 순서를 유지하며 A에서 B로 실행되는 화살표가 아니다. 실제 다음 자식 실행 여부는 아래 Success/Failure/InProgress 정의로 결정한다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| A 완료 독립 Selector는 A=Failure, Sequence는 A=Success인 선택 사례에서 각각 B로 진행한다. |
|
|
| B 진행 B가 Running이면 현재 Child를 기다린다. |
|
|
| 완료 결과 B의 성공·실패에 따라 Composite를 진행/종료한다. |
|
|
위 그림은 같은 자식 순서 A, B를 두고 선택자와 시퀀스가 결과를 어떻게 다르게 처리하는지 비교합니다. 두 그림 모두 컴포지트 아래에 A가 왼쪽, B가 오른쪽에 놓여 있으며, 컴포지트에서 각 자식으로 향하는 선은 소속을 뜻할 뿐 A에서 B로 실행이 넘어가는 화살표가 아닙니다. 다음 자식을 실행할지는 아래의 결과 규칙이 정합니다.
선택자에서는 A가 실패하면 B를 시도하고, 모두 실패하면 선택자도 실패합니다. A가 성공하면 그 자리에서 성공으로 끝나므로 이번 선택에서는 B를 실행하지 않습니다. A가 실행 중이면 A의 완료를 기다리며 B로 넘어가지 않습니다. 시퀀스에서는 반대로, A가 실패하면 그 자리에서 실패로 끝나 이번 실행에서 B를 실행하지 않고, A가 성공하면 B로 나아가며 모두 성공해야 시퀀스도 성공합니다. A가 실행 중이면 마찬가지로 완료를 기다립니다.
그림 아래의 설명처럼 실제 트리에서는 조건과 중단 정책도 함께 봐야 합니다. 이 그림은 두 컴포지트의 기본 결과 처리만 비교하며, A와 B는 구체적인 공격이나 이동의 구현, 실행 기록이 아닙니다. 데코레이터의 Observer Aborts 같은 설정은 진행 중인 가지를 바꿀 수 있고, 비동기 태스크는 완료를 알려야 하며 중단되면 요청, 타이머, 델리게이트를 정리해야 합니다. 단계 그림은 선택자에서 A가 실패한 경우와 시퀀스에서 A가 성공한 경우처럼 둘 다 B에 이르는 사례를 골라, B가 실행 중이면 기다리고 B의 결과가 컴포지트의 끝을 정하는 과정을 따라갑니다.
구체적인 예로 따라가 봅시다. 데코레이터로 인한 중단이나 조건 변화는 없다고 정합니다. 선택자의 첫 자식 A가 공격 조건이 모자라 실패하면 B를 시도하고, B가 순찰 중이라 실행 중이면 B가 완료를 알릴 때까지 진행 중 상태가 됩니다. 실패를 보고 다음 선택지를 찾는 것과, 실행 중을 실패로 처리하는 것은 다른 일입니다.
시퀀스에 대상 확인, 이동, 공격의 세 자식을 두었다면, 대상 확인이 성공하고 이동이 실패했을 때 시퀀스는 실패하며 이번 실행에서 공격은 하지 않습니다. 블랙보드의 Object 키 TargetActor에 유효한 대상이 들어 있다는 것과 그 대상까지 이동할 경로가 있다는 것도 서로 다른 조건입니다. 또 키를 만든다고 그 값을 찾아 채워 주는 컨트롤러, 서비스, 인지 연결이 저절로 생기지는 않습니다.
행동 트리를 잘 설계하는 방법
계층으로 나누기
거대한 행동 트리 하나보다 작은 행동 트리 여러 개를 만들고, Run Behavior 태스크로 다른 행동 트리를 서브트리처럼 불러 씁니다. 다시 쓰기 쉽고 관리하기도 편합니다. 위쪽 노드는 "목표를 달성하라" 같은 추상적인 행동을 정의하고, 아래쪽 노드에서 이를 구체적인 작업으로 나눕니다.
블랙보드 다루기
행동 판단에 필요한 키만 블랙보드에 두고, 원래 게임 상태나 큰 데이터 전체를 중복으로 복사하지 않습니다. 각 키를 누가 갱신하는지도 정해 둡니다. 키 이름은 무엇을 담는지 알 수 있게 분명히 짓고, 키에 저장하는 값의 타입이 맞는지도 주의합니다.
데코레이터와 Observer Aborts
데코레이터는 필요 없는 가지를 실행하지 않게 해 로직을 분명히 하고 비용도 줄입니다. 그리고 Observer Aborts 설정으로 조건이 바뀌었을 때 지금 실행 중인 가지를 중단하고 다시 평가할지 정할 수 있어, AI가 상황 변화에 빨리 반응하게 하는 데 중요합니다. None은 조건이 바뀌어도 현재 가지를 중단하지 않고, Self는 조건이 더 이상 맞지 않으면 자신이 붙은 가지를 중단합니다. Lower Priority는 자신보다 낮은 우선순위(오른쪽)의 가지를 중단해 높은 우선순위의 가지로 넘어가게 하고, Both는 두 범위를 함께 씁니다. 관찰하는 값과 Notify Observer 조건도 함께 확인합니다.
서비스는 가볍게
일정 주기마다 환경을 감지하거나 블랙보드 값을 갱신해야 할 때 서비스를 씁니다. 플레이어가 시야 안에 있는지 주기적으로 확인하는 것이 그 예입니다. 서비스는 자주 실행될 수 있으므로 안의 로직은 최대한 가볍게 두고, 무거운 계산은 다른 곳으로 옮기는 것을 검토합니다.
커스텀 태스크
블루프린트나 C++로 커스텀 태스크를 만들어, 복잡하거나 특정 게임 시스템과 연결되는 행동을 감싸 둡니다. 태스크는 입력 키와 성공, 실패의 조건을 분명히 합니다. 비동기 태스크는 실행 중(InProgress)을 돌려준 뒤 나중에 완료를 알리고, 중단되면 타이머, 델리게이트, 이동 요청을 정리해야 합니다. C++에서는 여러 AI가 노드 하나를 함께 쓸 수 있으므로, AI마다 다른 상태를 노드에 그대로 저장하지 말고 노드 메모리나 인스턴싱 정책을 확인합니다.
디버깅과 시각화
게임플레이 디버거(Gameplay Debugger)는 PIE에서 기본으로 작은따옴표 키로 열고, 대상 AI와 Behavior Tree, Perception 같은 분류를 골라 봅니다. 프로젝트의 단축키, 플러그인, 빌드 지원을 확인합니다. 행동 트리 에디터에서는 실행 중인 인스턴스를 골라 현재 노드, 실행 이력, 블랙보드 값을 함께 볼 수 있습니다. ai.debug [이름]이나 ai.blackboard [이름] 같은 명령이 어디서나 통한다고 가정하지 않습니다. 디버깅과 로그 관리에서 본 비주얼 로거로 AI의 이동 경로나 목표 변경을 기록하고 다시 보며 문제를 분석할 수도 있습니다.
C++에서의 성능
AI 컨트롤러와 컴포넌트의 필요 없는 틱은 끄고(CPU, GPU 병목 현상 해결 참고), 플레이어에게서 멀리 떨어진 AI는 갱신 빈도를 줄이거나 행동 트리를 단순하게 만듭니다. EQS는 복잡한 환경에서 AI가 알맞은 위치를 찾을 때 쓰는 유연한 도구이지만 비용이 들 수 있으므로, 필요할 때만 쓰고 쿼리를 다듬습니다.
반응이 늦거나 멈출 때
AI가 이상하게 움직일 때는 증상에 따라 먼저 확인할 곳이 다릅니다.
| 증상 | 먼저 확인할 곳 |
|---|---|
| 표적을 늦게 바꿈 | 인지 이벤트, 키 갱신, 서비스가 활성화된 가지와 주기 |
| 같은 행동을 되풀이해 취소함 | 키 값이 오락가락하는지, Observer Aborts의 범위 |
| 태스크에서 멈춤 | 비동기 완료 알림, 실패 반환, 중단 시 정리 |
| Move To가 실패함 | 유효한 목표, 내비게이션 메시의 연결, 경로 요청 결과 |
언리얼의 행동 트리는 이벤트 중심으로 동작합니다. 트리 전체를 매 프레임 훑는 대신 필요한 키의 변화와 실행 중인 서비스, 태스크를 살펴봅니다. 실행과 중단의 의미는 Epic의 Behavior Tree Overview 문서를 기준으로 확인합니다.
그 밖의 AI 기법
스테이트 머신(state machine)은 간단한 AI 행동이나 애니메이션 제어에 효과적이며, 행동 트리와 함께 쓸 수 있습니다. 유틸리티 AI(Utility AI)는 행동마다 점수를 매겨 가장 높은 점수의 행동을 고르는 방식으로, 행동 트리보다 유연하고 상황에 잘 적응할 수 있습니다. 최근에는 강화 학습(reinforcement learning) 같은 머신러닝으로 AI를 훈련시켜 더 복잡하고 예측하기 어려운 행동을 만들려는 시도도 활발하며, 언리얼의 Learning Agents 플러그인이 강화 학습과 모방 학습의 경로를 제공합니다. 버전마다의 실험 기능 상태와 별도의 학습, 런타임 추론 비용을 확인해야 합니다.
더 깊이: 언리얼의 행동 트리가 일반적인 행동 트리와 다른 점
다른 엔진이나 교재에서 행동 트리를 배웠다면, 매 틱 루트부터 트리 전체를 다시 훑는 방식을 떠올리기 쉽습니다. Behavior Tree 문서에 따르면 언리얼의 행동 트리는 매 프레임 불필요한 일을 하지 않도록 이벤트 기반(event-driven)으로 동작합니다. 트리를 매 틱 다시 훑지 않고, 관련된 값이 바뀌었다는 알림을 받을 때 반응하므로 성능이 훨씬 좋다고 문서는 설명합니다. 이 글에서 블랙보드 키의 변화와 Observer Aborts를 강조한 것도 이 구조 때문입니다.
조건을 다루는 방식도 다릅니다. 전통적인 행동 트리는 성공이나 실패만 돌려주는 조건 태스크를 잎(leaf)에 두는 경우가 많지만, 언리얼은 조건을 데코레이터로 두기를 강하게 권합니다. 그러면 잎에는 실제 행동을 하는 태스크만 남아 트리에서 무엇을 하는지가 잘 보입니다. 또 전통적인 트리에서 행동 시퀀스 옆에 조건 검사기를 병렬로 돌려 조건이 깨지면 행동을 멈추던 구조는, 언리얼에서는 데코레이터가 자신의 값을 관찰하다가 필요할 때 중단하는 방식으로 바뀝니다.
동시에 여러 일을 하는 방식도 단순화했습니다. 여러 자식을 한꺼번에 실행하는 일반적인 병렬 컴포지트 대신, 주 작업 하나와 보조 가지 하나를 갖는 단순 병렬 노드를 두고, 주기적으로 해야 하는 일은 컴포지트 노드에 붙는 서비스에 맡깁니다. 문서는 이렇게 하면 트리가 더 단순해지고 최적화하기도 쉬워진다고 설명합니다.
정리
- 폰 기반 AI는 AI 컨트롤러가 빙의해 조종하며, 인지는
AIPerceptionComponent, 이동은MoveTo함수와 내비게이션 메시, 판단은 행동 트리가 맡습니다. - 내비게이션 메시는
Nav Mesh Bounds Volume으로 만들고P키로 확인하며, 지상 이동의 길 찾기에 쓰입니다. - 행동 트리는 루트, 컴포지트(선택자, 시퀀스, 단순 병렬), 데코레이터, 서비스, 태스크로 이루어지고, 블랙보드가 노드들이 함께 쓰는 데이터를 담습니다.
- 선택자는 성공할 자식을 찾고 시퀀스는 모든 자식을 성공시키며, 둘 다 실행 중인 자식의 완료를 기다립니다.
- Observer Aborts로 조건 변화에 따라 가지를 중단하고, 비동기 태스크는 완료를 알리고 중단 시 정리하며, 공유 노드에 AI별 상태를 두지 않습니다.
- 게임플레이 디버거, 행동 트리 에디터, 비주얼 로거로 실행 상태를 확인하고, 증상에 따라 키 갱신, 중단 범위, 태스크 완료, 경로를 점검합니다.
연습 문제
1. A가 늦게 성공하면
같은 A, B 순서에서 A가 처음에는 실행 중이다가 나중에 성공을 알렸습니다. 중단은 없다고 할 때, B는 선택자와 시퀀스에서 각각 언제 실행될까요?
해설 보기
A가 실행 중인 동안에는 두 컴포지트 모두 B로 바로 넘어가지 않고 기다립니다. A가 성공을 알리면, 선택자는 성공할 자식을 찾았으므로 성공으로 끝나 B를 실행하지 않고, 시퀀스는 다음 단계인 B를 이어서 실행합니다. 이것은 기본 결과 규칙을 적용한 것이며, 쓰는 엔진의 실행 로그나 모든 데코레이터 정책을 대신하지는 않습니다.
2. 중단된 뒤에 날아온 완료
비동기 태스크 A가 데코레이터 때문에 중단되었는데, A가 걸어 둔 타이머가 남아 있다가 나중에 완료를 다시 알렸습니다. 어떤 연결을 점검해야 할까요?
해설 보기
태스크가 중단될 때의 정리 경로와 완료를 알리는 경로를 점검합니다. 중단되면 그 태스크가 건 타이머, 이동 요청, 델리게이트를 모두 해제해야 하는데, 이를 빠뜨리면 이미 끝난 태스크가 엉뚱한 시점에 완료를 알려 트리의 흐름을 흐트러뜨립니다.
또 그 태스크의 상태가 AI마다 따로 있는지, 여러 AI가 공유하는 노드에 저장되어 있는지도 구별해야 합니다. 공유 노드에 상태를 두었다면 다른 AI의 타이머가 섞일 수 있습니다.
3. 키를 만들었는데 비어 있다
블랙보드에 Object 키 TargetActor를 만들고, 시퀀스의 첫 자식에 이 키가 설정되어 있는지 확인하는 데코레이터를 붙였습니다. 플레이어가 바로 앞에 있는데도 시퀀스가 실행되지 않습니다. 무엇이 빠졌을까요?
해설 보기
TargetActor에 값을 채우는 연결이 빠졌을 가능성이 큽니다. 키를 만드는 것은 저장할 자리를 만드는 일일 뿐이며, 인지 이벤트나 서비스, 컨트롤러가 플레이어를 찾아 그 키에 값을 써 넣어야 데코레이터의 조건이 참이 됩니다. 행동 트리 에디터나 게임플레이 디버거에서 블랙보드의 현재 값을 확인하고, 그 키를 누가 언제 갱신하는지 정합니다.
다음 글 머신러닝 및 실시간 렌더링에서는 언리얼에서 머신러닝을 활용하는 방법과 루멘, 나나이트 같은 실시간 렌더링 기술을 살펴봅니다.