[Design Pattern] 행동 패턴 4. 상태 (State)
Table of Contents
상태 패턴 #
- 객체 내부의 상태가 바뀌었을 때 객체가 행동을 바꾸도록 한다.
- 이렇게 하면 마치 객체가 자신의 클래스를 바꾸는 것처럼 보인다.
- 특징
- 기존 상태를 변경하지 않고 쉽게 새로운 상태를 추가할 수 있다.
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로 바뀜
}
}