행동 트리, 유한 상태 머신, GOAP 등 조건 기반 AI 기법의 한계와 Utility AI가 이를 어떻게 보완하는지, 그리고 Utility AI의 단점까지 정리한다.
behavior tree finite state machine goap utility ai
전통적인 조건 기반 AI 기법인 행동 트리(BT), 유한 상태 머신(FSM), 목표 지향 행동 계획(GOAP)을 수년간 다루면서, 나는 이들에 많은 단점이 있다는 것을 알게 되었다. 한동안은 그 한계를 우회하려고 많은 시간을 썼지만, 결국 좋은 해법을 찾지 못했다. 그러다 Utility AI를 접했고, 이 기법이 기존 기법들의 많은 제약을 극복할 수 있다는 것을 깨달았다. 그것이 내가 Utility Intelligence를 만들게 된 계기다.
아래는 과거 내가 겪었던 이러한 기법들의 단점들과, Utility AI가 이를 어떻게 해결하는지에 대한 내용이다.
Info
전통적인 조건 기반 AI 기법인 BT와 FSM에서는, 에이전트가 **조건(conditions)**과 **결정의 순서(order of decisions)**에 따라 의사결정을 내린다. 하지만 이 둘은 게임 디자인 변화 등에 따라 바뀌기 쉽다.
Note
GOAP도 조건 기반이긴 하지만, BT와 FSM처럼 의사결정 간 시간적 결합이 존재하지는 않는다. GOAP은 조건뿐 아니라 **총 비용(total cost)**을 기준으로 행동을 선택하며, BT/FSM처럼 결정의 순서에 의존하지 않기 때문이다.
BT와 FSM에서 에이전트는 예/아니오 질문을 한 번에 하나씩, 그리고 고정된 순서로 답하면서 결정을 내린다.
if(ShouldMoveTowardsEnemy())
MoveToEnemy();
else if(ShouldAttackEnemy())
AttackEnemy();
else if(ShouldFleeFromEnemy())
FleeFromEnemy();
else
Idle();
bool ShouldMoveTowardsEnemy()
{
return DistanceToEnemy > 10 && MyHealth > 50;
}
bool ShouldAttackEnemy()
{
return DistanceToEnemy < 10;
}
bool ShouldFleeFromEnemy()
{
return DistanceToEnemy < 20 && MyHealth < 20;
}
각 결정마다, 에이전트가 그 결정을 할지 말지를 판단하는 조건을 정의해야 한다. 그리고 어떤 결정을 다른 결정보다 우선시키고 싶다면, 결정을 나열한 순서 자체를 바꿔야 한다. 위 예시에서 DistanceToEnemy = 5이고 MyHealth = 10이면, 에이전트는 도망이 아니라 공격을 선택한다. 하지만 이 두 결정의 순서를 바꾸면, 에이전트는 공격 대신 도망을 선택한다.
if(ShouldFleeFromEnemy())
FleeFromEnemy()
else if(ShouldAttackEnemy())
AttackEnemy()
bool ShouldAttackEnemy()
{
return DistanceToEnemy < 10;
}
bool ShouldFleeFromEnemy()
{
return DistanceToEnemy < 20 && MyHealth < 20;
}
간단한 AI라면 문제가 없다. 하지만 AI가 복잡해질수록 결정의 수는 늘어나고, 의사결정 조건은 체력, 에너지, 적과의 거리, 엄폐물까지의 거리, 공격 쿨다운 등 수많은 요소에 얽히게 된다.
그래서 게임 디자인이 바뀌면, 그 변경사항을 게임에 반영하는 데 매우 많은 시간이 든다. BT/FSM의 구조가 크게 바뀔 수 있고, 조건과 결정을 추가/삭제/재정렬하면서 사실상 처음부터 다시 설계해야 할 수도 있다.
게다가 테스터는 이 AI들의 모든 행동을 처음부터 다시 검증해야, 의도대로 동작하는지 확인할 수 있다. 결과적으로 AI를 게임 디자인과 동기화하는 비용은 시간이 지날수록 증가한다. 결국 변경 비용이 너무 커져서, AI를 바꾸는 것 자체가 불가능해질 수도 있다.
아래는 내가 이전에 만들었던 게임 중 하나에서 사용한 에이전트의 행동 트리다.
보이는 것처럼 꽤 복잡하다. 이런 수준으로 복잡해졌을 때, 디자이너가 게임 디자인을 바꿀 때마다 변경을 반영하느라 정말 많은 시간을 썼던 기억이 난다. 행동 트리를 계속 다시 설계해야 했다. 악몽 같았고, 이 플러그인을 만든 큰 이유 중 하나다.
Info
Utility AI는 BT/FSM처럼 조건과 결정의 순서가 아니라, **점수(scores)**를 기반으로 결정을 내린다. 그래서 결정들 사이에 **결합(coupling)**이 없고, 서로 독립적이다.
BT/FSM과 달리, **유틸리티 기반 에이전트(Utility-Based Agent)**가 답해야 하는 질문은 이것이다.
지금 당장 내가 가장 하고 싶은 건 무엇인가?
즉 각 결정에 대해 에이전트는 스스로에게 이렇게 묻는다.
지금 이 결정을 얼마나 하고 싶은가?
그리고 그 답에 따라 각 결정에 점수를 부여하고, 모든 결정을 서로 비교한 뒤, 점수가 가장 높은 결정을 선택한다.
그 결과, 결정의 순서는 더 이상 중요하지 않다. 조건과 결정 순서를 걱정할 필요도 없다. 중요한 것은 단순히 지금 이 순간 가장 중요한 행동이 무엇인가뿐이다.
예를 들어 에이전트의 체력이 30, 에너지가 50, 적까지의 거리가 40이라면, 에이전트가 가장 하고 싶은 것은 무엇일까?
전통적인 조건 기반 AI 기법(BT, FSM, GOAP)의 큰 단점 중 하나는, 에이전트가 타깃을 기준으로 결정하지 않는다는 점이다. 이들은 결정을 내린 다음에야 그 결정에 대한 타깃을 선택한다. 즉 의사결정 과정에서 타깃을 고려하지 않게 된다. 그러다 보니, 선택된 **결정-타깃 쌍(decision-target pair)**이 최선이 아닐 수 있다.
if(IsCurrentHealthLow())
SeekAndEatHealingItem()
else
Idle()
예를 들어 체력이 20인 에이전트가 있고, 주변에 아이템이 두 개 있다고 하자.
어떤 아이템으로 가서 먹어야 할까? 최선의 아이템을 고르려면 이 액션에 어떤 조건을 추가해야 할까?
또한 에이전트에서 5m 거리로 체력 1인 적이 있다면 어떻게 될까? 당연히 회복 아이템을 찾기보다 가까운 적을 죽이는 편이 더 낫다.
조건 기반 기법을 쓴다면, 에이전트는 가까운 적을 죽일까, 아니면 회복 아이템을 찾을까? 최선의 결정-타깃 쌍이 선택되도록 하려면 어떤 조건을 추가해야 할까?
전통적인 기법과 달리, Utility AI는 가능한 모든 결정-타깃 쌍을 평가하고, 그중 점수가 가장 높은 것을 실행한다.
이 접근은 선택된 결정-타깃 쌍이 항상 최선이 되도록 보장한다. 그래서 유틸리티 기반 에이전트는 전통적 조건 기반 AI로 만든 에이전트보다 훨씬 자연스럽게 행동한다.
전통적인 조건 기반 AI 기법의 또 다른 단점은, 선택된 결정에 대해 조건을 만족하는 어떤 타깃이든 선택될 수 있다는 점이다.
예를 들어 에이전트의 공격 로직을 다음처럼 구성했다고 하자.
if(HasEnemiesInAttackRange()) //decision-making conditions
{
foreach(var enemy in enemies)
{
if(IsEnemyHealthLow(enemy)) //target-selection conditions
{
AttackEnemy(enemy);
break;
}
}
}
else
{
Idle()
}
bool IsEnemyHealthLow(Enemy enemy)
{
return enemy.Health <= 20;
}
공격 범위가 10m라고 하자. 만약 Health <= 20이면서 10m 안에 있는 적이 여러 명이라면 어떻게 될까? 조건을 만족하는 적이 3명 있다고 가정하자.
10m, Health = 204m, Health = 108m, Health = 5에이전트는 누구를 공격할까? 결과는 조건을 가장 먼저 만족한 Enemy 1을 공격하게 된다. 하지만 이 경우 최선의 선택은 아니다.
물론, 공격 결정을 위해 항상 최선의 적이 선택되도록 더 많은 조건을 추가할 수 있다. 하지만 이를 매번 수동으로 해야 하고, 에이전트가 내리는 모든 결정에 대해 최선의 타깃을 고르기 위한 조건을 고민하는 데 많은 시간이 든다.
또한 타깃 선택 로직에 IsFrozen 조건을 다음처럼 추가하면 어떨까?
if(HasEnemiesInAttackRange()) //decision-making conditions
{
foreach(var enemy in enemies)
{
if(IsEnemyHealthLow(enemy) || IsFrozen(enemy)) //target-selection conditions
{
AttackEnemy(enemy);
break;
}
}
}
else
{
Idle()
}
bool IsEnemyHealthLow(Enemy enemy)
{
return enemy.Health <= 20;
}
또다시 순서 때문에 골치 아파진다. 이번에는 결정의 순서가 아니라 조건의 순서다.
예를 들어 적이 두 명 있다고 하자.
Health = 20, IsFrozen = falseHealth = 40, IsFrozen = true이 경우 에이전트는 LowHealth를 IsFrozen보다 우선시하므로 Enemy 1을 공격한다. 하지만 очевид히 최선은 아니다. 얼어붙어서 반격할 수 없는 Enemy 2를 공격하는 편이 더 낫다.
결국 게임 요구사항을 맞추기 위해 조건 순서를 재정렬해야 한다. 하지만 앞서 말했듯 조건은 바뀌기 쉬우므로, 게임 디자인이 바뀔 때마다 조건을 또 재정렬해야 한다.
Info
이 단점은 BT/FSM만의 문제가 아니다. GOAP 기반 에이전트 역시 행동이 선택된 뒤에 타깃을 고르기 때문에, GOAP도 동일한 문제가 있다.
앞서 언급했듯, 유틸리티 기반 에이전트는 각 결정에 대해 가능한 모든 타깃을 평가하고 점수가 가장 높은 타깃을 선택한다.
이 방식에서는 선택된 결정에 대해, 선택된 타깃이 최선인지 걱정할 필요가 없다. 당신은 결정-타깃 쌍을 어떻게 점수화할지에만 집중하면 되고, 에이전트가 자동으로 점수가 가장 높은 타깃을 선택한다.
전통적인 AI 기법(BT, FSM)의 또 다른 단점은, 액션 간 전이가 고정되어 있다는 점이다. 즉 다음 액션은 미리 정의된 전이 중 하나로만 갈 수 있으므로, BT/FSM 기반 에이전트가 무엇을 할지 비교적 쉽게 예측할 수 있다.
stateDiagram
[*] --> Idle
Idle --> Patrolling: No Enemy Detected
Patrolling --> Chasing: Enemy Detected
Chasing --> Attack: Enemy In Range
Attack --> Chasing: Enemy Out of Range
Chasing --> Idle: Enemy Lost
Utility AI는 모든 결정을 평가하고 점수가 가장 높은 결정을 실행한다. 평가 과정은 현재 월드 상태(환경, 장애물)와 다른 에이전트 상태(체력, 마나, 상태이상 등) 같은 많은 요소를 고려한다.
Utility AI에는 전이(transition)가 없으므로, 어떤 결정이든 다음 결정이 될 수 있다. 따라서 유틸리티 기반 에이전트의 행동은 예측하기 더 어렵지만 여전히 논리적이다. 그래서 내 생각에 Utility AI는 창발적(emergent) 행동을 만드는 데 좋은 선택이다.
조건 기반 의사결정에 의존하는 전통적 AI 기법(BT, FSM, GOAP)의 또 다른 단점은, 조건이 보통 **임계값(threshold)**에 기반한다는 점이다. 아래는 적 AI의 의사결정 로직 예시다.
if(IsPlayerInAttackRange())
AttackPlayer()
else
Idle()
bool IsPlayerInAttackRange()
{
return DistanceToPlayer < 10;
}
이 로직에서는 플레이어가 공격 범위(10m) 안으로 들어오는 순간 적이 갑자기 공격을 시작한다. 그리고 플레이어가 10m 밖에 있으면(예: 11m) 아무것도 하지 않는다. 따라서 플레이어가 각 적의 공격 범위를 알고 있다면, 체력을 한 방울도 잃지 않고도 쉽게 적을 처치할 수 있다.
Utility AI에서는, 의도적으로 그렇게 만들지 않는 한 이런 상황이 발생할 가능성이 매우 낮다. Utility AI는 지금 이 결정을 얼마나 하고 싶은가를 측정하기 때문이다. 즉 플레이어까지의 거리가 11m이라는 것은, 10m일 때보다 공격 욕구가 조금 낮다는 의미일 뿐이다.
예를 들어 플레이어가 10m 안이면 AttackPlayer 점수가 1.0이라고 하자. 그러면 거리가 11m일 때 AttackPlayer 점수는 0.9가 될 수 있다. 따라서 거리가 11m이든 10m이든, AttackPlayer 점수가 Idle보다 높다면 여전히 AttackPlayer가 선택된다.
이 또한 유틸리티 기반 에이전트가 다른 AI 기법보다 더 자연스럽게 행동하는 이유 중 하나다.
전통적 AI 기법(BT, FSM)의 큰 단점은, **의사결정(decision-making)**과 **의사결정 실행(decision execution)**이 같은 프로세스의 일부라는 점이다. 즉 의사결정이 실행과 같은 빈도로 돌도록 강제된다.
그 결과 에이전트가 이미 현재 상황에서 최선의 결정을 가지고 있더라도, 그 결정을 실행하기 위해 계속 **“무슨 결정을 내려야 하지?”**를 다시 계산해야 한다.
그리고 에이전트가 매 프레임 결정을 실행해야 한다면, 매 프레임 결정을 내려야 한다. 이미 최선의 결정을 알고 있는데도 왜 계속 결정을 내려야 할까? 자원 낭비다.
당신이 달리러 가기로 결정했는데, 이미 달리고 있으면서 계속 스스로에게 **“달리러 가야 하나?”**를 물어야 한다고 상상해보라. 기분이 어떨까? 악몽일 것이다.
Utility AI는 본질적으로 의사결정 기법이며, 의사결정 실행에는 관여하지 않는다. 따라서 의사결정과 의사결정 실행을 두 개의 분리된 프로세스로 만들고, 서로 다른 빈도로 돌릴 수 있다.
예를 들어, 실행은 매 프레임 수행하되, 의사결정은 게임 요구에 맞게 **의사결정 간격(decision-making interval)**을 조정하여 0.1초마다 또는 0.5초마다만 수행할 수 있다.
또한 의사결정 작업을 여러 프레임에 분산하거나, 스레드에 분산하거나, 필요할 때 수동으로 의사결정을 수행하도록 할 수도 있다. 이런 접근은 게임 성능을 크게 개선한다.
Note
이런 것들이 가능하긴 하지만, Utility Intelligence는 현재 의사결정 작업을 여러 프레임에 분산하는 것만 지원한다. 다만 향후에는 모두 지원할 계획이다.
다른 AI 기법에서는 이런 분리가 어렵다. 그 시스템들에서는 본질적으로 의사결정이 실행과 밀접하게 결합되어 있어 분리하기가 힘들기 때문이다.
본질적으로 BT와 FSM은 순차적으로 실행되는 일련의 If/Else 문으로 구성된다.
if(ShouldMoveTowardsEnemy())
MoveToEnemy();
else if(ShouldAttackEnemy())
AttackEnemy();
else if(ShouldFleeFromEnemy())
FleeFromEnemy();
else
Idle();
위 코드에서 어떻게 의사결정을 실행으로부터 분리할 수 있을까?
전통적인 AI 기법을 AI 솔루션으로 사용하면, 복잡도가 증가할수록 에이전트가 왜 잘못된 결정을 내리는지 디버깅하기가 어려워질 수 있다.
특정 결정이 기대한 대로 선택되지 않은 이유를 파악하려면, 체력, 에너지, 타깃까지의 거리, 방어력, 데미지 등 의사결정 과정에 관여한 모든 조건과 파라미터를 이해해야 한다. 파라미터가 늘어날수록 디버깅은 점점 더 어려워진다.
파라미터와 조건이 몇 개 안 될 때는 몇 초 만에 문제를 찾을 수도 있다. 하지만 수십, 수백 개라면?
에이전트가 의사결정 시점에 100개의 서로 다른 파라미터를 고려해야 한다고 상상해보라. 예를 들면:
또한 100개의 서로 다른 조건도 평가해야 한다. 예를 들면:
마지막으로, 선택 가능한 100개의 결정이 있다고 하자. 예를 들면:
몇 초나 몇 분 만에 최선의 결정을 빠르게 찾아낼 수 있을까? 할 수 있다면 축하한다. 당신은 세상에서 가장 똑똑한 사람들 **상위 1%**에 속한다.
우리 같은 보통 사람에게는, 에이전트가 왜 잘못된 결정을 내리는지 디버깅하려고 게임을 반복 재생해야 하고, 이는 매우 시간도 많이 들고 지루하다. 원인을 찾는 데 몇 시간, 심지어 며칠이 걸릴 수도 있다. 나는 과거에 여러 번 그런 일을 겪었다. 정말 고되고 다시는 겪고 싶지 않다.
앞서 말했듯 Utility AI는 의사결정과 실행을 분리하여 두 개의 별도 프로세스로 만들 수 있다. 따라서 게임을 실제로 플레이하지 않고도, 에디터 시간에 의사결정 프로세스를 독립적으로 실행해 어떤 결정이 선택되는지를 에디터에서 바로 미리 볼 수 있다.
그래서 에이전트가 잘못된 결정을 내리면, 의사결정에 관여한 모든 파라미터를 Console Window에 로깅한 다음, 그 값을 Intelligence Editor에 입력해 원인을 파악하면 된다.
만약 의사결정과 실행이 같은 프로세스에 묶여 있었다면, 이런 방식은 불가능하다. 실행은 게임 월드의 실시간 상태에 의존하기 때문이다. 예를 들어 에디터 시간에는 적이 런타임에 인스턴스화되므로 존재하지 않아, 에이전트를 적에게 이동시키는 실행 자체를 할 수 없다.
Utility AI는 Play Mode로 들어가지 않고도 에디터에서 어떤 결정이 선택되는지 미리 볼 수 있게 해주지만, 모든 Utility AI 프레임워크가 이 기능을 제공하는 것은 아니다. 다행히 Utility Intelligence는 이 기능을 지원한다. Intelligence Editor에서 입력 값을 바꾸며 어떤 결정이 선택되는지 미리 볼 수 있다. 이 기능은 많은 시간을 절약해줄 것이라 믿는다. 자세한 내용은 여기서 볼 수 있다: Decision Making Preview.
Utility AI는 조건 기반 AI 기법의 많은 단점을 갖고 있지 않지만, 자체적인 단점도 있다. 예를 들면:
하지만 Utility Intelligence를 사용하면, 우리는 이런 문제들을 해결하기 위한 도구들을 제공한다. 완전히 해결한다고 장담하진 못하지만, 어느 정도는 도움을 줄 수 있다.
다음에 해당한다면, BT, FSM, GOAP 같은 전통적 조건 기반 AI 기법 대신 Utility AI를 사용하는 편이 좋다.
다음에 해당한다면 전통적 조건 기반 AI 기법이 더 적합할 수 있다.
Note
이 글은 어디까지나 내 개인적인 의견이다. 동의할 수도 있고, 동의하지 않을 수도 있다. 괜찮다. 당신은 당신의 관점을 가질 권리가 있고, 나는 그것을 존중한다.
마지막 업데이트: 2025년 11월 28일
작성일: 2024년 9월 9일