게임 프로그래밍 패턴 : [6] 상태 패턴
유한 상태 기계(FSM)
- 가질 수 있는 상태가 한정된다.
- 한 번에 한 가지 상태만 될 수 있다.
- 입력이나 이벤트가 기계에 전달된다.
- 각 상태에는 입력에 따라 다음 상태로 바뀌는 전이(transition)가 있다.
FSM 구현 방법1) enum
상태 기계를 구현하는 가장 간단한 방법.
상태에 따라 분기함.
FSM 구현 방법2) 상태 패턴
상태 인터페이스.
class PlayerState
{
public:
virtual ~PlayerState();
virtual void HandleInput(Player& player, Input input){}
virtual void Update(Player& player){}
};
상태별 클래스
class IdleState : public PlayerState
{
public:
virtual void HandleInput(Player& player, Input input)
{
if(input == SPACE)
{
player.SetAnimation(JUMP);
}
}
virtual void Update(Player& player)
{
//TODO
}
};
동작을 상태에 위임
class Player
{
public:
virtual void HandleInput(Input input)
{
state->HandleInput(*this, input);
}
virtual void Update() { state->Update(*this); }
private:
PlayerState* state;
};
상태 객체는 어디에 둘까?
1)정적 객체
상태 객체에 필드가 없다면 인스턴스는 다 똑같으므로 하나만 있으면 된다.
정적 객체는 상위 상태 클래스에 두자.
웬만하면 정적 객체로 만들기.
2)전이할 때마다 상태 객체 만들기
상태에 필드가 있는데 FSM이 여러개면 전이할 때마다 상태 객체를 만들어야 한다.
즉 FSM이 상태별로 인스턴스를 갖는다.
void Player::HandleInput(Input input)
{
PlayerState* playerState = state->HandleInput(*this, input);
if(state != nullptr)
{
delete state;
state = playerState;
}
}
새로운 상태를 반환할 때만 현재 상태를 삭제하고 새로 만든다.
PlayerState* IdleState::HandleInput(Player& player,Input input)
{
if(input == SPACE)
return new JumpState();
return nullptr;
}
입장(Enter)과 퇴장(Exit)
예를 들어 플레이어의 스프라이트를 상태에 따라 바꾸는 작업은
새로운 상태에 들어왔을 때 각 상태에 맞게 해주는 것이 바람직하다. (캡슐화)
Enter 함수에서 이를 처리하게 한다.
상태가 전이되기 전에 호출되는 Exit 함수도 활용할 수 있다.
class IdleState : public PlayerState
{
public:
virtual void Enter(Player& player)
{
player.SetAnimation(IDLE);
}
};
void Player::HandleInput(Input input)
{
PlayerState* playerState = state->HandleInput(*this, input);
if(state != nullptr)
{
delete state;
state = playerState;
state->Enter(*this);
}
}
FSM 보완1) 병행 상태 기계
플레이어가 총을 장착했을 때도 장착하지 않을 때와 마찬가지로 달리기, 점프, 엎드리기 등을 할 수 있어야 하는데
FSM 방식에서는 이를 구현하려면 총을 들었을 때와 안들었을 때 두 가지 버전으로 상태를 각각 만들어야 한다.
이러면 상태도 많아질뿐더러 중복이 많아지므로, 상태 기계를 둘로 나누면 된다.
무엇을 하는가에 대한 상태 기계와 무엇을 들고 있는가에 대한 상태 기계를 따로 정의하고
Player 클래스는 이들 상태를 각각 참조한다.
class Player
{
private:
PlayerState* state;
PlayerState* equipmentState;
}
각각의 상태 기계는 입력에 따라 동작을 실행하고 독립적으로 상태를 변경할 수 있다.
FSM 보완2) 계층형 상태 기계
상위 상태 - 하위 상태 구조. 클래스 상속으로 계층을 구현한다.
이벤트가 들어올 때 하위 상태에서 처리되지 않으면 상위 상태로 넘어간다.
class AttackState : public PlayerState
{
public:
virtual void HandleInput(Player& player, Input input)
{
if(input == PRESS_A)
//기본공격
else if(input == PRESS_D)
//스킬공격
}
};
class SkillAttackState : public AttackState
{
public:
virtual void HandleInput(Player& player, Input input)
{
if(input == PRESS_D)
//연속스킬
else
AttackState::HandleInput(player,input);
}
};
구현하는 다른 방법 : 상태 스택을 만들어 관리
스택의 최상위에 있는 상태는 현재 상태이고, 밑에는 그 상위 상태가 있다.
어느 상태든 동작을 처리할 때까지 스택 위에서부터 밑으로 전달한다.
FSM 보완3) 푸시다운 오토마타
상태 스택을 활용하여 FSM을 확장하는 방법이다.
FSM에서는 현재 상태는 알 수 있지만 직전 상태를 저장해놓지 않기 때문에 이전 상태로 돌아가기 어렵다.
푸시다운 오토마타는 상태를 스택으로 관리한다.
새로운 상태가 되면 스택에 넣는다. 스택의 최상위 상태가 현재 상태가 된다.
최상위 상태를 스택에서 빼면 직전의 상태가 현재 상태가 된다.
총 쏘기를 구현할 때 좋다.
총 쏘는 상태를 스택에 넣은 후 총 쏘는 애니메이션이 끝났을 때 스택에서 빼면
총 쏘기 전의 상태로 쉽게 돌아갈 수 있다.
FSM, 언제 사용할까?
요즘 게임 AI는 행동 트리나 계획 시스템을 더 많이 쓰는 추세.
FSM은
-내부 상태에 따라 객체 동작이 바뀔 때
- 이런 상태가 그다지 많지 않은 선택지로 분명하게 구분될 수 있을 때
- 객체가 입력이나 이벤트에 따라 반응할 때
사용하면 좋다.