상태 패턴(State Pattern)은 소프트웨어 디자인 패턴 중 하나로, 객체의 상태에 따라 객체의 행동이 달라지는 경우에 사용됩니다. 이 패턴을 사용하면 객체의 상태를 캡슐화하고, 상태에 따라 필요한 행동을 수행할 수 있습니다. 예를 들어, 게임 캐릭터의 상태가 "공격 중", "이동 중", "대기 중" 등 여러 상태가 있을 때, 각 상태마다 적절한 행동을 구현할 수 있습니다. 이를 통해 코드의 유지보수성을 높이고, 각 상태를 쉽게 추가하거나 변경할 수 있습니다.
장점
- 유연한 상태 전환: 상태를 캡슐화하여 상태 전환 로직을 각 상태 클래스에 분리
- 상태 전환을 더 쉽게 관리하고 확장할 수 있게 함
- 가독성 향상: 상태 전환 로직이 각 상태 클래스에 분리되어 있어, 코드의 가독성이 향상
- 유지보수 용이성: 상태를 추가 및 변경 시, 다른 상태 클래스의 영향 없이 독립적으로 작업
단점
- 클래스 증가: 상태별로 별도의 클래스가 필요하기 때문에 클래스의 수가 증가
- 복잡성 증가: 상태가 많아질수록 관리해야 할 클래스와 코드의 복잡성이 증가
유니티에서 상태 패턴을 시스템화 시켜놓은 것을 FSM, Finite State Machine(유한 기계 상태)라고 합니다. FSM은 시스템이 가질 수 있는 모든 상태와 한 상태에서 다른 상태로 전환하는 규칙을 정의하는 모델입니다.
- 상태(State): 시스템이 가질 수 있는 각각의 상태
- 전환(Transition): 한 상태에서 다른 상태의 전환 규칙
- 이벤트(Event): 상태 전환을 일으키는 외부 입력이나 조건
- 동작(Action): 상태 전환 시 또는 특정 상태에 있을 때 수행되는 행동
간단한 상태 패턴 구현
using UnityEngine;
public enum PlayerControllerState
{
Idle,
Walk
}
public class PlayerController : MonoBehaviour
{
private PlayerControllerState state;
private void Update()
{
GetInput();
switch (state)
{
case PlayerControllerState.Idle:
Idle();
break;
case PlayerControllerState.Walk:
Walk();
break;
}
}
private void GetInput()
{
// 예시: W 키를 누르면 IdleState로 전환
if (Input.GetKeyDown(KeyCode.W))
{
state = PlayerControllerState.Idle;
}
// 예시: S 키를 누르면 WalkState로 전환
if (Input.GetKeyDown(KeyCode.S))
{
state = PlayerControllerState.Walk;
}
}
private void Idle()
{
Debug.Log("Update Idle State");
}
private void Walk()
{
Debug.Log("Update Walk State");
}
}
이렇게 작성하면 작동은 하더라도 PlayerController 스크립트가 금방 복잡해질 수 있습니다. 상태와 복잡도를 추가하려면 PlayerController 스크립트의 내부를 매번 재확인해야 합니다.
즉, 상태 패턴으로 다음 문제를 해결해야 하지만 두 번째 문제는 해결하지 못합니다.
- 오브젝트는 내부 상태 변경 시 자신의 동작을 변경해야 합니다.
- 상태별 동작은 독립적으로 정의됩니다. 새로운 상태를 추가해도 기존 상태의 동작에 영향을 주지 않습니다.
이러한 문제를 해결하기 위해 다음과 같이, 구조화하여 새로운 상태를 추가할 때 기존 상태에 대한 영향을 최소화하고 상태를 오브젝트로 캡슐화합니다.
개선된 상태 패턴
public interface IState
{
void Enter(); // 상태에 처음 진입할 때 실행되는 코드
void Update(); // 프레임당 로직. 새로운 상태로 전환하는 조건 포함
void Exit(); // 상태에서 벗어날 때 실행되는 코드
}
public class IdleState : IState
{
private PlayerController _player;
public IdleState(PlayerController player)
{
_player = player;
}
public void Enter()
{
UnityEngine.Debug.Log("Enter Idle State");
}
public void Update()
{
UnityEngine.Debug.Log("Update Idle State");
}
public void Exit()
{
UnityEngine.Debug.Log("Exit Idle State");
}
}
public class WalkState : IState
{
private PlayerController _player;
public WalkState(PlayerController player)
{
_player = player;
}
public void Enter()
{
UnityEngine.Debug.Log("Enter Walk State");
}
public void Update()
{
UnityEngine.Debug.Log("Update Walk State");
}
public void Exit()
{
UnityEngine.Debug.Log("Exit Walk State");
}
}
각 상태는 IState 인터페이스를 실행합니다.
- Enter: 이 로직은 상태에 처음 진입할 때 실행됩니다.
- Update: 이 로직은 매 프레임마다 실행됩니다. 물리에 대한 FixedUpdate, LateUpdate 등을 사용하여 MonoBehaviour처럼 Update 메서드를 추가로 세그먼트화할 수 있습니다. Update의 모든 기능은 상태 변경을 트리거하는 조건이 감지될 때까지 각 프레임을 실행합니다.
- Exit: 이 로직의 코드는 상태에서 벗어나 새로운 상태로 전환되기 전에 실행됩니다.
using System;
[Serializable]
public class StateMachine
{
public IState CurrentState { get; private set; }
public IdleState IdleState { get; private set; }
public WalkState WalkState { get; private set; }
public StateMachine(PlayerController player)
{
IdleState = new IdleState(player);
WalkState = new WalkState(player);
}
public void Initialize(IState startingState)
{
CurrentState = startingState;
CurrentState.Enter();
}
public void TransitionTo(IState nextState)
{
CurrentState.Exit();
CurrentState = nextState;
CurrentState.Enter();
}
public void Update()
{
CurrentState?.Update();
}
}
상태 패턴을 따르기 위해 StateMachine은 관리를 받는 각 상태에 대한 공용 오브젝트를 참조합니다. StateMachine은 MonoBehaviour에서 상속받지 않으므로 다른 MonoBehaviour(PlayerController 또는 EnemyController 등)에 맞게 생성자를 만들어 각 인스턴스를 설정합니다.
Serializable 속성을 사용하여 StateMachine 및 공용 필드를 인스펙터에 표시할 수 있습니다. 그러면 다른 MonoBehaviour(PlayerController 또는 EnemyController 등)가 StateMachine을 필드로 사용할 수 있습니다.
- CurrentState 프로퍼티는 읽기 전용입니다. StateMachine 자체는 명시적으로 이 필드를 설정하지 않습니다. 그러면 PlayerController 같은 외부 오브젝트는 Initialize 메서드를 호출하여 기본 상태를 설정할 수 있습니다.
- 상태 전환 조건이 만족되면, TransitionTo 메서드를 호출하여 상태 전환을 관리합니다. 상태 전환 로직은 상태 머신(StateMachine) 클래스에서 관리합니다.
using UnityEngine;
public class PlayerController : MonoBehaviour
{
private StateMachine _playerStateMachine;
public StateMachine PlayerStateMachine => _playerStateMachine;
private void Awake()
{
_playerStateMachine = new StateMachine(this);
}
private void Start()
{
_playerStateMachine.Initialize(_playerStateMachine.IdleState);
}
private void Update()
{
_playerStateMachine.Update();
TransitionTo();
}
private void TransitionTo()
{
// 예시: W 키를 누르면 IdleState로 전환
if (Input.GetKeyDown(KeyCode.W))
{
_playerStateMachine.TransitionTo(_playerStateMachine.IdleState);
}
// 예시: S 키를 누르면 WalkState로 전환
if (Input.GetKeyDown(KeyCode.S))
{
_playerStateMachine.TransitionTo(_playerStateMachine.WalkState);
}
}
}