Skip to main content

[Design Pattern] 행동 패턴 4. 상태 (State)




상태 패턴 #

상태

  • 객체 내부의 상태가 바뀌었을 때 객체가 행동을 바꾸도록 한다.
    • 이렇게 하면 마치 객체가 자신의 클래스를 바꾸는 것처럼 보인다.
  • 특징
    • 기존 상태를 변경하지 않고 쉽게 새로운 상태를 추가할 수 있다.
    • switch 문을 길게 작성하는 것보다 단순하다.

게임에서의 예시 #

  • 플랫포머 게임을 만들고 싶다.
void Heroine::handleInput(Input input)
{
  if (input == PRESS_B)
  {
    // 잠깐! 공중 점프가 안 되게 플래그를 추가해야 한다. 
    // 잠깐! 엎드린 상태는 아닌지 플래그를 추가해야 한다. 
    // 잠깐! 내려찍기 중인건 아닌지 플래그를 추가해야 한다. 
    
    velocity = JUMP_VELOCITY;
    setGraphics(IMAGE_JUMP);
  }
  else if (input == PRESS_DOWN)
  {
    // 똑같이 수많은 플래그 검사 필요...
    // 이런 식이면 버그에 파묻혀서 구현을 못 끝내겠다.
  }
}

  • FSM(Finite-State Machine: 유한 상태 기계) 의 특징
    • 가질 수 있는 ‘상태’가 한정된다.
    • 한 번에 ‘한 가지’ 상태만 될 수 있다.
    • ‘입력’이나 ‘이벤트’가 기계에 전달된다.
    • 각 상태에는 입력에 따라 다음 상태로 바뀌는 ‘전이’가 있다.

  • 우리 게임도 한 번에 한 가지 상태만 가능하므로
    • 필요한 상태들을 열거형으로 바꿀 수 있겠다.
    • 그리고 캐릭터가 state라는 변수 하나만 갖게 한다.
enum State
{
  STATE_STANDING,
  STATE_JUMPING, 
  STATE_DUCKING, 
  STATE_DIVING
};

void Heroine::handleInput(Input input)
{
  switch (state)
  {
    case STATE_STANDING:
      if (input == PRESS_B)
      {
        state = STATE_JUMPING;
        velocity = JUMP_VELOCITY;
        setGraphics(IMAGE_JUMP);
      }
      else if (input == PRESS_DOWN)
      {
        state = STATE_DUCKING;
        setGraphics(IMAGE_DUCK);
      }
      break;
    case STATE_JUMPING:
      if (input == PRESS_DOWN)
      {
        state = STATE_DIVING;
        setGraphics(IMAGE_DIVING);
      }
      break;
    case STATE_DUCKING:
      if (input == RELEASE_DOWN)
      {
        state = STATE_STANDING;
        setGraphics(IMAGE_STAND);
      }
      break;
  }
}

  • 하지만 여기에다가 엎드려 있으면 기를 모아서 놓는 순간 특수 공격을 쏘게 한다면?
    • switch 문 사이에 각종 플래그와 입력처리를 추가해야 된다.
    • 이제 상태 패턴을 사용해볼 때가 되었다.
class HeroineState
{
public:
  virtual ~HeroineState() {}
  virtual void handleInput(Heroine& heroine, Input input) {}
  virtual void update(Heroine& heroine) {}
};

// 상태별로 클래스를 만든다. 
class DuckingState : public HeroineState
{
private:
  int chargeTime;

public:
  DuckingState() : chargeTime(0) {}
  
  virtual void handleInput(Heroine& heroine, Input input)
  {
    if (input == RELEASE_DOWN)
      heroine.setGraphics(IMAGE_STAND);
  }
  
  virtual void update(Heroine& heroine)
  {
    chargeTime++;
    if (chargeTime > MAX_CHARGE)
      heroine.superBomb();
  }
};

class Heroine
{
private:
  // 이제 상태를 바꾸려면 state만 바꾸면 된다. 
  HeroineState* state;

public:
  virtual void handleInput(Input input)
  {
    state->handleInput(*this, input);
  }
  
  virtual void update()
  {
    state->update(*this);
  }
};

  • 여기서 각 상태들의 인스턴스는 어떻게 만들어야 할까?
  • (1) 정적 객체로 만든다.
class HeroineState
{
public:
  static StandingState standing;
  static DuckingState ducking;
  static JumpingState jumping;
  static DivingState diving;
};
// 이렇게 사용하면 된다. 
if (input == PRESS_B)
{
  heroine.state = &HeroineState::jumping;
  heroine.setGraphics(IMAGE_JUMP);
}

  • 주인공이 여럿이라면 정적 객체로는 안 되겠다.
  • (2) 전이할 때마다 상태 객체를 만든다.
// 각 상태 클래스에서 새로운 상태를 리턴하도록 한다. 
void StandingState::handleInput(HeroineState& heroine, Input input)
{
  if (input == PRESS_DOWN) 
  {
    return new DuckingState();
  }
  
  return NULL;
}

void Heroine::handleInput(Input input)
{
  HeroineState* newState = state->handleInput(*this, input);
  
  // 상태가 변했다면 새로운 상태 객체를 할당한다. 
  if (state != NULL)
  {
    delete state;
    state = newState;
  }
}

개념적인 예시 #

// 클라이언트가 원하는 것을 담고 있는 Context
class Context
{
  // 현재 State를 가지고 있다. 
  private State _state = null;

  public Context(State state)
  {
    this.TransitionTo(state);
  }

  // 런타임에 State를 바꿀 수 있게 한다. 
  public void TransitionTo(State state)
  {
    this._state = state;
    this._state.SetContext(this);
  }

  // 현재 State에 맞는 행동을 하도록 한다. 
  public void Request1()
  {
    this._state.Handle1();
  }

  public void Request2()
  {
    this._state.Handle2();
  }
}

// State
abstract class State
{
  // State가 상태를 바꿀 수 있게 하기위해 Context를 가지고 있다. 
  protected Context _context;

  public void SetContext(Context context)
  {
    this._context = context;
  }

  public abstract void Handle1();
  public abstract void Handle2();
}  

class ConcreteStateA : State
{
  public override void Handle1()
  {
    Console.WriteLine("ConcreteStateA handles request1.");
    Console.WriteLine("ConcreteStateA wants to change the state of the context.");
    this._context.TransitionTo(new ConcreteStateB());
  }

  public override void Handle2()
  {
    Console.WriteLine("ConcreteStateA handles request2.");
  }
}

class ConcreteStateB : State
{
  public override void Handle1()
  {
    Console.Write("ConcreteStateB handles request1.");
  }

  public override void Handle2()
  {
    Console.WriteLine("ConcreteStateB handles request2.");
    Console.WriteLine("ConcreteStateB wants to change the state of the context.");
    this._context.TransitionTo(new ConcreteStateA());
  }
}

class Program
{
  static void Main(string[] args)
  {
    var context = new Context(new ConcreteStateA());
    
    context.Request1(); // State A에 따라 행동 -> State가 B로 바뀜
    context.Request2(); // State B에 따라 행동 -> State가 A로 바뀜
  }
}



References #