오늘 팀프로젝트 발표 후 피드백을 받았는데 HFSM을 가지고 구현한 기능을 BTFSM 이란 기술로 구현해보는 것을 추천한다고 하여 어떤식으로 구현하는지 알아 보았다.
일단 BTFSM 이란 Behavior Tree Finite State Machine (행동 트리, 유한 상태 기계) 의 약어로 복잡한 캐릭터나 객체의 행동을 관리하기 위해 두가지 패턴의 장점만 활용 한 기술이다.
// 몬스터 상태
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;
}
}
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;
}
}
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;
}
}
EnemyState enum 으로 적의 상위 상태를 관리한다.TransitionToState 를 통해 상태 전이 시 해당 상태의 Behavior Tree를 생성한다.Sequence와 Selector Node 를 사용하여 행동 흐름을 제어한다.ActionNode와 ConditionNode를 통해 실제 행동과 조건을 구현한다.IdleAction, PatrolAction, ChaseAction, AttackAction 메소드에서 각 행동을 정의한다.PlayerDetected, PlayerInAttackRange 메소드에서 조건을 검사한다.BTFSM은 복잡한 AI 시스템을 구현하는 데 강력한 도구가 될 수 있지만, 그만큼 초기 설계와 구현에 많은 고려가 필요하다.