Design Pattern

상태 (State)

coucou3 2020. 11. 25. 01:25
반응형

게임 프로그래밍 패턴 7장

상태

 

상태 패턴은 객체의 상태에 따라 행동이 달라지는 상황에서, 상태를 객체화하여 스스로 행동할 수 있도록 하는 패턴이다. 유한상태기계(FSM : Finite State Machine)로 구현한다.

객체의 내부 상태에 따라 스스로 행동을 변경할 수 있게 허가하는 패턴으로, 이렇게 하면 객체는 마치 자신의 클래스를 바꾸는 것처럼 보입니다. (GoF의 디자인 패턴 395쪽)

 

 

플랫포머 게임에서 플레이어를 구현한다고 해보자. B 버튼을 누르면 플레이어는 점프한다.

# Player.cs

public class Player : MonoBehaviour
{
    public void HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.B))
        {
            yVelocity = JUMP_VELOCITY;
            setGraphics(IMAGE_JUMP);
        }
    }
}

 

이때 점프 중인지 확인하지 않으면 공중에서도 무한대로 점프할 수 있다. isJumping 플래그를 하나 넣어 방지한다.

public class Player : MonoBehaviour
{
    public void HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.B))
        {
            if (isJumping == false)
            {
                isJumping = true;
                yVelocity = JUMP_VELOCITY;
                setGraphics(IMAGE_JUMP);
            }
        }
    }
}

 

이번엔 땅에 있을 때 아래 버튼을 누르면 엎드리고, 버튼을 떼면 다시 일어나는 기능을 추가한다.

public class Player : MonoBehaviour
{
    public void HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.B))
        {
            if (isJumping == false)
            {
                isJumping = true;
                yVelocity = JUMP_VELOCITY;
                setGraphics(IMAGE_JUMP);
            }
        }
        else if (Input.GetKeyDown(KeyCode.DownArrow))
        {
            if (isJumping == false)
            {
                setGraphics(IMAGE_DUCK);
            }
        }
        else if (Input.GetKeyUp(KeyCode.DownArrow))
        {
            setGraphics(IMAGE_STAND);
        }
    }
}

 

이때 점프 중 아래 버튼을 떼면 서 있는 모습으로 보이는 버그가 있다.

이번에도 플래그를 하나 추가하여 막을 수 있겠지만, 이런 식으론 끝이 없다.

 


 

FSM

  • 가질 수 있는 '상태'가 한정된다. - 위 예시에서는 서기, 점프, 엎드리기 상태에 해당한다.
  • 한 번에 '한 가지' 상태만 될 수 있다. - 플레이어는 점프와 동시에 서 있을 수 없다. FSM에서는 동시에 두 가지 상태가 될 수 없다.
  • '입력'이나 '이벤트'가 기계에 전달된다. - 위 예시에서는 버튼 누르기와 버튼 뗴기에 해당한다.
  • 각 상태에는 입력에 따라 다음 상태로 바뀌는 '전이'가 있다. - 입력이 들어왔을 때 현재 상태에 전이가 있다면 전이에 따라 다음 상태로 전환된다.

 

위 예제를 FSM으로 바꿔보자.

# StateType.cs

public enum StateType
{
    STADING,
    JUMPING,
    DUCKING,
}

여러 상태 중 한 가지 상태만 갖기 때문에 열거형(enum)을 사용한다.

 

# PlayerState.cs

public class PlayerState
{
    public virtual void HandleInput() { }
    public virtual void Update() {}
}

public class StandingState : PlayerState
{
    public override void HandleInput(Player player)
    {
        if (Input.GetKeyDown(KeyCode.DownArrow))
        {
            // 엎드린 상태로 바꾼다.
            player.SetState(StateType.DUCKING);
        }
    }

    public override void Update(Player player)
    {
        
    }
}

public class JumpingState : PlayerState
{
    ...
}

...

플레이어의 상태를 나타내는 객체. 각각의 state는 PlayerState를 상속 받는다.

State 객체 내 HandleInput에서 다른 키 입력이 들어왔을 경우 player의 상태를 바꾼다.

 

# Player.cs

public class Player : MonoBehaviour
{
    private PlayerState m_state;
    private StandingState m_standingState;
    private JumpingState m_jumpinState;
    private DuckingState m_duckingState;

    private void Update()
    {
        m_state.Update(this);
    }

    private void HandleInput()
    {
        m_state.HandleInput(this);
    }

    public void SetState(StateType type)
    {
        PlayerState newState = null;
        switch (type)
        {
            case StateType.STADING:
                newState = m_standingState;
                break;
            case StateType.JUMPING:
                newState = m_jumpinState;
                break;
            case StateType.DUCKING:
                newState = m_duckingState;
                break;
        }
        
        if (newState != m_state)
        {
            m_state = newState;
        }
    }
}

플레이어는 현재 상태를 나타내는 PlayerState를 갖고 있다.

m_state에서 SetState플레이어의 현재 상태와 새로운 상태가 다를 경우 새로운 상태로 현재 상태를 바꿔준다.

 

 

입장과 퇴장

플레이어의 상태가 변경될 때 이미지 변경 등 기능이 추가적으로 필요하다고 해보자.

public class StandingState : PlayerState
{
    public override void HandleInput(Player player)
    {
        if (Input.GetKeyDown(KeyCode.DownArrow))
        {
            // 엎드린 상태로 바꾼다.
            player.SetState(StateType.DUCKING);
            
            // 이미지를 바꾼다.
            player.SetImage(IMAGE_DUCKING);
        }
    }

    ...
}

이런 식으로 스테이트가 바뀌는 부분에서 기능을 넣어줄 수 있겠지만, FSM에 입장/퇴장 기능을 넣어 좀 더 상태머신처럼 만들 수 있다.

 

# PlayerState.cs

public class PlayerState
{
    public virtual void HandleInput() { }
    public virtual void Update() {}
    public virtual void EnterState() {}
    public virtual void ExitState() {}
}

스테이트의 Enter와 Exit 시점에 호출하는 EnterState와 ExitState를 각 자식 스테이트에서 오버라이드한다.

 

# Player.cs

public class Player : MonoBehaviour
{
    ...
    
    public void SetState(StateType type)
    {
        ...

        if (newState != null && 
            newState != m_state)
        {
            m_state.ExitState();
            m_state = newState;
            m_state.EnterState();
        }
    }
}

Player의 스테이트가 바뀌는 시점에 현재 스테이트의 ExitState를 호출하고, 

현재 스테이트를 새 스테이트로 바꾸고 EnterState를 호출한다.

 

PlayerState를 상속받는 각각의 자식 클래스에서는 상황에 맞는 기능을 작성하면 된다.

public class StandingState : PlayerState
{
    ...
    public override void EnterState()
    {
        player.SetImage(IMAGE_STANDING);
    }

    public override void ExitState()
    {
        ...
    }
}

 

반응형

'Design Pattern' 카테고리의 다른 글

싱글턴 (Singleton)  (0) 2020.09.19
프로토타입 (Prototype)  (0) 2020.07.19
관찰자 (Observer)  (0) 2020.07.05