원문 http://tonyandpaige.com/tutorials/game1.html
게임에서 여러 다른 상태가 있다는 걸 몇년 전 한 데모를 보면서 알게 되었다. 이 데모란 것이 미출시 게임에 대한 맛보기판이 아니라, scene 에 서 본 옛날 학교(old-school)이었지만. 어쨌든 이 데모는 한 효과에서 다음 효과로 매끄러운 동작 경로를 갖고 있었다. 2차원 효과가 보여지더니, 3차원 렌더링 환경으로 곧바로 넘어가는 식이었다. 나는 이것들이 몇 개의 프로그램이고, 하나로 묶여있었다고 기억한다.
다양한 상태는 데모에서 중요하지 않는다. 그러나 게임에서는 다르다. 모든 게임은 소개 상태에서 시작해서, 몇 가지의 메뉴를 보여준다. 그리고 게임이 시작된다. 게임이 끝나면, 종료 상태로 이동된다. 그리고 메뉴로 돌아간다. 대부분의 게임에서 한번에 한 상태 이상을 보여준다. 예를 들어, 게임을 하면서 메뉴를 열 수 있다.
여러 상태를 다루는 고전적 방법은 일련의 if, switch, 순환문들로 구성된다. 프로그램은 소개 상태에서 시작되어, 키가 눌러질때까지 계속된다. 그 다음에는 선택될 때까지 메뉴가 떠있는다. 게임이 시작되면, 종료될 때까지 순환을 한다. 순환하는 동안 매번 프로그램은 메뉴를 표시할 지, 다음 프레임을 표시할 지 결정해야 한다. 게다가 프로그램 중 일부는 입력이 메뉴에서 있었는지, 게임에서 있었는지 확인해야 한다. 이런 모든 것들이 순환문에 모여있어서 추적하기란 상당히 어렵다. 그래서 디버그나 유지보수도 어려워진다.
상태란?
이전에 힌트를 주었듯이, 상태는 게임에서 분리된 프로그램과 같다. 각 상태는 서로 다른 이벤트를 다루고, 화면에 그리는 것도 다르다. 각 상태는 각자의 이벤트를 갖고, 게임 세상을 바꾸어내고, 화면에 그린다. 그래서 우리는 상태 클래스가 가져야할 세 개의 메소드를 인식하게 될 것이다.
보통 게임에서 각 상태는 그래픽을 불러오고, 초기화해야 하며, 완료되었을 때 할당한 자원을 제거해야 한다. 그리고 상태를 잠시 중지시키고, 다시 재개해야 한다. 예를 들어, 게임 상태가 중지되고 메뉴가 불러질 수 있다. 게임 상태는 이렇게 보여질 수 있다.
class CGameState
{
public:
void Init();
void Cleanup();
void Resume();
void HandleEvents();
void Update();
void Draw();
};
이런 레이아웃은 게임 상태에서 원하는 우리 요구와 쉽게 맞아떨어진다. 이것은 훌륭한 기반 클래스가 될 것이며, 게임에서 필요로 하는 각 상태 클래스로 상속받을 수 있다 - 소개 상태, 메뉴 상태, 진행 상태.
상태 관리자
다음에 우리는 이들 상태를 관리할 방법이 필요하다. 바로 상태 관리자다. 내 코드에서 상태 관리자는 게임 엔진의 일부이다. 다른 사람들은 분리된 상태 관리자 클래스를 만드는 방법을 택할 수도 있다. 하지만 내게는 엔진에 직접 적용하는 편이 훨씬 쉬웠다. 다시 말하면, 우리는 게임 엔진이 필요로 하는 게 뭔지, 이런 함수를 관리하는 게임 엔진 클래스를 만드는 것에 대해 볼 수 있다.
첨부된 간단한 예제에서, 엔진이 필요로 하는 건 SDL 을 초기화하고 완료될 때 제거하는 것이다. 메인 루프에서 엔진을 사용하기 때문에, 엔진이 계속 실행되는지 확인해야 하고, 끝나는지, 일반적인 처리 이벤트를 관리해야 하고, 정보도 바꾸고, 화면도 그려내야 한다.
게임 엔진의 일부인 상태 관리자는 아주 간단하다. 상태들의 순서를 조정하기 위해, '상태 스택'이 필요하다. 나는 STL 의 벡터를 스택으로 사용했다. 그리고 push, pop 외에도 상태를 바꾸는 것도 필요하다.
그래서 게임 엔진 클래스는 아래와 같이 표현된다.
class CGameEngine
{
public:
void Init();
void Cleanup();
void ChangeState(CGameState* state);
void PushState(CGameState* state);
void PopState();
void HandleEvents();
void Update();
void Draw();
bool Running() { return m_running; }
void Quit() { m_running = false; }
private:
// the stack of states
vector<CGameState*> states;
bool m_running;
};
이들 함수들은 쓰기 정말 쉽다. HandleEvents(), Update(), Draw()는 스택의 꼭대기에 있는 상태에서 실행된다. 이들은 게임 엔진 정보에 접근할 필요도 있으므로, 상태 클래스에 엔진에 대한 포인터를 멤버 변수로 두었다.
마지막으로 상태 간의 변경이다. 엔진이 상태가 변해야 할 때를 어떻게 알 수 있을까? 답은 - 모른다이다. 다음 상태로 바뀔 때를 아는 건 오직 현재 상태뿐이다. 그래서 우리는 상태 클래스에 바뀔 상태에 대한 함수를 추가해줘야 한다.
상태 클래스를 수정하는 동안, 우린 이걸 추상 클래스로 만들어야 한다. 멤버 대부분은 순수 가상 함수가 된다. 이것들은 파생된 클래스가 멤버 함수를 구현하면서 완성된다. 변경이 모두 이뤄지면, 최종적인 상태 클래스는 아래 모습처럼 된다.
class CGameState
{
public:
virtual void Init() = 0;
virtual void Cleanup() = 0;
virtual void Pause() = 0;
virtual void Resume() = 0;
virtual void HandleEvents(CGameEngine* game) = 0;
virtual void Update(CGameEngine* game) = 0;
virtual void Draw(CGameEngine* game) = 0;
void ChangeState(CGameEngine* game, CGameState* state) {
game->ChangeState(state);
}
protected:
CGameState() { }
};
여러분의 게임에 상태를 추가하려면 기본 클래스를 상속받고, 7개의 순수 가상 함수를 구현해주면 된다. 각 상태에 대한 인스턴스는 하나 이상 필요하지 않으므로, 싱글턴 으로 구현하는 편이 좋다. 싱클턴 패턴이 생소하면, 객체가 하나만 확실하게 있도록 하면 된다. 그러려면 생성자를 protected 속성으로 하면 된다. 그리고 함수가 클래스의 정적 인스턴스에 대한 포인터를 반환하도록 한다.
이 메소드가 게임에서 얼마나 구현하기 쉬울지 보여주기 위해, main.cpp에 포함된 것들을 모두 보여주려 한다.
#include "gameengine.h"
#include "introstate.h"
int main ( int argc, char *argv[] )
{
CGameEngine game;
// initialize the engine
game.Init( "Engine Test v1.0" );
// load the intro
game.ChangeState( CIntroState::Instance() );
// main loop
while ( game.Running() )
{
game.HandleEvents();
game.Update();
game.Draw();
}
// cleanup the engine
game.Cleanup();
return 0;
}
내려받기
예제는 세 개의 상태를 갖고 있다. 검은 화면에서 페이드되는 소개 상태, 진행 상태, 그리고 메뉴는 진행을 멈추고 닫히면 게임이 재개된다. 각 상태는 간단한 배경 그림으로 표시된다.
stateman.zip - 예제 소스, 그래픽, Visual C++ 6 용 프로젝트 파일
stateman.tar.gz - 예제 소스, 그래픽, 리눅스 용 Makefile
이 예제 코드는 SDL을 사용한다. SDL을 잘 모른다면, 본인의 SDL 튜토리얼을 보라. 컴퓨터에 SDL이 설치되어 있지 않으면, 예제를 컴파일하거나 실행할 수 없다.
주
이 튜토리얼은 The Code Project 에 올라온 State Pattern in C++ 에서 영감을 받았다. 불행히도, 여기 튜토리얼은 MFC로 작성되었고, 나는 보기가 힘들었다. 그리고, 이 저자는 상태 스택은 구현하지 않았다.
'코드' 카테고리의 다른 글
원과 파이 그리기 (0) | 2006.03.10 |
---|---|
행렬 곱셈 계산기 (0) | 2006.03.10 |
테트리스 (0) | 2006.02.13 |
ACM 10050, Hartals (0) | 2006.01.26 |
ACM 105, The Skyline Problem (0) | 2006.01.24 |