Unity 공부

[Unity] Behavior Tree(BT)

때류기 2023. 12. 11. 21:55

간단한 AI는 FSM를 통해서 만들어왔습니다만 상태가 많아지고 조건이 추가될 수록 보기가 힘들었습니다.

FSM의 경우엔 상태가 많지 않은 경우 유용하여 주로 사용했으나 팀 프로젝트에서 보스AI를 담당하면서 많은 상태를 가진 AI를 제작해야했습니다. 찾아본결과 AI의 많은 상태를 효율적으로 관리하기위해선 Behavior Tree를 사용하는 것이 좋다 판단, 보스 AI를 만들기 위해 BT를 직접 구현해 봤습니다.

 

 

Behavior Tree는 게임 내 캐릭터와 몬스터, 보스에 대한 인공 지능(AI) 및 의사 결정 시스템을 만드는 데 널리 사용되는 디자인 패턴입니다. 행동 트리는 에이전트의 의사 결정 논리를 모델링하고 구성하는 시각적인 방법을 제공하므로 구조화된 모듈식 방식으로 복잡한 행동을 더 쉽게 설계할 수 있습니다.

 

BT의 요소

BT는 트리의 형태를 하고 있으며, 아래와 같은 노드를 포함(이 글에서 서술이 안된게 있으며 차후 수정 예정)

  • 루트(root)
  • 흐름 제어 노드 (flow-control node)
    • Sequence node : and 역할을 하는 노드
    • Selector node : or 역할을 하는 노드
  • 리프 노드(leaf node, action node 라고도 함) : 실제 행동이 들어가 있는 노드
  • 조건 노드(Condition node) : if 역할을 하는 노드
  • 데코레이터 노드(Decorator node): 이 노드는 하위 노드의 동작을 수정
    (ex. Inverter(자식 노드의 결과를 반전)  Repeater(자식 노드의 실행을 특정 횟수만큼 반복)

루트에서 여러 흐름제어 노드를 타고 내려가 최종적으로 리프 노드에 도달하는 형태이기에 트리의 리프 노드는 항상 Action을 담고 있어야 하고 루트와 리프를 제외하고는 전부 흐름 제어 노드들이 차지하게 됩니다.

 

흐름 제어 노드들의 역할을 자세히 보기 전 각 노드의 상태를 알아야 합니다. 각 노드는 3가지 상태중 하나를 가질 수 있으며 그 목록은 아래와 같습니다.

  • Success 
  • Failure
  • Running

말 그대로 각 노드들의 상태를 의미 합니다.

그리고 상태에 따라 흐름 제어 노드들이 반환하는 결과값이 달라지게 됩니다.

  • Sequence node
    • and 역할
    • 하나라도 Failure라면 Failure 반환
    •  Running, Success 이라면 둘 중 하나 반환 (Running이 우선순위 높음)
  • Selector node
    • or 역할
    • 하나라도 Running, Success라면 바로 반환
  • Condition node
    • if 역할
    • 조건식이 true면 하위 노드 실행, false이면 Failure 반환

 

각 노드의 경우 Evaluate() 와 같은 함수를 가지고 있으며 이 함수에서 각 노드의 행동을 하고 상태를 반환하게 됩니다.

 

아래는 BT를 유니티에서 구현한 코드들 입니다.

 

INode

//노드의 기본 인터페이스
public interface INode
{
    public enum ENodeState {Running, Success, Failure}

    /// <summary> 현재 노드가 어떤 상태인지 반환 </summary>
    public ENodeState Evaluate();
}

 

 

 

SelectorNode

using System.Collections.Generic;


/// <summary> 자식 노드에서 처음으로 Success 나 Running 상태를 가진 노드가 발생하면 그 노드까지 진행하고 멈추는 노드 </summary>
public class SelectorNode : INode
{
    private List<INode> _childs;

    public SelectorNode(List<INode> childs)
    {
        _childs = childs;
    }

    public INode.ENodeState Evaluate()
    {
        if (_childs == null)
            return INode.ENodeState.Failure;


        foreach (INode child in _childs)
        {
            switch (child.Evaluate())
            {
                //자식 상태: Running일 때 -> Running 반환
                case INode.ENodeState.Running:
                    return INode.ENodeState.Running;

                //자식 상태: Success 일 때->Success 반환
                case INode.ENodeState.Success:
                    return INode.ENodeState.Success;

                    //자식 상태: Failure일 때 -> 다음 자식으로 이동
            }
        }

        return INode.ENodeState.Failure;
    }
}

 

 

 

SequenceNode

using System.Collections.Generic;


/// <summary> 자식 노드를 순서대로 진행하면서 Failure 상태가 나올 때까지 진행하는 노드 </summary>
public class SequenceNode : INode
{
    private List<INode> _childs;

    public SequenceNode(List<INode> childs)
    {
        _childs = childs;
    }

    public INode.ENodeState Evaluate()
    {

        if (_childs == null || _childs.Count == 0)
            return INode.ENodeState.Failure;

        foreach (INode child in _childs)
        {
            switch (child.Evaluate())
            {
                case INode.ENodeState.Running:
                    return INode.ENodeState.Running;

                //자식 상태: Success 일 때->다음 자식으로 이동
                case INode.ENodeState.Success:
                    continue;

                case INode.ENodeState.Failure:
                    return INode.ENodeState.Failure;
            }
        }

        return INode.ENodeState.Failure;
    }

}

 

 

 

ConditionNode

using System;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;


/// <summary> 조건문을 확인 후 true일 경우 하위 노드 실행, false일 경우 Failure반환</summary>
public class ConditionNode : INode
{
    private INode _node;

    private Func<bool> _condition;

    public ConditionNode(Func<bool> condition, INode node)
    {
        _condition = condition;
        _node = node;
    }


    public INode.ENodeState Evaluate()
    {         
        bool conditionResult = _condition.Invoke();
        return conditionResult ? _node.Evaluate() : INode.ENodeState.Failure;
    }
}

 

 

 

ActionNode

using System;


/// <summary> 실제로 어떤 행위를 하는 노드 </summary>
public class ActionNode : INode
{
    private Func<INode.ENodeState> _onUpdate;

    public ActionNode(Func<INode.ENodeState> onUpdate)
    {
        _onUpdate = onUpdate;
    }

    public INode.ENodeState Evaluate() => _onUpdate?.Invoke() ?? INode.ENodeState.Failure;

}

 

 

 

BehaviorTree

/// <summary> BehaviorTree를 관리하는 클래스</summary>
public class BehaviorTree
{
    private INode _rootNode;

    public BehaviorTree(INode rootNode)
    {
        _rootNode = rootNode;
    }

    public void Operate() => _rootNode.Evaluate();
}

 

 

 

추후 유니티 적용 코드 업데이트 예정입니다.