役割ごとにスレッドを分けよう

前置き

私のプログラムは、基本的に3つにスレッドに分かれています。
WinMainから始まる「ウインドウメッセージ処理スレッド」
定期的に入力デバイスを監視し続ける「入力スレッド」
1ループするたびにゲームを1フレーム更新する「ゲームスレッド」
 
これについて少し解説をしたいと思います。
なので今回はスレッドクラスを実装してみました。
 
また、今後このプログラムを基本として機能を拡張していきます。
なので、今までのクラスも全て含めてstatic link libraryにしておきました。

ウインドウメッセージ処理スレッド

int APIENTRY _tWinMain(略)
{
	!ウインドウ生成

	!ゲームスレッドを生成し、スタートさせる

	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	!アプリが終了するので、ゲームスレッドを停止させる
}

これしかしません。
おとなしくウインドウメッセージのみを処理していてね。
 
何故これを独立させるかと言いますと、もしここで処理を行おうとすると「ウインドウをドラッグしている間処理ができない」という状況になります。
ツールとかだとそれでも良いのですが、ゲームだと少しおかしいですよね?

入力スレッド

void run()
{
	while(true)
	{
		!16.666msが経過するまで待ちます

		!現在の入力デバイスの情報を取得。入力履歴dequeに貯えます
	}
}

単純ですね。
入力デバイスの監視を、ゲームスレッドと同じスレッドでしているとします。
もしそうしてしまうと、ゲームスレッドが処理落ちした場合に、入力デバイスの監視も一緒に処理落ちしてしまいます。
 
「処理落ちしたらスローになるゲーム」というのも世の中にはありますが、対戦ゲームは普通フレームスキップ機能で対応しますよね。
というわけで入力スレッドは独立している必要があります。

ゲームスレッド

void run()
{
	while(true)
	{
		!入力スレッドに貯えられている入力履歴dequeを全て奪い、自分の入力履歴dequeに追加します
		!入力スレッド側の入力履歴dequeはクリアされます

		//	自分の入力履歴dequeに1つも入力がない場合、ちょっと待ってcontinue;します
		if (m_input.empty())
		{
			//	入力スレッドに履歴が貯まるまで待ちましょう
			::Sleep(1);
			continue;
		}

		//	入力データを1つ消費します
		KeyData key = m_input.front();
		m_input.pop_front();

		//	入力に基づいて、ゲームを1フレーム分進めます
		task(key);

		//	自分の入力履歴dequeにまだ入力がある場合は、描画をスキップします。そうでないなら描画
		if (m_input.empty())
		{
			draw();
		}
	}
}

ゲームスレッドが外部から受け取る情報は「入力スレッドからの、入力履歴のみ」です。
とてもスッキリしていますよね。

拡張性

入力スレッドを「キーボード版」「ゲームパッド版」「インターネット通信ストリーム入力版」「リプレイファイルストリーム版」と作ることで、お手軽にゲームのリプレイが作れます。
電装天使ヴァルフォースの通信対戦、及びリプレイはこの思想に基づいて作られています。

試しにリプレイ版を考えてみよう

入力スレッドに、少し機能を足してみましょう。
入力履歴に貯えるとともに、ファイルにその取得した内容を次々と出力していくのです。
(もちろん、スレッド終了時にfcloseしましょう)
 
リプレイファイルの完成です!
開発中バージョンでは、常にこの機能を仕込んでおきましょう。
「遊んでたら強制終了したよ」という報告とともに、リプレイファイルを送ってもらうことができます。
 
そしてリプレイファイルから、1秒間に60個ほどの履歴を読みだす入力スレッドを作ればOK。
ゲームスレッドは、キーボードからなのかリプレイファイルからなのかは一切気にせずに、ゲーム進行を再現してくれるはずです。
VisualStudioのデバッグ起動上で再現させれば、原因も一発で分かることでしょう。

蛇足?ロードスレッド

残念ながら世の中「フレーム単位での時間経過」のみで済ませられないものがあります。
それが「ロード」です。
ロード中は「NowLoading...」みたいな文字や絵がアニメーションをしますよね?
 
となると、データのロードは「ロードスレッド」にやってもらう必要があります。
しかしロードスレッドが何秒で終了するかは環境依存です。(PCスペックはみんな違いますからね)
 
試合のリプレイファイルであれば、ロードが終わってからの履歴のみを記録すれば良いでしょう。
実際、ヴァルフォースの試合リプレイファイルは「ロードが終わって試合が始まった時点から、試合が終わるまで」の入力履歴です。
 
ですがせっかく「ゲーム全てのリプレイ」をデバッグ用に作ることができたのです。
使いたいですよね。
 
というわけで、開発中バージョンのロードの長さは固定にしてしまうのをお勧めします。

//	ロード演出が終了しているか?
if (FRAME_LOADING <= frameCounter)
{
	//	ロードスレッドが作業を終了したか?
	if (!threadLoad->isRunning())
	{
		//	終了しているので、次のシーンに移行
		nextScene();
		return;
	}
#ifdef __DEVELOPMENT_VERSION__
	while(threadLoad->isRunning())
	{
		::Sleep(100);
	}
	nextScene();
	return;
#endif
}

ヴァルフォースはこんな感じです。
弊害として、予想以上にロードが長引いた場合フレームスキップが発生してしまいますが…。
まぁ良いんじゃないでしょうか。
気にしないか、ロード時間を長めにとるか、その辺りはそれぞれにおまかせします。
ヴァルフォースはロード後に「ラウンド1 ファイト!」という演出があるため、スキップが発生しても気にしませんでした。