FPSを調べる

ゲームループはイベントを待つことなく無限ループしている。しかし、描画処理はリフレッシュ・レートに合わせて行われるため、1秒間に画面が更新される回数(FPS)はリフレッシュ・レートと一致するはずである。しかし、重い処理を行うとゲーム・ループが回るスピードが遅くなり、リフレッシュ・レートに間に合わなくなる。そうすると画面を更新する回数が減り、処理落ちなどが発生してしまう。

ここでは、FPSを実際に計測し、デバッグ・ウィンドウに表示する方法を紹介する。前準備として、(1)でWinMain関数に対して行った修正を削除しておこう。

考え方

FPS(1秒間に何回ループするか)を調べるには、ゲームループが始まる前に0をセットし、ゲームループが1回ループするたびにカウントアップする。1秒たったら何回ループしたかを表示すればよい。

ここで問題になるのは、1秒たったどうかをどうやって調べるかである。
TimeGetTime関数を使えば、マシンを起動してからの経過時間をミリ秒単位で調べることができる。この関数を使い、ゲーム開始時とループ途中の経過時間を調べ、差分が1秒(1000ミリ秒)を超えるかどうかで判断する。

1秒を超えたら、回った回数を表示する。これにより、1秒間で何回ループしたかを調べることができる。

実際のプログラム

FPSはどのシーンでも調べたいため、シーン別の処理に組み込むべきではない。ゲーム・ループのたびに実行されるのはUpdateFrame関数であるため、UpdateFrame関数内に処理を記述する。

//-----------------------------------------------------------------------------
// 関数名 : UpdateFrame()
// 機能概要: ゲームメイン処理
//-----------------------------------------------------------------------------
BOOL UpdateFrame(void)
{
    static DWORD beforeTime = 0;    // 以前の時間を格納
    DWORD nowTime;                  // 現在の時間を格納
    static int fps = 0;             // FTPカウンタ
    char buff[80];                  // 文字列表示用バッファ

    /* 現在の時間を取得 */
    nowTime = timeGetTime();

    /* 現在のキー情報を取得 */
    if ( !GetKeyboardState(gl_KeyTbl) ) {
        MessageBox(hWnd, "キー情報の取得に失敗", "ERROR", MB_OK);
        return (FALSE);
    }

    /* 画面のクリア */
    gl_lpD3ddev->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(255,255,255), 0.0, 0);
    /* シーン開始 */
    gl_lpD3ddev->BeginScene();
    /* 頂点フォーマットを設定 */
    gl_lpD3ddev->SetVertexShader(FVF_TLVERTEX);

    /* 処理の振り分け */
    switch ( g_FrameNo ) {
        case START_INIT:
            // スタート画面を表示する前に実行する初期化関数を実行
            StartInit();
        case START_FRAME:
            // スタート画面表示処理関数を実行
            StartFrame();
            break;
        case GAME_INIT:
            // ゲーム画面を表示する前に実行する初期化関数を実行
            GameInit();
        case GAME_FRAME:
            // ゲーム画面表示処理関数を実行
            GameFrame();
            break;
        default:
            MessageBox(hWnd, "g_FrameNoの値が例外です。", "ERROR", MB_OK);
            return (FALSE);
    }

    /* FPSを求めて表示する */
    fps++; // カウントアップ
    if ( nowTime - beforeTime >= 1000 ) {
        // FPSの表示
        wsprintf(buff, "%04d FPS\n", fps);
        OutputDebugString(buff);
        // 初期化
        fps = 0;
        beforeTime = nowTime;
    }

    /* シーン終了 */
    gl_lpD3ddev->EndScene();
    /* フリップ */
    gl_lpD3ddev->Present(NULL, NULL, NULL, NULL);

    return (TRUE);
}

上記修正を行い、デバッグ・モードで実行すると、約1秒ごとにFPSが表示される。実行結果を見ると、常にリフレッシュ・レートの値と同じ回数でループしているわけではないことが分かる。

応用

FPSの表示部分を次のように修正すると、現在のシーン番号と一緒にFPSを表示できる。

wsprintf(buff, "シーンNo:%2d / %04d FPS\n", g_FrameNo, fps);

この実行結果を見ると、シーンが変わった直後はFPSが遅くなることが分かる。この技術を応用し、どの場面で処理が遅くなるかを調べることができる。

ゲームに最適なFPS

一般的に、ゲームはFPSが50〜60で動くように作るのが良いとされている。少なくとも50を下回ることがないように、プログラムを作成・最適化するべきである。実際のゲーム製作では、FPSに注意しながらプログラムを作成しよう。

しかし、FPSの表示はデバッグやテストの際に参考にするものなので、完成したゲームにはFPSを表示させないようにしたほうがよい。完成を確認したら、FPSを表示するプログラムを削除すればよいのだが、コンパイルオプションを使うという方法もあるので紹介しておく。

NKC_Common.hにマクロを追加

/* デバッグオプション(完成したらコメントアウトする) */
#define DEBUG          TRUE

FTP表示処理にコンパイルオプションを付ける

//-----------------------------------------------------------------------------
// 関数名 : UpdateFrame()
// 機能概要: ゲームメイン処理
//-----------------------------------------------------------------------------
void UpdateFrame(void)
{
#ifdef DEBUG
    static DWORD beforeTime = 0;    // 以前の時間を格納
    DWORD nowTime;                  // 現在の時間を格納
    static int fps = 0;             // FTPカウンタ
    char buff[80];                  // 文字列表示用バッファ

    /* 現在の時間を取得 */
    nowTime = timeGetTime();
#endif

    /* 現在のキー情報を取得 */
    if ( !GetKeyboardState(gl_KeyTbl) ) {
        MessageBox(hWnd, "キー情報の取得に失敗", "ERROR", MB_OK);
        return (FALSE);
    }

    /* 画面のクリア */
    gl_lpD3ddev->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(255,255,255), 0.0, 0);
    /* シーン開始 */
    gl_lpD3ddev->BeginScene();
    /* 頂点フォーマットを設定 */
    gl_lpD3ddev->SetVertexShader(FVF_TLVERTEX);

    /* 処理の振り分け */
    switch ( g_FrameNo ) {
        case START_INIT:
            // スタート画面を表示する前に実行する初期化関数を実行
            StartInit();
        case START_FRAME:
            // スタート画面表示処理関数を実行
            StartFrame();
            break;
        case GAME_INIT:
            // ゲーム画面を表示する前に実行する初期化関数を実行
            GameInit();
        case GAME_FRAME:
            // ゲーム画面表示処理関数を実行
            GameFrame();
            break;
        default:
            MessageBox(hWnd, "g_FrameNoの値が例外です。", "ERROR", MB_OK);
            return (FALSE);
    }

#ifdef DEBUG
    /* FPSを求めて表示する */
    fps++; // カウントアップ
    if ( nowTime - beforeTime >= 1000 ) {
        // FPSの表示
        wsprintf(buff, "%04d FPS\n", fps);
        OutputDebugString(buff);
        // 初期化
        fps = 0;
        beforeTime = nowTime;
    }
#endif

    /* シーン終了 */
    gl_lpD3ddev->EndScene();
    /* フリップ */
    gl_lpD3ddev->Present(NULL, NULL, NULL, NULL);

    return (TRUE);
}

このようにプログラムすると、defineでDEBUGが宣言されていれば#ifdefから#endifの間がコンパイルされる。逆に、DEBUGが宣言されていなければ、#ifdefから#endifの間はコンパイルされない。この技をうまく使えば、キャラクタの各パラメータなどをデバッグ用に画面に表示させるなど、様々なデバッグに使える。


BACK(時間の考え方) NEXT(一定時間経過したら、自動的に次の処理を行う)