DirectX でゲームを作ってみよう
プログラム開発者向けのページです。
ここに書いてある内容は自分で試行錯誤した結果です。
決してここに書かれているやり方が正解とは限りませんので、
他のサイトなどもいろいろと調べてプログラムしてください。

第 2 回 初期化とダブルバッファリング

§ダブルバッファリングとは
ここではダブルバッファリングについて解説します。
ゲームの内部処理には、例えばシューティングゲームなら自機の移動(キー判定)、敵機の移動、当たり判定、画面描画、音楽・効果音の再生などなどの処理がありますが、この中でもっとも負荷の高い(処理時間の掛かる)ものは画面描画です。

シューティングゲームでは自機、敵機、弾、背景、爆発、得点などを描く必要がありますが、これらをいかに素早く描画するかがプログラマーの腕の見せ所になるわけです。
が、最近の PC は高速なので 2D 描画ならそれほど高速化を考えなくてもゲームに支障の無い程度の描画速度は得られます。

で、ダブルバッファリング。
これは高速描画のための手法というよりも画面のちらつきを抑えるための手法です。
まぁ DirectDraw に限らず、ゲーム製作では当然というくらい一般的な手法です。

どういうものかというと、表示される画面(表画面)と全く同じサイズと色深度を持った見えない裏画面を用意して、その裏画面に自機や敵機、背景などを描画します。描画が完了したら、この裏画面の内容を表画面へ転送します。
これによって描画の過程が画面に表示されなくなるためちらつきを抑えられます。

表画面のことをプライマリ・サーフェス、裏画面のことをセカンダリ・サーフェスやバック・サーフェスと呼んだりします。

このダブルバッファリングを使わずに直接プライマリ・サーフェスへ描画すると、背景を描いて次に自機を描く場合、その過程で自機の下にある背景が一瞬見えてしまいこれがちらつきになります。

一通り画面を描き終えてから"転送"という言葉を使いましたが、これをフリップといって実際にはプライマリ・サーフェスを指すポインタをセカンダリ・サーフェスの先頭を指すように入れ替えるだけなのでメモリ間の転送は行われず処理時間はほぼゼロで済みます。

フリップは DirectDraw の持つ Flip 関数で行います。DirectDraw のフリップは VSYNC 信号( 垂直帰線期間 )に合わせてプライマリ・サーフェスとセカンダリ・サーフェスを入れ替えるのでちらつきが発生しません。
実はこのフリップに DirectDraw の落とし穴があるのですが、詳細は第 5 回で解説しています。

//  フリップ
lpPrimarySurface->Flip(NULL, DDFLIP_WAIT);

§ウィンドウの生成
では、実際のプログラムへと移ります。
まず、やらなければいけないのが初期化です。初期化には大きく分けてウィンドウの生成と DirectX の初期化という2つの作業があります。

まずウィンドウの生成です。
ウィンドウ内で動作するゲームはもちろん、フルスクリーンで動作するゲームを動かす場合でもウィンドウを生成する必要があります。

ウィンドウの生成は通常のウィンドウズアプリのウィンドウ生成と大きな違いはありませんが、CreateWindowEx 関数で、第 1 パラメータ( dwExStyle )に WS_EX_TOPMOST を指定して、第 7・第 8 パラメータ( nWidth と nHeight ) に GetSystemMetrics(SM_CXSCREEN)、GetSystemMetrics(SM_CYSCREEN) を指定してください。
もちろんメッセージループも必要です。

ウィンドウ生成処理とメッセージループは次のような感じです。
メッセージループ( MainThread 関数)では、メッセージを受信しなかった時に UpdateFrame 関数を呼びます。
この UpdateFrame 関数の中で描画処理を行います。

なお、下のソースの中に出てくる、Init、MainThread、UpdateFrame、DirectDrawInit 関数は独自の( DirectX APIではない )関数です。

#define APP_NAME    "DDTEST"
#define APP_TITLE   "DirectDraw Test"

//  プログラムの初期化(ウィンドウ作成など)
BOOL Init(HINSTANCE hInstance)
{
    hInst = hInstance;

    //  ウィンドウクラスの登録
    WNDCLASS wc;
    wc.style = CS_HREDRAW | CS_VREDRAW; // | CS_DBLCLKS;
    wc.lpfnWndProc = (WNDPROC)WndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = sizeof(DWORD);
    wc.hInstance = hInst;
    wc.hIcon = NULL;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
    wc.lpszMenuName = NULL;
    wc.lpszClassName = APP_NAME;

    if(!RegisterClass(&wc)){
        //  ウィンドウクラスの登録に失敗しました
        return FALSE;
    }

    //  ウィンドウの生成
    hWndMain = CreateWindowEx(WS_EX_TOPMOST,
        APP_NAME, APP_TITLE,
        WS_POPUP | WS_VISIBLE,
        0, 0,
        GetSystemMetrics(SM_CXSCREEN),
        GetSystemMetrics(SM_CYSCREEN),
        NULL, NULL, hInst, NULL);
    if(!hWndMain){
        //  ウィンドウの生成に失敗しました
        return FALSE;
    }

    //  DirectDrawの初期化
    if(!DirectDrawInit(hWndMain)){
        //  DirectDrawの初期化に失敗しました
        return FALSE;
    }

    //  準備ができたら描画
    ShowWindow(hWndMain, SW_SHOW);
    UpdateWindow(hWndMain);

    return TRUE;
}

//  メッセージループ
int MainThread(void)
{
    MSG     msg;

    while(1){
        if(PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)){
            if(!GetMessage(&msg, NULL, 0, 0)){
                break;
            }
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else{
            UpdateFrame();
        }
    }

    return msg.wParam;
}

§DirectDraw の初期化
続いて DirectX の初期化です。
初期化の手順は、(1)DirectDraw7 オブジェクトの作成 (2)画面モードの設定 (3)画面サイズと色深度の設定 (4)プライマリ・サーフェスの作成 (5)セカンダリ・サーフェスの作成 となります。

初期化処理は次のようなコードになります。

//  DirectDrawの初期化
BOOL DirectDrawInit(HWND hWnd)
{
    HRESULT hr;

    //  (1)DirectDraw7オブジェクトの作成
    hr = DirectDrawCreateEx(NULL, (void**)&lpDD, IID_IDirectDraw7, NULL);
    if(hr != DD_OK){
        //  DirectDraw7オブジェクトの生成に失敗しました
        return FALSE;
    }

    //  (2)画面モードを設定する
    hr = lpDD->SetCooperativeLevel(hWnd, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN);
    if(hr != DD_OK){
        //  画面モードの設定に失敗しました
        RELEASE(lpDD);
        return FALSE;
    }

    //  (3)画面サイズと色深度を設定する
    hr = lpDD->SetDisplayMode(640, 480, 16, 0, 0);
    if(hr != DD_OK){
        //  画面サイズの設定に失敗しました
        RELEASE(lpDD);
        return FALSE;
    }

    //  (4)プライマリサーフェースの作成
    DDSURFACEDESC2 ddsd;
    ZeroMemory(&ddsd, sizeof(ddsd));
    ddsd.dwSize = sizeof(ddsd);
    ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
    ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE |
                          DDSCAPS_FLIP | DDSCAPS_COMPLEX;
    ddsd.dwBackBufferCount = 1;
    hr = lpDD->CreateSurface(&ddsd, &lpPrimarySurface, NULL);
    if(hr != DD_OK){
        //  プライマリーサーフェスの作成に失敗しました
        RELEASE(lpDD);
        return FALSE;
    }

    //  (5)セカンダリ・サーフェスの作成
    DDSCAPS2 ddscaps;
    ZeroMemory(&ddscaps, sizeof(ddscaps));
    ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
    hr = lpPrimarySurface->GetAttachedSurface(&ddscaps, &lpBackSurface);
    if(hr != DD_OK){
        //  セカンダリ・サーフェスの作成に失敗しました
        RELEASE(lpPrimarySurface);
        RELEASE(lpDD);
        return FALSE;
    }

    return TRUE;
}

上のソースの解説です。

(1)DirectDraw7 オブジェクトの作成
ここでは DirectDrawCreateEx 関数を使って DirectDraw7 オブジェクトを作成します。このオブジェクトを作成しない事には他の DirectDraw の命令( 関数 )が使えません。
第 1 引数は NULL。これで標準のディスプレイドライバを選択します。通常は NULL で問題ありません。
第 2 引数は DirectDraw オブジェクト( へのポインタ )を保持する変数のアドレス。
第 3 引数は IID_IDirectDraw7 と書かなければいけません。
第 4 引数は NULL を指定します( 将来の互換のためのパラメータのため )。
なお、この DirectDrawCreateEx 関数に限りませんが、DirectDraw 関連の関数は HRESULT 型の戻り値を持ち、正常終了の場合は DD_OK を返します。

(2)画面モードの設定
ここではウィンドウをどのように表示するかを設定します。これはウィンドウモードで表示するか、フルスクリーンモードで表示するかでパラメータが異なります。

(3)画面サイズと色深度の設定
この関数で画面のサイズ(ピクセル数)と色深度を設定します。上のソースでは 640x480/65536 色(16bit) に設定しています。
画面のサイズは、640x480 や 800x600、1024x768 などがあります。大きくすれば細かい描画が可能になりますが、その分処理は重たくなりますし、VRAM の容量が必要なため古いマシンではサポートされていない場合もあり注意が必要です。

色深度は画面で使える色数です。通常、256 色(8bit)、65536 色(16bit)、16777216 色(24bit) があり、ビットの数で指定します。色数が増えればそれだけ綺麗な絵を表示できますがデータ量が多くなり処理が遅くなる場合があります。また、ビデオカードによっては、画面サイズが 800x600 で 65536 色はサポートしているが、800x600 でも 16777216 色はサポートしていないなど設定できない組み合わせもあります。

(4)プライマリ・サーフェスの作成
サーフェスの作成は CreateSurface 関数を使います。
この関数を呼ぶ前に DDSURFACEDESC2 構造体に作成するサーフェスの情報をセットしておきます。
プライマリ・サーフェスを作成する場合の DDSURFACEDESC2 構造体には、dwFlags パラメータに DDSD_BACKBUFFERCOUNT を指定してセカンダリ・バッファを持つことを明示して、ddsCaps.dwCaps ではプライマリ・サーフェスを作成する事とフリップを使う事を明示します。

(5)セカンダリ・サーフェスの作成
次にセカンダリ・サーフェスを作成します。
プライマリ・サーフェス作成時にはいろいろとパラメータを指定しましたが、セカンダリ・サーフェスはプライマリ・サーフェスの分身を作るようなものなのでパラメータは少なく、プライマリ・サーフェスにアタッチする( GetAttachedSurface 関数を呼ぶ)だけでセカンダリ・バッファが作成されます。

§サンプルはこちら
大雑把な解説をしましたが、あまり深く考えずにエラーが発生せずに起動したら良しというくらいの気持ちでいいかもしれません。
それよりも生成したオブジェクトは、使い終わったら忘れずに解放してください。DirectX8 だとメモリの解放忘れはデバッグ情報で教えてくれるようなのでそれを活用するのもいい方法です。

今回からサンプルプログラムを載せます。
今回のサンプルではウィンドウ生成と DirectDraw の初期化の後、DirectDraw サーフェスに対して GDI で文字を書きます。
この sample1.lzh には実行ファイル( sample1.exe )とソースとヘッダーが格納されています。コンパイルをする際には dxguid.lib と ddraw.lib をリンクしてください。
なお、このサンプルは ESC キーで終了します。

( 15.5KB )