상태 패턴(state pattern)은 객체 지향 방식으로 상태 기계를 구현하는 행위 소프트웨어 디자인 패턴이다. 상태 패턴을 이용하면 상태 패턴 인터페이스의 파생 클래스로서 각각의 상태를 구현함으로써, 또 패턴의 슈퍼클래스에 의해 정의되는 메소드를 호출하여 상태 변화를 구현함으로써 상태 기계를 구현한다.
<FSM 유한 상태 기계 특징>
- 상태 : 가질 수 있는 상태가 한정되며 한 번에 한가지 상태만 유지합니다.
- 입력 : 입력이나 이벤트가 기계에 전달됩니다.
- 전이 : 각 상태에는 입력에 따라 다음상태로 바뀌는 전이가 있습니다.
대부분 열거형을 통해 상태를 구분하고 그 중 하나를 현재의 상태로 저장합니다.
저장된 상태에 해당하는 행동을 하거나, 데이터를 유지합니다.
Ex) 전구의 ON / OFF
<일반적 구현>
일반적으로 상태패턴을 구현할때 제시되는 방법은
① 상태 인터페이스의 정의
상태의 기본이 되는 인터페이스 혹은 가상클래스를 만드는 것으로 시작합니다.
이때 기본 인터페이스에는 각각의 상태에 공통적으로 사용할 메서드를 만들어 주어야 합니다.
② 상태별로 기본 인터페이스를 구현하는 상태 클래스의 정의
정해진 상태가 되었을때 어떤 행동을 할지를 상태별로 구현해 줍니다.
③ 상태패턴의 적용
상태패턴이 적용되는 클래스에서는 기본 인터페이스를 필드로 가지고 상태가 변화함에 따라 인터페이스 필드에 상태 클래스들을 적용시키며 사용합니다.
// 기본 인터페이스 ====================================================
interface human
{
public void Init();
public void Update();
}
// 상태 클래스 =========================================================
public class Run : human // 상태 : 달리기
{
public void Init()
{
// 달리는 애니메이션
...
}
public void Update()
{
// 위치 이동
...
}
}
public class Attack : human // 상태 : 공격
{
public void Init()
{
// 공격 애니메이션
}
public void Update()
{
// 등등
}
}
// 상태패턴이 적용될 클래스 ================================================
enum state { run, attack }
class player
{
human _human = new human();
state _state = state.run;
// 상태 변경
public void stateChange(state stt)
{
_state = stt;
// 상태가 변경 될때 _human에 적용되는 상태도 달라집니다.
switch(_state)
{
Case state.run:
_human = new Run();
break;
Case state.attack:
_human = new Attack();
break;
}
_human.Init();
}
public void stateUpdate()
{
// 메서드는 항상 작동하지만
// 상태가 변경됨에 따라 작동 내용은 달라집니다.
_human.Update();
}
}
<간단한 구현>
또 다른 방법으로는 상태에 따라 클래스를 따로 두지않고 한 클래스에서 사용하는 방법이 있습니다.
상태는 enum으로 컨트롤하며 상태마다 공통되는 메서드들을 클래스에 정의하고 메서드 내부에서 switch문 등을 통해 상태별 작동을 하게 합니다.
enum state { idle, run, attack, die }
class player
{
// 기본 상태
state _state = state.idle;
public void changeState(state stt)
{
_state = stt;
stateInit();
}
public void stateInit()
{
switch(_state)
{
Case state.idle :
// idle 초기화
break;
Case state.run :
// run 초기화
break;
Case state.attack :
// attack 초기화
break;
Case state.die :
// die 초기화
break;
}
}
public void stateUpdate()
{
switch(_state)
{
Case state.idle :
// idle 업데이트 행동
break;
Case state.run :
// run 업데이트 행동
break;
Case state.attack :
// attack 업데이트 행동
break;
Case state.die :
// die 업데이트 행동
break;
}
}
}
구현은 더 간단하지만 상태가 많아지거나 하면 코드가 복잡해 질 수 있습니다.
::: 상태 패턴은 외부에서 어떤 입력이 들어오던간에
현재 상태에 정의되어 있는 행동만을 취할 수 있으므로
개체의 행동을 분화하고 구체화하는데 좋다고 생각합니다.
<(추천받은) 유니티에서 사용할만한 외부 라이브러리>
▶ enum을 통해 상태 목록을 정의하고 메서드 형식만 갖추면 쉽게 사용이 가능한 라이브러리입니다.
정의한 메서드는 호출규칙에 의해 새로운 상태로 전환될 때 트리거됩니다.
▶ 메서드 규칙은 enum에서 정의한 상태명 + _(언더바) + 내장 메서드명으로 구성되며
내장 메서드에는 Enter, Update, Exit등의 기본적인 메서드외에도 FixedUpdate, LateUpdate, OntriggerEnter2D등 다양하게 있다고 합니다.
※ 메서드는 내부적으로 리플렉션을 사용해 바인딩하는데 _언더바를 기준으로 메서드명을 읽는다고 합니다. 만약 double_jump 라는 상태가 있어 메서드명을 double_jump_Enter등으로 짓는다면 double이후의 이름을 내장메서드중 찾게 되고 실패하게 됩니다. 따라서 _는 이름중에 사용하지 않는 것이 좋겠습니다.
using MonsterLove.StateMachine; //1. Remember the using statement
public class MyGameplayScript : MonoBehaviour
{
public enum States
{
Init,
Play,
Win,
Lose
}
StateMachine<States> fsm;
void Awake(){
fsm = new StateMachine<States>(this); //2. The main bit of "magic".
fsm.ChangeState(States.Init); //3. Easily trigger state transitions
}
void Init_Enter()
{
Debug.Log("Ready");
}
void Play_Enter()
{
Debug.Log("Spawning Player");
}
void Play_FixedUpdate()
{
Debug.Log("Doing Physics stuff");
}
void Play_Update()
{
if(player.health <= 0)
{
fsm.ChangeState(States.Lose); //3. Easily trigger state transitions
}
}
void Play_Exit()
{
Debug.Log("Despawning Player");
}
void Win_Enter()
{
Debug.Log("Game Over - you won!");
}
void Lose_Enter()
{
Debug.Log("Game Over - you lost!");
}
}
<기본 사용팁>
1. using MonsterLove.StateMachine; 선언
2. 원하는 상태 enum 정의 ('_' 들어가면 안됨)
3. StateMachine<TState(enum명)> 선언 및 초기화
- StateMachine의 초기화에는 StateMachine<TState>.Initialize(this); 와 new StateMachine<TState>(this) 두가지 방법이 있습니다.
- 그 외에 StateMachine이 작동하기 위해서는 StateMachineRunner 컴포넌트가 필요합니다.
- StateMachine의 초기화중 new로 초기화한다면 아마 작동하지 않을 것입니다.
- StateMachine<TState>.Initialize(this);는 사실 초기화 과정중 new StateMachine<TState>(this)를 이미 포함하며 초기화시 내부적으로 상태패턴이 적용되는 개체에 StateMachineRunner 컴포넌트를 Add해주어 작동이 가능하게 해줍니다.
4. Changed 이벤트 사용
- Action<TState> Changed 이벤트 입니다.
- 상태가 전이될때 할 일이 있다면 TState를 인자로 받는 메서드를 이벤트 체이닝하여 사용할 수 있습니다.