[TIL] BTFSM

Dreamer·2024년 11월 22일

1. 오늘 주제

오늘 팀프로젝트 발표 후 피드백을 받았는데 HFSM을 가지고 구현한 기능을 BTFSM 이란 기술로 구현해보는 것을 추천한다고 하여 어떤식으로 구현하는지 알아 보았다.

일단 BTFSM 이란 Behavior Tree Finite State Machine (행동 트리, 유한 상태 기계) 의 약어로 복잡한 캐릭터나 객체의 행동을 관리하기 위해 두가지 패턴의 장점만 활용 한 기술이다.

2. 코드

  1. 코드 작성에 필요한 컴포넌트
  • NavMeshAgent
  • Animator
// 몬스터 상태
public enum EnemyState
{
    Idle,
    Patrol,
    Chase,
    Attack
}

// 모든 Node 가 구현해야 하는 인터페이스
public interface INode
{
    NodeState Evaluate();
}

// Node의 현재 상태
public enum NodeState
{
    Running,
    Success,
    Failure
}
// BaseNode 는 기본적으로 공통 기능을 제공
public abstract class BaseNode : INode
{
    protected NodeState state;
    public NodeState State => state;
    public abstract NodeState Evaluate();
}

2 .Composite Node

Selector와 Sequence Node를 구현
Selector 는 자식 Node 중 하나라도 성공하면 성공을 반환
Sequnce 는 모든 자식 Node 가 성공해야 성공을 반환

using System.Collections.Generic;

public class Selector : BaseNode
{
    protected List<INode> nodes = new List<INode>();

    public Selector(List<INode> nodes)
    {
        this.nodes = nodes;
    }

    public override NodeState Evaluate()
    {
        foreach (var node in nodes)
        {
            switch (node.Evaluate())
            {
                case NodeState.Success:
                    state = NodeState.Success;
                    return state;
                case NodeState.Running:
                    state = NodeState.Running;
                    return state;
                case NodeState.Failure:
                    continue;
                default:
                    continue;
            }
        }
        state = NodeState.Failure;
        return state;
    }
}

public class Sequence : BaseNode
{
    protected List<INode> nodes = new List<INode>();

    public Sequence(List<INode> nodes)
    {
        this.nodes = nodes;
    }

    public override NodeState Evaluate()
    {
        bool anyChildRunning = false;

        foreach (var node in nodes)
        {
            switch (node.Evaluate())
            {
                case NodeState.Failure:
                    state = NodeState.Failure;
                    return state;
                case NodeState.Success:
                    continue;
                case NodeState.Running:
                    anyChildRunning = true;
                    continue;
                default:
                    state = NodeState.Success;
                    return state;
            }
        }
        state = anyChildRunning ? NodeState.Running : NodeState.Success;
        return state;
    }
}
  1. 4 Leaf Node
    ActionNode와 ConditionNode를 구현
using System;

public class ActionNode : BaseNode
{
    private Func<NodeState> action;

    public ActionNode(Func<NodeState> action)
    {
        this.action = action;
    }

    public override NodeState Evaluate()
    {
        return action();
    }
}

public class ConditionNode : BaseNode
{
    private Func<bool> condition;

    public ConditionNode(Func<bool> condition)
    {
        this.condition = condition;
    }

    public override NodeState Evaluate()
    {
        state = condition() ? NodeState.Success : NodeState.Failure;
        return state;
    }
}
  1. EnemyAI 클래스 작성
    FSM 과 BT를 결합하여 적의 AI 구현
using UnityEngine;
using UnityEngine.AI;
using System.Collections.Generic;

public enum EnemyState
{
    Idle,
    Patrol,
    Chase,
    Attack
}

public class EnemyAI : MonoBehaviour
{
    public EnemyState currentState;

    private NavMeshAgent agent;
    private Animator animator;
    public Transform[] patrolPoints;
    private int patrolIndex = 0;
    public Transform player;

    // 행동 트리 노드들
    private INode currentBehaviorTree;

    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        animator = GetComponent<Animator>();

        TransitionToState(EnemyState.Patrol);
    }

    void Update()
    {
        // 현재 상태에 따른 행동 트리 실행
        currentBehaviorTree.Evaluate();
    }

    void TransitionToState(EnemyState newState)
    {
        currentState = newState;

        switch (currentState)
        {
            case EnemyState.Idle:
                currentBehaviorTree = CreateIdleBehaviorTree();
                break;
            case EnemyState.Patrol:
                currentBehaviorTree = CreatePatrolBehaviorTree();
                break;
            case EnemyState.Chase:
                currentBehaviorTree = CreateChaseBehaviorTree();
                break;
            case EnemyState.Attack:
                currentBehaviorTree = CreateAttackBehaviorTree();
                break;
        }
    }

    // 각 상태별 Behavior Tree 생성 메서드

    INode CreateIdleBehaviorTree()
    {
        return new Sequence(new List<INode>
        {
            new ActionNode(IdleAction),
            new ConditionNode(PlayerDetected),
            new ActionNode(() => { TransitionToState(EnemyState.Chase); return NodeState.Success; })
        });
    }

    INode CreatePatrolBehaviorTree()
    {
        return new Sequence(new List<INode>
        {
            new ActionNode(PatrolAction),
            new ConditionNode(PlayerDetected),
            new ActionNode(() => { TransitionToState(EnemyState.Chase); return NodeState.Success; })
        });
    }

    INode CreateChaseBehaviorTree()
    {
        return new Sequence(new List<INode>
        {
            new ActionNode(ChaseAction),
            new Selector(new List<INode>
            {
                new Sequence(new List<INode>
                {
                    new ConditionNode(PlayerInAttackRange),
                    new ActionNode(() => { TransitionToState(EnemyState.Attack); return NodeState.Success; })
                }),
                new Sequence(new List<INode>
                {
                    new ConditionNode(() => !PlayerDetected()),
                    new ActionNode(() => { TransitionToState(EnemyState.Patrol); return NodeState.Success; })
                })
            })
        });
    }

    INode CreateAttackBehaviorTree()
    {
        return new Sequence(new List<INode>
        {
            new ActionNode(AttackAction),
            new ConditionNode(() => !PlayerInAttackRange()),
            new ActionNode(() => { TransitionToState(EnemyState.Chase); return NodeState.Success; })
        });
    }

    // 행동 메서드들

    NodeState IdleAction()
    {
        animator.SetBool("isMoving", false);
        return NodeState.Success;
    }

    NodeState PatrolAction()
    {
        animator.SetBool("isMoving", true);
        if (!agent.pathPending && agent.remainingDistance < 0.5f)
        {
            patrolIndex = (patrolIndex + 1) % patrolPoints.Length;
            agent.SetDestination(patrolPoints[patrolIndex].position);
        }
        return NodeState.Success;
    }

    NodeState ChaseAction()
    {
        animator.SetBool("isMoving", true);
        agent.SetDestination(player.position);
        return NodeState.Success;
    }

    NodeState AttackAction()
    {
        animator.SetTrigger("attack");
        // 공격 로직 추가 (예: 데미지 주기)
        return NodeState.Success;
    }

    // 조건 메서드들

    bool PlayerDetected()
    {
        float detectionRange = 10f;
        return Vector3.Distance(transform.position, player.position) <= detectionRange;
    }

    bool PlayerInAttackRange()
    {
        float attackRange = 2f;
        return Vector3.Distance(transform.position, player.position) <= attackRange;
    }
}

설명

  • FSM
    • EnemyState enum 으로 적의 상위 상태를 관리한다.
    • TransitionToState 를 통해 상태 전이 시 해당 상태의 Behavior Tree를 생성한다.
  • Behavior Tree
    • 각 상태마다 Behavior Tree 를 생성하여 행동을 결정한다.
    • SequenceSelector Node 를 사용하여 행동 흐름을 제어한다.
    • ActionNodeConditionNode를 통해 실제 행동과 조건을 구현한다.
  • 행동 및 조건 메소드
    • IdleAction, PatrolAction, ChaseAction, AttackAction 메소드에서 각 행동을 정의한다.
    • PlayerDetected, PlayerInAttackRange 메소드에서 조건을 검사한다.

결론

BTFSM은 복잡한 AI 시스템을 구현하는 데 강력한 도구가 될 수 있지만, 그만큼 초기 설계와 구현에 많은 고려가 필요하다.

  • 프로젝트 규모와 복잡성: 프로젝트의 규모가 크고, 캐릭터의 행동이 복잡하다면 BTFSM이 적합하다.
  • 팀의 숙련도: 팀원들이 FSM과 Behavior Tree에 익숙하다면 효율적으로 활용할 수 있다.
  • 성능 요구사항: 성능이 중요한 프로젝트에서는 최적화 방안을 미리 고려해야 한다.
  • 유지보수 계획: 코드의 가독성과 유지보수성을 높이기 위한 코딩 규칙과 문서화가 필요하다.
profile
새로운 시작

0개의 댓글