[디자인 패턴] #6_상태 패턴, State Pattern
게임 디자인 패턴 중 "상태 패턴"에 대해 알아보겠습니다.
"게임 프로그래밍 패턴"의 7 항목, "상태"에 해당하는 내용입니다.
유한 상태 기계(FSM)
객체의 내부 상태에 따라 스스로 행동을 변경할 수 있게 허가하는 패턴으로,
이렇게 하면 객체는 마치 자신의 클래스를 바꾸는 것처럼 보입니다.
1. FSM, 유한 상태 기계
유한 상태 기계는 입력, 상태, 그리고 전이 (Transition)로 구성된 수학적 모델입니다.
FSM = 입력 + 상태 + 전이
주요 포인트는 아래와 같습니다.
1. 가질 수 있는 "상태"가 한정적입니다.
2. 한 번에 한 가지 "상태"만 허용됩니다. 예를 들면, 서 있는 동시에 점프할 수 없습니다!
3. "입력" 혹은 "이벤트"가 상태 기계에 전달됩니다. 예를 들면 버튼 누르기 혹은 떼기에 해당합니다.
4. 각 상태는 상태 기계에 전달받은 "입력"에 따라 다음 상태로 "전이"됩니다!
주로 아래와 같은 상황에서 사용하기 좋습니다.
1. 내부 상태에 따라 객체 동작이 바뀔 때
2. 상태 종류가 그다지 많지 않고, 각 선택지가 분명할 때
3. 객체가 "입력" 혹은 "이벤트"에 반응할 때
2. "FSM(유한 상태 기계)" 작성
먼저, Player의 상태 변화를 플로우 차트로 작성해봅시다!
1. 기본 상태는 "서기" 동작입니다. 어떠한 입력도 없는 상태에서 "Player"는 "서기" 상태입니다.
2. "↓" 입력을 누른 상태는 "엎드리기" 동작을 수행하고, 떼는 순간 "서기" 동작으로 돌아갑니다.
3. "B" 입력을 누르면 "점프" 동작을 수행하고, 점프 중에 "↓" 입력을 누르면 "내려찍기" 동작을 수행합니다.
열거형 사용
1. FSM의 "상태"들을 열거형을 통해 정의
enum State {
STATE_STANDING,
STATE_JUMPING,
STATE_DUCKING,
STATE_DIVING
};
FSM의 "상태"들을 정의하기 위해 우리는 "열거형"을 활용할 수 있습니다!
2. 객체 내부의 "상태" 관련 코드 작성
void Player::HandleInput(Input _input)
{
switch (state)
{
case STATE_STANDING:
if (_input == PRESS_B)
{
state = STATE_JUMPING;
velocity = JUMP_VELOCITY;
// ...
}
else if (_input == PRESS_DOWN)
{
state = STATE_DUCKING;
// ...
}
break;
case STATE_JUMPING:
if (_input == PRESS_DONW)
{
state = STATE_DIVING;
// ...
}
break;
case STATE_DUCKING:
if (_input == RELEASE_DOWN)
{
state = STATE_STANDING;
// ...
}
break;
}
}
대부문의 경우에 "State Machine(상태 기계)"를 구현하는데 열거형으로 충분하다고 합니다.
다만, 이번 포스팅에서 주목하는 디자인 패턴이 "상태 패턴"인 만큼 위 예제 코드에 "상태 패턴"을 적용해보겠습니다!
상태 패턴
1. 상태 인터페이스 작성
class PlayerState
{
public:
virtual ~PlayerState();
virtual void HandleInput(Player& player, Input input);
virtual void Update(Player& player);
};
class DuckingState : public PlayerState
{
public:
DuckingState(): chargeTime(0){}
virtual void HandleInput(Player& plyaer, Input input)
{
if(input == RELEASE_DOWN)
{
// 상태를 "서기 상태"로 변경
// "서기" 동작 구현
}
}
virtual void Update(Player& player)
{
// 실시간 Update 내용
}
};
직전 "Player" 내부의 "상태"로부터 귀결된 코드들, 즉 "동작"을 구현하는 코드들을 상태 객체 내부에 가상 멤버 메서드로 선언합니다.
그리고, 각 상태 별 객체들을 작성하고 이들은 모두 "PlayerState" 클래스를 상속합니다.
쉽게 말해서 객체의 "동작"관련 코드를 "상태 객체"에 위임하는 것입니다!
2. Player 객체 내부의 "상태 객체 " 데이터 멤버
class Player
{
public:
virtual void HandleInput(Player& player, Input input)
{
state->HandleInput(*this, input);
}
virtual void Update(Player& player)
{
state->Update(*this);
}
private:
PlayerState* state; // Player가 갖는 상태 객체
};
"Player" 클래스 내부에 "PlayerState" 객체의 포인터 즉 상태 객체를 가리키는 포인터를 데이터 멤버로 가짐으로써, 바꾸려는 "상태 객체"를 할당하기만 하면 되겠죠!
* "상대 패턴"의 요점은 "동작"을 위임하는 객체 혹은 "상태 객체"를 변경함으로써 클래스의 동작을 변경하는 게 목표입니다.
상태 객체의 형태
1. 정적 객체
class PlayerState
{
public:
static StadingState standing;
static DuckingState ducking;
static JumpState jumping;
static DivingState diving;
// ...etc
};
// HandleInput() 내부..
if (input == PRESS_B)
{
Player.state = &PlayerState::jumping;
// "점프" 동작 구현
}
상태 클래스에 특별히 데이터 멤버도 없고 가상 멤버 함수만 존재한다면 더욱더 단순화해서 "상태 클래스"를 정적 인스턴스를 하나만 생성해서 같이 사용하면 됩니다!
쉽게 말해, "PlayerState" 객체 인스턴스만 생성하면 "PlayerState" 객체 내부에 존재하는 상태별 정적 객체들을 활용하면 되겠죠!
2. 전이 시 새로운 상태 객체 생성
void Player::HandleInput(Input input)
{
PlayerState* newState = state->HandleInput(*this, input);
// 이전 상태에서 새로운 상태로 "전이"가 발생하면?
if (newState != null)
{
// 전이가 발생하면, 이전의 상태 객체를 지웁니다!
delete state;
state = newState;
}
}
// StandingState 객체 내부의 HandleInput 메서드
PlayerState* StandingState::HandleInput(Player& player, Input input)
{
if (input == PRESS_DOWN)
{
// "엎드리기" 상태 구현
return new DuckingState();
}
// PRESS_DOWN 입력이 아닐 경우 현재 상태를 유지합니다.
return NULL;
}
Multi Player를 지원하는 게임의 경우 정적 객체만으로 부족할 수 있습니다.
이때, 우리는 "전이"할 때마다 새로운 "상태 객체"를 생성해야 합니다!
이렇게 되면, 각 "상태"별 인스턴스를 한 개씩 갖게 되겠죠.
메모리 할당이 빈번하게 발생하는 위와 같은 방법보다, 이전의 "정적 객체"를 활용하는 방안을 염두해보는 것이 좋겠네요...
입장과 퇴장
// 다음 "상태"와 관련된 Sprite를 이전의 "상태"에서 담당하고 있습니다!
PlayerState* DuckingState::HandleInput(Player& player, Input input)
{
// "엎드리기" 동작 중 버튼을 떼는 순간 "서기" 동작을 화면에 그려냅니다.
if (input == RELEASE_DOWN)
{
player.setSprite(STAND_IMAGE);
return new StadingState();
}
}
아직까지 Player의 스프라이트 변경을 현재 상태로 "전이"되기 이전의 상태 객체에서 수행하고 있습니다.
캡슐화를 위해 각각의 상태 객체가 해당되는 Sprite 변경을 담당하는 코드를 작성해보겠습니다.
1. 캡슐화를 위한 입장 코드
// "서기" 상태 클래스
class StandingState : PlayerState
{
public:
// Player의 Sprite를 변경하는 "입장 코드"
virtual void enter(Player& player)
{
player.setSprite(IMAGE_STAND);
}
// ...
};
// "Player" 클래스의 "HandleInput()" 멤버 메서드 정의
void Player::HandleInput(Input input)
{
PlayerState* newState = state->HandleInput(*this, input);
if (newState != null)
{
delete state;
state = newState;
// 입장 코드 호출
state->enter(*this);
}
}
먼저 각 "상태" 클래스 내부에 "enter()" 메서드를 정의합니다.
"Player" 클래스의 "HandleInput()" 메서드의 내부에서 "전이"가 발생하면, 이전의 상태 객체를 지우고, 새로운 객체를 생성하는 동시에 새로운 상태 객체의 입장 코드를 함께 호출해줍니다!
이로써, 이전 상태 객체가 새로운 상태 객체가 수행해야 할 동작을 수행하는 커플링을 해소합니다!
상태 기계의 단점
상태 기계는 제한된 구조를 강제함으로써, 코드의 가독성 그리고 캡슐화를 지원합니다.
다만, 이러한 장점들이 되려 단점이 되는 경우도 존재합니다.
1. 병행 상태 기계
class Player
{
public:
//...
private:
PlayerState* state;
PlayerState* equipment;
};
void Player::HandleInput(Input* input)
{
// 상태 객체와 무기 객체 모두에게 입력을 전달합니다.
state->HandleInput(*this, input);
equipment->HandleInput(*this, input);
}
만약, 우리가 구현할 캐릭터 인스턴스가 "무기"를 들고 있는 상태라면?
"서기" 동작과 별개로 "무기를 들고 서기"동작이 추가적으로 필요해집니다.
한 개의 "상태 기계"에 비 무장 상태 객체들과 무장 상태 객체들을 모두 때려 넣는 것보다,
두 개의 "상태 기계"를 활용하는 것을 "병행 상태 기계"라고 합니다!
2. 계층형 상태 기계
class OnGroundState : public PlayerState
{
public:
virtual void HandleInput(Player& player, Input, input)
{
if (input == PRESS_B)
{
// 점프 동작 구현
}
else if (input == PRESS_DOWN)
{
// 엎드리기 동작 구현
}
//...
}
};
class DuckingState : public OnGorundState
{
public:
virtual void HandleInput(Player& player, Input, input)
{
if (input == RELEASE_DOWN)
{
// 서기 동작 구현
}
else
{
// 따로 입력을 처리하지 않고, 상위 클래스로 위임합니다.
OnGroundState::HandleInput(player, input);
}
}
};
다음 방법은 "상속(객체 지향화)"을 통해 여러 상태가 공통된 상위 상태의 코드를 공유하는 방법입니다.
예를 들면, "서기", "걷기", "달리기", 그리고 "미끄러지기" 등의 상태들은 모두 "땅 위에 있는" 상태를 공유합니다.
결과적으로, 하위 상태들은 공통의 상위 상태를 갖고, 하위 상태들은 상위 상태를 상속받아 그들 고유의 세부 동작들을 추가하면 됩니다!
3. 푸시다운 오토마타(상태 스택)
"상태 스택"을 활용하여 "FSM(유한 상태 기계)"의 단점을 보완하는 방법도 존재합니다!
기존의 "FSM"은 "이력" 기능이 없습니다.
쉽게 말해, 현재 상태의 직전 상태는 기억하지 않고, 다시 돌아가기 힘들다는 단점이 있습니다.
1. 새로운 상태를 스택에 push 합니다. 스택의 최상위 상태는 "현재 상태"입니다.
2. 현재 상태의 동작 구현이 완료되면 pop 합니다. 스택의 최상위 상태는 "직전 상태"가 됩니다.
이때, "푸시 다운 오토마타"를 활용합니다!
"FSM"이 오직 한 개의 상태를 포인터로 관리했다면, "푸시다운 오토마타"는 상태를 "스택"으로 관리합니다.
'언어 > 디자인 패턴' 카테고리의 다른 글
[디자인 패턴]#8_게임 루프, Game Loop Pattern (0) | 2022.08.28 |
---|---|
[디자인 패턴]#7_이중 버퍼, Double Buffer (0) | 2022.08.21 |
[디자인 패턴]#5_싱글턴 패턴, Singleton Pattern (0) | 2022.08.02 |
[디자인 패턴]#4_프로토타입 패턴, Prototype Pattern (0) | 2022.07.22 |
[디자인 패턴]#3_관찰자 패턴, Observer Pattern (0) | 2022.07.15 |