디자인 패턴

[디자인패턴] 상태 패턴 : FSM 유한 상태 기계

자가라o 2021. 10. 5. 06:09

상태 패턴(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;
        }
    }
}

구현은 더 간단하지만 상태가 많아지거나 하면 코드가 복잡해 질 수 있습니다.

 

::: 상태 패턴은 외부에서 어떤 입력이 들어오던간에

   현재 상태에 정의되어 있는 행동만을 취할 수 있으므로

   개체의 행동을 분화하고 구체화하는데 좋다고 생각합니다.

 


 

<(추천받은) 유니티에서 사용할만한 외부 라이브러리>

- MonsterLove 상태머신

▶ 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를 인자로 받는 메서드를 이벤트 체이닝하여 사용할 수 있습니다.