State
객체의 내부 상태에 따라 스스로 행동을 변경할 수 있게 허가하는 패턴, 객체는 마치 자신의 클래스를 바꾸는 것처럼 보임.
- 유한 상태 기계(FSM), 계층형 상태 기계, 푸시다운 오토마타 또한 다룸.
We’ve All Been There
- 간단한 횡스크롤 플랫포머를 만든다고 가정.
- 주인공이 사용자 입력에 따라 반응하도록 구현해야한다.
무작정 구현하면 코드는 복잡해진다.
- B버튼을 누르면 점프하는 코드는 다음과 같다.
1
2
3
4
5
6
7
8
void Heroine::handleInput(Input input)
{
if (input == PRESS_B)
{
yVelocity_ = JUMP_VELOCITY;
setGraphics(IMAGE_JUMP);
}
}
- 이 코드는 ‘공중 점프’를 허용한다.(계속 공중에 떠 있을 수 있다.)
isJumping_
필드를 추가하면 간단히 고칠 수 있다. (토글할 필요가 있다)
1
2
3
4
5
6
7
8
9
10
11
void Heroine::handleInput(Input input)
{
if (input == PRESS_B)
{
if (!isJumping_)
{
isJumping_ = true;
// Jump...
}
}
}
- 주인공이 땅에 있을 때 아래 버튼을 누르면 엎드리고, 버튼을 떼면 다시 일어서는 기능은 다음과 같을 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void Heroine::handleInput(Input input)
{
if (input == PRESS_B)
{
// Jump if not jumping...
}
else if (input == PRESS_DOWN)
{
if (!isJumping_)
{
setGraphics(IMAGE_DUCK);
}
}
else if (input == RELEASE_DOWN)
{
setGraphics(IMAGE_STAND);
}
}
- 이 코드의 버그
- 엎드리기 위해 아래 버튼을 누른 뒤, B버튼을 눌러 엎드린 상태에서 점프하고나서 공중에서 아래버튼을 떼면, 점프 중에 땅에 서 있는 모습으로 보임.
- 플레그 변수가 더 필요하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void Heroine::handleInput(Input input)
{
if (input == PRESS_B)
{
if (!isJumping_ && !isDucking_)
{
// Jump...
}
}
else if (input == PRESS_DOWN)
{
if (!isJumping_)
{
isDucking_ = true;
setGraphics(IMAGE_DUCK);
}
}
else if (input == RELEASE_DOWN)
{
if (isDucking_)
{
isDucking_ = false;
setGraphics(IMAGE_STAND);
}
}
}
- 점프 중, 아래 버튼을 눌러 내려찍기 공격을 할 수 있게 하는 코드는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void Heroine::handleInput(Input input)
{
if (input == PRESS_B)
{
if (!isJumping_ && !isDucking_)
{
// Jump...
}
}
else if (input == PRESS_DOWN)
{
if (!isJumping_)
{
isDucking_ = true;
setGraphics(IMAGE_DUCK);
}
else
{
isJumping_ = false;
setGraphics(IMAGE_DIVE);
}
}
else if (input == RELEASE_DOWN)
{
if (isDucking_)
{
// Stand...
}
}
}
- 이번에도 버그가 생긴다. - 또 플래그 변수를 넣어야한다.
- 이런 식으로 코드를 건드리면 계속해서 망가진다.
좋은 개발자는 어떤 코드가 버그가 생기기 쉬운지에 대한 감각이 있다. 분기가 복잡하거나 상태가 변경 가능한 코드들은 버그가 쉽게 생긴다.
Finite State Machines to the Rescue
- 위 에서 다룬 동작들을 플로차트로 그려보면 다음과 같다.
- 위와 같은 플로차트를 유한 상태 기계(FSM)이라 한다.
FSM은 오토마타 이론에서 나왔다.
오토마타 중 튜링 기계가 유명하다.
핵심은 상태, 입력, 전이
- 가질 수 있는 ‘상태’가 한정: 서기, 점프, 엎드리기, 내려찍기
- 한번에 ‘한가지’ 상태만 될 수 있다.: 주인공은 점프와 동시에 서있을 수 없다. 동시에 두가지 상태가 되지 못하도록 막는게 FSM을 쓰는 이유 중 하나.
- ‘입력’이나 ‘이벤트’가 기계에 전달: 버튼 누르기와 버튼 떼기.
- 각 상태에는 입력에 따라 다음 상태로 바뀌는 ‘전이’가 있다.: 입력이 들어왔을 때, 현재 상태에 해당하는 전이가 있다면 전이가 가리키는 다음 상태로 변경된다.
서 있는 동안 아래 버튼을 누르면 엎드리기 상태로 전이한다.
- 현재 상태에서 들어온 입력에 대한 전이가 없을 경우 입력을 무시한다.
Enums and Switches
위에서 다룬 Heroin 클래스의 문제점은, 불리언 변수 값 조합이 유효하지 않을 수 있다는 점이다.
- 점프와 엎드리기 동시에 참 불가.
여러 플래그 변수 중 하나만 참일 경우 열거형(enum)을 사용하는 것이 좋다.
열거형 사용
1
2
3
4
5
6
7
enum State
{
STATE_STANDING,
STATE_JUMPING,
STATE_DUCKING,
STATE_DIVING
};
- 먼저 상태에 따라 분기하게 했다.
- 이제 플래그 여러개 대신 state_ 필드 하나만 있어도 됨.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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;
}
}
분기 문을 다 없애진 못했지만, 업데이트해야 할 상태변수를 하나로 줄였고, 하나의 상태를 관리하는 코드는 한곳에 있다.
- 열거형은 상태 기계를 구현하는 가장 간단한 방법이다.
열거형만으로 부족할 수 있다.
이동을 구현하고, 엎드려있으면 기가 모여, 놓는 순간 특수 공격을 쏠 수 있게 만들면, 엎드려서 기를 모으는 시간 또한 기록해야한다.
- 이와 관련 있는 패턴:
매서드 패턴
- 이와 관련 있는 패턴:
chargeTime 추가
- 이를 위해 Heroin에 chargeTime_필드 추가
1
2
3
4
5
6
7
8
9
10
11
void Heroine::update()
{
if (state_ == STATE_DUCKING)
{
chargeTime_++;
if (chargeTime_ > MAX_CHARGE)
{
superBomb();
}
}
}
- 엎드릴 때마다 시간을 초기화해야하니
handleInput()
또한 수정해야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void Heroine::handleInput(Input input)
{
switch (state_)
{
case STATE_STANDING:
if (input == PRESS_DOWN)
{
state_ = STATE_DUCKING;
chargeTime_ = 0;
setGraphics(IMAGE_DUCK);
}
// Handle other inputs...
break;
// Other states...
}
}
- 기 모으기 공격을 추가하기 위해 함수 두 개를 수정하고
chargeTime_
을 추가했다.
- 이렇게 chargeTime_을 추가하는 것은 좋지 않다.
- 모든 코드와 데이터를 한곳에 모아둘 수 있는 게 낫다.
The State Pattern
- 모든 분기문을 동적 디스패치(C++에서는 가상함수)로 바꾸려 하는 것은 과하다.
- 때로는 if문으로 충분
- 하지만 위의 예제라면, 객체지향,
상태 패턴
을 사용하는게 좋다.
A state interface
- 다중 선택문에 있던 동작을 인터페이스의 가상 메서드로.
1
2
3
4
5
6
7
8
class HeroineState
{
public:
virtual ~HeroineState() {}
virtual void handleInput(Heroine& heroine, Input input) {}
virtual void update(Heroine& heroine) {}
};
Classes for each state
- 상태별로 인터페이스를 구현하는 클래스 정의.
- 메서드: 어떤 행동을 하는지 정의.
- case 별로 클래스를 만든다.
- chargeTime_같은 경우 더 분명하게 보여준다.(엎드린 상태에서만 유의미)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class DuckingState : public HeroineState
{
public:
DuckingState()
: chargeTime_(0)
{}
virtual void handleInput(Heroine& heroine, Input input) {
if (input == RELEASE_DOWN)
{
// Change to standing state...
heroine.setGraphics(IMAGE_STAND);
}
}
virtual void update(Heroine& heroine) {
chargeTime_++;
if (chargeTime_ > MAX_CHARGE)
{
heroine.superBomb();
}
}
private:
int chargeTime_;
};
Delegate to the state
- 주인공 클래스에 현재 상태 객체 포인터 추가, 상태 객체에 위임(다중 선택문 제거)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Heroine
{
public:
virtual void handleInput(Input input)
{
state_->handleInput(*this, input);
}
virtual void update()
{
state_->update(*this);
}
// Other methods...
private:
HeroineState* state_;
};
- 상태 변경 == state_에 다른 객체 할당
전략패턴, 타입 객체 패턴과 비슷, 하위 객체에 동작을 위임하지만 의도에서 차이.(전략 == 일부 동작으로 부터 디커플링, 타입객체 == 같은 타입 객체 레퍼 공유, 상태 == 동작 변경)
Where Are the State Objects?
- 상태를 변경: state_에 새로운 상태 객체 할당.
- 실제 인스턴스가 필요
Static states
- 상태 객체에 필드가 따로 없으면, 인스턴스는 하나만 있으면됨.
- 여러 FSM이 동시에 돌더라도 상태 기계는 다 같음.
필드가 없고, 메서드도 하나라면, 정적함수 사용가능. (state_는 함수 포인터)
1
2
3
4
5
6
7
8
9
10
class HeroineState
{
public:
static StandingState standing;
static DuckingState ducking;
static JumpingState jumping;
static DivingState diving;
// Other code...
};
1
2
3
4
5
if (input == PRESS_B)
{
heroine.state_ = &HeroineState::jumping;
heroine.setGraphics(IMAGE_JUMP);
}
Instantiated states
정적 객체만으로 부족할 때
chargeTime_
필드가 캐릭마다 다를 경우
전이할 때마다 상태 객체 생성
- FSM이 상태별로 인스턴스를 가짐.
- 새로 할당하는 것 == 이전 상태 해제
1
2
3
4
5
6
7
8
9
void Heroine::handleInput(Input input)
{
HeroineState* state = state_->handleInput(*this, input);
if (state != NULL)
{
delete state_;
state_ = state;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
HeroineState* StandingState::handleInput(Heroine& heroine,
Input input)
{
if (input == PRESS_DOWN)
{
// Other code...
return new DuckingState();
}
// Stay in this state.
return NULL;
}
- 저자는 매번 상태 객체를 할당하기 위해 메모리와 CPU를 낭비하지 않아도 되는 정적 상태를 쓰는 편.
- 동적할당 메모리 단편화 => 오브젝트 풀 패턴
Enter and Exit Actions
- 상태 패턴의 목표: 같은 상태에 대한 모든 동작과 데이터를 클래스 하나에 캡슐화하는 것
1
2
3
4
5
6
7
8
9
10
11
HeroineState* DuckingState::handleInput(Heroine& heroine,
Input input)
{
if (input == RELEASE_DOWN)
{
heroine.setGraphics(IMAGE_STAND);
return new StandingState();
}
// Other code...
}
- 입장 기능을 추가하여 그래픽을 제어하도록할 수 있다.
1
2
3
4
5
6
7
8
9
10
class StandingState : public HeroineState
{
public:
virtual void enter(Heroine& heroine)
{
heroine.setGraphics(IMAGE_STAND);
}
// Other code...
};
- 주인공 클래스에서 새로운 상태에 들어 있는 enter함수를 호출하도록 상태 변경 코드를 수정.
1
2
3
4
5
6
7
8
9
10
11
12
void Heroine::handleInput(Input input)
{
HeroineState* state = state_->handleInput(*this, input);
if (state != NULL)
{
delete state_;
state_ = state;
// Call the enter action on the new state.
state_->enter(*this);
}
}
- 이제 처음 코드를 다음과 같이 단순화할 수 있다.
1
2
3
4
5
6
7
8
9
10
HeroineState* DuckingState::handleInput(Heroine& heroine,
Input input)
{
if (input == RELEASE_DOWN)
{
return new StandingState();
}
// Other code...
}
주인공 클래스에서 상태를 변경하기만 하면 그 상태로 그래픽을 처리할 수 있다.
- 실제로는, 상태그래프에는 동일한 상태로 여러 번 전환된다.
- 이 때 이전에는 전이시 모든 곳에 중복코드를 넣지만
enter
를 통해 그를 최소화한다.
- 상태가 새로운 상태로 교체되기 직전에 호출되는
퇴장
또한 이렇게 구현 가능하다.
What’s the Catch?
FSM의 장점은 동시에 단점
상태 기계는 엄격하게 제한된 구조를 강제함
- 복잡하고 얽힌 코드 정리
- FSM에는 미리 정해높은 상태와 현상태만 하드코딩되어 있는 전이만 존재
FSM: 튜링완전하지 않음. 오토마타 이론은 추상 모델을 이용해 더 복잡한 문제를 계산한다.
튜링완전: 시스템이 튜링 기계를 구현할 수 있을 정도로 충분히 강력하다.
Concurrent State Machines
- 총을 들 수 있게 만든다고 가정.
- 이 때 구현한 모든 동작이 돌아가게 해야하고 총도 쏠 수 있어야한다.
FSM은 이 때 무장, 비무장으로 두 개 만들어야한다.
- 무기 추가 => 계속 늘어남.
- 해결: 상태기계를 둘로 나눔.
- 무엇을 들고 있는가에 대한 상태 기계를 따로 정의.
- 주인공은 이를 각각 참조.
1
2
3
4
5
6
7
8
class Heroine
{
// Other code...
private:
HeroineState* state_;
HeroineState* equipment_;
};
- 입력을 상태에 위임할 때 양쪽에 전달
- 첫 상태에서 입력을 씹어서 다음 상태 기계까지 입력이 가지 않도록 가능
1
2
3
4
5
void Heroine::handleInput(Input input)
{
state_->handleInput(*this, input);
equipment_->handleInput(*this, input);
}
- 각각의 상태가 서로 전혀 연관이 없으면 이 방법은 잘 들어맞는다.
- 여러 상태기계가 상호작용할 경우 바람직한 방법은 아니지만, 문제 해결가능
Hierarchical State Machines
- 땅위에 있는 상태 등을 상속받아, 고유 동작 처리
- 계층형 상태 기계
- 상속받은 메서드를 오버라이드하는 것과 같음.
상속: 두 코드가 강하게 커플링
- 상속받은 메서드를 오버라이드하는 것과 같음.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class OnGroundState : public HeroineState
{
public:
virtual void handleInput(Heroine& heroine, Input input)
{
if (input == PRESS_B)
{
// Jump...
}
else if (input == PRESS_DOWN)
{
// Duck...
}
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class DuckingState : public OnGroundState
{
public:
virtual void handleInput(Heroine& heroine, Input input)
{
if (input == RELEASE_DOWN)
{
// Stand up...
}
else
{
// Didn't handle input, so walk up hierarchy.
OnGroundState::handleInput(heroine, input);
}
}
};
- 메인 클래스에 상태를 하나만 두지 않고 상태 스택을 만들어 명시적으로 현재 상태의 상위 상태 연쇄를 모델링 할 수 있다.
- 현재 상태가 스택 최상위인 형태.
- 스택 위에서부터 아래로.
Pushdown Automata
상태 스택을 활용하여 FSM을 확장하는 다른 방법
FSM에는 히스토리 개념이 없다.
- 직전 상태가 무엇인지 몰라 쉽게 돌아갈 수 없음.
- 어느 상태에서 공격을 했는지?
- 이전 상태로 되돌아가야함.
- 공격하기 이전의 상태를 저장하고 불러오는게 좋다.
- 이 때 푸시다운 오토마타를 이용한다.
푸시다운 오토마타: 상태를 스택으로 관리
- FSM은 이전 상태를 덮어쓰고 새로운 상태로 전이하는 방식.
- 부가적인 명령어 두가지
- 새로운 상태를 스택에
push
: 스택의 최상위 == 현재, 이전 상태는 그 밑에. - 최상위 스택을
pop
: 빠진 상태는 제거, 바로 밑 상태가 현재
- 새로운 상태를 스택에
- 총소기 상태를 구현할 때.
- 어떤 상태든 발사버튼 => 총쏘기 스택에 넣
- 총 쏘기 애니메이션 끝 => 스택에서 뺌
- 이전 상태가 됨.
So How Useful Are They?
- FSM의 한계
- 게임 AI에서는 행동트리(behavior tree)나 계획 시스템(planning system)을 더 많이 쓰는 추세.
- 사용할만한 곳
- 내부 상태에 따라 객체 동작이 바뀔 때
- 이런 상태가 그다지 많지 않은 선택지로 분명하게 구분될 때
- 객체가 입력이나 이벤트에 따라 반응할 때
- 입력 처리, 메뉴 화면 전환, 문자 해석, 네트워크 프로토콜, 비동기 동작을 구현하는데 사용