2025. 5. 8. 21:14ㆍ[Unity] Game Programing
Unity 기반 게임 AI: 행동 트리(Behaviour Tree)
1. 행동 트리 개요 및 등장 배경 (FSM과의 비교 포함)
행동 트리(Behaviour Tree, BT)는 게임 AI에서 에이전트의 결정 및 행동 구조를 정의하기 위해 2000년대 초반 비디오 게임 업계에서 처음 고안되었다. BT는 계층적 트리 구조로 구성되며, 루트 노드에서 시작하여 조건에 따라 자식 노드를 순차적으로 탐색함으로써 캐릭터의 행동을 결정한다. FSM(유한 상태 머신)과 달리 BT는 **모듈화(modularity)**와 재사용성이 뛰어나며, 복잡도가 커져도 유지보수가 비교적 용이하다는 장점이 있다. 예를 들어, BT는 트리의 각 노드가 서로 독립적으로 추가·삭제될 수 있어 행동을 빠르게 조정할 수 있는 반면, FSM은 상태가 많아지면 상태 전이가 복잡해지기 쉽다. 실제로 연구에 따르면 작업이 복잡해질수록 FSM보다 BT가 관리하기 더 쉬워지는 경향이 나타났다.
- FSM vs BT 차이: FSM은 하나의 상태에만 머무르며 조건에 따라 상태 전이를 한다. 설계가 단순하므로 소규모 AI에는 적합하지만, 행동의 수와 전이가 늘어나면 설계 복잡도가 급격히 증가한다. 반면 BT는 여러 계층과 노드로 행동을 분할하여 시퀀스 또는 선택과 같은 컴포지트 노드를 통해 동작을 제어하므로, 큰 행동 집합도 계층 구조로 관리할 수 있다. BT는 반응성, 가독성, 모듈성이 우수하여 로봇 제어 등에서도 각광받는 반면, FSM은 직관적이고 구현이 용이하다는 장점이 있다.
- 등장 배경: 게임 개발에서는 캐릭터 수가 많고 행동이 복잡한 콘솔/PC 게임에서 FSM의 한계를 극복하기 위해 BT가 도입되었다. 또한, 로봇공학 분야에서도 FSM보다 유연한 정책 설계 도구로 주목받고 있다.
*예: IO Interactive의 **Hitman(2016)*에서는 수백 명의 NPC를 상황에 반응하는 행동 트리로 제어하여 동적 시뮬레이션을 구현했다.
2. 행동 트리의 기본 구성 요소와 노드 동작 방식
행동 트리는 크게 루트(Root) 노드, 컴포지트 노드(Composite), 장식자 노드(Decorator), **리프 노드(Leaf)**로 구분된다.
- 루트 노드(Root): 트리의 시작점이다. 일반적으로 게임 루프에서 매 틱마다 Tick 신호를 받아 자식 노드를 탐색한다. 별도의 기능은 없으며, 단순히 트리를 실행하는 진입점 역할을 한다.
- 컴포지트 노드(Composite): 복수의 자식 노드를 가지며, 자식 노드들을 실행하는 제어 흐름을 담당한다. 대표적으로 **시퀀스(Sequence)**와 **셀렉터(Selector, Fallback)**가 있다.
- 시퀀스(Sequence): 자식 노드들을 순서대로 실행한다. 자식이 실패(Failure) 상태를 반환하면 즉시 시퀀스도 실패로 끝내고, 모든 자식이 성공하면 성공(Success)을 반환한다. 예를 들어, 적에게 접근했다가 타격하는 두 작업을 순차적으로 수행하고, 만약 중간에 한 작업이 실패하면 전체 행동을 종료하도록 한다.
- 셀렉터(Selector, Fallback): 자식 노드들 중 첫 번째 성공 노드를 찾아 실행한다. 자식이 실패하면 다음 자식으로 넘어가며, 하나라도 성공하면 즉시 성공으로 끝낸다. 모든 자식이 실패할 경우 셀렉터도 실패를 반환한다. 즉, 여러 방법 중 하나라도 성공하면 성공하는 방식이다.
- (참고: 특정 구현에서는 병렬(Parallel) 노드나 확률(Probabilistic) 노드를 추가하기도 한다.)
- 장식자 노드(Decorator): 단 하나의 자식 노드만 가지며, 자식 노드의 실행을 수정하거나 반복하는 역할을 한다. 예를 들어 Inverter(반전)는 자식의 성공/실패를 뒤집어 반환하고, Succeeder는 항상 성공을 반환한다. Repeater(반복)나 Wait(대기) 같은 장식자는 자식을 여러 번 반복하거나 일정 시간 동안 지연하는 등의 동작을 수행한다.
- 리프 노드(Leaf): 실행 가능한 작업(Action) 또는 조건(Condition) 노드이다. 리프 노드는 외부 시스템과 상호작용하며 실제 행동이나 검사를 수행한다. 예를 들어 WalkToTarget 액션이나 IsEnemyVisible 조건이 해당한다. 조건 노드는 주로 단일 틱 안에 성공/실패를 반환하고, 액션 노드는 긴 실행을 위해 Running 상태를 반환할 수 있다.
각 노드는 성공(Success), 실패(Failure), 실행중(Running) 중 하나의 상태를 반환한다. 이 상태 값은 부모 노드의 흐름 제어에 사용되며, 노드별로 해석 방식이 다르다. 예를 들어 시퀀스는 자식이 실패하면 즉시 실패하며, 자식이 실행중이면 실행중을 반환한다. 반면 셀렉터는 자식이 성공하면 성공으로 종료하고, 실패하면 다음 자식으로 넘어가며, 모든 자식이 실패하면 실패 상태가 된다. 이러한 구조를 통해 복잡한 의사결정 로직을 트리 형태로 표현할 수 있다.
예시: 행동 트리 구조 (간단한 ASCII 다이어그램)
Root
└─ Selector
├─ Sequence
│ ├─ Condition: IsEnemyVisible (적 발견 여부)
│ └─ Action: AttackEnemy (공격 행동)
└─ Action: PatrolArea (순찰 행동)
위 예시에서 루트는 셀렉터를 실행한다. 셀렉터는 먼저 시퀀스를 검사한다. 시퀀스는 적을 발견했는지 확인한 뒤(조건), 발견되었다면 공격 행동을 수행한다. 만약 적 발견 조건이 실패하면 시퀀스는 즉시 실패로 간주되고, 셀렉터는 다음 자식인 PatrolArea 행동을 실행하여 순찰한다.
3. 실행 흐름과 상태 시스템 (Success, Failure, Running)
행동 트리의 실행 흐름은 매 틱마다 루트에 Tick() 신호를 보내며 시작된다. 루트는 자식 컴포지트 노드로 신호를 전달하고, 컴포지트 노드는 자신의 규칙에 따라 자식 노드를 차례로 평가한다. 각 노드가 실행되면 성공, 실패, 실행중 중 하나의 상태 값을 상위 노드로 반환한다. 이 상태는 다음과 같은 의미를 가진다:
- Success: 노드가 행동을 성공적으로 완료했음을 나타내며, 부모 노드가 다음 작업을 이어가도록 한다.
- Failure: 노드가 행동을 실패했음을 의미하며, 부모 노드는 현재 분기를 종료할 수 있다(예: 시퀀스의 경우 실패시 즉시 종료).
- Running: 노드의 행동이 아직 완료되지 않았음을 나타내며, 부모는 매 틱마다 이 노드를 다시 실행해야 한다.
컨트롤 노드(시퀀스, 셀렉터 등)는 자식들의 상태 값을 기반으로 판단한다. 예를 들어 시퀀스는 자식이 실패를 반환하면 전체가 실패로 종료하고, 실행중을 반환하면 실행중 상태로 멈춘다. 반면 셀렉터는 자식이 성공을 반환하면 즉시 성공으로 끝내고, 실패 시 다음 자식으로 진행한다. 이러한 방식으로 매 프레임마다 트리가 갱신되며, 에이전트는 환경 변화를 반영하여 동적·반응적으로 행동을 선택하게 된다.
4. Unity에서의 행동 트리 구현 개념과 핵심 코드 예시
Unity에서 행동 트리를 구현할 때는 C# 클래스를 사용하여 노드 구조를 정의하고, 검사(Evaluate) 메서드를 통해 상태를 반환하는 방식을 많이 사용한다. 예를 들어 각 노드는 Evaluate() 또는 Tick() 메서드를 구현하며, 실행 결과로 NodeState(Success, Failure, Running) 값을 반환한다. 대표적인 구현 방식은 추상 Node 클래스를 정의하고, 이를 상속해 SequenceNode, SelectorNode 등을 구현하는 것이다.
// 노드 상태 열거형
public enum NodeState { Success, Failure, Running }
// 추상 기본 노드 클래스
public abstract class Node {
public abstract NodeState Evaluate();
}
// Sequence 노드 예시
public class SequenceNode : Node {
private List<Node> children;
public SequenceNode(List<Node> nodes) { children = nodes; }
public override NodeState Evaluate() {
foreach (var child in children) {
NodeState result = child.Evaluate();
if (result == NodeState.Failure)
return NodeState.Failure; // 자식 중 실패가 있으면 즉시 실패
if (result == NodeState.Running)
return NodeState.Running; // 자식이 실행 중이면 계속 실행중 반환
}
return NodeState.Success; // 모든 자식이 성공하면 성공 반환
}
}
// Selector 노드 예시
public class SelectorNode : Node {
private List<Node> children;
public SelectorNode(List<Node> nodes) { children = nodes; }
public override NodeState Evaluate() {
foreach (var child in children) {
NodeState result = child.Evaluate();
if (result == NodeState.Success)
return NodeState.Success; // 자식 중 성공이 있으면 즉시 성공
if (result == NodeState.Running)
return NodeState.Running; // 자식이 실행 중이면 계속 실행중 반환
}
return NodeState.Failure; // 모든 자식이 실패하면 실패 반환
}
}
위 코드 예시는 시퀀스와 셀렉터 노드의 기본 구조를 보여준다. 시퀀스 노드는 자식들을 순서대로 평가하며, 자식 중 하나라도 실패하면 실패를 반환하고, 모두 성공했을 때만 성공을 반환한다. 반면 셀렉터는 자식 중 하나라도 성공하면 즉시 성공으로 종료하고, 모두 실패하면 실패를 반환. 이러한 로직은 [대부분의 행동 트리 구현 자료]에서 공통으로 제시된다.
Unity에서는 이 구조에 MonoBehaviour나 ScriptableObject를 결합하여 트리를 구성할 수 있다. 예를 들어 BehaviorTree라는 컴포넌트를 만들어 루트 노드를 가지고 매 프레임 Evaluate()를 호출하도록 한다. Unity 에디터용 플러그인(Behavior Designer, NodeCanvas 등)도 있는데, 공식 설명처럼 Unity는 행동 트리를 위한 전용 툴을 제공하기도 한다.
// 간단한 BehaviorTree 클래스 예시
public class BehaviorTree : MonoBehaviour {
public Node root; // 에디터에서 설정된 트리의 루트
void Update() {
if (root != null)
root.Evaluate(); // 매 프레임 트리를 실행
}
}
실제 구현 시 노드 간 상호작용을 위해 **블랙보드(Blackboard)**를 함께 사용한다(다음 섹션 참조). 각 노드는 블랙보드의 값을 읽어 조건을 판단하거나 동작을 결정할 수 있다. 위 예시 코드는 단순히 트리 구조와 흐름을 보여준 것이며, 프로젝트에 따라 큐 형태로 Tick()을 하는 방식이나 리스너 기반 방식 등 다양한 최적화 기법을 적용할 수 있다.
참고: 행동 트리 구현에 대한 자세한 예시는 Lem Apperson의 글에서 Sequence/Selector 구현 예제를 확인할 수 있다.
5. 블랙보드(Blackboard) 시스템 설명 및 예제
블랙보드는 공유 데이터 저장소로, 행동 트리의 노드들이 의사결정에 필요한 데이터를 읽고 쓸 수 있도록 한다. 각 AI 캐릭터마다 별도의 블랙보드를 사용할 수도 있고, 분대 등 여러 개체가 정보를 공유하도록 전역 블랙보드를 사용할 수도 있다. 블랙보드는 주로 딕셔너리 형태로 구현하며, 키-값 쌍으로 정보를 저장한다. 예를 들어 적 발견 여부, 목표 위치, 체력 상태 등의 정보를 블랙보드에 기록하고, 노드가 필요할 때 조회하여 사용한다.
// 간단한 블랙보드 클래스 예시
public class Blackboard {
private Dictionary<string, object> data = new Dictionary<string, object>();
// 값 설정
public void SetValue<T>(string key, T value) {
data[key] = value;
}
// 값 조회 (제네릭)
public bool TryGetValue<T>(string key, out T value) {
if (data.TryGetValue(key, out object obj) && obj is T) {
value = (T)obj;
return true;
}
value = default;
return false;
}
}
예를 들어, 시야 검사 노드가 적을 발견하면 blackboard.SetValue("PlayerVisible", true)로 기록하고, 이후 공격 행동 노드에서 blackboard.TryGetValue("PlayerVisible", out bool visible)로 값을 읽어 공격 여부를 결정할 수 있다. 블랙보드는 이벤트 기반 동작을 구현하거나 값 캐싱에도 유용하다. 블랙보드 값의 변화가 감지되면 트리의 분기를 조정하거나 별도의 이벤트를 발생시켜 트리 실행 흐름을 변화시킬 수 있다. 또한, 계산 비용이 큰 데이터(예: 거리 계산 결과)를 블랙보드에 저장해 두면, 동일한 값을 여러 노드에서 반복 계산하지 않고 재사용할 수 있다.
예: 블랙보드는 전통적으로 행위 트리와 함께 사용되며, 의사결정에 필요한 정보를 중앙에 저장하는 역할을 한다. 이를 통해 이벤트 기반으로 트리를 동적으로 수정하거나 복잡한 계산 결과를 캐시할 수 있다.
6. 고급 행동 트리 기능: Abort, Priority BT, Repeater, Wait 등
행동 트리에는 기본 노드 외에도 중단(abort), 우선순위와 같은 고급 기능이 있으며, 이는 복잡한 AI 동작을 구현할 때 유용하다.
- 중단(Conditional Abort): 특정 조건이 변할 때, 현재 실행 중인 행동을 중단하고 트리의 다른 분기를 다시 평가하도록 하는 기능이다. 예를 들어 대기(Wait)나 반복(Repeater) 동작이 실행 중일 때, 상위 노드에 설정된 조건이 새로 충족되면 현재 노드를 바로 중단할 수 있다. 대부분의 BT 구현체는 트리 전체를 매 틱 다시 평가하도록 설계되지만, 연산 비용을 줄이기 위해 조건 노드가 자신의 Conditional Abort 속성을 사용해 필요한 부분만 재평가하도록 할 수 있다.
- 중단 타입에는 Self, Lower Priority, Both 등이 있다. Self는 동일한 부모 노드 범위 내에서만 중단을 허용하고, Lower Priority는 우선순위가 낮은 다른 분기를 중단할 수 있게 한다. (예: 가장 왼쪽 분기를 최우선, 오른쪽은 낮은 우선순위로 가정).
- 중단이 발생하면 해당 노드의 자식(예: 대기 중이던 Action)이 즉시 멈추고, 조건 노드가 다시 평가되어 새로운 행동 분기가 선택된다. 이로써 변화하는 게임 상황에 빠르게 대응할 수 있다.
- 단, 주의할 점으로 중단된 후 재개가 자동으로 되지 않는 점이 있다. 예를 들어 대기 노드가 중단되면, 이후 트리의 흐름에 따라 새로운 작업이 실행되고, 원래 대기 행동은 취소된다. 또한 한 개발자 포럼에서는 “조건 충족 시 다른 작업이 조건부 중단을 막을 수 없으므로 Wait/Repeater는 중단될 수밖에 없다”고 조언했다. 즉, 대기/반복 노드는 중단 상황에서 별도 처리 없이 상위 레벨로 옮겨야 한다.
- 우선순위 행동 트리(Priority BT): 일반적인 셀렉터는 자식 노드를 왼쪽부터 순서대로 검사하지만, 동적으로 우선순위를 조정하거나 가중치를 부여하는 확장된 방식을 사용할 수 있다. 예를 들어 **유틸리티 셀렉터(Utility Selector)**는 각 자식 노드의 유틸리티 값을 계산한 뒤 가장 높은 값을 가지는 노드를 선택한다. Game AI Pro의 제안에 따르면, 자식 노드의 중요도를 동적으로 계산하여 “가장 우선순위 높은 행동”을 선택하게 할 수 있다. 이 방법은 전통적 BT의 정적 우선순위 한계를 극복하며, 특정 상황에서는 일정 확률에 기반한 선택도 가능하다.
예를 들어 전투 상황에서 공격과 치료 행동이 있을 때, 목표 체력이 낮으면 치료 행동의 유틸리티가 높아져 자동으로 우선순위가 상승하도록 할 수 있다. Halo 2의 AI도 “우선순위 리스트” 방식으로 동작했는데, 자식 행동을 우선순위대로 나열해 가장 높은 우선순위 행동을 먼저 실행하며, 이후 틱마다 더 높은 우선순위의 행동으로 전환할 수 있도록 설계했다. - Repeater와 Wait:
- Repeater(반복): 자식 노드를 반복해서 실행하게 하는 데코레이터이다. 일반적인 Repeater는 자식 노드가 성공/실패 상태를 반환할 때마다 동일한 자식을 다시 실행하며, 필요에 따라 반복 횟수나 무한 반복 기능을 설정할 수 있다. 트리의 최하단에 배치하여 계속해서 행동을 반복하거나, 특정 횟수만큼 반복하도록 사용할 수 있다.
- Repeat Until Fail: Repeater의 변형으로, 자식이 실패를 반환할 때까지 계속 반복한다. 자식이 실패하면 Repeater는 성공을 반환하며 반복을 종료한다. 예를 들어 도어 목록을 순회하다가 더 이상 처리할 문이 없을 때 실패를 반환하여 반복을 종료할 수 있다.
- Wait(대기): 주로 액션 노드로 구현하며, 일정 시간 동안 다음 행동으로 넘어가지 않고 대기 상태를 유지하는 기능을 수행한다. 장식자로 구현할 수도 있다. 예를 들어 공격 후 짧은 대기 시간을 두거나, 특정 이벤트까지 행동을 멈출 때 사용한다.
이러한 고급 기능들을 적절히 조합하면 행동 트리가 훨씬 유연해진다. 예를 들어 **“우선순위 셀렉터 + 반복 + 중단”**을 함께 사용하면, 에이전트는 높은 우선순위 행동을 계속 시도하다가, 조건 변화 시에는 즉시 중단하고 새 행동으로 전환할 수 있다.
7. 진화형 행동 트리 설계
최근에는 전통적 BT를 넘어 다양한 확장/혼합 기법이 연구되고 있다. 대표적인 예로는 다이내믹 BT, 유틸리티 기반 BT, GOAP 연계, 하이브리드 BT(FSM+BT) 등이 있다.
- 다이내믹 BT(Dynamic BT): 런타임 중에 트리 구조를 동적으로 수정하거나 확장하는 개념이다. 예를 들어 Far Cry Primal에서는 새 행동을 “삽입(injection)”하는 시스템을 사용했다. 플레이어가 동료에게 지시할 때, 게임은 기존 행동 트리에 서브 트리를 실시간으로 주입해 새로운 행동을 실행한다. 이 방식은 기존 트리를 건드리지 않고 빠르게 행동을 추가할 수 있어, 동료 AI처럼 상황별 행동을 즉시 반영해야 할 때 유용하다. 동적 트리는 필요에 따라 새로운 분기를 활성화/비활성화하거나 트리를 다시 구성할 수 있다.
- 유틸리티 기반 BT (Utility-Driven BT): 앞서 언급한 우선순위 셀렉터 개념을 좀 더 확장한 것이다. 각 행동에 점수(유틸리티)를 부여하고, 현재 상황에 가장 알맞은 행동을 선택한다. Game AI Pro에서는 기존 셀렉터를 “Utility Selector”로 바꿔, 각 자식 노드의 현재 가치를 계산해 가장 가치 높은 행동을 선택하도록 예시를 제시했다. 이 접근법은 일종의 동적 의사결정으로, 특정 상황에서 우선순위가 정적으로 고정된 기존 BT의 단점을 보완한다.
- GOAP 연계 (Goal-Oriented Action Planning): GOAP는 목표 지향적 계획 수립 방식으로, 플래너가 목표를 설정하고 일련의 행동을 계획해 수행한다. 일부 시스템에서는 BT와 GOAP를 함께 사용한다. 예를 들어 “Goal-Oriented Behavior Tree (GOBT)” 프레임워크는 BT, GOAP, 유틸리티를 결합한 구조를 제안하였다. GOBT에서는 전통적인 BT로 기본 의사결정을 처리하고, 필요한 경우 GOAP 기반의 동적 계획 기능과 유틸리티 기반 선택을 통합하여 더욱 유연하게 동작한다. 이를 통해 여러 목표 간 전환이나 복잡한 상황 대응이 가능해진다.
- 하이브리드 BT (FSM+BT 혼합): FSM과 BT의 장점을 결합하는 시도이다. 예를 들어, 행동 트리의 말단 작업이 내부적으로 작은 FSM을 돌리는 방식이 있다. 또는 계층적 FSM(HFSM) 구조와 BT 컴포지트를 함께 활용하기도 한다. 실제 사례로는, 많은 게임이 전체적인 의사결정은 BT로 처리하되, 근접전투나 특수 상태 전환은 FSM으로 구현하여 두 방법을 혼합한다. 이 경우 BT는 큰 흐름을, FSM은 세부 상태를 관리한다.
이처럼 진화형 BT 설계는 상황에 맞춰 BT 구조를 확장하거나 다른 AI 기법과 융합하는 방향으로 연구된다. 게임에 따라 고급 트리 구조를 도입해 BT의 유연성을 높이거나, 외부 플래너와 연동해 더 높은 수준의 행동 계획을 달성할 수 있다.
8. 최적화 전략 (트리 구조 최적화, 노드/상태 캐싱 등)
실제 게임 개발에서는 행동 트리의 성능과 확장성을 고려한 최적화가 필요하다. 다음은 대표적인 최적화 전략이다:
- 트리 구조 단순화: 불필요하게 깊거나 복잡한 트리 구조는 성능을 저하시킨다. 자주 사용하지 않는 분기는 제거하고, 여러 노드를 하나로 합칠 수 있다면 합치는 식으로 트리를 최적화해야 한다. 예를 들어, 단순한 연속 행동은 한 시퀀스에 묶거나, 빈번히 실패하는 경로는 재구성하여 가지치기한다.
- 노드/조건 캐싱: 여러 노드에서 같은 정보를 반복 계산하면 비용이 커진다. 예를 들어 에이전트와 타겟 사이의 거리를 여러 노드에서 사용한다면, 매 틱 거리를 계산하지 않고 한 번만 계산해 블랙보드에 캐시해두면 성능 이점을 얻을 수 있다. 즉, 반복적으로 사용되는 조건 검사의 결과를 저장해두고 필요할 때 재사용한다.
- 컨디셔널 어보트 활용: 중단 기능을 통해 트리의 일부만 재평가하도록 설계할 수 있다. 모든 분기를 매 틱마다 체크하는 대신, 조건 노드 변화 시 그 이하 서브트리만 갱신하게 하면 불필요한 연산을 줄일 수 있다. 예를 들어 우선순위 셀렉터에서 상위 행동이 실행 중일 때는 하위 행동을 검사하지 않도록 설계한다.
- 업데이트 빈도 조절 (LOD 적용): 모든 에이전트의 행동 트리를 매 프레임 전부 업데이트할 필요는 없다. 거리에 따라 업데이트 주기를 조절하거나, 카메라 시야 밖에 있는 AI는 저해상도로 처리하는 LOD(Level-of-Detail) 방식이 있다. 실제로 Hitman에서는 원거리의 AI는 업데이트 빈도를 낮추고 애니메이션을 줄이는 방식으로 최적화했다. 이처럼 주변 상황에 따라 트리 검사 빈도를 조정하면 CPU 부하를 크게 줄일 수 있다.
- 멀티스레드/잡 활용: Unity에서는 C# Job System이나 타 스레드를 이용해 AI 연산을 분산 처리할 수 있다. 대량의 에이전트를 처리할 때는 메인 스레드 부하를 피하기 위해 트리 연산을 분할하여 실행하는 것도 한 방법이다(필요시).
위 전략들은 게임 장르와 상황에 맞게 조합하여 사용한다. 예를 들어, 싱글 플레이어 액션 게임에서는 시야 안의 몇몇 주요 에이전트만 정밀하게 업데이트하고 나머지는 간략화할 수 있다. 반면 수많은 적 유닛이 등장하는 RTS나 MMO류 게임에서는 BT보다는 FSM 또는 다른 AI 기법을 고려하거나, BT를 대규모에 맞게 경량화해야 한다. 중요한 점은 트리의 불필요한 부분을 줄이고, 중복 연산을 피하며, 상황에 따른 업데이트 제어를 통해 최적의 성능을 유지하는 것이다.
9. 실제 게임 사례 분석
여러 AAA 게임들은 행동 트리를 적극 활용하여 NPC AI를 구현했다. 몇 가지 대표적인 사례를 살펴보자.
- Hitman (2016) – IO Interactive: 이 게임은 1개 레벨에 300명이 넘는 NPC가 복잡한 상호작용을 하도록 설계되었다. 상황 중심의 행동 트리를 사용하여, 각 NPC가 주변 상황에 따라 적절히 반응하도록 했다. 예를 들어 경비원은 플레이어가 총격을 시작하면 추격하고, 일반 시민은 놀라 달아나는 등의 분기를 수행한다. 이때 **지식 기반(Knowledge Base)**과 센서 시스템을 통해 NPC가 인지한 정보를 저장하고, 행동 트리가 이 정보를 기반으로 결정을 내린다. Hitman의 AI는 BT를 사용해 반응성 높은 행동을 구조화했으며, 멀리 있는 NPC는 업데이트 빈도를 낮추는 LOD 기법도 도입했다.
- Halo 2 – Bungie: Halo 2의 AI는 본질적으로 계층적 행동 그래프(Behavior DAG) 형태였다. GDC 발표에 따르면, Halo 2 AI는 HFSM 또는 행동 트리(정확히는 비순환 그래프) 구조를 사용했으며, 약 50여 개의 행동이 있다. 각 행동 모듈은 자식 행동의 우선순위를 평가하는 방식으로 동작했다. 특히 우선순위 방식(priority-list)을 통해 높은 우선순위부터 행동을 검사하였고, 실행 도중에도 더 높은 우선순위 행동이 나타나면 **항상 인터럽트(interrupt)**할 수 있도록 설계되었다. 이는 동시다발적으로 변화하는 전장 상황에서 적절히 행동을 전환하기 위함이었다. Halo 2 AI 논문은 유틸리티 개념을 도입하여 가장 중요한 행동을 선택하는 방식을 설명하기도 했다.
- Far Cry Primal – Ubisoft: 프라이멀에서는 야수 동료 시스템에 동적 행동 트리 삽입(Dynamic Behaviour Tree Injection) 기법을 사용했다. 기존 Far Cry의 행동 트리를 수정하지 않고, 플레이어의 명령에 따라 특정 행동(예: ‘저 동물을 공격하라’)을 런타임에 삽입했다. 이로 인해 새 동물 동료나 행동이 빠르게 추가될 수 있었다. 삽입된 행동은 자신의 우선순위를 가지며, 필요시 본래 AI 행동을 일시적으로 무시했다. 또한, 필수적인 반응(예: 죽음에 대한 두려움)은 삽입된 행동을 중단시키는 식으로 동작하여, 자연스러운 행동 전환을 유지했다. 이 시스템 덕분에 Far Cry Primal에서는 동료와 환경 AI가 부드럽게 어우러져 작동할 수 있었다.
- Middle-earth: Shadow of Mordor – WB Games: 그림자 모드르에서는 네메시스 시스템을 통해 적 오크들의 동적 관계와 기억을 관리했다. 알려진 바에 따르면 이 시스템도 행동 트리를 사용하여 적들의 행동을 제어했다. 예를 들어, 오크들은 플레이어와의 조우 기록, 계급, 특성 등에 따라 트리를 통해 행동(추적, 도주, 도발 등)을 선택했다. (Shadow of Mordor에 대한 구체적 개발 문서는 드러나 있지 않지만, 일반적으로 Nemesis 시스템은 BT와 유사한 계층적 의사결정 모델을 사용한 것으로 전해진다.) 또한 Horizon Zero Dawn, The Last of Us 같은 게임들도 BT 기반 AI를 사용하여 복잡한 NPC 행동을 구현했다. 이러한 게임들에서 BT는 동적으로 적응하는 복합적 행동을 가능하게 함으로써, 높은 몰입감과 예측 불가능한 적 AI를 실현했다.
요약: Hitman, Far Cry, Halo, Shadow of Mordor 등 다수의 게임들은 행동 트리를 핵심 AI 시스템으로 채택했다. 예를 들어 Hitman의 NPC는 상황 중심 트리로 제어되며, Halo 2는 우선순위 기반 DAG를, Far Cry Primal은 동적 서브트리 삽입을 활용했다, Shadow of Mordor는 네메시스 시스템에 BT를 사용했다.
10. 행동 트리 사용 시 장단점 및 상황별 추천 전략
장점:
- 모듈성 및 확장성: BT는 행동을 작은 단위 노드로 분리하므로, 새로운 행동 추가나 수정이 상대적으로 쉽다. 예를 들어, 트리의 일부 분기를 교체하거나 새로운 서브트리를 추가하는 식으로 기능을 확장할 수 있다.
- 가시성 및 디버깅 용이: 트리는 계층적 구조이므로 전체 행동 흐름을 시각적으로 파악하기 쉽다. 개발자/디자이너는 행동 트리 에디터를 통해 트리 구조를 그려보며 의도대로 동작하는지 확인할 수 있다.
- 재사용성: 동일한 행동을 다른 트리나 다른 AI에 재활용할 수 있다. 특히 블랙보드와 결합하면 동일한 노드를 다양한 컨텍스트에서 사용할 수 있다.
- 동적 반응성: BT는 매 틱마다 상태를 갱신하므로 환경 변화에 빠르게 적응할 수 있다. 위젯 트리를 통해 상황별로 분기점만 수정해도 행동 패턴을 유연하게 변경할 수 있다.
- 우선순위 및 인터럽트 지원: 우선순위 셀렉터나 조건부 중단 기능을 통해 중요한 행동을 우선시하고, 긴급 상황에서 현재 행동을 멈추도록 할 수 있다.
단점:
- 초기 설계 복잡성: 복잡한 트리는 처음 설계하기 어렵고, 잘못된 조건 순서로 버그가 발생하기 쉽다. 노드 간 의존성이 없는 게 권장되지만, 실제로는 상호작용을 고려해야 해 설계에 주의가 필요하다.
- 정적 우선순위: 기본 셀렉터는 자식 노드의 순서에 따라 우선순위가 고정된다. 동적 상황 대응에 한계가 있어, 이를 해결하려면 유틸리티 BT 같은 확장 기법을 사용해야 한다.
- Overhead: 틱마다 트리를 순회하므로, 노드 수가 많고 갱신 빈도가 높아지면 연산량이 커질 수 있다. 특히 수백 개의 AI가 있을 때는 최적화가 필수다.
- FSM 대비 구면성 부재: FSM은 명시적 상태 전이가 직관적이지만, BT는 상태 개념이 암묵적이라 디버깅이 어려울 수 있다.
- 단순 작업에는 과도함: 단순한 상태 변화에는 오히려 FSM이나 하드코딩 방식이 개발이 쉽다. BT는 주로 복잡한 행동과 다단계 의사결정에 적합하다.
상황별 추천:
- 작은 범위의 명확한 상태 전이가 필요한 경우(FSM): 단순한 상태 기계로 처리하는 것이 좋다. 예를 들어, UI 상태 변화나 단순한 적 패턴 등은 FSM이 더 간편하다.
- 다양한 조건과 행동이 얽힌 복합 로직이 필요한 경우(BT): NPC의 순찰/추격/공격 등 여러 행동과 조건이 계층적으로 연계되어야 할 때 BT가 적합하다. 복잡도가 증가해도 모듈화를 통해 관리하기 용이하다.
- 계획적 의사결정 필요시(GOAP or 혼합): 목표지향적 계획이 필요하면 GOAP이나 GOBT와 같은 하이브리드 방식을 고려한다. BT와 플래너를 결합하면 단기/장기 목표를 모두 처리할 수 있다.
- 많은 에이전트 동시 제어: 수많은 유닛을 한꺼번에 처리해야 하면, 가벼운 FSM이나 이벤트 기반 시스템을 고려하고, BT는 주요 보스나 복잡한 적에게 집중 적용한다. Hitman의 예처럼 모든 Crowd를 BT로 제어하기보다는 일부 핵심 NPC만 BT로 세부 제어하고 나머지는 단순 FSM/행동으로 처리하는 것이 성능에 유리하다.
요약: 행동 트리는 가독성·모듈성이 뛰어나 복잡한 AI에 강점을 보이나, 작은 규모나 단순 동작에는 오버헤드가 될 수 있다. 설계 전에 요구사항과 대상 플랫폼의 성능을 고려해 FSM과 BT 중 적합한 방식을 선택하는 것이 중요하다. 특히 BT는 우선순위나 중단 같은 기능을 적극 활용해 상황에 맞는 유연한 AI를 구성할 때 빛을 발한다.
11. 총 정리 및 느낀 점
이번 TIL(오늘 배운 점)에서는 Unity 게임 AI 구현에서 행동 트리의 개념과 활용을 깊이 있게 정리해 보았다. 행동 트리는 에이전트 행동을 구조화할 때 매우 유용한 도구였다. 특히, 모듈화된 구조를 통해 설계 변경이 쉽고, 복잡한 행동을 작은 단위로 나누어 관리할 수 있다는 장점이 인상적이었다. 또한 BT의 **상태 시스템(성공/실패/실행중)**과 **제어 노드(시퀀스/셀렉터)**의 작동 원리를 구체적으로 코드로 구현해 보면서, 이 이론이 실제로 어떻게 동작하는지 체감할 수 있었다.
고급 기능으로 살펴본 **Abort(중단)**과 우선순위/유틸리티 확장은 BT를 훨씬 더 역동적으로 만들어 주었고, Far Cry Primal과 같은 사례를 보며 동적 행동 삽입이나 계획 기능 통합의 중요성도 깨달았다. 예를 들어 우선순위 리스트나 유틸리티 셀렉터를 통해 행동을 동적으로 결정하는 방식은, 단순한 정적 트리보다 훨씬 현실적인 NPC 반응을 만들 수 있음을 알게 되었다.
최적화 부분에서는, 대규모 NPC를 동시에 처리할 때의 성능 부담과 이를 해결하기 위한 방법들을 배웠다. Hitman 사례처럼 멀리 있는 AI는 업데이트 주기를 늦추거나, 중복 계산을 블랙보드에 캐시해두는 방법은 실제 개발 시 매우 유용한 팁이었다.
전체적으로 행동 트리는 비교적 배우기 쉬운 추상화 계층임에도 불구하고, 적절히 활용하면 매우 강력하다는 것을 다시 한번 느꼈다. 하지만, 그만큼 설계 단계에서 로직을 명확히 정의하고, 지나치게 복잡해지지 않도록 주의해야 함을 느꼈다. FSM과 BT의 장단점을 상황에 맞게 조합하는 것이 실무적인 AI 구현의 핵심이라는 인상도 받았다. 앞으로 Unity에서 AI를 개발할 때, 이번에 정리한 행동 트리 개념과 코드 패턴을 바탕으로 더욱 현실감 있고 유지보수성 높은 AI를 구현할 수 있을 것 같다.
참고문헌: 각 절마다 인용한 자료를 통해 행동 트리의 개념과 실제 사례들을 검토했다. 특히 Hitman, Halo, Far Cry 등의 사례는 GameDeveloper 기사를 참고했고, 행동 트리 일반 구조는 BehaviorTree.CPP 문서와 전문가 블로그를 참조했다. 이러한 자료를 바탕으로 Unity 환경에 맞게 행동 트리 구현 전략을 정리할 수 있었다.
'[Unity] Game Programing' 카테고리의 다른 글
| 🎓 Unity CustomEditor TIL (Today I Learned) (0) | 2025.05.14 |
|---|---|
| TIL(Today I Learned) Addressables와 FishNet으로 구현하는 비동기 씬 로딩 흐름 (0) | 2025.05.09 |
| TIL(Today I Learned) 병행 상태 머신 (2) | 2025.05.02 |
| TIL: Unity Addressable Asset System 정복하기 (0) | 2025.04.30 |
| TIL : 유니티 게임 시스템 구현 (1) | 2025.04.29 |