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

 

7.2 유한 상태 기계의 일종인 플로 차트를 그리자

유니티의 애니메이터 컨트롤러는 상태 머신을 사용하여 애니메이션 클립 전환을 관리한다.

FSM의 특징

1. 가질 수 있는 상태가 한정된다. (점프, 엎드리기, 내려찍기 등)

2. 한 번에 한 가지 상태만 가질 수 있다. (점프와 동시에 엎드리기는 안된다.)

3. 입력이나 이벤트가 기계에 전달된다. (버튼 누르기와 버튼 떼기와 같은 입력)

4. 각 상태에는 입력에 따라 다음 상태로 바뀌는 전이(조건)가 있다. (현재 상태에 전이 조건이 없으면 어느 입력이 들어와도 무시한다.)

상태 패턴은 FSM을 구현하는 방법 중 하나이다.

 

7.3 상태 패턴을 구현 하는 방법 - 열거형과 다중 선택문

enum State{

  STATE_JUMPING,

  STATE_DUCKING,

}

입력이나 이벤트를 받는 부분에서는 상태에 따라 분기한 뒤에 입력에 따라 분기한다.

 

열거형만으로 기능을 충분히 구현할 수 있는 경우는 굳이 인터페이스를 쓰지 않아도 된다.

상태를 바꾸는데 기록해야 될 것들이 많고 복잡해지면 열거형 보다는 상태 별로 클래스를 정의하여 구현하는 방법이 있다.

7.4 인터페이스를 사용하여 구현하는 상태 패턴

1. 상태 인터페이스를 정의한다.

STATE_XXXXX를 사용하는 모든 코드 ( 상태에 의존하는 코드들, 예제에서는 handleInput()과 update() )들을 인터페이스의 가상 메서드로 만든다.

class HeroineState { // 상태 인터페이스
public:
  virtual ~HeroineState() {} 
  virtual void handleInput(Heroine& heroine, Input, input) {}
  virtual void update(Heroine& heroine) {}
};

2. 상태별 클래스를 만든다. (상태 인터페이스를 구현)

class JumpingState : public HeroineState {
public:
  JumpingState() : {}
  
  virtual void handleInput(Heroine& heroine, Input input){
    if(input == PRESS_DOWN) {
      heroine.setGraphics(IMAGE_DIVE);
      // 내려찍기 상태로 전환
  }
  
  virtual void update(Heroine& heroine){
    // jumpTime_을 update에서 감소시키고, jumpTime_이 0 보다 작아지면
    // heroine.state_에 StandingState를 대입하여 상태 전이를 구현할 수 있을 것이다.
  }
  
  private:
    int jumpTime_; // 점프 상태에서만 의미 있는 변수 (체공 시간?)
};

3. 대상(캐릭터)에 상태 객체 포인터를 추가한다.

class Heroine{
public:
  virtual void handleInput(Input input){
    state_->handleInput(*this, input);
  }
  
  virtual void update() { state_->update(*this); }
  
private:
  HeroineState* state_; // 상태 객체 포인터
};

ㄴ 상태를 바꾸려면 이 포인터에 상태별 클래스 객체를 할당하면 된다.

 

7.5 상태별 클래스 객체를 유지하는 두 가지 방법

1. 정적 객체

일반적으로 상위 상태 클래스에 정적 상태별 클래스 객체를 만들어 둔다.

 

2. 상태 객체 만들기

캐릭터 마다 점프력이 달라 체공시간을 다르게 두어야 할 수도 있기 때문에 정적 객체만으로 부족한 경우 사용하는 방법이다. 이 경우에는 상태를 전이할 때마다 상태 객체를 만들어야 한다.

이 방법은 매번 상태 객체를 할당하기 위해 new 키워드를 사용하여 메모리와 CPU를 사용해야 한다.

상태를 동적으로 할당하면서 메모리 단편화가 생길 수 있다.

void Heroine::handleInput(Input input) {
// 상태가 바뀌는 경우(상태를 바꾸는 입력이 들어온 경우) NULL이 아닌 상태 객체를 반환한다.
  HeroineState *state = state_->handleInput(*this, input); 
  if (state != NULL) {
    delete state_; // 기존 상태를 해제하고 새로운 상태를 가리키도록 한다.
    state_ = state;
  }
}


HeroineState* JumpingState::handleInput(Heroine& heroine, Input input) {
  if(input == PRESS_DOWN) {
    return new DivingState();
  }
  return NULL;
}

 

7.6 상태의 입장과 퇴장 함수

현재 상태에서 다음 상태로의 전이가 일어날 때 (state를 대입할 때) 다음 상태의 설정을 초기화 하는 것은 어떻게 보면 현재 상태에서 다음 상태를 설정하는 것이다.

 

상태 객체에 입장 함수 enter를 정의하여 설정을 초기화하는 코드를 넣고,  전이가 일어난 이후 enter를 호출하도록 하면

상태 객체는 캡슐화 되었다고 할 수 있다.

 

7.7 단점

인공지능같이 복잡한 곳에 적용하기 힘들다.

상태 전이 구조를 바꾸기 어렵다.

 

7.8 병행 상태 기계

두 종류의 상태를 유지해야 하는 경우, 두 상태 기계가 서로 전혀 연관이 없다면 이 방법을 사용할 수 있다.

class Heroine {
private:
  HeroineState* state_;
  HeroineState* equipment_;
};

7.9 계층형 상태 기계

서로 다른 여러 가지 상태에서 동일한 입력으로 하나의 상태로 전이되는 경우 단순하게 상태 기계를 구현한다면 상태 전이 코드를 상태마다 중복해 넣어야 한다.

 

계층형 상태 기계

상태를 상위층과 하위층으로 나누고, 하위 상태는 상위 상태를 상속받는 개념이다. 클래스 상속으로 구현할 수 있다.

 

7.10 푸시다운 오토마타

FSM에는 이력 개념(현재 상태는 알 수 있지만 직전 상태가 무엇인지를 따로 저장하지 않음)이 없기 때문에 이전 상태로 쉽게 돌아갈 수 없다.

예를 들면 서있는 상태나 엎드려있는 상태에서 총을 쏘면 FiringState로 전이하고 다시 원래의 상태로 돌아가야 한다고 하면 이전에 엎드려있는 상태였다면 엎드려있는 상태로, 서 있는 상태라면 서 있는 상태로 되돌아가야 한다.

 

푸시다운 오토마타에서는 상태를 스택으로 관리한다.

class Heroine {
public 
  void pushState(HeroineState* state) {}; // s에 새로운 상태를 넣는다.
  void popState() {}; // s에서 최상위 상태(현재 상태)를 빼는 함수를 정의해야 한다.
  						// 수행하면 이전 상태가 현재 상태가 된다.

private:
  // HeroineState* state_;
  // HeroineState* equipment_;
  
  std::stack<HeroineState*> s;
};

+ Recent posts