상태 패턴 (State Pattern)
상태패턴 : 객체의 내부 상태에 따라 스스로 행동을 변경 할 수 있게 허가하는 패턴으로, 이렇게 하면 객체는 마치 자신의 클래스를 바꾸는 것 처럼 보입니다.
언리얼 엔진에서 ai 나 블루프린트로 애니메이션을 구현 해 본적이 있다면 본인도 모르게 상태패턴을 사용 해왔을 수도 있다.
상태패턴을 설명하기 위해서는 유한 상태기계(FSM)을 언급 할 수 밖에 없다.
1
2
3
4
5
6
7
|
void Heroine::handleInput(Input input) {
if(input == PRESS_B) {
yVelocity = JUMP_VELOCITY;
setGrapthics(IMAGE_JUMP);
}
}
|
cs |
간단한 점프 기능을 만든다고 생각하자.
지금 코드에는 점프중인지 확인하는 코드가 없어
무한점프를 할 것이다.
1
2
3
4
5
6
7
8
9
|
void Heroine::handleInput(Input input) {
if(input == PRESS_B) {
if(!isJumping_) {
isJumping_ = true;
// more...
}
}
}
|
cs |
ㅇ
1
2
3
4
5
6
7
8
9
10
11
12
|
void Heroine::handleInput(Input input) {
if(input == PRESS_B) {
// 점프 중이 아니라면 점프한다.
} else if(input == PRESS_DOWN) {
if(!isJumping_) {
setGraphics(IMAGE_DUCK);
}
} else if(input == RELEASE_DOWN) {
setGraphics(IMAGE_STAND);
}
}
|
cs |
이를 해결하기 위해 점프중일 때 isjumping을 true로 바꾸고
점프중이 아닐 때 엎드리는 기능도 추가하였다.
하지만 이번에도 버그가 있다.
- 엎드리기 위해 아래 버튼을 누른 뒤
- B 버튼을 눌러 엎드린 상태에서 점프하고 나서
- 공중에서 아래 버튼을 때면
점프 중인데도 땅에 서 있는 모습으로 보인다.
게임 내에는 많은 이동들을 지원한다.
하지만 이런식으로 코딩하다가는 버그가 엄청 생기고이동 관련 코드 자체가 매우 복잡해질것이다.또 새로운 이동 관련 로직을 추가한다면엄청난 버그가 발생 할 것이다.이를 해결하기 위해 유한상태기계(FSM) 을 도입해보자.
FSM은 컴퓨터 과학 분야 중의 하나인 오토마타 이론에서 나왔다.
요점은 이렇다.
- 가질 수 있는 '상태'가 한정된다.
- 한 번에 '한 가지' 상태만 될 수 있다.
- '입력'이나 '이벤트'가 기계에 전달된다.
- 각 상태에는 입력에 따라 다음 상태로 바뀌는 '전이'가 있다.
예를 들어 서 있는 동안 B를 누르면 점프로 전환되고 점프 상태일 때 아래 키를 누르면 내려찍기로 전환된다.
1
2
3
4
5
6
7
|
enum State {
STATE_STANDING,
STATE_JUMPING,
STATE_DUCKING,
STATE_DIVING
};
|
cs |
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
36
|
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;
}
}
|
cs |
Geroine 클래스에서 플래그 변수 여러개 대신 state필드 하나만 두었다.
업데이트 해야 할 상태 변수를 줄였다.
열거형은 상태 기계를 구현하는 가장 간단한 방법이다.
하지만 위의 클래스 같은 경우는 상태 패턴을 쓰는것이 낫다.
상태 인터페이스
상태 인터페이스부터 정의하자. 상태에 의존하는 모든 코드, 즉 다중 선택문에 있던 동작을 인터페이스의 가상 메서드로 만든다. 예제에서는 handleInput() 과 update() 가 해당된다.
1
2
3
4
5
6
7
|
class HeroineState {
public:
virtual ~HeroineState() {}
virtual void handleInput(Heroien& heroine, Input input) {}
virtual void update(Heroien& heroine) {}
};
|
cs |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
class DuckingState : public HeroineState {
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();
}
private:
int chargeTime_;
}
|
cs |
1
2
3
4
5
6
7
8
9
|
class HeroineState {
public:
static StandingState standing;
static DuckingState ducking;
static JumpingState jumping;
static DivingState diving;
// More...
};
|
cs |
이러한 상태 객체를 정적 인스턴스 원하는 곳에 두면 되다.
현재는 heroine클래스 즉 캐릭터의 state를 관리하는 HeroineState클래스를 두어
캐릭터의 상태를 두었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
void Heroine::handleInput(Input input) {
HeroineState* state = sate_->handleInput(*this, input);
if(state != NULL) {
delete state_;
state_ = state;
}
}
HeroineState* StandingState::handleInput(
Heroine& heroine, Input input) {
if(input == PRESS_DOWN) {
// 다른 코드들...
return new DuckingState();
}
// 지금 상태를 유지한다.
return NULL;
}
|
cs |
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
HeroineState* DuckingState::handleInput(
Heroine& heroine, Input input) {
if(input == RELEASE_DOWN) {
heroine.setGraphics(IMAGE_STAND);
return new StandingState();
}
// More...
}
//이렇게 하는 것보다는 상태에서 그래픽까지 제어하는 게 바람직하다. 이를 위해 입장 기능을 추가하자.
class StandingState : public HeroineState {
public:
virtual void enter(Heroine& heroine) {
heroine.setGraphics(IMAGE_STAND);
}
// 다른 코드들...
};
// Heroine 클래스에서는 새로운 상태에서 들어 있는 enter 함수를 호출하도록 상태 변경 코드를 수정한다.
void Heroine::handleInput(Input input) {
HeroineState* state = state_->handleInput(*this, input);
if(state != NULL) {
delete state_;
state_ = state;
// 새로운 상태의 입장 함수를 호출한다.
state_->enter(*this);
}
}
HeroineState* DuckingState::handleInput(
Heroine& heroine, Input input) {
if(input == RELEASE_DOWN) {
return new StandingState();
}
// More...
}
|
cs |
FSM을 확정하여 상태 스택을 만들어 활용하는 방법도 있다.
FSM은 이전 상태가 무엇인지 이력 개념이 없다는것이 문제가 된다. 현재 상태는 알 수 있지만 이전 상태는 알기 힘들다는 것이다.
총 쏘기를 구현 할 때 총을 쏜 후 어느 상태로 되돌아가야하는가.
이럴 때 써먹을만 한 것으로 푸시다운 오토마타가 있다.
FSM이 한 개의 상태를 포인터로 관리했다면 푸시다운 오토마타는 상태를 스택으로 관리하는 것이다.
서있다가 발사를 하게되면 발사 이후에 pop하여 다시 서기상태로 되돌아 가는 것이다.
이전 상태를 쉽게 저장 할 수 있다.
FSM에는 몇가지 확장판이 나와있지만 FSM만으로는 한계가 있다. 요즘 게임 AI는 행동 트리나 계획 시스템을 더 많이 쓰는 추세다.
FSM은 다음 경우에 사용하면 좋다.
- 내부 상태에 따라 객체 동작이 바뀔 때
- 이런 상태가 그다지 많지 않은 선택지로 분명하게 구분될 수 있을 때
- 객체가 입력이나 이벤트에 따라 반응할 때
게임에서는 FSM이 AI 말고도 입력 처리나 메뉴 화면 전환, 문자 해석, 네트워크 프로토콜, 비동기 동작을 구현하는 데에도 많이 사용되고 있다.
필자의 경우에 언리얼에서 애니메이션 블루프린트를 이용해 캐릭터 상태에 따라 애니메이션을 관리했다.
노드의 시작은 entry로 시작되어 아무 행동도 하지 않을때의 기본동작은 IDLE 모션이다.
이후 idle에서 jog_start로 애니메이션을 전환하는 기준은 속도값이 0이고(이동하지않았을 때)
그리고 이동하지 않더라도 점프중이 아니어야한다.
매 프레임마다 캐릭터의 Speed와 캐릭터가 낙하중인지의 체크하고
IsInAir에 bool값을 저장한다 결과적으로 블루프린트에서 이 값을 불러와 다음과 시각화 코딩하였다.
위 조건들이 만족하면 다음 노드의 애니메이션으로 연결되어 모션을 실행하게 된다.
Jog start는 달리기를 시작하는 애니메이션이다.
Jog Start에서 본격적인 달리기 모션인 Run으로 이동하는 노드와 JogStop으로 이동하는 노드
총 2가지 선택지가 있다.
Jog_Start -> Run 조건 : 달리기 시작 애니메이션이 끝나면 자동재생
Jog Start -> JogStop 조건:
가속중이지 않을 때. 즉 이동을 시작하여 jog Start 애니메이션이 진행중일때 멈추면 본격적인 run 애니메이션이 진행되지 않고 바로 stop애니메이션이 진행됨.
Jog Stop -> Jog Start 조건 :
가속중일 때. jog Stop 애니메이션이 진행중 다시 이동을 시작할 때 기본 idle 애니메이션으로 넘어가지않고 다시 jog start애니메이션으로 이동
이런 식으로 캐릭터 상태에 따라
다른 애니메이션을 출력하고 싶다면
FSM방식이 가독성도 좋고 버그도 적게 생긴다