상태 패턴(state pattern)은 객체가 상태에 따라 다른 행위를 해야하는 상황에서 상태를 객체화하여 상태에 행동을 위임하는 것을 말한다.
객체 지향 프로그래밍에서는 사물이나 생물에 국한된 것이 아닌, 무형의 상태나 행위 등을 객체로 구현하는 것이 가능하기에 상태를 클래스로 구현하여 객체의 행동을 상태에 위임할 수 있는 것이다.
상태 패턴은 객체의 상태가 런타임 내에서 주기적으로 바뀌고, 상태에 따라 행동이 다른 경우에 많이 사용되며, 상태 전이 조건을 각 State 클래스에 넣어 내부에서 상태가 전이될 수 있도록 하는 것이 중요하다.
상태 패턴의 구조는 다음과 같이 구성된다.
State Interface
: 상태를 추상화한 인터페이스State Controller
: 상태의 전이 등을 관리하는 컨트롤러Concrete State
: 각각의 상태를 클래스로 구체적 구현플레이어를 추적하는 간단한 몬스터 AI를 예시로 들어보자.
State Interface
public interface IMonsterState
{
// 각 상태 진입시 초기값을 설정하기 위한 메서드
public void Enter();
// 각 상태 별 행동을 나타내는 메서드
public void Tick();
// 해당 상태에서 벗어날 때 추가된 설정을 초기화하는 메서드
public void Exit();
}
StateController
각 상태에서 필요한 속성을 정의하고 상태의 전이를 담당하는 ChangeState()
를 구현했다.
public class MonsterController : MonoBehaviour
{
[Header("Drag&Drop")]
[SerializeField]
private Transform player;
public Transform Player => player;
[SerializeField]
private Transform[] patrolPoints;
public Transform[] PatrolPoints => patrolPoints;
[Header("Input")]
[SerializeField]
private float detectRange;
public float DetectRange => detectRange;
[SerializeField]
private float maxChaseDistance;
public float MaxChaseDistance => maxChaseDistance;
private NavMeshAgent agent;
public NavMeshAgent Agent => agent;
private Vector3 spawnPos;
public Vector3 SpawnPos => spawnPos;
private IMonsterState _curState;
// 메모리 최적화를 위한 캐싱
private IMonsterState _patrolState;
public IMonsterState PatrolState => _patrolState
private IMonsterState _chaseState;
public IMonsterState ChaseState => _chaseState;
private IMonsterState _returnState;
public IMonsterState ReturnState => _returnState;
private void Awake()
{
Init();
}
private void Update()
{
_curState?.Tick();
}
private void Init()
{
agent = GetComponent<NavMeshAgent>();
spawnPos = transform.position;
_patrolState = new PatrolState(this);
_chaseState = new ChaseState(this);
_returnState = new ReturnState(this);
// 초기 상태 설정(순찰 상태)
ChangeState(_partolState);
}
// 상태의 전이를 담당하는 메서드
public void ChangeState(IMonsterState newState)
{
// 상태의 전이를 확인하기 위한 로그 출력
Debug.Log($"from {_curState.GetType().Name} to {newState.GetType().Name}");
// 현재 상태에서의 설정 초기화
_curState?.Exit();
// 상태 전이
_curState = newState;
// 전이한 상태에서의 설정값 부여
_curState?.Enter();
}
}
PatrolState
(순찰 상태)플레이어의 감지가 없는 경우 NavMeshAgent
컴포넌트를 활용해 특정 지점을 반복하여 배회하도록 구현했다.
public class PatrolState : IMonsterState
{
private MonsterController monster;
private int _curIndex = 0;
// 생성자 오버로딩
public PatrolState(MonsterController monster)
{
this.monster = monster;
}
public void Enter()
{
monster.Agent.isStopped = false;
monster.Agent.SetDestination(monster.PatrolPoints[_curIndex].position)
}
public void Tick()
{
float distance = Vector3.Distance(monster.transform.position, monster.Player.position)
if(distance < monster.DetectRange)
{
monster.ChangeState(monster.ChaseState);
return;
}
if(!monster.Agent.pathPending && monster.Agent.remainingDistance < 0.5f)
{
_curIndex = (_curIndex + 1) % monster.PatrolPoints.Length
monster.Agent.SetDestination(monster.PatrolPoints[_curIndex].position)
}
}
public void Exit()
{
// 추가 조건이 생기는 경우 할당
}
}
ChaseState
(추적 상태)플레이어 감지에 성공한 경우 몬스터의 상태가 전이되어 플레이어를 따라가도록 구현했다.
public class ChaseState : IMonsterState
{
private MonsterController monster;
// 생성자 오버로딩
public ChaseState(MonsterController monster)
{
this.monster = monster;
}
public void Enter()
{
monster.Agent.isStopped = false;
}
public void Tick()
{
monster.Agent.SetDestination(monster.Player.position);
float distanceFromSpawn = Vector3.Distance(monster.transform.position, monster.SpawnPos);
if(distanceFromSpawn > monster.MaxChaseDistance)
{
monster.ChangeState(monster.ReturnState);
}
}
public void Exit()
{
// 추가 조건이 생기는 경우 할당
}
}
ReturnState
(복귀 상태)최대 추적 거리를 벗어난 몬스터가 다시 원래의 spawnPos
로 돌아가도록 구현했다.
public class ReturnState : IMonsterState
{
private MonsterController monster;
// 생성자 오버로딩
public ReturnState(MonsterController monster)
{
this.monster = monster;
}
public void Enter()
{
monster.Agent.isStopped = false;
monster.Agent.SetDestination(monster.SpawnPos);
}
public void Tick()
{
if(!monster.Agent.pathPending && monster.Agent.remainingDistance < 0.5f)
{
monster.ChangeState(monster.PatrolState);
}
}
public void Exit()
{
// 추가 조건이 생기는 경우 할당
}
}