ゆとりーなの日記

日記的な事を書いて行くと思はれる

親切って裏目ることもあるよね。

そうです。C++には人生のいろはが集約されているのです。前回ゲームメインループの審議をしたので今回は描画に関する審議です。ゲームメインループはrun関数に関数オブジェクトを渡す形式を採用したとして話を進めます。
Direct3D9を使う場合は、描画を行うう部分はIDirect3DDevice9::BeginSceneメソッドとIDirect3DDevice9::EndSceneメソッドで囲われている必要があり、実際に画面に出すにはIDirect3DDevice9::Presentメソッドを呼ぶ必要があります。これらの操作を行ってくれる関数graphic::begin_scene、graphic::end_scene、graphic::presentを用意したとすると、これを最も親切にラップしてやると、次のようになる気がします。

int run(const std::function<void ()> &update) {
    MSG message;
    for (;;) {
        if (PeekMessage(&message, nullptr, 0, 0, PM_NOREMOVE)) {
            const BOOL result(GetMessage(&message, nullptr, 0, 0));
            if (!(result && ~result)) {
                break;
            }
            TranslateMessage(&message);
            DispatchMessage(&message);
        } else {
            graphic::begin_scene();
            update();
            graphic::end_scene();
            graphic::present();
        }
    }
    return message.wParam;
}

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
    return run([]() {
        // 描画が出来る
    });
}

run関数内、run関数内から飛んでいく場所であればどこでも何もしなくても描画が可能になる体系です。非常に便利で親切そうに見えるこの系ですが、run関数外で描画を行うと当然落ちます。何もしなくても描画出来るという便利さに慣れて、全然関係ないところで描画を行ってなんか知らないけど落ちるという現象に出会ったとき、原因の特定が難しくなるのではないかと思うのです。
一方自分でgraphic::begin_scene、graphic::end_scene、graphic::present関数呼び出しを書かせる方式にすると、

int run(const std::function<void ()> &update) {
    MSG message;
    for (;;) {
        if (PeekMessage(&message, nullptr, 0, 0, PM_NOREMOVE)) {
            const BOOL result(GetMessage(&message, nullptr, 0, 0));
            if (!(result && ~result)) {
                break;
            }
            TranslateMessage(&message);
            DispatchMessage(&message);
        } else {
            update();
        }
    }
    return message.wParam;
}

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
    return run([]() {
        graphic::begin_scene();
        // 描画が出来る
        graphic::end_scene();
        graphic::present();
    });
}

こうなります。自分で書かなければいけない分手間が増えた様に感じられますが、自分で書く分これらの関数呼び出しで囲われていなければいけないという意識が刷り込まれてバグの原因の特定がしやすくなる様な感じがします。一応この方式にしておけば、run関数外部でも描画は可能になるわけですし。一方run関数内部でgraphic::begin_scene、graphic::end_scene、graphic::present関数呼び出しを行ってやる方式にする場合は、ユーザーコード側との呼び出だしと干渉しないようにこれらの関数を非公開にしたくなることが多くなる気がするので、run関数外部でも描画は難しくなるのではという感じがします。
最後にもうひとパターン紹介して今回は〆ようと思います。run関数からgraphic::begin_scene、graphic::end_scene、graphic::presentメソッドによりきちんと管理された状態の描画オブジェクトを渡すというパターンです。

int run(const std::function<void ()> &update) {
    MSG message;
    for (;;) {
        if (PeekMessage(&message, nullptr, 0, 0, PM_NOREMOVE)) {
            const BOOL result(GetMessage(&message, nullptr, 0, 0));
            if (!(result && ~result)) {
                break;
            }
            TranslateMessage(&message);
            DispatchMessage(&message);
        } else {
            graphic_device graphic;
            graphic.begin_scene();
            update(&graphic);
            graphic.end_scene();
            graphic.present();
        }
    }
    return message.wParam;
}

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
    return run([](graphic_device * const graphic) {
        // graphic経由で描画が出来る
    });
}

この方式では、ゲームメインループに管理されたが描画オブジェクト渡されてくるのでこのオブジェクトを使って描画してやることになります。引数としてオブジェクトを渡してやることにより、他所では描画は出来ないんだよということを暗に示してやることが出来る気がします。しかしこの方法では、run関数から飛んでいった先で描画を行うためには描画オブジェクトをいちいち引数として渡してやらなくてはいけないのと、グローバルなところに保存して使いまわすという輩が現れた時にどうしようもないという問題があります。
何だかんだ言って今回の様な場合はgraphic::begin_scene、graphic::end_scene、graphic::present関数を明示的に呼ばせるのがいい気がするのですがいかがでしょう。