通信管理クラスを使おう
通信対戦は当たり前?
最近は同人ゲームであっても、通信対戦が実装されて当たり前になってきています。
しかし、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));
こんな感じで、構造体をやりとりしています。
実際は改竄を検知できるような工夫を加えたり、解析しにくいように複雑化したりしていますが。
注意!
もちろんポインタはやりとりできません。
接続先は(普通は)違うアプリケーションですから、ポインタの指すアドレスが相手には理解不能ですからね。
構造体の中にポインタは含めないようにしましょう。
ゲーム中は、スクリーンセーバーやディスプレイ休止を回避しよう
ゲームパッドでゲームをしていたら、いきなり画面が真っ暗に。
という経験をされた方は多いかと思います。
慌ててマウスを動かすも、画面が復帰した頃にはボロボロになった自キャラが…。
というわけで、今回はゲーム側でその対処を行いましょう。
電装天使ヴァルフォースでも、途中から対処を行いました。
その時も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; }
こうやって、マウスの左クリックイベントを定期的に発生させることで防いでいます。
役割ごとにスレッドを分けよう
前置き
私のプログラムは、基本的に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 ファイト!」という演出があるため、スキップが発生しても気にしませんでした。
同じ入力をしたら、同じ出力になるようにしよう
前置き
ゲームを作っていると「再現性」というものが重要になることが多くあります。
例えば「リプレイ機能」は完全な再現性がないと困ります。
他にも「ある場所でエラーが出た」場合、状況を再現できれば再度同じエラーを起こすことができ、直すのも非常に楽になります。
そのためには「同じ入力をしたら、同じ出力になる」のがとても重要です。
具体的に考えていきましょう。
初期設定
電装天使ヴァルフォースは、1対1のバトルものです。
バトルを管理するクラスは独立しており、外部から受けつつけるのは「初期設定」「キー入力」のみとなっています。
「初期設定」とは具体的には何でしょうか?
- 使用キャラクター
- ステージ
- ハンデ内容
- 乱数のシード値
などがそれにあたります。
ここで重要なのは乱数のシード値です。
プログラムで使われる乱数生成関数は、シード値を同じに設定しておけば、出力される乱数内容が完全に一致するように作られています。
つまり、乱数のシード値が同じならば、プログラムは必ず同じ結果を再現してくれるわけです。
再現性を無くす要因
逆に、再現してくれなくなる要因を考えてみましょう。
- 変数の初期化忘れ
- timeGetTimeなどの、時間取得による分岐
- シード値を設定しないで使う、乱数生成器(その場合、大抵現在時刻がシード値として利用されるため)
- 非同期処理を目的としたマルチスレッド処理
となります。
つまり、これらを避けるのが再現性を確保するためのコツです。
特に変数の初期化忘れは厄介です。
Debugビルドでは変数内容が0xcdcdcdcdなどで初期化されます。
おかげでDebugビルド版では初期化忘れをしても再現性があるのですが、逆にReleaseビルド時に再現性がまったくとれなくなります。
とても良くハマる原因なので、変数の初期化忘れはしつこいほどチェックしましょう。
タイミング依存性の排除
説明したとおり、再現性を確保したい場合「現実の時間(timeGetTimeの返り値など)」に左右されてはいけません。
そのため、バトルクラスは
battle->update(key1P, key2P);
このようにキー入力データを受け取ると、内部的に1フレーム分処理が進むようになっています。
ゲームが早く動きすぎないようにタイミングをとっているのは、バトルクラスの外側の仕事です。
逆に、高速でupdateを呼ぶことにより早送りすることもできますし、逆にスローにすることも簡単です。
とにかくゲームプログラムにとっての「時間」は「ゲームが開始されてから何フレーム目か」で管理しましょう。
決して「現実の時間(timeGetTimeの返り値など)」に依存したゲームシステムを作ってはいけません。
ファイルロード管理クラスを使おう
ソースコード
2010/03/10 内容を修正更新しました。
http://yumesoft.net/program.html
まずはファイルローダー
私は、ゲームを作る際に真っ先に作らなければならないものとして、必ずファイルローダーを挙げます。
画像を表示するにしても、文章を表示するにしても、データファイルを読み込めなければ何もできません。
※ソースコードに文章を書くのは「ハードコード」と言って、とても良くないことです。文言を変えるたびに再コンパイルなんて面倒すぎます。デバッグの時くらいにしたいものです。
このファイルローダの仕組みは、私が尊敬するやねうらお氏の著書「Windowsプロフェッショナルゲームプログラミング」にて学んだものです。
やねうらお氏のブログ→ id:yaneurao
この著書は非常に役に立つテクニックが盛り沢山であり、電装天使ヴァルフォースの基礎設計の大半はこの本から得た知識で作られています。
絶版本となり一時値段が高騰しましたが、現在はAmazonのユーズドで1500円程度と手ごろな値段のようです。
是非手に入れておきましょう。
※やねうらお氏…。「再販するくらいなら新しいの作るからちょっと待て」とおっしゃられてから大分経ちますが、いかがでしょうかw
効果
CVirtualFileLoaderクラスは、複数のフォルダを1つのフォルダのように扱うことができます。
テストコードでは
- "archive.arc"
- "data/master"
- "data/patch"
の3つを1つのフォルダのように扱っています。
この状態で"sample.txt"を読もうとしたとします。
そうすると
"data/patch/sample.txt"があれば、それをロード。無ければ…、
"data/master/sample.txt"があれば、それをロード。無ければ…、
archive.arc内の"sample.txt"があれば、それをロード。無ければロード失敗。
という挙動をします。
さて、これの何が便利なのでしょうか?
キーワードは「差分」です。
パッチに便利
この機構はゲームのパッチをリリースする際にとても役に立ちます。
「最新のパッチに含まれているファイルを優先的に読み、パッチに含まれていないファイルについては既存のものを読む」
というのは、パッチというシステムと実にマッチしています。
アーカイブファイルは便利に扱おう
ゲームをリリースする際は、ファイル隠蔽や悪戯防止のためにファイルをアーカイブするかと思います。
しかし開発中、いちいちファイルを更新するたびにアーカイブファイルを生成するのは面倒です。
そこで、以下のようにします。
#ifdef _DEBUG loader.pushLoader(new CFileLoader, "data/"); #else loader.pushLoader(new CArchiveLoader("data.arc"), ""); #endif
開発中に便利
開発中、データファイルを頻繁に増やしたり上書きしたりすると思います。
そうした場合、共同開発者とファイルを同期させる必要がありますよね。
一部のファイルを上書きするたびに全部渡すのは面倒ですし、どのファイルを上書きしたのか覚えておくのも面倒です。
そこで…。
#ifdef _DEBUG loader.pushLoader(new CFileLoader, "data/"); loader.pushLoader(new CFileLoader, "20100301/"); loader.pushLoader(new CFileLoader, "20100302/"); loader.pushLoader(new CFileLoader, "20100303/"); #else loader.pushLoader(new CArchiveLoader("data.arc"), ""); #endif
フォルダ分けしてしまいましょう。
CVirtualFileLoaderを使えば、実際にファイルを上書きしなくても同じ効果が得られますよね。
それに、過去のファイルをバックアップとして保持できるのもグッドです。
スマートポインタを使いましょう
std::tr1::shared_ptrというスマートポインタを利用しています。
スマートポインタって何?という人は、ぐぐってすぐ勉強しましょう。
C++がとてもとてもとても扱いやすくなる、魔法のポインタです。
これがないとゲーム作るのは10倍くらい大変になります。
ちなみに、上記で紹介した書籍は本の結構な量を使いスマートポインタの仕組みについて教えてくれています。
そういう意味でも「買い」ですね。
Xファイルの甘い罠。巨大Xファイルの読み込みが超遅い
Xファイルは素晴らしいフォーマットです。
初心者が初めて触って理解するためという意味では。
大抵の中級〜上級者曰く。3Dフォーマットは自前フォーマットを用意しろ。
ゲームを作り終わった今、まったくもってその通りだと思います。
電装天使ヴァルフォースのモデルは、Xファイルで出来ています。
色々なツールが対応していること。
ヴァルフォースの制作が初3Dプログラムだったこと。
テキストフォーマットが読みやすかったこと。
これらが採用の理由です。
しかしいくつか弱点があります。
古いフォーマットゆえ、複数のテクスチャUVが設定できないのが最たるものでしょうか。
拡張可能フォーマットなので実際は作れるのですが、そんな特殊なXファイルを出力してくれるツールはまずありません。
そして一番の問題点は、読み込みが超遅いことです。
原因は拡張可能フォーマットであること。
1つ1つのデータの整合性などをきちんとチェックしているからでしょうか。異常に読み込みが遅いのです。
これが顕著になるのは、アニメーションデータを大量に持たせた場合です。
ヴァルフォースのキャラクターは100個程度のAnimationSetを持っていますが、ロード時間は10秒を超えてしまいました。
結局のところ、一度Xファイルとして作成したあと、アニメーションデータ部分だけ切り離して独自のバイナリデータとしました。
内容はID3DXAnimationControllerのアニメーションキー配列のダンプです。
ロード時、モデルのロードは通常通りに行い、ID3DXAnimationControllerは自前で生成。
ダンプデータをアニメーションキーとして登録します。
いっきに生成できるのでとても速く、10秒のロードが1.5秒程度に縮まりました。
(1)注意点としては、ID3DXAnimationControllerに登録されている四元数は反転されているということ。(仕様です)
(2)ID3DXKeyframedAnimationSetの最初もしくは最後のキーを編集した場合は、ID3DXKeyframedAnimationSetごと作り直さないと正しく反映されない。どころかメモリーが破壊されるバグがあること。
(3)静的リンク時代のD3DXは、10MB程度以上のサイズのXファイルを読み込むと高確率でメモリー破壊を起こすこと。
この3つです。
(2)は、ゲームならAnimationKeyはロード時に生成するだけでしょうから問題にならないと思います。
問題になったのはヴァルフォース用にアニメーションエディタを作った時です。えらい目にあいました。
(最初は自分の能力の無さを疑ったのですが、あれは完全にバグです。海外フォーラムでも同様の指摘が少しですがありました)
ソースについてはこちらからどうぞ。
http://yumesoft.net/program.html
余談ですが、黄昏フロンティア様の「ひぐらしデイブレイク」のロードが異様に遅いのは、Xファイルをそのまま使っているからだと思います。違ってたらごめんなさい。