Hatena::ブログ(Diary)

yumesoftの日記

2010-03-24

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

ソースコード

http://yumesoft.net/program.html

前置き

私のプログラムは、基本的に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 ファイト!」という演出があるため、スキップが発生しても気にしませんでした。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/yumesoft/20100324/1269438906