15:タスク処理を実装する(5)

 Direct3Dの描画処理タスクを作成し、リストに追加します。描画開始処理の実行優先度は0、描画終了処理の実行優先度はINT_MAXにし、敵キャラなど描画に必要なデータの優先度は1~(INT_MAX - 1)の範囲とします。関数の中でCGameObject派生クラスを直接
new
で作っても全く問題ありません。autodelete引数を
true
にしておけば、削除はプログラムが勝手にやってくれます。

 では、「GameMain.h」「GameMain.cpp」の2つのファイルを新規にプロジェクトに追加し、描画開始処理と終了処理を行うためのクラスを作成しましょう。
ウェイト処理を用意する
 家庭用テレビが1秒間あたり60回の表示更新を行うこともあってか、たいていのゲームは1秒間に付き60回、つまり約16ms(=0.016秒)につき1回描画を行うような設計になっています。しかし、単純に指定時間だけ待機するウィンドウズ命令「Sleep(16);」を記入しただけでは不都合が起こります。例えば、ゲーム処理の計算に10msかかったとしたら、実際に画面が更新されるのは「10ms + Sleep(16) = 26ms」後になり、コンピュータにそれほど負担がかかっているわけでもないのに、処理落ちしたかのような挙動になってしまいます。

 これを解決するには、処理計算にかかった時間を取得して、ゲーム標準の待機時間から差し引けばよいことになります。
    const DWORD WAIT_TIME = 16;    // 約60FPS

    //最後に行った処理からの時間を調べる
    DWORD ntime = timeGetTime();    // システムが起動してからの時間を取得
    DWORD rtime = ntime - lasttime;
    lasttime = ntime;
    if(rtime < WAIT_TIME){
        //ウェイト処理を行う
        Sleep(WAIT_TIME - rtime);
    }
 しかしながら、Sleepで指定した待ち時間はあいまいで、時と場合によっては、指定した時間の倍以上の待機が行われることもあります。この待ち時間を厳密にするには、WinMain関数のはじめに次の命令を追加してください。
timeBeginPeriod(1);
 これにより、時間計測の厳密さが1ミリ秒単位に変更されるので、正確なウェイト処理が行われるようになります。

描画開始・終了処理を用意する
 おおまかにすると、Direct3Dによる描画に必要な一連の命令は以下のようになります。
  • Clear()で描画済みのデータを消去します。
  • BeginScene()で、バックグラウンドにてシーンの描画を始めます。
  • ID3DXSprite::Draw()などをつかって、描画を行います。
  • EndScene()でシーンの描画を終わります。
  • Present()でウィンドウに描画結果を転送し、実際に表示されます。
 これをタスク処理に割り当てると、描画開始時に「Clear()」と「BeginScene()」、描画終了時には「EndScene()」と「Present()」を行えばよいことになります。これらをふまえてクラスを作成した結果がこちらです。
#pragma once

#include "GameObject.h"

class CTaskHead : public CGameObject
{
private:
    DWORD lasttime;
protected:
    void Init();
    void Exec();
};

class CTaskTail : public CGameObject
{
protected:
    void Exec();
};
#include "GameMain.h"

void CTaskHead::Init()
{
    lasttime = 0;    // 値の初期化
}

void CTaskHead::Exec()
{
    const DWORD WAIT_TIME = 16;    //約60FPS

    //最後に行った処理からの時間を調べる
    DWORD ntime = timeGetTime();
    DWORD rtime = ntime - lasttime;
    lasttime = ntime;
    if(rtime < WAIT_TIME){
        //ウェイト処理を行う
        Sleep(WAIT_TIME - rtime);
    }

    //画像のクリア
    pD3Ddevice->Clear(
        0,                        // クリアする領域の配列個数
        NULL,                    // クリアする領域の配列
        D3DCLEAR_TARGET,        // 対象を指定の色でクリアする
        D3DCOLOR_XRGB(0,0,64),    // クリアする色を紺色に指定
        1.0f,                    // z方向のクリア(1.0fですべてをクリア)。
        0                        // ステンシルバッファのクリア(使用していないので0を指定)
    );

    //開始宣言
    pD3Ddevice->BeginScene();
}

void CTaskTail::Exec()
{
    // 表示
    pD3Ddevice->EndScene();
    pD3Ddevice->Present(
        NULL,    // 転送元の領域(NULLで全域指定)
        NULL,    // 転送先の領域(NULLで全域指定)
        NULL,    // 転送先のウィンドウを示すハンドル(NULLで標準のウィンドウ)
        NULL    // 現行バージョンでは常にNULLを指定
    );
}
 この2つのクラスをWinMain()内において、初期化処理の次の行あたりでタスクリストに追加します。
    HWND hWnd = CreateWindow(
        wc.lpszClassName,
        _T("クラスまみれのゲームプログラミング入門"),
        WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
        CW_USEDEFAULT, CW_USEDEFAULT, 640, 480,    
        NULL, NULL, hInstance, NULL );

    CGameObject game;
    game.Initialize(hWnd, hInstance);

    game.AppendObject(new CTaskHead(), 0, true);
    game.AppendObject(new CTaskTail(), INT_MAX, true);

    ShowWindow(hWnd, nCmdShow);
 こうしてできたプログラムをコンパイル(timeGetTime()を有効にするため「winmm.lib」のリンクを忘れずに!)して実行してみましょう。紺色の画面が延々と表示され続けていたら成功です。一見静止しているように見えますが、裏では1秒間に60回ものスピードで画面が更新されているんですねぇ。