Twitter @HigenekoTwitter #XNA
ビット数の少ない方が多いものより消費するスペースは小さくなります。
もしint型の値が0~100までの範囲しか取らないと判っているのなら、そのまま4バイトのint型として送るより、byte型にキャストして送ることができます。
場合によっては値が表す範囲を値をずらすことによって減らすことができます。例えば、キャラクターの高さのデータを送る必要があり、高さはcmで表されるとします。このゲーム中のキャラクターの高さの範囲は、ドワーフ(100cm)から巨人(300cm)まであります。
300という値はbyteで表現できる範囲(0-255)を超えているので、キャストすることはできません。
しかし、100cm以下のキャラクターが存在しないと判っているのなら、100cmを基点とすることで値の取る範囲を小さくすることができます。
PacketWriter.Write( (byte)( height - 100 ) );
受信側では値を使う前に100を足すだけです。これで、キャラクターの高さの範囲は0-200になり、byteにキャストすることができます。
他にもスケーリングすることで値の取る範囲を小さくすることができます。例えば、浮動小数点で表されるラジアン角度を送る場合があるとします。ラジアンで円を表す範囲は0~2πになります。この値を0~255の範囲に収まるようにスケーリングすれば、byteにキャストして送ることができます。
float rotationEncodeScale = 255.0f / MathHelper.TwoPi; PacketWriter.Write( (byte)( rotation * rotationEncodeScale ) );
受信側では逆数を掛けることで、元の値にします。
float rotationDecodeScale = MathHelper.TwoPi / 255.0f; float rotation = (float)PacketReader.ReadByte() * rotationDecodeScale;
この量子化では、精度が多少失われてしまいますが、通常は4バイトのデータを1/4に減らすことの方が価値があります。
訳注:原文のコードでは255ではなく256を使っていますが、この場合に変換できる値は上限値未満の時に動作します。もし、上限値を指定した場合、結果は256になり、byteにキャストすると結果は0になってしまいます。ですが、この例で扱っている値はラジアンなので円を表す場合、値が0とMathHelper.TwoPiの時は結果が同じになるので問題なく動作します。
同じ手法はシェーダープログラムなどで使うのですが、この場合に良く用いられるのは0~1までの値で1以下の値を取るので、この場合はスケールを255にしないと動作しません。
以上の理由から、ここではスケール値を255に変更しました。
原文:http://blogs.msdn.com/shawnhar/archive/2007/12/24/network-compression-quantization.aspx
限られたネットワーク帯域の中では、送信するデータを圧縮することは非常に重要なことです。
Zip等の一般的な圧縮アルゴリズムはネットワークゲーム向けではありません。これらの圧縮はある程度のデータ量がある場合は効率良い圧縮が期待できますが、ネットワークパケットのように小さいデータを圧縮するには不向きです。ここで必要なのは20バイトのデータを10バイトにするような圧縮方法です。
初心者がよく考える手法として、送信側で複数のパケットを続けてバイトストリームして一般的な圧縮アルゴリズムを使って圧縮し、圧縮されたデータをパケットに分割して送受信するというものがあります。確かに圧縮率は高くなるのですが、この手法には致命的名欠点があります。この手法では、圧縮したデータを展開するに全てのパケットが失われること無く、順番に配信される必要があります。この為にはSenDataOptions.ReliableInOrderを使う必要があり、レイテンシが増加する原因になってしまいます。
また、変化量(前の状態との差)を送ることで全体のデータを送るより少ないデータ量で済ますことができますが、この手法も一般的な圧縮アルゴリズムを使うのと一緒で、全てのデータが順番に送信されるという保障があるときにのみ使える手法です。
通常は古典的なビットフィールドをまとめたり、量子化を使った方が良い結果になります。これらの手法は4KBのRAMを積んだ8ビットマイコン時代に良く使われたものですが、ギガバイト単位のデータを扱える.Netの世界では忘れ去られた手法でもあります。
例えば、文字列を送るのでなく、整数のIDかenum値を送る。
もし、行列が回転と移動の組み合わせで、スケールや、せん断、射影をしないと判っているのなら行列データを送らない。変わりに12バイトのVector3であるMatrix.Translateと、16バイトのQuaternion.CreateFromRotationMatrix(matrix)を送ります。これで64バイトの行列データが28バイトに圧縮されたことになります。
以下、次回に続く。
原文http://blogs.msdn.com/shawnhar/archive/2007/12/22/network-compression.aspx
XNAフレームワークはボイスチャットをサポートしており、ヘッドセットがある場合に自動でチャットができるようになっています。便利な機能ではありますが、使用中はより多くのネットワーク帯域が必要になることに注意が必要です。
音声データは500 B/s以下の帯域に圧縮され、ヘッドセットに向かって喋った時のみにデータ転送を行います。
デフォルトの状態では全てのプレイヤー同士で会話することができるようになっています。もし、ひとりが他の15人のプレイヤーに話しかけた場合、
ひゃー!! 8KB/sが目標だったことを覚えていますか?これではゲームデータを送る前に殆どの帯域を使い切ってしまいます。
では、この厄介なボイスデータ帯域問題をどのようにして解決することができるのでしょうか?
原文:http://blogs.msdn.com/shawnhar/archive/2007/12/20/network-bandwidth-voice.aspx
ネットワーク帯域とは、どれだけの量のデータを送受信できるかを表します。データ量が上限に近づくほどにパケットロスの量が増え、この上限を超えたデータ量を送ろうとした場合、結果的にセッションから切断されることになってしまいます。
XNA Frameworkでは帯域をバイト/秒で表します。まぎらわしいことにネットワークベンダーはビット/秒で表すのが好きで(多分、より数字を大きく見せるため)、さらにまぎらわしいことにどちらの表記もbpsやkbpsを使うことです。この表記が出てきた場合にはバイト/秒なのかビット/秒のどちらのことを表しているのか注意する必要があります。(訳注:私は個人的にバイト/秒は大文字のB/s、ビット/秒はをbit/sと区別して表記するようにしています)
より多くのユーザーに遊んで貰うために色々な家庭のインターネット接続を考慮してXboxのゲームでは最小で上り、下り8KB/sの帯域で動作することが求められます。
ネットワーク帯域と言うと、ゲーム内で送受信するデータ量x接続するマシン数と考えがちですが、ここで忘れてはいけないのはデータパケットのヘッダー部分のサイズです。
これで~50バイトのパケットヘッダーサイズになります。
例えばプレーヤー毎にBoolean値を、60フレーム/秒の間隔で転送すると:
ホァ!Boolean値一個のデータ転送だけでも3KB/sになってしまいます(ゴールは8KB/sということを思い出してください)。98%の帯域がパケットヘッダーで無駄に消費されてしまいます。
では、どうやってこの厄介なヘッダー問題を回避することができるのでしょうか?
原文http://blogs.msdn.com/shawnhar/archive/2007/12/18/network-bandwidth-packet-headers.aspx
以前から、XNAを.NetのControl内で使いたいという要望を何度か聞くことがありました。本当はXNA 2.0で導入される予定でしたがテスト期間が間に合わなかったために見送りとなりました。ですが以前書いたようにXNA 2.0でGraphicsDeviceが仮想化されたことによってウィンドウ内でXNAを使う実装が非常にシンプルになったので、Creators Club Onlineにサンプルがアップされました。
WinForms Series 1: Graphics Device Sample
このサンプルでは、XNAをWinFormsのカスタムコントロールとして使う方法が実装されています。単体のコントロールを配置したときに動作するのはもちろん、複数のコントロールを配置した場合でも動作するので3Dゲーム用のエディター等で3面図+カメラビューの四つの画面を表示したりというときに便利です。実装的にはひとつのGraphicsDeviceを使いまわしているので、モデルデータ等は別コントロールであっても同じものが使えます。
WinForms Series 2: Content Loading Sample
ゲームエディタなどを作るときには、コンテントパイプラインを通る前のfbxファイルやxファイルを直接指定できたほうが便利ですが、一度Content.Load<T>に慣れてしまうと自前でNodeContentやMeshContentからモデル描画するのは非常に面倒ですし、パフォーマンス的な問題もあります。そこで、このサンプルではfbxファイルを直接読むのではなく、ツール上でコンテントパイプラインのビルド処理をすることで普段のゲームと同じようにContent.Load<T>を使えるようにしています。コンテントパイプラインのビルド処理はMSBuildを使っているのですが、このサンプルのContentBuilder.csファイル内でMSBuildのタスクを実行することで実現しています。
ある程度規模の大きなゲームになってくると、ゲームエディタなどのツールが必要になってきますが、上の二つのサンプルが参考になるのではないでしょうか?
ネットワークは信頼できません。
データーパケットを送ったときにはいろいろな事がおこります。
4つ目はXNAフレームワークベースのゲームでは問題になりません。XNAフレームワークではLiveのライブラリを使っているので送られるパケットは全て自動的に暗号化されているので、データ自体が改ざんされたり、壊れたりということを気にする必要はありません。
2と3の問題についてはSendDataOptions.ReliableとSendDataOptions.InOrderフラグを指定することで解決できます。なぜフラグ指定をしないといけないのか疑問に思う人もいるかもしれません。常にSendDataOptions.ReliableInOrderで送ればいいじゃないか、それ以前になぜ他のネットワークアプリケーションの用にTCPを使わないの?
その理由はゲームでは常にシミュレーション結果をリアルタイムに更新する必要があるからです。TCPやSendDataOptions.ReliableInOrderを指定した場合、以下のような動作をします。
この仕組みはウェブページのHTMLデータ等をダウンロードする時などには有効な方法です。何故なら、AのパケットにはHTMLのヘッダーやレイアウト部分を含んでいるわけですが、その情報無しに残りのデータを表示するのは意味がありません。
ゲームの場合を考えてみましょう。例えばプレイヤーの座標をゲームループ内で送る場合は
なんて間抜けな…。もし受信者がCの時点での座標を知っているのなら、それより過去の時間であるAとBを気にするのは無意味なことです。この状況ではAとBの情報は無視して、最新の座標に更新するのがより自然な方法です。
SendDataOptions.ReliableInOrderを指定したときにパケットロスした場合、レイテンシは増加します。このフラグを指定しない場合にパケットロスが発生した瞬間だけガクっとしますが、その後は問題なくゲームを続けることができます。しかし、フラグを指定した場合、たった一つのパケットロスでさえ、それに続くパケット全てが失ったパケットが再び届くまでの時間分だけ遅延するこになります。
SendDataOptions.ReliableかSendDataOptions.InOrderのどちらかを指定したときには問題にならないことに注意してください。この問題は両方のフラグを指定した(TCPの振舞いと同じ)時のみに発生します。
転送を確実に行いたいが、パケットの送信順を気にする必要が無い(SendDataOptions.Reliableを指定)場合、パケットAがロスしてBが届いた時はゲーム側では単にBを受信したことになり、Aのパケットは送信しなおされますが続くほかのパケットの遅延には影響ありません。
パケットの送信順は気にするが、パケットロスは気にする必要が無い(SendDataOptions.InOrderを指定)場合、それぞれのパケットには番号がつけられ、番号が古いものが、新しいものより後に届いた場合は古いものは無視されます。この方法が最もレイテンシが少ない方法です。
ショーン(Shawn)曰く
原文http://blogs.msdn.com/shawnhar/archive/2007/12/14/network-packet-loss.aspx
「さて」とセイウチはいった「ネットワークの話をしましょう」
(訳注:元ネタはルイス・キャロルのセイウチと大工から)
ネットワークゲームプログラマーには以下の三つの不死の敵がいます
以上の三つについて順に話しましょう。
レイテンシは物理的理由によって決まります。SFの世界では何十年もの昔に実現しているのに、物理学者は未だに光速を超える手段を発見していません。ですから、秒速30万キロ/秒を超える速度でデータを送ることができません。これは物理学者のせいです(そんなに難しいことなのか?)
十分に速いだろって?
僕はシアトルに住んでいて、同僚のイーライ(Eli)はニューヨークに住んでいた。その距離はおよそ4,000Km、光の速度で13ミリ秒掛かる。
僕は以前イギリスに住んでいたけど、イギリスからシアトルまでは7,800Km、光の速度で26ミリ秒掛かることになる。
60フレーム/秒のゲームの場合、各フレームは16ミリ秒毎に更新されるので、僕がイギリスの友達と遊んでる場合は2フレーム近い差(ラグ)が生じてしまう。
ちょっと待ってくれ、話はそれだけではないんだ
最悪ケースは?
どうやったら、家で再現できるの?
NetworkSession.SimulateLatency = TimeSpan.FromMilliseconds(200)
どうすればいいの?
ここまでがShawn Hargreaves氏の投稿の翻訳です。
ちなみに日本国内の場合は距離的問題は少ないと思います。例えば東京~札幌間の距離は850Kmで3ミリ秒となりますが、記事の中の通り、問題になるのはルーター間でレイテンシが大きな問題となるでしょう。
予測アルゴリズムですが、以下のサンプルが参考になると思います。
http://creators.xna.com/Headlines/developmentaspx/archive/2007/01/01/Network-Prediction-Sample.aspx
追記:
ゲーム中に表示されるPing値と、この記事で言及しているレイテンシとは違うものです。
行きと帰りが同じ速度と仮定すると0.2秒というレイテンシは0.4秒のPing値に相当することに注意してください。