Chapter 11. 상태 패턴(State Pattern)

Date:     Updated:

카테고리:

태그:

인프런에 있는 이재환님의 강의 게임 디자인 패턴 with Unity 를 듣고 정리한 필기입니다. 😀

Chapter 11. State Pattern

🔔 개념

상태 패턴을 사용하지 않은 경우

상태 플래그 변수 사용할 때

슈퍼마리오 같은 횡스크롤 게임을 만든다면 서있기 / 점프 / 엎드리기 / 엎드려 기모으기 / 이동 과 같은 동작을 만들 것이다. 이단 점프는 허용 안 한다고 가정.

  • 일단 스페이스바 누르면 점프하도록 만들기
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            StartCoroutine("HandleJump");  // 점프를 처리하는 코루틴 함수
        }
    }
    
  • 이단 점프를 허용 안한다고 가정하므로 이단 점프를 막기 위해 다음과 같이 추가한다.
     void Update()
     {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            if (!is.Jumping)
            {
                isJumping = true;
                StartCoroutine("HandleJump");  // 점프를 처리하는 코루틴 함수
            }
        }
     }
    
  • 이제 캐릭터가 땅에 있을 때 아래쪽 버튼을 누르면 엎드리고, 버튼을 떼면 다시 일어서는 기능을 추가해보자.
    void Update()
     {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            if (!is.Jumping)
            {
                isJumping = true;
                StartCoroutine("HandleJump");  // 점프를 처리하는 코루틴 함수
            }
        }
        else if (Input.GetKeyDown(KeyCode.DownArrow))  // ↓ 키를 누르면
        {
            if (!is.Jumping)  // 점프 상태가 아닐 때만 엎드릴 수 있어야 함
            {
                StartCoroutine("HandleDown");  // 엎드리기를 처리하는 코루틴 함수
            }
        }
        else if (Input.GetKeyUp(KeyCode.DownArrow))  // ↓ 키를 떼면
        {
            StartCoroutine("HandleStand");  // 일어나기를 처리하는 코루틴 함수
        }
     }
    
  • 그런데 이렇게 해도 또 수정해야할 버그가 있다. 예를 들어 엎드린 상태에서 점프를 하고 공중에서 아래 버튼을 뗀다면 점프 중인데도 잠시 동안 땅에 서있다가 다시 올라가는 버그가 생길 것이다.
    • 👉 상태 변경을 더 체크해야 하므로 상태 변경을 체크할 isJumping같은 플래그 변수를 더 추가 해야 한다.
      • 근데 그래도 또! 버그가 생길 것이고 플래그 변수를 더 추가해야 한다. 너무 복잡

FSM 을 사용할 때

위와 같은 문제를 FSM (유한 상태 기계) 사용으로 해결해보자.

  • 캐릭터가 할 수 있는 동작들을 모두 적고 상태도를 그림
    • image
  • switch 문으로 case : STATE_Standing , case : STATE_Jumping … 등등
    • 이런식으로 각각의 상태들 경우에 따라 따로 처리를 해주는 방식이다.
  • 그러나 FSM을 제어하기 위한 열거문 만으로도 부족할 때가 있다
    • 코드가 꼬인다.

상태 패턴 정의 및 적용하기

상태를 별도의 클래스로 캡슐화한 다음 현재 상태를 나타내는 객체에게 행동을 위임한다.

  • 따라서 내부 상태가 바뀜에 따라서 행동이 달라지게 된다.
  • 동적으로 행동을 교체할 수 있다.
  • 전략 패턴과 구조는 거의 동일하나 쓰임의 용도가 다르다.

핵심 정리

  • 상태 패턴을 사용하면 내부 상태를 바탕으로 여러가지 서로 다른 행동을 사용할 수 있다.
  • 상태 패턴을 사용하면 FSM을 쓸 때와 달리 각 상태를 클래스를 이용하여 표한하게 된다.
  • 상태를 사용하는 Context 객체에서는 현재 상태에게 행동을 위임한다.
  • 각 상태를 클래스로 캡슐화함으로써 나중에 변경시켜야 하는 내용을 국지화시킬 수 있다.
  • 상태 패턴전략 패턴 의 용도 차이점
    • 전략 패턴
      • 행동 또는 알고리즘을 Context 클래스를 만들 때 설정한다.
    • 상태 패턴
      • Context의 내부 상태가 바뀜에 따라 알아서 행동을 바꿀 수 있도록 할 수 있다.
  • 상태 전환은 State 클래스에 의햇서 제어할 수도 있고 Context 클래스에 의해서 제어할 수도 있다.
  • State클래스를 여러 Context 객체의 인스턴스에서 공유하도록 디자인 할 수도 있다.


🔔 예제 1

📜 State.cs

// 상태 인터페이스 클래스
public interface State
{
    void Action();
};

// 추상 클래스로 구현해도 됨


📜 Move.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Move : State
{
    public void Action()  // 오버라이딩
    {
        Debug.Log("이동 !!!");
    }
}


📜 Attack.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Attack : State
{
    public void Action() // 오버라이딩
    {
        Debug.Log("공격 !!!");
    }
}


📜 Monster.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Monster
{
    private State state;

    // Constructor
    public Monster(State state) // 어떤 상태가 들어올지 모르지만 일단 상태 입력을 받고
    {
        this.state = state;
    }

    public void setState(State state) // setter 상태를 set
    {
        this.state = state;
    }

    public void act()  // getter 상대를 get. ⭐상태에 따른 행동을 알아서 한다.⭐
    {
        state.Action();
    }
};


📜 StateManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class StateManager : MonoBehaviour {

	void Start () {
        Monster monster = new Monster(new Move()); // 상태를 만들고 상태에 따른 행동을 위임
        monster.act();

        monster.setState(new Attack()); // 상태를 Attack로 바꿔줌
        monster.act();

        monster.setState(new Move());  // 상태를 Move로 바꿔줌
        monster.act();
    }
}


🔔 예제 2

    void Update()
    {
		switch (state) {
		case MyState.STATE_STANDING:
			if (Input.GetKeyDown(KeyCode.Space))
			{
				state = MyState.STATE_JUMPING;
				StartCoroutine("HandleJump");
			} 
            else if (Input.GetKeyDown(KeyCode.DownArrow))
			{
				chargeTime = 0;

				state = MyState.STATE_DUCKING;
				StartCoroutine("HandleDown");
			}
			break;
		case MyState.STATE_JUMPING:
			if (Input.GetKeyDown(KeyCode.DownArrow))
			{
				state = MyState.STATE_DIVING;
				StartCoroutine("HandleAttack");
			}
			break;
		case MyState.STATE_DUCKING:
			if (Input.GetKeyUp(KeyCode.DownArrow))
			{
				state = MyState.STATE_STANDING;
				StartCoroutine("HandleStand");
			}

			chargeTime ++;
			if ( chargeTime > MAX_CHARGE ) {
				// 스킬 발동;
				state = MyState.STATE_STANDING;
				StartCoroutine("HandleSkill");
			}

			break;
		default:
			break;
		}
    }

상태 패턴으로 구현

STATE_DUCKING 상태일 때 기모으기를 해서 스킬을 발동시키려면 chargeTime을 증가시켜야 하는데 MyState.STATE_STANDING 서 있을 때 미리 초기화 하여 chargeTime = 0 하는 작업이 필요하다. A 상태에서 B 상태가 발동될 수 있을 때, B 상태에 필요한 작업을 미리 해놔야 한다는게 강의에서 언급한 상태 패턴의 단점이다.


🔔 예제 3

public abstract class State : MonoBehaviour
{
    public abstract void DoAction(MyState state);
};
  • 📜State (인터페이스이며 DoAction 함수를 오버라이딩 하라고 강제)
    • 📜ConcreteStateAttack (DoAction 오버라이딩 👉 내려찍는 공격)
    • 📜ConcreteStateDown (DoAction 오버라이딩 👉 엎드리고 기모으기)
    • 📜ConcreteStateJump (DoAction 오버라이딩 👉 점프)
    • 📜ConcreteStateSkill (DoAction 오버라이딩 👉 기 모은 후 공격)
    • 📜ConcreteStateStand (DoAction 오버라이딩 👉 서 있기)
public enum MyState
{
    STATE_STANDING,
    STATE_JUMPING,
    STATE_DUCKING,
    STATE_DIVING,
    STATE_SKILL
}

public class MyAction8 : MonoBehaviour {

    private MyState state;

	// Concrete클래스들의 접근점(인터페이스)
	private State myState;

	// 상태 클래스 교환...
	public void setActionType (MyState state) {

        // 현재 상태 저장
        this.state = state;

        // 다양한 상태 중에 어떤 것을 가져와야 할 지 모르므로
        // 인터페이스를 대표로 해서 가져온다.
		Component c = gameObject.GetComponent<State>() as Component;

		if (c != null) {
			Destroy(c);
		}

        switch (state)
        {
            case MyState.STATE_STANDING:
                myState = gameObject.AddComponent<ConcreteStateStand>();
                myState.DoAction(state);
                break;
            case MyState.STATE_JUMPING:
                myState = gameObject.AddComponent<ConcreteStateJump>();
                myState.DoAction(state);
                break;
            case MyState.STATE_DUCKING:
                myState = gameObject.AddComponent<ConcreteStateDown>();
                myState.DoAction(state);
                break;
            case MyState.STATE_DIVING:
                myState = gameObject.AddComponent<ConcreteStateAttack>();
                myState.DoAction(state);
                break;
            case MyState.STATE_SKILL:
                myState = gameObject.AddComponent<ConcreteStateSkill>();
                myState.DoAction(state);
                break;
            default:
                break;
        }
    }

    void Start () 
	{
        setActionType(MyState.STATE_STANDING);
	}

    void Update()
    {
        switch (state)
        {
            case MyState.STATE_STANDING:
                if (Input.GetKeyDown(KeyCode.Space))
                {
                    setActionType(MyState.STATE_JUMPING);
                }
                else if (Input.GetKeyDown(KeyCode.DownArrow))
                {
                    setActionType(MyState.STATE_DUCKING);
                }
                break;
            case MyState.STATE_JUMPING:
                if (Input.GetKeyDown(KeyCode.DownArrow))
                {
                    setActionType(MyState.STATE_DIVING);
                }
                break;
            case MyState.STATE_DUCKING:
                if (Input.GetKeyUp(KeyCode.DownArrow))
                {
                    setActionType(MyState.STATE_STANDING);
                }
                break;
            default:
                break;
        }
    }
}


🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Design Pattern 카테고리 내 다른 글 보러가기

댓글 남기기