一般的なゲームプログラムの構造

Windowsアプリケーションもゲームも、同じWin32 Applicationである。しかし、Windowsアプリケーションとゲームでは、プログラミングの考え方に大きな違いがある。ここでは、一般的なゲームプログラムの考え方を解説する。

この章で使用するプログラム

Windowsプログラムの構造

Windowsプログラミング基礎で学んだとおり、Windowsプログラムは次のような構造で作成されている。

《解説》

このプログラムは、Windowsから何かのメッセージが届くまで制御が戻ってこず、止まったままになっている(イベント・ドリブン方式)。

一般的なゲームプログラミングの構造

一般的なゲームは(おそらく)次のような処理を行っている。

1.初期化処理
・環境の確保、パラメータ(変数)の初期化など
2.メインループ(1フレーム表示ごとに1回ループ)
・オープニング表示(タイトル画面、オープニングムービーなど)
・ゲームメイン
・エンディング表示(ゲームオーバー画面、エンディングムービーなど)
3.終了処理
・環境の開放など

《解説》

Windowsからのメッセージを待つのではなく、永久ループにする。

Windowsプログラムをゲームプログラム構造にあわせる

上記のフローチャートのようにWinMain関数を改造すると次のようになる。

//-------------------------------------------------------------------------------------------------
//      メイン関数(エントリーポイント)プログラムはここから始まる
//-------------------------------------------------------------------------------------------------
int WINAPI WinMain(HINSTANCE hThisInst, HINSTANCE hPrevInst, LPSTR lpszArgs, int nWinMode)
{
    MSG msg;		// メッセージ構造体変数
    HRESULT hr;

    //表示するウィンドウの定義、登録、表示
    if ( !InitApp(hThisInst, nWinMode) )    // InitApp関数を呼び出し、
        return (FALSE);                     // 正常に終了すれば次にメッセージループへ

    // DirectX8の初期化
    hr = InitDX8();
    if ( FAILED(hr) ) return (FALSE);

    // ゲームループ
    while ( TRUE ) {
    // メッセージがあるかどうか
        if ( PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE) ) {
            // メッセージを取得し、WM_QUITかどうか
            if ( !GetMessage(&msg, NULL, 0, 0) ) break;
            TranslateMessage(&msg);  //キーボード利用を可能にする
            DispatchMessage(&msg);  //制御をWindowsに戻す
        } else {
            // ゲームメイン処理
            //UpdateFrame();
            Sleep(1);
        }
    }

    // DirectX8オブジェクトの削除
    ReleaseD3D();

    return msg.wParam;
}

《解説》

※このページからダウンロードしたプログラムに上記の修正を行い、正常に動作するかどうかを確認する(見た目は何も変わらないが、無限ループしている)。ただし、UpdateFrame関数はまだ作成していないため、コメントアウトしないとエラーになるので注意!!

《Sleep()関数について》

ゲームループにおいて、UpdateFrame()関数実行後、Sleep()関数を呼び出している。Sleep()関数は引数で指定された時間(ミリ秒)だけ、プログラムの処理を中断する関数である。
 ここでSleep()関数を実行している理由は、ウィンドウを最小化したときなどにWindowsが固まったようになる現象を防ぐためである。もしSleep()関数が入っていないと、DirectXプログラムが最大速で動き続けることになるため、その分だけWindows全体の処理が遅くなり、結果、Windowsが固まったようになってしまうことがある。Sleep()関数を入れることにより、プログラムが休止している間はWindowsがほかの処理を実行できるようになるため、固まったようにはならなくなる。

ウィンドウの切り替えに対応させる

ゲーム中にALT+TABによってウィンドウを切り替えたりWindowsキーを押すなどしたあとでゲームに戻ろうとすると、今まで動いていたゲームが動かなくなることがある。
これは、ゲームのウィンドウがアクティブ状態から非アクティブになると、DirectXの各オブジェクトが開放されてしまったり、非アクティブでもゲームループが回り続けることが原因である。
実際、現在のプログラムを実行(フルスクリーンになり、全面真っ黒になる)後、ALT+TABを押してWindowsの画面に戻り、タスクバーにあるゲームウィンドウをクリックすると、真っ黒の画面は表示されるがフルスクリーンに戻らないことがわかる(ESCキーで終了はできる)。

よって、次のような対策を取ることが望ましい。

  1. 非アクティブの間は一切の処理を行わない。
  2. アクティブになった際に、ウィンドウの状態を復元し、開放されたDirectXの各オブジェクトを復活させる。

DirectXの各オブジェクトの復活についての解説は、そのつど説明するとして、ここでは非アクティブの間は一切の処理を行わず、アクティブになったらウィンドウの状態を復元するようにプログラムを修正する方法を解説する。

1.アクティブ状態の監視する変数の宣言

ゲームウィンドウがアクティブかどうかを判断するための変数を宣言する。いろいろなところで使うので、グローバル変数として宣言する。

// グローバル変数
HWND hWnd;                              // ウィンドウハンドル
BOOL g_appActive = FALSE;               // ウィンドウの状態
char szWinName[] = "Exer003";           // ウィンドウクラス用文字列
char szWinTitle[] = "ゲーム用にプログラムを改造する"; // ウィンドウクラス用文字列
LPDIRECT3D8 gl_lpD3d = NULL;            // Direct3D8インターフェイス
LPDIRECT3DDEVICE8 gl_lpD3ddev = NULL;   // Direct3DDevice8インターフェイス
D3DPRESENT_PARAMETERS gl_d3dpp;         // ディスプレイパラメータ

2.ウィンドウの再生成を行う関数を作成

ウィンドウの復帰時に再生成を行う関数を作成する。プロトタイプ宣言も行うこと

//-----------------------------------------------------------------------------
// 関数名 : ResetWindow()
// 機能概要: ウィンドウの再設定
//-----------------------------------------------------------------------------
void ResetWindow(void)
{
    if ( gl_lpD3ddev ) {
        // ディスプレイ・パラメータの値を使ってウィンドウをリセットする
        gl_lpD3ddev->Reset(&gl_d3dpp);
    }
}

《POINT》

3.メッセージ処理の修正

ゲーム中にディスプレイ・モードが変更されると、ゲームに戻っても元に戻らなくなる。よって、ウィンドウがアクティブになったときにウィンドウ・モードを再設定する必要がある。
アクティブになったのか非アクティブになったかを調べ、ウィンドウの状態を復元できるよう、WinProc関数を次のように修正する。

//-------------------------------------------------------------------------------------------------
//      ウィンドウプロシジャ関数(WindowProcedure)
//      メッセージ処理を行う
//-------------------------------------------------------------------------------------------------
LRESULT CALLBACK WinProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch ( message ) {
        case WM_SIZE:
            if ( wParam == SIZE_MAXHIDE || wParam == SIZE_MINIMIZED )
                g_appActive = FALSE;
            else
                g_appActive = TRUE;
            ResetWindow();
            break;
        case WM_MOVE:
            ResetWindow();
            break;
        case WM_KEYDOWN: // キーを押したとき
            switch (wParam) {
                case VK_ESCAPE:
                    PostMessage(hWnd, WM_CLOSE, 0, 0);
                    break;
            }
            break;
        case WM_SETCURSOR: // カーソルの設定
            SetCursor(NULL);
            break;
        case WM_DESTROY:        // 閉じるボタンをクリックした時
            PostQuitMessage(0); // WM_QUITメッセージを発行
            break;
        default: // 上記以外のメッセージはWindowsへ処理を任せる
            return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

《POINT》

4.ゲーム・ループの修正

ウィンドウが非アクティブのとき、処理を待機するようゲーム・ループを次のように修正する。

    // ゲームループ
    while ( TRUE ) {
    // メッセージがあるかどうか
        if ( PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE) ) {
            // メッセージを取得し、WM_QUITかどうか
            if ( !GetMessage(&msg, NULL, 0, 0) ) break;
            TranslateMessage(&msg);  //キーボード利用を可能にする
            DispatchMessage(&msg);  //制御をWindowsに戻す
        } else if ( g_appActive ) {
            // ゲームメイン処理
            //if ( FAILED( UpdateFrame() ) ) break;
            Sleep(1);
        } else {
            WaitMessage();
        }
    }

《POINT》

※以上の修正を行い、実行後、ALT+TABによってウィンドウを切り替え、復帰してくるかをチェックすること。



NEXT(ゲームメイン処理)