第6章 キャラクタ表示の様々なテクニック

6−4 ゲーム画面にFPSを表示する

メッセージ・ウィンドウではなく、ゲーム画面にFPSを表示する方法を考える。スタート画面、ゲーム画面などいろんな場面が存在するが、各画面表示後、フリップ処理を行う前にFPSを表示させれば、場面に関係なくFPSを表示できる。

任意の文字列をプライマリ・サーフェイスに転送する方法はあるが、これでは処理が重いため、FPSを画像として転送する方法を考えると、次のようになる。

  1. DirectDrawオブジェクト初期化時、文字列画像(A〜Z、0〜9など)を用意する
  2. 場面ごとの画像をバック・バッファに転送する
  3. FPSを表示するための文字列画像をバック・バッファに転送する
  4. フリップ

1.DirectDrawオブジェクト初期化時、文字列画像(A〜Z、0〜9など)を用意する

文字列表示用画像として、string.bmpを用意した。ダウンロードしてリソースにインポート後、文字列画像用オフスクリーン・サーフェイスとして宣言する。

リソース名 "STRINGBMP"
DirectDrawサーフェイスオブジェクト名変数名 g_pDDSString

【common.h】

    ・
    ・
    ・
//-----------------------------------------------------------------------------
// 外部変数(本体は別ソース)
//-----------------------------------------------------------------------------
/* DirectDrawオブジェクト関係 */
extern LPDIRECTDRAW7 g_pDD;  // DirectDraw オブジェクト
extern LPDIRECTDRAWSURFACE7 g_pDDSPrimary;  // プライマリサーフェイス
extern LPDIRECTDRAWSURFACE7 g_pDDSBack;  // バックバッファ 
extern LPDIRECTDRAWSURFACE7 g_pDDSStart;  // スタート画像用サーフェイス
extern LPDIRECTDRAWSURFACE7 g_pDDSGame;  // ゲーム背景画像用サーフェイス
extern LPDIRECTDRAWSURFACE7 g_pDDSChara;  // キャラクタ画像用サーフェイス
extern LPDIRECTDRAWSURFACE7 g_pDDSString;  // 文字列画像用サーフェイス
    ・
    ・
    ・

【myDraw.cpp】

    ・
    ・
    ・
//-----------------------------------------------------------------------------
// 外部変数(元)
//-----------------------------------------------------------------------------
LPDIRECTDRAW7 g_pDD = NULL;  // DirectDraw オブジェクト
LPDIRECTDRAWSURFACE7 g_pDDSPrimary = NULL;  // プライマリサーフェイス
LPDIRECTDRAWSURFACE7 g_pDDSBack = NULL;  // バックバッファ 
LPDIRECTDRAWSURFACE7 g_pDDSStart = NULL;  // スタート画像用サーフェイス
LPDIRECTDRAWSURFACE7 g_pDDSGame = NULL;  // ゲーム画像用サーフェイス
LPDIRECTDRAWSURFACE7 g_pDDSChara = NULL;  // キャラクタ画像用サーフェイス
LPDIRECTDRAWSURFACE7 g_pDDSString = NULL;  // 文字列画像用サーフェイス

//-----------------------------------------------------------------------------
// グローバル変数
//-----------------------------------------------------------------------------
static LPDIRECTDRAWPALETTE g_pDDPal = NULL;  // プライマリ・サーフェイス・パレット
static char szStartBmp[] = "STARTBMP";  // スタート画像リソース名
static char szGameBmp[] = "GAMEBMP";  // ゲーム画像リソース名
static char szCharaBmp[] = "CHARABMP";  // ゲーム画像リソース名
static char szStringBmp[] = "STRINGBMP";  // ゲーム画像リソース名

//-----------------------------------------------------------------------------
// 関数名 : InitializeDraw() 
// 機能概要: Direct Draw オブジェクトの生成
// 戻り値 : 正常終了のとき:DD_OK、異常終了のとき:エラーコード
//-----------------------------------------------------------------------------
HRESULT InitializeDraw(HWND hWnd)
{
    ・
    ・
    ・
    // オフスクリーンサーフェイスを生成する(画像毎)
    /* スタート画像 */
    g_pDDSStart = DDLoadBitmap(g_pDD, szStartBmp, 0, 0);
    if (g_pDDSStart == NULL)
        return InitFail(hWnd, hRet, "DDLoadBitmap StartBmp FAILED");
    DDSetColorKey(g_pDDSStart, RGB(255, 255, 255));  // カラーキーを設定
    /* ゲーム背景画像 */
    g_pDDSGame = DDLoadBitmap(g_pDD, szGameBmp, 0, 0);
    if (g_pDDSGame == NULL)
        return InitFail(hWnd, hRet, "DDLoadBitmap GameBmp FAILED");
    /* キャラクタ画像 */
    g_pDDSChara = DDLoadBitmap(g_pDD, szCharaBmp, 0, 0);
    if (g_pDDSChara == NULL)
        return InitFail(hWnd, hRet, "DDLoadBitmap CharaBmp FAILED");
    DDSetColorKey(g_pDDSChara, CLR_INVALID);  // カラーキーを設定
    /* 文字列画像 */
    g_pDDSString = DDLoadBitmap(g_pDD, szStringBmp, 0, 0);
    if (g_pDDSString == NULL)
        return InitFail(hWnd, hRet, "DDLoadBitmap StringBmp FAILED");
    DDSetColorKey(g_pDDSString, CLR_INVALID);  // カラーキーを設定

    return DD_OK;

}
    ・
    ・
    ・
//-----------------------------------------------------------------------------
// 関数名 : ReleaseDraw()
// 機能概要: Direct Draw オブジェクトの削除
//-----------------------------------------------------------------------------
void ReleaseDraw(void)
{
    if (g_pDD != NULL)
    {
        if (g_pDDSPrimary != NULL)  // プライマリーサーフェイス
        {
            g_pDDSPrimary->Release();
            g_pDDSPrimary = NULL;
        }
        if (g_pDDSStart != NULL)  // スタート画像用サーフェイス
        {
            g_pDDSStart->Release();
            g_pDDSStart = NULL;
        }
        if (g_pDDSGame != NULL)  // ゲーム背景画像用サーフェイス
        {
            g_pDDSGame->Release();
            g_pDDSGame = NULL;
        }
        if (g_pDDSChara != NULL)  // キャラクタ画像用サーフェイス
        {
            g_pDDSChara->Release();
            g_pDDSChara = NULL;
        }
        if (g_pDDSString != NULL)  // 文字列画像用サーフェイス
        {
            g_pDDSString->Release();
            g_pDDSString = NULL;
        }
        g_pDD->Release(); // DirectDrawオブジェクト
        g_pDD = NULL;
    }

}

2.場面ごとの画像を転送する

スタート処理であればスタート画面を、ゲーム処理であればゲーム画面をそれぞれ転送する。

3.FPSを表示するための文字列画像を転送する

OutputDebugString関数でメッセージ・ウィンドウにFPSを文字列として表示するのではなく、バック・バッファに画像として転送する。
用意した文字列用画像は、半角の空白文字(文字コード:32)から文字コード順に並べてあり、1文字の大きさは8×16ピクセルである。(文字コードについてはC言語教科書38ページを参照)

つまり、表示したい文字列が「A」であった場合、'A' - ' '34番目の文字の矩形を表示すればよい。

今後のことを考え、表示したい文字列と、表示する座標を与えるとその位置に文字列の画像を表示する関数を、myDraw.cppに作成する。(myDraw.hにプロトタイプ宣言を行うこと)

【myDraw.cppに追加する関数】

//-----------------------------------------------------------------------------
// 関数名 : StringDraw()
// 機能概要: 文字列の描画
// 引数  : *str:表示したい文字列の先頭アドレス
//       x, y:転送先座標
//       colorNo:色番号(文字列画像の1段目、2段目、・・・)
//-----------------------------------------------------------------------------
BOOL StringDraw(char *str, DWORD x, DWORD y, int colorNo)
{
    HRESULT     hRet;
    RECT        strRect;
    DWORD       i, j, wx;

    strRect.top = colorNo * 16;
    strRect.bottom = strRect.top + 16;
    for(i = 0; i < strlen(str); i++)
    {
        wx = i * 8 + x;
        j = str[i] - ' ';
        strRect.left = j * 8;
        strRect.right = strRect.left + 8;

        hRet = g_pDDSBack->BltFast(wx, y, g_pDDSString, &strRect, DDBLTFAST_SRCCOLORKEY);
        if (hRet != DD_OK)
            return FALSE;
    }

    return TRUE;
}

【myDraw.h】

//-----------------------------------------------------------------------------
// myDraw.cppの関数のうち、他のソースで利用される関数のプロトタイプ宣言
//-----------------------------------------------------------------------------
HRESULT InitializeDraw(HWND);
void ReleaseDraw(void);
BOOL StringDraw(char* , DWORD , DWORD , int );

※この関数は、string.bmpファイルの画像を利用しているため、その画像にない文字(小文字や日本語など)は出力できないことに注意!!

この関数を、OutputDebugString関数の代わりに使用する。WinMain.cppのUpdateFrame関数を次のように修正する。

【UpdateFrame関数】

//-----------------------------------------------------------------------------
// 関数名 : UpdateFrame()
// 機能概要: 画面更新処理
//-----------------------------------------------------------------------------
static void UpdateFrame(HWND hWnd)
{
    static int fps = 0, wfps = 0;  // FPSカウント
    char buff[80]; // 文字列表示用バッファ

    /* 現在のキー情報を取得 */
    if (!GetKeyboardState(KeyTbl))
        return;

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

    /* 処理の振り分け */
    switch (g_FrameNo)
    {
        case START_INIT:
            StartInit(hWnd);
        case START_FRAME:
            StartFrame(hWnd);
            break;
        case GAME_INIT:
            GameInit(hWnd);
        case GAME_FRAME:
            GameFrame(hWnd);
            break;
        default:
            OutputDebugString("g_FrameNoの値が例外です。\n");
    }

    /* FPSを求めて表示する */
    fps++;
    if (nowTickCount - backTickCount >= 1000)
    {
        wfps = fps;
        backTickCount = nowTickCount;
        fps = 0;
    }
    wsprintf(buff, "%04d FPS", wfps);
    if (!StringDraw(buff, 0, 0, 1))
        return;

    /* フリップ処理 */
        g_pDDSPrimary->Flip(NULL, 0);

}

※FPSの表示がif文の外になったことに注意!!

これにより、画面(0,0)の位置に常にFPSが表示される。スペックの違うマシンでどれくらいのFPSが出るかを確かめてみよう。

■6-4確認問題(必須)

プログラムを修正し、FPSが画面に表示されるかどうかを確認する。


※FPSが常に60の謎

ゲームループは無限ループ、つまり、マシンスペックが高ければ高いほど高速にループする(はずである)。であれば、例えばキャラクタの移動量を1と固定していても、描画スピードが速いマシンと遅いマシンでは移動スピードが違ってしまうことが考えれれる。例えば、VGA(640×480)の画面を移動するキャラクタの移動量が1のとき、画面左から右までの移動時間を考えてみよう。

1.FPSが60ms(1秒間に60回ループ)である場合
640(ドット) ÷ 60 = 10.67 ・・・ 約10秒
2.FPSが100ms(1秒間に100回ループ)である場合
640(ドット) ÷ 100 = 6.4 ・・・ 約6秒

しかし、3で表示したFPSは、どのマシンで実行してもほぼ60である。これは何故だろうか?

フルスクリーンのゲームを作る場合、プライマリ・サーフェイスとバック・バッファを切り替えるためにFlipメソッドを使っている。DirectX7の日本語Helpを見ると、下のほうに次のような記述がある。

注意

IDirectDrawSurface7 の場合、デフォルトでは、このメソッドはアクセラレータが終了するまで待機する。つまり、デフォルト状態では、このメソッドは DDERR_WASSTILLDRAWING を返さない。エラー コードを受け取り、フリップ処理が成功するまで待機しない場合は、DDFLIP_DONOTWAIT フラグを使用する。

このメソッドを呼び出すことができるのは、DDSCAPS_FLIP および DDSCAPS_FRONTBUFFER 能力のあるサーフェスだけである。以前にフロント バッファに関連付けられたディスプレイ メモリは、バック バッファと関連付けられる。

lpDDSurfaceTargetOverride パラメータは、バック バッファがフロント バッファになるべきバッファではないという場合に使用されるが、このような場合はまれである。通常、このパラメータは NULL である。

IDirectDrawSurface7::Flip メソッドは、常に垂直帰線消去と同期する。サーフェスがビデオ ポートに割り当てられていると、このメソッドは表示状態のオーバーレイ サーフェスおよびビデオ ポートのターゲット サーフェスを更新する。

詳細については、「フリッピング サーフェス」を参照すること。

垂直帰線消去とは、モニタのリフレッシュレートのことである。コントロールパネルから画面の設定を選び、モニタの設定を見てみると、リフレッシュレートが60ヘルツに設定されていることが分かる。つまり、Flipメソッドは、実行された瞬間にプライマリ・サーフェイスとバック・バッファを切り替えるのではなく、リフレッシュレートのタイミングで切り替えを行い、それまでは待機するということである。その結果、スペックの違うマシンでも同じスピード(60FPS)でゲーム・ループが回るのである。

よって、フルスクリーンのゲームを作る場合は、スペックの違いによるスピードの変化を意識する必要は無い。ただし、ウィンドウ・モードではFlipメソッドは使わないため、FPSがマシン・スペックに左右されてしまう。ウィンドウ・モードについては後に解説する。

FPSが60以上にはならないは分かったが、60未満になる場合はある。ゲーム中に常に60を下回るようなら、リフレッシュレートよりもゲームループのほうが時間がかかっているということである。一般的に、ゲームはFPSが50〜60で動くように作るのが良いとされている。少なくとも50を下回ることがないように、プログラムを最適化しよう。


[ TOP ]