最初の準備

もくじへ戻る


Win32APIな話である.

その昔, Windows 98SE 上で初めて Win32API なプログラムを書いたときには
「中身が何もないウィンドウを表示するだけで1日潰れました.吐きそう」
みたいなちょっとした地獄だった記憶があるのだが,その点,現代の Visual Studio 様ならば 「プロジェクトを作って」とお願いしさえすれば最初からウィンドウが表示されるひな形コードを用意してくれる.慈愛に満ちている.

なので,ゲーム作りを始めるにあたってとりあえず最初にやるべきことは

くらいであろう.

※もちろん「メッセージドリブンでメッセージポンプがウィンドウプロシージャうんぬんで,描画処理は WM_PAINT でどうの」みたいな話に関する最低限の知識は必要.
(そのあたりについては,私は上記の吐きそうな経験をした際にある程度学び済みなので,そういった前準備はとりあえず不要.)


多重起動は防止しとくか

ゲームなら Save/Load みたいな処理とかがあるだろうから「複数起動して→そういった処理が同時に…!」とかいう地獄を避けたい気がする.
具体的な方法は……もちろんググって出てきた話をパクる.OK.

int APIENTRY wWinMain( /*略*/ )
{//こんな感じのをかけばいいらしい
    //---多重起動の防止措置
    HANDLE hPreventMultiRunEvent = CreateEventW( NULL, FALSE, FALSE, L"X102_Prevent_multi_run" );

    if( hPreventMultiRunEvent == NULL )
    {   return Err( L"処理に失敗しました", L"多重起動チェック" );    }

    if( GetLastError() == ERROR_ALREADY_EXISTS )
    {
        CloseHandle( hPreventMultiRunEvent );
        hPreventMultiRunEvent = NULL;
        return Err( L"既にAPPが実行中です", L"多重起動チェック" );
    }
    //---

    int Result = MainLoop();  //この中でウィンドウ作ってメインループ回す.まぁわざわざ別関数にしなくても良い気もするが.

    //---多重起動の防止措置
    CloseHandle( hPreventMultiRunEvent );
    //---

    return Result;
}

Err() はどこかにエラーメッセージを出す関数.


メインループでは Sleep 使うのか問題

PeekMessage() とゲーム処理を激しくビジーループするというのでは困るだろうから,ループ内に「Sleep的なもの」を入れる必要があるだろう.
で,それには実際に Sleep() を用いるので良いのであろうか?

Sleep() だとガチで俺のプログラムがその間停止するわけで,その間にメッセージが来ても反応できないということになるハズだけど,いいの?っていう話.
まぁ実際問題,それでメッセージ処理への着手が 10[ms] とか遅れたところで,別にどうでもいいレベルの話なのかもだけど,仮に
Sleep() な感じで指定時間だけ寝るんだけど,その期間内にメッセージが来た場合には即処理を返すよ!」
みたいな何かが存在するとしたら,そういうのを使いたい気がするじゃん?

……とかなんとかでググってみたところ,どうやら MsgWaitForMultipleObjects() なるAPIがそういう物であるらしいので,これを Sleep() の代わりに使ってみることにする.

とりあえず寝る時間を決定するために,経過時間を計測するためのものをさくっと用意し……

//Start()からEnd()までの時間[ms]を計測するだけのもの
#include <chrono>
class CTimeMeasure
{
public:
    CTimeMeasure(){ Start();    }
    void Start(){   m_ST = std::chrono::system_clock::now();    }
    double End()
    {
        auto Curr = std::chrono::system_clock::now();
        return std::chrono::duration< double, std::milli >( Curr - m_ST ).count();
    }
private:
    std::chrono::system_clock::time_point m_ST;
};

いや,これ,とても「さくっと」とか言えるレベルではないぞ.
<chrono> とかいうやつ,リファレンス的なの読んでも使い方が微塵も理解できないんですが!(俺だけなのか?)
これでいいの? これで合ってるの?

まぁ上記のようなのでなんとなく時間を測れるのだとして……
MainLoop() の中を↓のような感じに書いてみた.とりあえずこんな感じの実装で様子を見ていくことにする.

int MainLoop()
{
    //ウィンドウ生成あたり
    //(略)CreateWindowEx() だとかそういう
    
    //ゲーム用の初期準備処理とか
    MyGame Game;  //ゲームの実装は MyGame というクラスなのだとして…
    if( !Game.Init( /*必要な引数*/ ) )return 1;  //メインループに入る前に何か初期に必要な準備みたいなのをやる

    //メインループ
    constexpr double MaxWait_ms = 33;
    double Wait_ms = 1.0;
    CTimeMeasure CTM;
    bool ShouldQuit = false;
    do
    {
        CTM.Start();
        if( ::MsgWaitForMultipleObjects( 0, NULL, FALSE, (DWORD)std::round(Wait_ms), QS_ALLINPUT ) == WAIT_FAILED )
        {   return Err( L"MsgWaitForMultipleObjects() returns WAIT_FAILED" );   }

        MSG msg;
        while( ::PeekMessageW( &msg, NULL, 0,0, PM_REMOVE ) != 0 )
        {
            if( msg.message == WM_QUIT )
            {   return 0;   }

            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }

        Wait_ms -= CTM.End();
        if( Wait_ms <= 0 )
        {
            CTM.Start();
            
            if( !Game.Update( /*必要な引数*/ ) )return 0; //ゲームの処理
            
            Wait_ms = std::max( 1.0, MaxWait_ms - CTM.End() );
        }
    }while( !ShouldQuit );
    
    return 0;
}

とりあえずこのくらいで, Game.Update() が定期的(というわけでもないだろうが)に呼ばれるという枠組みができたわけだ.
あとは MyGame クラス内にゲーム処理を実装していけばよい的な話になる.

//メインループ側から見れば,こんな仕様
class MyGame
{
public:
  //初期処理.メインループに入る前に1度だけ呼ばれるべき.
  //戻り値は成否:
  //falseが返された場合,メインループを開始せずにAPPを終了させるべきことを示す.
  bool Init( /*必要な引数*/ );
  
  //ゲームの処理をちょっと進める.メインループ内から繰り返し呼ばれるべき.
  //表示の更新(描画)もこの中でやる.
  //戻り値は続行するか否か:
  //falseが返された場合,そこでメインループから抜けてAPPを終了させるべきことを示す.
  bool Update( /*必要な引数*/ );
};