QMAの自習プログラムを作る 番外編

この記事では特にゲームを作るにあたって知っておくべきことを紹介します。

動きのあるゲームアプリケーションを作るとき

ディスプレイに何かを表示するには、それに対して、どういう表示をするかの信号を送る制御装置が必要です。 それがグラフィックボード(ビデオカードとも)です。 グラフィックボードには VRAM と呼ばれる、 CPU が主に使うメモリとは異なるメモリが載っています。VRAM の V は Video(ビデオ) の略であり、表示に使われるメモリです。 ゲームプログラムは OS を介してゲームの画面をこの VRAM に書き込みます。 するとグラフィックボード上の制御チップがこの VRAM の内容をケーブルを通して ディスプレイに出力します。このようにして画面の表示が行われます。

自作する方ならば分かると思いますが、グラフィックボードは普通独立した 1 枚の基板です。 しかしノートPCや一部のマザーボードは、省スペース化のためにグラフィックボードの機能をそのままマザーボード上に 実装しています。 ですが、仕組みは一緒です。

一般的にゲームの画面は、色々なオブジェクトを重ねていって、1 つの画面を作ります。 アドベンチャーゲームを想定してみましょう。その画面はおおまかに、 背景を描く→キャラクターを描く→メッセージウィンドウやユーザインターフェイスを描く という工程で完成します。 この画面を描画する工程 1 回をフレームと呼びます。 画面に変化をつける度にフレームを描きなおす必要があります。

ちなみに、ビデオカメラで撮影する動画はどのように記録されるのでしょうか? 一部のカメラには連写機能がありますが、これの発展形がビデオカメラです。 非常に短い間隔で、その一瞬一瞬の風景を連続して保存します。 動画を再生する時は、その連続した画像を記録した時と同じ時間間隔で、それこそパラパラ漫画のように画面に映します。 この「一瞬の画像」もやはりフレームです。 テレビも仕組みは一緒で、電波塔から連続したフレームの情報を射出し、家庭のアンテナで受信してテレビ画面に フレームを連続して映しだすことで実現されます。

ゲームでオブジェクトが動いているように見せるには、1 フレームごとにオブジェクトの位置を少しずつずらします。 フレームの描画間隔が短いと、人間の眼にはスムーズに移動しているように見えます。 パソコンでゲームやゲーム系のベンチマークを取る人は次の言葉を聞いたことがあると思います: fps (frames per second)。 fps は 1 秒間で何フレームの描画を行うかを示す単位です。 パソコンの画面描画は一般的に 60 fps、テレビの画面は 29.97 fps(日本のNTSCの場合)、 アニメーションや映画の制作は 24 fps で行われます。

パソコンの画面描画は一般的に 60 fpsで行われていると述べました。 これは誰が決めているのでしょう。プログラム?Windows?

答えはグラフィックボードです。グラフィックボードは正確に 1 秒間に 60 の回数 (つまり 60 Hz) で、 ディスプレイの画面を書き換えるリフレッシュ動作を行います。 このリフレッシュ動作をするタイミングをとることを垂直同期(Vertical Synchronizing, VSYNC)といい、 1 秒あたりのリフレッシュの回数を特にリフレッシュレートといいます。 グラフィックボードは垂直同期のタイミングに基いて VRAM の内容をディスプレイに送り、 ディスプレイはこれを表示します。

ちらつきのない画面描画をする

さてここからが本題です。 先程のアドベンチャーゲームの例を使います。 背景を描く→キャラクターを描く→メッセージウィンドウやユーザインターフェイスを描く …という工程でした。 これを順番に API を使いながら VRAM に書き込んでいきます。

「背景を描く」から「メッセージウィンドウ~を描く」までを全て終えてやっと1フレームです。 この 1 フレームがディスプレイに出力されればいいわけですが、 VRAM からディスプレイに送信されるリフレッシュのタイミングは、 プログラム側の都合を考えずにやってきます。 背景を描き終わった所で、ディスプレイに送信されてしまったらどうなるでしょうか? キャラクターなどを描画していないまま、中途半端な画面をディスプレイに送信してしまいます(下図)。 これがちらつきの原因です。

ちらつきは、プレイヤーにほんの一瞬だけ(リフレッシュレートが60Hzの場合約16.67ミリ秒)オブジェクトが点滅したように見えます。 ちらつきが多いとプレイヤーはイライラしますし、眼にも悪いです。 これを防ぐには「中途半端な状態」がディスプレイに送信されなければいいわけですが、 それにはどうすればよいでしょうか?

1 フレームの描画にかかる時間が長いと、それだけ「中途半端な状態」に リフレッシュのタイミングが当たりやすくなってしまいます。 なので 1 フレームの描画時間をうんと短くすればその確率は減ります。 それでもその確率は 0 にはなりませんし、 より美しいゲーム画面を作るなら描画には時間がかかるもので、 単純に描画時間を短くするのは結構難しいことです。 (もちろん、ハードウェアの性能を格段に上げれば描画時間は短くなります。 しかしそれをプレイヤーに強いるのは良くありません。)

このために良く使われるのがバックバッファリング(ダブルバッファリングとも)という手法です。 プログラムは画面を作るにあたって直接 VRAM に書き込んでいくのではなく、 まずメモリ上に VRAM と互換のあるバックバッファを用意し、 それを VRAM に見立ててゲームの画面を書き込んでいきます。 この描画の最中 VRAM には手を加えないので、中途半端な画面がディスプレイに送られることはなくなります。

バックバッファに 1 フレーム分を書き込んだら、この完成した画面を VRAM にまるごと書き込んでいきます。 もちろん、この VRAM への転送の最中にもリフレッシュ動作が起きるわけですが、 画面の描画に比べたら単なるメモリの転送はわずかな時間で出来るので、リフレッシュに当たる確率は低くなります。 また例え転送中にリフレッシュ動作が当たってしまったとしても、 「切り替わっている途中の画面」は見えてしまいますが、 「作り途中の中途半端な画面」が見えることはありません。

バックバッファから VRAM への転送中にリフレッシュに当たると、 「切り替わっている途中の画面」がディスプレイに送信される、と言いました。 特に動きの激しい画面では切り替わっている途中のその境界が、画面のひび割れのように映ることがあります。 この現象をティアリングと言います。

ティアリングを防ぐ

簡単なティアリング再現プログラムを作ったので、コンパイルしてみてください。
※白い背景に黒の描画と、コントラストがキツいので眼に悪いです。 体調の優れない方は実行しないでください。 また長時間実行しないでください。

#include <Windows.h>

LPCWSTR ClassName = L"wc_tearing";
LPCWSTR ApplicationName = L"ティアリング再現";

LRESULT CALLBACK WndProc( HWND hwnd, UINT msg, WPARAM wp, LPARAM lp ) {
    switch( msg ) {
    case WM_DESTROY:
        PostQuitMessage( 0 );
        break;
    default:
        return DefWindowProc( hwnd, msg, wp, lp );
    }
    return 0;
}

// ウィンドウを準備・表示する
HWND PrepareWindow( HINSTANCE instance, int cmdshow ) {
    // ウィンドウクラスの作成
    WNDCLASSEX windowclass;
    ZeroMemory( &windowclass, sizeof(windowclass) );
    windowclass.cbSize = sizeof(windowclass);
    windowclass.hInstance = instance;
    windowclass.lpszClassName = ClassName;
    windowclass.lpfnWndProc = WndProc;
    windowclass.hbrBackground = (HBRUSH)GetStockObject( WHITE_BRUSH );
    windowclass.hCursor = LoadCursor( NULL, IDC_ARROW );
    windowclass.hIcon = LoadIcon( NULL, IDI_APPLICATION );
    RegisterClassEx( &windowclass );
    HWND mywindow;
    RECT windowrect = { 0, 0, 800, 600 };
    AdjustWindowRect( &windowrect, WS_CAPTION|WS_BORDER|WS_SYSMENU, FALSE );
    mywindow = CreateWindow( ClassName,
        ApplicationName,
        WS_CAPTION|WS_BORDER|WS_SYSMENU,
        CW_USEDEFAULT, CW_USEDEFAULT,
        windowrect.right-windowrect.left, windowrect.bottom-windowrect.top,
        NULL, NULL, instance, NULL );
    ShowWindow( mywindow, cmdshow|SW_SHOW );
    return mywindow;
}

int WINAPI wWinMain( HINSTANCE ins, HINSTANCE prev, LPWSTR cmd, int show ) {
    HWND window;
    MSG msg;
    window = PrepareWindow( ins, show );
    while( true ) {
        if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) ) {
            if( msg.message == WM_QUIT ) break;
            TranslateMessage( &msg );
            DispatchMessage( &msg );
        } else {
            HDC hdc;
            hdc = GetDC( window );
            HDC myhdc;
            // バックバッファを作成
            myhdc = CreateCompatibleDC( hdc );
            HBITMAP mybitmap;
            mybitmap = CreateCompatibleBitmap( myhdc, 800, 600 );
            SelectObject( myhdc, mybitmap );
            SelectObject( myhdc, GetStockObject( WHITE_BRUSH ) );
            // 画面をクリア
            Rectangle( myhdc, 0, 0, 800, 600 );
            SelectObject( myhdc, GetStockObject( BLACK_BRUSH ) );
            // 黒い縦棒を描画
            for( int i=0; i<800; i+=100 )
                Rectangle( myhdc, (GetTickCount()/2+i)%800, 0, (GetTickCount()/2+i)%800+20, 600 );
            // フロントへ転送
            BitBlt( hdc, 0, 0, 800, 600, myhdc, 0, 0, SRCCOPY );
            DeleteObject( mybitmap );
            DeleteDC( myhdc );
            ReleaseDC( window, hdc );
            Sleep( 16 );
        }
    }
    return msg.wParam;
}

Windows Vista 以降を使用中の方は、テーマを変更して Windows Aero を切ってください。 Aero は垂直同期制御(後述)を行うので、ティアリングが生じません。

これは黒い縦棒を右方向に動かしているのですが、 時々画面の上下で黒い棒がずれるのが見えると思います。 バックバッファからVRAMに書き込んでいる途中は、 VRAM に新しく書き込んだフレームの部分と、1 つ前の古いフレームの部分が共存している状態になります。 この時点でリフレッシュ動作が起こると、 画面の上下で(行単位で順番に書き込むので)フレームが違ったままディスプレイに送信されてしまいます。 この結果、画面の上下でずれて表示されます。

これを防ぐには、リフレッシュ動作が起こるタイミングを調べて、 バックバッファから VRAM へ転送する動作とリフレッシュ動作とが重ならないようにします(垂直同期を取る)。

残念ながら、GDI では垂直同期を取ることはできないようです。 これを行うには OpenGL か Direct3D を使う必要があります。 そもそもゲームを作る場合はこれらのグラフィック API を使うことが多く、 GDI ではごく普通の描画機能しかサポートされていません。

(書き途中)

滑らかなアニメーションにするには

(書き途中)

プレイヤーの入力が画面に反映されるまで

(書き途中)

3D の描画をしたい時

(書き途中)



作成日 2011/02/25 ... 最終更新日 2011/02/25
by zeroichi

QMAは株式会社コナミデジタルエンタテインメントの登録商標です