ゆとりーなの日記

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

英語嫌いっぷりが異常だからという理由で精神科に連れて行かれたということは内緒

前々回ゲームにおけるメイン関数の書き方について審議したような感じがしますが、今回はゲームメインループをどうするかについて審議することになる感じです。
win32を使ったアプリケーションでは、多分例外なく次のようなメッセージループを書く必要があります。

    MSG message;
    for (;;) {
        const BOOL result(GetMessage(&message, nullptr, 0, 0));
        if (!(result && ~result)) {
            break;
        }
        TranslateMessage(&message);
        DispatchMessage(&message);
    }

これにゲームの処理を組み込むことになるわけです。
上の様なメッセージループはゲームにはそのまま使えないことは以前にも触れたことがあると思います。簡単に触れるとGetMessage関数がウィンドウに対してなんらかのメッセージが来るまで待機してしまうので、60FPSで動くゲームを作ろうとすると一工夫必要になります。
上のメッセージループの形を守った形で書くとすれば、それはマルチスレッドで書くということになると思います。

案1 マルチスレッド

int run() {
    MSG message;
    for (;;) {
        const BOOL result(GetMessage(&message, nullptr, 0, 0));
        if (!(result && ~result)) {
            break;
        }
        TranslateMessage(&message);
        DispatchMessage(&message);
    }
    return message.wParam;
}

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
    // boost.threadなり__beginthreadexなりを使ってゲームのメインループ関数を実行
    const int end_code(run());
    // スレッドを適切に終わらせる
    return end_code;
}

マルチスレッド作戦で書くとメインループは大体こんな感じになると思います。この方法だと、メッセージループとゲームのメインループが完全に独立した形になるので、ウィンドウにメッセージが飛んできた時もゲームが止まらないという特徴があります。例えばウィンドウを動かしている間もゲームのアニメーションは止まらないといった演出が可能です。
私の見解としては、マルチスレッドは人間の頭で処理するには少々高等過ぎる概念であると思っているので、どうしても使わなければいけないという場合を除いては使うべきではないと考えております。今回の場合、上の様な効果の為だけににマルチスレッドを使う必要があるかといえば少々疑問が残ります。マルチスレッドがらみのバグはそれだけ困難で深刻なんです。
そこでやはりここらへんはシングルスレッドで耐えようという話になります。シングルスレッドでゲームメインループを実行するためには、メッセージループに埋め込んでやる必要があるでしょう。ただ埋め込んでもGetMessage関数が律儀に休んでしまうので、PeekMessage関数でメッセージがあるかを調べてあるときだけGetMessage関数が仕事をするように仕向けてやります。PeekMessage関数は勝手に休まない真面目な子なんです。

    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);
        }
    }

シングルスレッドで行く場合メッセージループの雛型は大体こんな感じになると思います。あとはゲームメインループをこいつにどう埋め込むかという問題になってきます。

案2 仮想関数

applicationクラスなるものを作ってこの子のrunメソッドからvirtualなupdateメソッドを呼び出すという戦法がこれです。

class application {
 public:
    applicaion() {}
    virtual ~application() {}
    int run() {
        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;
    }
 private:
    virtual void update() {}
}

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
    application app;
    return app.run();
}

各ゲームはapplicationクラスを継承してそれぞれupdateメソッドをオーバーライドする的な感じにしておけば、それなりに再利用もできる感じはします。
この方法に対する個人的な不満は、最近仮想関数がマイブームではないこと、クラスの継承は意外と面倒ということと、インスタンスを生成することにそこまで意義を感じられないというところです。普通この手のクラスのインスタンスは一つしか作りませんからね。むしろ作らせたくないくらいです。しかしシングルトンにするとこの手の手法は使えなくなります。正直仮想関数を使いたいがために無理矢理クラスにしているという感じがするわけです。

案3 無限ループを書かせる

run内で無限ループを書かないで、無限ループはユーザーコードに書いてもらうという戦法です。

bool run() {
    MSG message;
    if (PeekMessage(&message, nullptr, 0, 0, PM_NOREMOVE)) {
        const BOOL result(GetMessage(&message, nullptr, 0, 0));
        if (!(result && ~result)) {
            return false;
        }
        TranslateMessage(&message);
        DispatchMessage(&message);
    }
    return true;
}

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
    while (run()) {
        // ゲームメインループを書く
    }
    return 0;
}

このタイプで書くと、メイン関数に書くコードがゲームメインループとして自然な形で書ける気がします。しかしこの手法だと、WinMain関数が返す値はmessage.wParamでなければならないということを守るには少々複雑なコードを書かなくてはいけなくなります。正直殆ど0が返ることになるので気にしなくてもいいといえばいいかもしれませんが、ここらへんはきちんとしたいところです。

案4 関数オブジェクトを渡す

run関数にゲームメインループ関数オブジェクトを渡してやるという戦法です。

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([]() {
        // ゲームメインループを書く
    });
}

このタイプで書くと、メイン関数に書くコードがゲームメインループとしてそれなりに自然な形で書け、且つWinMain関数が返す値はmessage.wParamでなければならないということも簡単に守れます。関数オブジェクトを渡す形になっているので、0x非対応でもひとまずなんとかなるのもいい感じです。取り敢えずこの形が現状では一番いいのかなと思っている次第です。