Game AI

Behavior Tree (4) - 데이터 전달하기(Blackboard)

tsyang 2022. 9. 5. 16:30

이전 글: 2022.08.30 - [Game AI] - Behavior Tree (3) - Parallel

 

Behavior Tree (3) - Parallel

이전 글 : 2022.08.27 - [Game AI] - Behavior Tree (2) - 확률, Decorator, 리소스 보호 Behavior Tree (2) - 확률, Decorator, 리소스 보호 이전 글 : 2022.08.21 - [Game AI] - Behavior Tree (1) - 기초 Behav..

tsyang.tistory.com

 

 

데이터 전달은 어떻게?


 

캐릭터가 여러 명의 적들을 발견한다. 캐릭터는 그 중 한 명의 타겟을 선택해서 공격한다. 그렇다면 이 캐릭터의 Behavior Tree에서 [타겟을 선택]하는 Task가 어떻게 타겟에 대한 데이터를 [타겟을 공격]하는 Task에 넘겨줄 수 있을까?

 

즉, 한 Behavior Tree 내에서 한 Task가 다른 Task에 어떻게 데이터를 넘겨줄 수 있을까?

 

지금까지 다룬 Behavior Tree로는 까다로워 보인다. 다른 Task의 메서드를 직접 호출하자니 Task API의 일관성을 해치는 꼴이 된다.

 

이런 문제를 해결하기 위한 방법은 Task들이 필요로 하는 Data와 Task를 분리 시키는 것이다. 한 가지 방법으로 외부에 데이터 저장소를 두고 이를 Behavior Tree가 참고하는 것이다. 이 때, 외부 저장소를 Blackboard라고 부른다.

 

 

다시 앞의 예시에서 [타겟을 선택]하는 블록은 타겟을 선택한 뒤 Blackboard에 다음과 같이 쓴다.

 

'플레이어 발견 위치' : (150,110,24)

 

[타겟을 공격] Task는 Blackboard에서 타겟에 대한 데이터를 얻어 온 뒤 타겟을 공격한다.

 

만약 Blackboard에 데이터를 읽거나 쓰는 것에 실패한다면 Task는 실패한다. 

 

 

구현


//[타겟을 공격]
class AttackTarget : Task
{
    private BlackBoard _blackBoard;
    
    public override bool Run()
    {
        var target = _blackBoard.Get("target");
        
        if (target == null) return false;

        Attack(target);
        
        return true;
    }
}
//[타겟을 선택]
class SelectTarget : Task
{
    private BlackBoard _blackBoard;

    public override bool Run()
    {
        //현재 캐릭터
        var character = _blackBoard.Get("character");
        
        //캐릭터의 시야에 있는 가장 가까운 적을 불러옴
        var target = closestEnemyVisibleTo(character);

        if (target == null) return false;

        //blackboard에 데이터 쓰기
        _blackBoard.Set("target", target);

        return true;
    }
}

[타겟을 선택] , [타겟을 공격] Task는 blackBoard에서 타겟의 위치를 읽고 쓴다.

 

 

 

그렇다면 Blackboard는 어떻게 생성할까? 

 

Decorator를 이용하여 Sub-Tree에 Blackboard를 제공하는 BlackboardManager를 만들어 볼 수 있다.

class BlackboardManager : Decorator
{
    private BlackBoard _blackBoard;
    
    public override bool Run()
    {
        _blackBoard = new BlackBoard();
        bool result = child.Run();
        
        //블랙보드 제거
        _blackBoard = null;

        return result;
    }
}

이런식의 구조는 Task들이 접근할 수 있는 Data에 일종의 Scope를 제공한다고 볼 수 있다.

 

 

Blackboard는 다음과 같이 만들 수 있다.

class Blackboard
{
    private BlackBoard _parent;

    private Dictionary<string, object> _data;

    public object Get(string name)
    {
        if (_data.ContainsKey(name))
            return _data[name];
        
        //나한테 없으면 부모한테 찾는다.
        if (_parent != null)
            return _parent.Get(name);

        return null;
    }
}

위와 같이 parent blackboard를 사용하므로써 마치 프로그래밍 하듯 data를 scope chain에 담아둘 수 있다. 

 

 

그렇다면 이제 Blackboard를 어떻게 Task에 넘겨줘야 할까? 

abstract class NewTask
{
    public abstract bool Run(BlackBoard _blackBoard);
    public abstract bool Terminate();
}

가장 쉬운 방법은 그냥 위처럼 Run메서드에 Parameter로 전달해 주는 것이다. 

 

BlackboardManager는 이제 새로운 Blackboard를 만든 뒤 부모 blackboard를 지정해준다. (sub-tree에 하나의 scope를 더 추가했다고 생각하면 된다)

class BlackboardManager : Decorator
{
    private Blackboard _blackboard;

    public override bool Run(Blackboard blackboard)
    {
        _blackboard = new Blackboard();
        _blackboard.Parent = blackboard;
        
        bool result = child.Run(_blackboard);
        
        //블랙보드 제거
        _blackboard = null;

        return result;
    }
}

 

다른 방법으로는 Task가 상위 task로 이동하며 blackboard가 있는지 찾을 수 있다. 이렇게 되면 Run()메서드에 Parameter를 추가하지 않아도 되지만 각각의 task가 tree를 타고 올라가며 blackboard를 찾는 과정이 추가되어 복잡성이 증가한다.

 


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

'Game AI' 카테고리의 다른 글

퍼지 로직(Fuzzy Logic) - 1  (2) 2022.10.02
Behavior Tree(5) - 트리 생성, 한계  (0) 2022.09.05
Behavior Tree (3) - Parallel  (1) 2022.08.30
Behavior Tree (2) - 확률, Decorator, 리소스 보호  (0) 2022.08.27
Behavior Tree (1) - 기초  (0) 2022.08.21