첫 팀프로젝트가 시작되었다. 우리팀은 스카이워드 소드를 레퍼런스 삼아서 개발하기로 했는데 여기서 몬스터 AI를 구현하기 위한 디자인패턴으로 FSM / B-Tree를 강사님께서 추천해주셨다.
FSM은 콘솔 게임으로 한번 구현해봤고 이번 프로젝트도 몬스터의 상태가 많아봤자 3~4개라서 FSM으로 구현하는게 충분하다고 생각하지만 학습 목적 에셋 스토어 없이 순수 C# Behaviour Tree로 구현해보려고 한다.
Behaviour Tree(BT, 행동 트리)는 AI가 이번 순간에 무엇을 할지 고르는 방식이다.
트리는 여러 개의 노드로 이루어진 계층 구조고, 매 프레임(또는 매 틱) 루트부터 아래로 내려가며 평가한다.
핵심은 이 한 줄이다.
지금 가능한 일 중에서, 미리 정해 둔 우선순위대로 하나만 실행한다.
FSM이 “나는 지금 추격 상태다”라고 말한다면,
BT는 “이번 틱에 추격 가지가 성공했다”라고 말한다.
게임 AI에서 BT가 쓰이는 이유는 행동이 조건 + 실행의 묶음으로 잘게 나뉘기 때문이다.
이 질문들을 코드의 if-else 탑으로 쌓지 않고, 트리의 가지로 늘어놓는다.
유니티에서 돌릴 때는 보통 이런 루프다.
void Update()
{
blackboard.SyncFromWorld();
root.Tick(blackboard);
}
| FSM | Behaviour Tree | |
|---|---|---|
| 중심 | 지금 어떤 상태인가 | 이번 틱에 어떤 Node가 성공하는가 |
| 흐름 | 특정 상태에서 로직 실행 + 상태 전이 체크 | Root 노트부터 매번 평가 |
| 확장 | 상태 종류가 늘면 복잡해진다 | 노드를 붙이면 우선순위를 바꿀 수 있다 |
| 디버그 | 현재 상태가 한눈에 보인다 | 어떤 노드가 Running 인지 찍어 봐야 한다 |
그림과 코드로 좀 더 살펴보도록 하자.
몬스터 AI에 정찰 / 추격 / 공격 / 사망 네가지 상태가 있을 경우

enum MonsterState { Patrol, Trace, Attack, Die }
void Update()
{
switch (state)
{
case MonsterState.Patrol:
Patrol();
if (CanSeePlayer()) state = MonsterState.Trace;
break;
case MonsterState.Trace:
Trace();
if (!CanSeePlayer()) state = MonsterState.Patrol;
else if (InAttackRange()) state = MonsterState.Attack;
break;
case MonsterState.Attack:
Attack();
if (!InAttackRange()) state = MonsterState.Trace;
break;
case MonsterState.Die:
Die();
break;
}
if (hp <= 0) state = MonsterState.Die;
}
그러면 이제 BT구조를 살펴보자

Blackboard bb;
Node root;
void Awake()
{
bb = new Blackboard { Self = transform, Target = target, Hp = hp };
root = new Selector(
new Sequence(
new Condition(isDead),
new ActionNode(Die)
),
new Sequence(
new Condition(canAttack),
new ActionNode(Attack)
),
new Sequence(
new Condition(CanTrace),
new ActionNode(Trace)
),
new ActionNode(Patrol)
);
}
void Update()
{
bb.Hp = hp;
bb.Target = target;
root.Tick(bb);
}
BT는 Pre-Order로 왼쪽 자식부터 순회하게 된다. 그래서 Patrol의 경우 맨 마지막 else에 해당하는 상태이기 때문에 Condition노드가 추가되지 않은걸 볼 수 있다.
BT는 구성요소들이 FSM과는 달리 비교적 많기 때문에 하나하나씩 나열하며 개념을 정리해보자.
트리의 한 칸을 말한다. Tick을 받아서 행동하며 NodeStatus의 결과 3개중 하나를 반환한다. 노드는 역할에 따라 다시 3가지로 나눌 수 있다.
public enum NodeStatus
{
Success,
Failure,
Running
}
노드들이 반환하게 되는 상태값이다. 여러 Tick이 걸리는 상태는 Running을 반환하게 되어 상태를 유지한다
최상단 노드로써 Update 이벤트함수에서 호출한다. 주로 Selector 노드가 된다.
자식을 어떤 규칙으로 실행할지 결정하는 노드로 Selector와 Sequence 가 해당된다.
if / else-if / else 구문의 느낌으로 왼쪽부터 자식을 순회하며 반환되는 NodeStatus를 보고 Success 또는 Running 상태가 반환되면 멈춘다.
순서대로 전부를 수행하게 된다. Success면 다음 자식을 수행하게 되며 Failure 또는 Running이면 반환하게 된다.
자식 하나를 감싸서 결과를 바꾸거나 실행 여부를 바꾸거나 반복하게 된다.
| Decorator | 하는 일 | 예 |
|---|---|---|
| Inverter | Success ↔ Failure | “타겟이 없다”를 “타겟이 있다”의 반대로 |
| Succeeder | 실패해도 Success | 부가 행동을 실패로 전체 가지를 끊고 싶지 않을 때 |
| Failer | 성공해도 Failure | 거의 디버그/특수 케이스 |
| Repeater | 자식을 N번 또는 무한 반복 | 순찰 웨이포인트를 계속 돌 때 |
| Repeat Until Fail | 실패할 때까지 반복 | |
| Cooldown | 한 번 성공 후 일정 시간 실패 | 공격 후 1초 쉬기 |
| Timeout | Running이 너무 길면 실패 | 추격이 10초 넘으면 포기 |
가장 끝의 노드로 실제 게임의 실행과 맞닿는 곳이다
condition : Acition을 실행하기 전에 판단을 하는 곳으로 Success/Failure를 반환한Action : 조건이 맞는 경우 실행된다. 여러 Tick이 걸린다면 Running 상태를 반환한다Root 노드가 실행될 때 매개변수로 함께 받는 것으로 각종 노드의 Condition을 판별할 때 사용하는 정보다.
public class Blackboard
{
public Transform Self;
public Transform Target;
public bool IsDead;
public bool CanTrace;
public bool InAttackRange;
public float Hp;
public float DistanceToPlayer;
public int PatrolIndex;
public int AttackRemainTicks;
}
부모 노드가 자식 노드에게 “너 이번엔 뭐니?”라고 묻는 호출. 유니티 Update와 1:1일 필요는 없다. 0.1초마다 Tick해도 된다. 다만 한 Tick 안에서는 Root → 자식 → 손자 순으로 동기 호출된다.
Selector / Sequence 여러 자식을 규칙으로 실행
Decorator 자식 하나의 결과를 변조
Condition / Action 실제로 판단하거나 움직인다
Blackboard 그 판단의 재료
Tick 평가 한 번
이 다음 포스팅에는 간단한 C# 콘솔게임에 BT를 적용해 보도록 하겠다.