Hatena::ブログ(Diary)

yumesoftの日記

2010-08-13

通信管理クラスを使おう

ソースコード

http://yumesoft.net/program.html

通信対戦は当たり前?

最近は同人ゲームであっても、通信対戦が実装されて当たり前になってきています。

しかし、WinSockは色々と低レベルすぎて手が出しにくく、DirectPlayはサポートが終了してしまい死に体です。

そこで気軽に通信をするためのクラスを作りました。

CSocketクラスは、電装天使ヴァルフォースでも使用しているものです。

CSocketの使い方

  • connectで接続
  • sendでデータ送信
  • recvで受信データにアクセス
  • closeで通信終了

WinSockとほぼ変わりません。

しかし、WinSockで色々と義務付けられている面倒事をクラス内部で吸収するようになっています。

CServerの使い方

  • startで待ち受け開始
  • acceptで接続SOCKETを取得
  • stopで待ち受け終了

bindがどうとか、listenがどうとか、そういうWinSockの面倒な手順を吸収してあります。

通信内容を考えよう

サンプルを動かし、通信できることを確認したら、次は通信内容を考えましょう。

通信内容は単なるバイトデータのストリームです。

 

初心者が陥りやすい罠のため、まず真っ先に説明しておくべきことがあります。

それは「1回のsendで送ったからといって、1回のrecvで受信できるとは限らない」ということです。

5byteのデータをsendしたとしても、相手には1秒ごとに1byteずつ届く可能性もあります。

なのでまずは「これから何byteのデータを送ろうとしているか」を相手に認識させる必要があります。

下記のように、送信データの先頭に「送ろうとしているデータのサイズ」をつけるのが一般的です。

 

送信側

DWORD size = strlen(message);
socket->send(&size, sizeof(DWORD));
socket->send(message, size);
socket->update();

受信側

socket->update();
vector<byte> &data = socket->recv();
while(sizeof(DWORD) <= data.size())
{
  // サイズを表すDWORDが受信できていることを確認しました
  DWORD size = *(DWORD*)&data[0] + sizeof(DWORD);
  if (size <= data.size())
  {
    // データの内容も既に受信できていることを確認しました

    // ここで、データの中身を取り出して処理を行います

    // 処理が終わったので、受信バッファから処理済み部分を削除します
    data.erase(data.begin(), data.begin() + size);
  }
  else
  {
    // データの内容がまだ届いていないので、しばらく待つことにします
    break;
  }
}

これでもう、自由にデータをやりとりすることができます。

電装天使ヴァルフォースではどんな感じ?


// 構造体番号を定義
enum
{
  STRUCT_ID_ClientSitdown = 1,
  STRUCT_ID_... //他にもたくさん
};

// 構造体を定義
struct ClientSitdown
{
  DWORD size;     // 構造体のサイズ
  DWORD structId; // 構造体番号
  DWORD tableId;  // 筐体ID
};

struct... //他にもたくさん

// 筐体に着席し、対戦開始を待ちます
ClientSitdown c = {sizeof(ClientSitdown), STRUCT_ID_ClientSitdown, tableId};
socket->send(&c, sizeof(c));

こんな感じで、構造体をやりとりしています。

実際は改竄を検知できるような工夫を加えたり、解析しにくいように複雑化したりしていますが。

注意!

もちろんポインタはやりとりできません。

接続先は(普通は)違うアプリケーションですから、ポインタの指すアドレスが相手には理解不能ですからね。

構造体の中にポインタは含めないようにしましょう。

2010-05-07

CThreadの実装と挙動を変更しました

ソースコード

http://yumesoft.net/program.html

説明

CThreadは、そのインスタンスが破壊されてもスレッド自体が止まることはありませんでした。

しかし、スレッド終了時(や、スレッドの中)にメンバー変数にアクセスするため、メモリー破壊の原因ともなりえます。

間違いが起こらないよう、デストラクタでstopAndWait()を呼び出すようにするとともに、実装方法を変更しました。

インターフェースに変化はありません。

2010-04-07

ゲーム中は、スクリーンセーバーやディスプレイ休止を回避しよう

ゲームパッドでゲームをしていたら、いきなり画面が真っ暗に。

という経験をされた方は多いかと思います。

慌ててマウスを動かすも、画面が復帰した頃にはボロボロになった自キャラが…。

 

というわけで、今回はゲーム側でその対処を行いましょう。

電装天使ヴァルフォースでも、途中から対処を行いました。

その時もNyaRuRuさんの記事を参考にさせていただきました。ありがたいことです。

id:NyaRuRu:20080925

 

この記事があるなら、私の記事は要らない?とも思いますが、NyaRuRuさんのは流石に少し情報が古いためちょっとだけ補足を。

ほとんどがNyaRuRuさんもおっしゃっていることだと前置きしておきます。

case WM_SYSCOMMAND:
    if ( (wParam & 0xFFF0) == SC_SCREENSAVE )
    {
        return 1;
    }
    return (DefWindowProc(hWnd, message, wParam, lParam));

マイクロソフトによると、本来であればウインドウメッセージを上記のように処理すれば回避できるはずです。

ですが、今はこれだとダメなケースがあります。

「パスワードによる保護」をONにしている場合です。

NyaRuRuさんによると

Windows Vista 以降,ここにチェックが入っていると,SC_SCREENSAVE トラップを行ってもスクリーンセーバは起動します.

とありますが、実際はWindowsXPでも起動してしまいます。

なのでこの方法はスッパリあきらめたほうが良いでしょう。

 

電装天使ヴァルフォースでは、ShortTimerを使って

case WM_TIMER:
{
    if (::GetFocus() == hWnd)
    {
        ::keybd_event(VK_LBUTTON, 0, 0, 0);
        ::keybd_event(VK_LBUTTON, 0, KEYEVENTF_KEYUP, 0);
    }
    break;
}

こうやって、マウスの左クリックイベントを定期的に発生させることで防いでいます。

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

2010-03-16

同じ入力をしたら、同じ出力になるようにしよう

前置き

ゲームを作っていると「再現性」というものが重要になることが多くあります。

例えば「リプレイ機能」は完全な再現性がないと困ります。

他にも「ある場所でエラーが出た」場合、状況を再現できれば再度同じエラーを起こすことができ、直すのも非常に楽になります。

そのためには「同じ入力をしたら、同じ出力になる」のがとても重要です。

具体的に考えていきましょう。

初期設定

電装天使ヴァルフォースは、1対1のバトルものです。

バトルを管理するクラスは独立しており、外部から受けつつけるのは「初期設定」「キー入力」のみとなっています。

 

「初期設定」とは具体的には何でしょうか?

  • 使用キャラクター
  • ステージ
  • ハンデ内容
  • 乱数のシード値

などがそれにあたります。

ここで重要なのは乱数のシード値です。

プログラムで使われる乱数生成関数は、シード値を同じに設定しておけば、出力される乱数内容が完全に一致するように作られています。

 

つまり、乱数のシード値が同じならば、プログラムは必ず同じ結果を再現してくれるわけです。

再現性を無くす要因

逆に、再現してくれなくなる要因を考えてみましょう。

  • 変数の初期化忘れ
  • timeGetTimeなどの、時間取得による分岐
  • シード値を設定しないで使う、乱数生成器(その場合、大抵現在時刻がシード値として利用されるため)
  • 非同期処理を目的としたマルチスレッド処理

となります。

つまり、これらを避けるのが再現性を確保するためのコツです。

特に変数の初期化忘れは厄介です。

Debugビルドでは変数内容が0xcdcdcdcdなどで初期化されます。

おかげでDebugビルド版では初期化忘れをしても再現性があるのですが、逆にReleaseビルド時に再現性がまったくとれなくなります。

とても良くハマる原因なので、変数の初期化忘れはしつこいほどチェックしましょう。

タイミング依存性の排除

説明したとおり、再現性を確保したい場合「現実の時間(timeGetTimeの返り値など)」に左右されてはいけません。

そのため、バトルクラスは

battle->update(key1P, key2P);

このようにキー入力データを受け取ると、内部的に1フレーム分処理が進むようになっています。

 

ゲームが早く動きすぎないようにタイミングをとっているのは、バトルクラスの外側の仕事です。

逆に、高速でupdateを呼ぶことにより早送りすることもできますし、逆にスローにすることも簡単です。

 

とにかくゲームプログラムにとっての「時間」は「ゲームが開始されてから何フレーム目か」で管理しましょう。

決して「現実の時間(timeGetTimeの返り値など)」に依存したゲームシステムを作ってはいけません。

やってて良かった「完全再現性」

電装天使ヴァルフォースで「斑鳩セツナがレーザーを撃つと稀に強制終了する」というバグが一時出たのをご存じでしょうか?

(実際はセツナ以外でも起こりえたのですが)

この際私は、クライアントに「強制終了しても、そこまでのリプレイファイルを出力する処理」を追加しました。

 

結果、数日後に届いた「強制終了するリプレイファイル」を元に、手元でエラーを再現。

デバッグビルドで原因を一発で突き止めたのでした。

 

もしリプレイ機能を実装する気が無かったとしても、リプレイファイル出力機能はつけておくとデバッグにとても役立ちますよ。