오늘은 프로젝트에 적용한 FSM(유한 상태 기계)에 대해 알아보자.
FSM은 명확한 상태들을 정의하여 복잡한 동작이나 프로세스를 단순화 하는데 사용 된다. 게임 개발에서 예를 들면 디테일한 움직임을 요구하는 전투 게임이라던가, 하나의 상태만 가질 수 있도록 설계된 게임의 오브젝트 등에서 사용 할 수 있다.
단순하게 상태를 변경 할 수 있도록 하는 코드를 예시로 보도록 하자.
using UnityEngine;
using UnityEngine.AI;
public enum EnemyState
{
Idle,
Patrol,
Chase,
Attack
}
public class EnemyFSM : MonoBehaviour
{
public EnemyState currentState = EnemyState.Idle;
public Transform[] patrolPoints;
private int currentPatrolIndex = 0;
public Transform player;
public float chaseRange = 10f;
public float attackRange = 2f;
private NavMeshAgent agent;
void Start()
{
agent = GetComponent<NavMeshAgent>();
TransitionToState(EnemyState.Patrol);
}
void Update()
{
switch (currentState)
{
case EnemyState.Idle:
Idle();
break;
case EnemyState.Patrol:
Patrol();
break;
case EnemyState.Chase:
Chase();
break;
case EnemyState.Attack:
Attack();
break;
}
}
void TransitionToState(EnemyState newState)
{
currentState = newState;
}
void Idle()
{
// Idle 상태에서는 아무것도 하지 않음
if (Vector3.Distance(transform.position, player.position) <= chaseRange)
{
TransitionToState(EnemyState.Chase);
}
}
void Patrol()
{
if (agent.remainingDistance <= agent.stoppingDistance)
{
currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length;
agent.SetDestination(patrolPoints[currentPatrolIndex].position);
}
if (Vector3.Distance(transform.position, player.position) <= chaseRange)
{
TransitionToState(EnemyState.Chase);
}
}
void Chase()
{
agent.SetDestination(player.position);
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
if (distanceToPlayer <= attackRange)
{
TransitionToState(EnemyState.Attack);
}
else if (distanceToPlayer > chaseRange)
{
TransitionToState(EnemyState.Patrol);
}
}
void Attack()
{
// 공격 애니메이션이나 로직 처리
transform.LookAt(player);
// 공격 범위를 벗어나면 추격 상태로 전환
if (Vector3.Distance(transform.position, player.position) > attackRange)
{
TransitionToState(EnemyState.Chase);
}
}
}
위 코드와 같이 currentState는 하나의 상태만 가질 수 있고 상태를 변경할 때마다 특정 행위를 정해 줄 수 있다.
그러나 FSM을 작성하여 사용하다 보면 한계가 명확해지는데 동시 상태를 가질 수 없도록 설계되기 때문에 병렬 처리는 어렵게 된다. 또한 상태가 많아질 수록 더 복잡해지는 경향이 있다.
그래서 단점을 보완한 방법도 있는데 예를 들면 HFSM이나 '상태 패턴', 이벤트 기반 시스템 등으로 대처 할 수 있다.
예를 들어서 HFSM 코드를 보면
using UnityEngine;
using UnityEngine.AI;
// IState 인터페이스
public interface IState
{
void Enter();
void Update();
void Exit();
}
// BaseState 추상 클래스
public abstract class BaseState : IState
{
protected EnemyHFSM enemy;
public BaseState(EnemyHFSM enemy)
{
this.enemy = enemy;
}
public virtual void Enter() { }
public virtual void Update() { }
public virtual void Exit() { }
}
public class EnemyHFSM : MonoBehaviour
{
public IState currentState;
public Transform[] patrolPoints;
public Transform player;
public NavMeshAgent agent;
public float detectionRange = 10f;
public float attackRange = 2f;
public float health = 100f;
void Start()
{
agent = GetComponent<NavMeshAgent>();
// 초기 상태를 AliveState로 설정
currentState = new AliveState(this);
currentState.Enter();
}
void Update()
{
currentState.Update();
}
public void ChangeState(IState newState)
{
currentState.Exit();
currentState = newState;
currentState.Enter();
}
public bool IsPlayerInDetectionRange()
{
return Vector3.Distance(transform.position, player.position) <= detectionRange;
}
public bool IsPlayerInAttackRange()
{
return Vector3.Distance(transform.position, player.position) <= attackRange;
}
}
지금처럼 IState 를 BaseState 추상 클래스에 붙여 기능을 구현하고 BaseState를 상속받은 하위 State 들은 구체적인 행동을 정의하여 ChangeState 를 통해 상태를 변경하면서 변경된 상태에 맞게 행동하도록 할 수 있다.
이러면 복잡한 FSM 구조를 좀 던 단순화하여 관리 할 수 있도록 한다.