ここではゲームを作るうえでの基本的なコーディング(プログラム)方法を説明します。
これは音ゲーとは限らず他のゲームを作るときなどにも応用が可能です。
なお、ここで説明するのは自分が良くやる手法なだけであり、
必ずこうしろという事ではありません。
上級者はここで説明するものよりもっと効率の良い方法などを使ったりしますが、
あまり変なコーディングをするとバグがあると修正が大変になったりするので、
このサイトではそこまで複雑なことは行いません。
ステート方式とはメインプログラムが現在どういう状況であるかを値として常に記録しておく方式です。
※厳密にステート方式という手法がある訳ではなく自分が勝手に付けた名前です
この値により現在タイトル画面中なのかメインゲーム中なのかを判断し、
プログラムをそのルーチンへ導くことでそれぞれの状況下で適切な処理を実行させることが出来ます。
具体的にはまず下のようなステート定数を定義します。
enum STATE { ST_INIT, // プログラム初期化 ST_GAMEINIT, // メインルーチン初期化 ST_GAMERUN, // メインルーチン実行中 ST_EXIT, // プログラム終了 };
そしてこれを管理するためにグローバル変数として以下のように定義しておきます。
STATE dwState = ST_INIT;
このdwState変数が現在どういう状態なのかを記録する変数となり、状態が変わるごとにこの値を変更していきます。
ちなみにここでは初期化の時点であらかじめST_INITという状態に設定しています。
メインループ内でステート状態を管理をするには以下のようにします。
// 以下の関数が実態として定義されている // 各関数は成功するとTRUE、失敗するとFALSEが返るとする BOOL Init( void ); // プログラムを初期化するための関数 BOOL GameInit( void ); // メインゲームルーチンの初期化をするための関数 BOOL GameRun( void ); // メインゲームルーチン int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow) { : // メインループ BOOL bLoop = TRUE; while( bLoop ) { MSG msg; if( PeekMessage(&msg,NULL,0,0,PM_REMOVE) ) { if( msg.message==WM_QUIT ) break; TranslateMessage(&msg); DispatchMessage(&msg); } // ステートごとに処理を行う switch( dwState ) { case ST_INIT: if( Init() ) dwState = ST_GAMEINIT; // 初期化が成功ならメインゲーム初期化 else dwState = ST_EXIT; // 失敗なら抜ける break; case ST_GAMEINIT: if( GameInit() ) dwState = ST_GAMERUN; // メインゲーム初期化が成功ならメイン処理へ移行 else dwState = ST_EXIT; // 失敗なら抜ける break; case ST_GAMERUN: if( !GameRun() ) dwState = ST_EXIT; // メインルーチンでESCAPEが押されたら抜ける break; case ST_EXIT: bLoop = FALSE; // メインループを抜ける break; } Sleep(15); } : return 0; }
この例ではメインループ内でdwStateをチェックして、現在のステートにあった処理を行っています。
そしてここが重要ですが、例えばあとでタイトルのルーチンを追加したいと思ったら、
enumにST_TITLEINITとST_TITLERUNを追加し、ST_INIT内でステートをST_TITLEINITに変更するだけで
タイトル初期化処理やとタイトルループ処理に飛ばすことが出来ます。
このようにステート方式ではあとからステートを追加したりデバッグ用に一時的に遷移先を変えたり、
また新しい画面を簡単に増やすことが出来るといった利点があります。
ちなみになぜこのような処理を行わなければならないのかというと、
Windowsはメッセージを処理するためにPeekMessageやTranslateMessage、DispatchMessageで
アプリケーションに通知されたメッセージを適切に処理させる必要があります。
別にこの処理を各関数ごとに入れてもよいですが、
これだと同じプログラムが複数存在することになって管理が大変になったり、
もしメッセージ処理を変更したい場合、全てのルーチンを手直しするのは
とても面倒だったり、そもそも手直しし忘れということも考えられます。
これらの問題を考慮しメインループにて必ず1回、
メッセージ処理を行えるように考えたのが上記のプログラムとなります。
上記のルーチンをsさらにクラス化することができれば、
初期化と終了処理を自動的に行ってくれるプログラムを作ることが出来ます。
つまりここではコンストラクタとデストラクタを使って、
安全に初期化と終了処理を行わせたいわけです。
以下にサンプルのクラスを紹介します。
// CGame.h #include "CWindow.h" #include "CDDPro81.h" #include "CDSPro81.h" // ステート enum STATE { G_INIT=0, // 初期化モード G_MAININIT, // メインルーチン初期化 G_MAIN, // メインループ G_END // 全てのゲーム終了処理 }; class CGame { // ライブラリなど CWindow win; // ウインドウ管理 CDDPro90 dd; // Direct3D管理クラス CDSPro81 ds; // DirectSound管理 STATE dwState; // ステート記録 public: CGame(); virtual ~CGame(); BOOL Run( HINSTANCE hinst ); // メインプログラムから実行される private: BOOL Init( HINSTANCE hinst ); BOOL GameInit( void ); BOOL GameRun( void ); };
ここではゲームクラスということでCGameという名前にしています。
まずはゲームに必要なライブラリをインクルードしておき、
あとはそれをクラスの中に実体として定義します。
以下がクラスの実体です。
// CGame.cpp #include "CGame.h" CGame::CGame() { dwState = ST_INIT; } CGame::~CGame() { // 開放処理 dd.Delete(); ds.Delete(); } BOOL CGame::Run( HINSTANCE hinst ) { // メインループ BOOL bLoop = TRUE; while( bLoop ) { MSG msg; if( PeekMessage(&msg,NULL,0,0,PM_REMOVE) ) { if( msg.message==WM_QUIT ) break; TranslateMessage(&msg); DispatchMessage(&msg); } // ステートごとに処理を行う switch( dwState ) { case ST_INIT: if( Init(hinst) ) dwState = ST_GAMEINIT; else dwState = ST_EXIT; break; case ST_GAMEINIT: if( GameInit() ) dwState = ST_GAMERUN; else dwState = ST_EXIT; break; case ST_GAMERUN: if( !GameRun() ) dwState = ST_EXIT; break; case ST_EXIT: bLoop = FALSE; // メインループを抜ける break; } Sleep(15); } return TRUE; } BOOL CGame::Init( HINSTANCE hinst ) { // ウィンドウ if( !win.Create(hinst,"CGame Test") ) return FALSE; // Direct3D if( !dd.Create(win.hWnd) ) return FALSE; // DirectSound if( !ds.Create(win.hWnd) ) return FALSE; return TRUE; } BOOL CGame::GameInit( void ) { // ゲームで使う画像をロード dd.AddTexture( 0,"BACK.BMP" ); // 切り抜き dd.SetPutRange( 0,0,0,0,640,480 ); // ゲームで使うサウンドをロード ds.AddSound( 0,"BGM.WAV" ); return TRUE; } BOOL CGame::GameRun( void ) { // 計算処理 if( (ESCがおされた場合) ) return FALSE; // 描画処理 dd.DrawBegin(); dd.Put( 0,0,0 ); dd.DrawEnd(); return TRUE; }
クラスの実体としてCGame内に定義されたCDDPro81やCDSPro81などは、
CGameクラス内部で通常のライブラリとして使用出来ます。
上記のクラスをメインプログラムに組み込むと以下のようになります。
// main.cpp #include "CGame.h" int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow) { CGame game; if( !game.Run(hInstance) ) return -1; return 0; }
全ての処理をCGameクラスで行いますが、
ここで呼び出すのはRun()のみとなり、Run内部で関連する全てのコンテンツが実行され、
ゲーム処理が終わるとリターンして返ってきます。
そしてreturnされる前にCGameクラスのデストラクタが実行されることで、
CGame内で使用したメモリなどが全て開放されるという仕組みです。
ちなみにCGameクラスには外に出ている(public)関数はRunだけで、
他のInitやGameInit、GameRunは外に出ていません。
つまり意図せず外部からこれらの関数を呼び出してしまうといった心配はありません。
ちなみにここではCGameクラスをWinMain関数内に置いていますが、
グローバル変数として置いても動作します。
その際はWinMain関数が終わってもまだ実体が残っているということになり、
デストラクタによる終了処理が行われるタイミングが分からないため、
その場合は明示的にCGameを終了する関数を作っておくか、
newとdeleteで確保と開放を制御してやる必要があります。
※上記のようにWinMain内にクラスの実体を定義した場合、
CGameクラスがあまりにも大きいとスタックオーバーフローが発生する場合があります。
この場合はCGameクラスをnewを使ってスタックではなくメモリ上に作成してください。
以下はCGameクラスの階層イメージになります。
WinMain | ||||
└ | CGame | |||
├ | CWindow | |||
├ | CDDPro81 | |||
│ | └ | CDDTexPro81 | ||
└ | CDSPro81 | |||
└ | CDSSegPro81 |
クラスの階層が下のものは上位のクラスが解放される前に、つられて上位より先に開放されます。
ということはCGameが開放される時にその下についているCWindowやCDDPro81などのクラスも自動的に開放されるため、
プログラム終了時にどれとどれを開放しなければならないのかといったことを考える必要がまったくありません。
ここではこのようなコーディング方法によってメインルーチンを作成していくので、
この方法での原理を確実に理解しておいてください。