Game AI

Behavior Tree(5) - 트리 생성, 한계

tsyang 2022. 9. 5. 23:42

이전 글 : 2022.09.05 - [Game AI] - Behavior Tree (4) - 데이터 전달하기(Blackboard)

트리 생성(Instantiating)


 

OOP에서는 클래스와 객체를 나눈다. 클래스는 추상적인 컨셉이고 인스턴스는 실제 존재하는 것으로 구분한다. 이런 구분은 웬만한 경우에 괜찮지만 아닌 경우도 있다. 특히 게임 개발에서는 클래스와 인스턴스 두 개의 layer가 아니라 세 개의 layer가 필요한 경우가 있다.

 

Layer1

이건 그냥 우리가 아는 클래스의 개념이다. 실제하지 않는 추상적인 개념.

 

Layer2

이건 우리가 아는 인스턴스의 개념이다. 

 

위 Behavior Tree에는 두 개의 [공격] Task 인스턴스가 있다고 볼 수 있다. 

 

Layer3

하나의 Behavior Tree가 여러 캐릭터에게 존재하는 상황을 보자. 각 캐릭터에 존재하는 Behavior Tree는 하위 Task 인스턴스들의 Set이라고 볼 수 있다. 그러나 Behavior Tree 그 자체도 인스턴스화 되어야할 대상이다.

 

 

이런 3가지 Layer의 추상화는 일반적으로 사용되는 개발 언어와 매치되지 않는다. 그래서 이를 해결할 몇 가지 방법이 필요하다.

 

 

1. 3가지 이상의 Layer를 지원하는 언어를 사용하기

당연히 비현실적인 해결책이다. 이런 언어를 프로토타입 기반 언어라고 한다.

 

2. Clone을 사용하기 (프로토타입 패턴)

가장 쉬운 방법이다. Behavior Tree를 하나 만들어놓고 필요할때마다 Clone해서 사용한다. 즉, Behavior Tree의 프로토타입을 하나 만들어두고 이걸 도장 찍듯이 생성해낸다. Unity에서는 Prefab이라고도 부르는 개념.

 

3. 중간 Layer를 표현할 새로운 포맷을 만든다.

Behavior Tree에 대한 아키타입이 아니라 단순하게 그 구성 정보를 저장한다.(Serialize해서 보관하는 것 처럼) 그리고 필요할때마다 이를 이용하여 인스턴스를 생성한다. Unity에서 Prefab을 씬에 생성할때 쓰는 방법이다. 유니티에서는 Prefab을 XML의 형태로 저장한다.

 

4. Behavior Tree와 State를 분리한다.

이렇게 되면 Behavior Tree를 여러 개 생성할 필요가 없다. 대신 캐릭터마다 State Object를 가진다. 그러나 Parallel Task 같은 경우에 캐릭터나 Behavior Tree마다 내부 상태가 다를 수 있기 때문에 (대표적으로 현재 수행 중인 child task list) 이를 별도로 저장해줘야 한다. 이 방법은 가장 효율적이지만 구현이 까다롭다.

 

 


 

Tree를 재사용하기

위 해결법에서 Clone을 이용한 해결법(2번)을 이용한다고 가정한다.

 

Tree 전체를 재사용하기 위해서 Tree를 Library에 넣어 두고 Factory 메서드를 만들어 Behavior Tree를 생성할 수 있다.

public Task CreateBehaviorTree(string type)
{
    var archeType = _behaviorTreeLibrary[type];
    return archeType.Clone();
}

 

Library에는 게임에서 쓰이는 모든 Behavior Tree가 아니라 특정 스테이지에 필요한 Tree만 넣을 수 있다.

 


Sub-Tree 재사용하기

 

일부 Sub-Tree는 빈번히 재사용 될 수 있다. 그때마다 Sub-Tree의 인스턴스를 생성하는 것은 비효율적이고 유지보수도 어려워진다.

 

이를 위해 Library에 Sub-Tree를 넣어 두고 이런 Sub-Tree를 참조하는 Reference Task를 만들 수 있다.

public class SubtreeRef : Task
{
    private string _refName;

    public override bool Run()
    {
        throw new Exception("이 코드는 수행되면 안 됨");
    }

    public override Task Clone()
    {
        return CreateBehaviorTree(_refName);
    }
}

 

 

더 나아가 sub-tree를 Lazy하게 생성함으로써 메모리 효율을 높힐수도 있다. 

public class LazySubTreeRef : Decorator
{
    private string _refName;

    public override bool Run()
    {
        if (child == null)
            child = CreateBehaviorTree(_refName);
        return child.Run();
    }
}

만약 메모리 절약을 극한까지 끌어올려야 한다면, sub-tree를 수행하고 다시 메모리를 해제시킬 수도 있다.

 

 


 

Behavior Tree의 한계


Behavior Tree는 상태(State)기반의 행동을 표현하기 까다롭다. 다만 단순히 행동의 실패/성공 여부로만 State의 Transition이 일어난다면 Behavior Tree로도 충분히 괜찮다.

 

외부의 Event에 반응하기 어렵다. 순찰을 돌다가 갑자기 알람이 울려서 다른 행동을 하는 등의 AI는 구현하기 까다롭다.

 

이런 문제 때문에 State Machine과 Behavior Tree를 섞어 쓰기도 한다. State에 따라 다른 Behavior Tree를 가지도록 하는 것이다. 이는 둘의 장점을 모두 취할 수 있어 좋지만 프로그래머나 Behavior Tree를 제작하는 기획자에게나 더 큰 부담이 된다.

 

 


참고 : Ian Millington, AI for GAMES 3rd edition, CRC press, [369~374p]