걷기, 점프, 엎드리기 등의 상태가 있고 어떤 이벤트에 따라 상태가 바뀔 때 isJumping
과 같은 플래그 변수를 무한히 늘리지 말고 유한 상태 기계를 이용하자.
isJumping
과 isDucking
은 동시에 참이 될 수 없다. 이렇게 플래그 변수 값 조합이 유효하지 않을 수 있다면, enum을 만들 때이다.
enum State {
STATE_STANDING,
STATE_JUMPING,
STATE_DUCKING,
STATE_DIVING
}
이제 State
변수 하나면 충분하다.
void Heroine::handleInput(Input input) {
switch (state_) {
case STATE_STANDING:
if (input == PRESS_B) {
state_ = STATE_JUMPING;
yVelocity_ = 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_DIVE);
}
break;
case STATE_DUCKING:
if (input == RELEASE_DOWN) {
state_ = STATE_STANDING;
setGraphics(IMAGE_STAND);
}
break;
}
}
그러나 엎드려 있으면 기를 모아서 놓는 순간 특수 공격을 쏘는 기능을 추가한다면, 엎드려서 기를 모으는 시간을 기록해야 한다.
이를 위해 매 프레임 호출되는 update()
와 handleInput()
을 수정하면 되겠지만, 관련된 모든 코드와 데이터는 한 곳에 모아두는 게 낫다. 이럴 때 상태 패턴을 사용하자.
다중 선택문에 있던 동작을 상태 인터페이스의 가상 메서드로 만든다.
class HeroineState {
public:
virtual ~HeroineState() {}
virtual void handleInput(Heroine& heroine, Input input) {}
virtual void update(Heroine& heroine) {}
};
상태별로 인터페이스를 구현하는 클래스를 정의한다.
class DuckingState : public HeroineState {
public:
DuckingState() : chargeTime_(0) {}
virtual void handleInput(Heroine& heroine, Input input) {
// ...
}
virtual void update(Heroine& heroine) {
// ...
}
private:
int chargeTime_;
};
chargeTime_
은 엎드리기 상태에서만 의미가 있었다. 이 변수를 Heroin
에서 DuckingState
클래스로 옮길 수 있다.
Heroin
클래스에 자신의 현재 상태 객체 포인터를 추가해, 거대한 다중 선택문을 제거하고 대신 상태 객체에 위임한다.
class Heroine {
public:
virtual void handleInput(Heroine& heroine, Input input) {
state_->handleInput(*this, input);
}
virtual void update(Heroine& heroine) {
state_->update(*this);
}
private:
HeroineState* state_;
};
상태를 바꾸기 위해서는 state_
에 HeroineState
을 상속받는 다를 객체를 할당하기만 하면 된다.
상태 객체에 필드가 따로 없다면, vtable 포인터만 있다는 것이다. 이럴 경우 모든 인스턴스가 동일한 상태 객체를 공유해도 되므로 인스턴스는 하나만 있어도 된다.
class HeroineState {
public:
static StadingState standing;
static Ducking ducking;
static Jumping jumping;
static DivingState diving;
// ...
};
if (input == PRESS_B) {
heroine.state_ = &HeroineState::jumping;
heroine.setGraphics(IMAGE_JUMP);
}
엎드리기 상태에서 사용하는 chargeTime_
필드는 주인공마다 다르다. 이런 경우 하나의 인스턴스를 공유하면 안되기 때문에 정적 객체로 만들 수 없다.
이럴 때는 전이할 때마다 상태 객체를 만들고 이전 상태를 해제해야 한다.
handleInput()
에서 상태가 바뀔 때만 새로운 상태 반환// 1. 상태가 바뀔 때만 새로운 상태 반환
void Heroine::handleInput(Input input) {
HeroineState* state = state_->handleInput(*this, input);
if )state != NULL) {
delete state_;
state_ = state;
}
}
// 2. 밖에서는 반환 값에 따라 예전 상태를 삭제하고 새로운 상태를 저장
HeroineState* StandingState::handleInput(Heroine& heroine, Input input) {
if (input == PRESS_DOWN) {
// ...
return new DeckingState(); // 엎드리기로 전이
}
return NULL; // 지금 상태 유지
}
그러나 매번 상태 객체를 할당해야 하는 문제가 있다.
상태 패턴의 목표는 같은 상태에 대한 모든 동작과 데이터를 클래스 하나에 캡슐화하는 것이다.
상태를 변경하면서 주인공의 스프라이트도 같이 바꾸는 코드를 지금까지는 이전 상태에서 작성했다. (엎드리기->서기라면 엎드리기 상태에서 변경)
이 방법보다는 상태에서 그래픽까지 제어하는 게 바람직하다. 이를 위해 입장 기능을 추가한다.
class StandingState : public HeroineState {
public:
virtual void enter(Heroine& heroine) {
heroine.setGraphics(IMAGE_STAND);
}
// ...
};
이제 enter()
를 Heroing::handleInput()
에서 불러주기만 하면 된다.
이전까지는 점프->착지, 내려찍기->착지 등 착지 상태에 여러 전이가 들어올 수 있기 때문에 전이가 일어나는 모든 곳에 중복 코드를 넣게 되지만, 이렇게 입장/퇴장 기능을 분리하면 입장/퇴장 기능에 모아둘 수 있다.
FSM은 업격하게 제한된 구조를 강제하기 때문에, 인공지능 같이 복잡한 곳에 적용하기엔 어렵다. 이를 해결할 수 있는 방법들 소개한다.
주인공은 총을 든 상태에서 달리기/점프/엎드리기 동작을 모두 할 수 있으면서 총도 쏠 수 있다.
FSM을 고수함다면, 모든 상태(달리기/점프/엎드리기)를 무장/비무장에 맞춰 두 개씩 만들어야 한다. (여기에 무기의 종류가 총 만이 아니라면...)
class Heroine {
// ...
private:
HeroineState* state_;
HeroineState* equipment_;
};
void Heroine::handleInput(Input input) {
state_->handleInput(*this, input);
equipment_->handleInput(*this, input);
}
이제 각각의 상태 기계는 독립적으로 상태를 변경할 수 있다.
만야 점프 도중에는 총을 쏘지 못하는 등 복수의 상태 기계가 상호작용하는 상황이 있다면, 어떤 상태 코드에서 다른 상태 기계의 상태가 무엇인지 검사하는 지저분한 코드로 해결할 수 있다...
서기, 걷기, 달리기 등 비슷한 상태들에서는 모두 B 버튼을 누르면 점프하고 싶다.
단순 FSM에서는 이 코드를 모든 비슷한 상태들에 중복해서 넣어야 하지만,
'땅 위에 있는' 상태 클래스를 정의해 서기, 걷기, 달리기가 이를 상속받는다면 한 번으로 해결 가능하다.
이러한 구조를 계층형 상태 기계라 한다. 이때 '땅 위에 있는' 상태는 상위 상태 이고 서기, 걷기, 달리기는 하위 상태다.
하위 상태가 handleInput
을 오버라이드하여 어떤 입력을 하위 상태가 처리하지 않는다면, 상위 상태가 처리한다.
FSM에는 이력 개념이 없다. (직전 상태가 무엇인지 알 수 없어, 직전 상태로 돌아갈 수 없다.)
이는 상태를 FSM처럼 포인터로 관리하지 않고, 스택으로 관리하여 해결하는 방법이 푸시다운 오토마타이다.