リアルタイム通信ゲームの実装例
2016.5.28株式会社 Aiming
山藤 智之
自己紹介
• 山藤 智之• 株式会社 Aiming 所属• 携わった作品– Blade Chronicle–剣と魔法のログレス( PC )–剣と魔法のログレス いにしえの女神
• 猫大好き!猫アレルギー系エンジニア
Aiming ?
• オンラインゲームの会社
• 各職人材募集中です!!
今日の内容
• オンラインゲームを作るのに必要になる通信技術のお話–通信規格–通信内容–通信ライブラリ• RPC ( Remote Procedure Call )• IDL ( Interface Definition Language )
通信規格
• UDP–単方向通信• 届いたか?までは責任持たない• 保証しようとすると TCP と同じ処理をアプリケー
ション側で適切に実装しないといけない
• TCP–双方向通信• 配送が順序通り届いた事を保証してくれる• UDP に比べて速度は遅い• UDP に比べて通信量は多い
常時接続 / 非常時接続
• 非常時接続(単方向通信)• リクエストに対してリプライ
• クライアントからの要求が必須
常時接続 / 非常時接続
• 常時接続(双方向通信)• リクエストに対してリプライ• サーバ側からのプッシュが可能
• クライアントに要求できる
通信内容
• キーフレーム形式– フレーム毎のコントローラーからの入力を交換し合う– 対戦格闘ゲームとかで有効
• メッセージ形式– 手続き呼び出しをメッセージ化して、必要なパラメー
タをやり取りする– MMO とかで有効– 手続き毎にコードを記述するので大規模向き
• ゲーム、プロジェクト規模に合わせて適切な企画・形式を選ぶ
メッセージ / キーフレーム
• 通信量の少なさ–キーフレーム > メッセージ
• 処理の簡単さ(高速)–キーフレーム > メッセージ
• 拡張の容易さ–メッセージ > キーフレーム
• 複雑なデータのやり取りのし易さ–メッセージ > キーフレーム
通信ライブラリ
• 作った理由–社内共通ライブラリにしたかった• アプリケーションの移植性向上
–当時はあまりちゃんとした物がなかった–送信・受信時のメモリ管理をキチンとやりた
かった–自社製品のそれまでのノウハウを活かした
かった
通信ライブラリ
• 要件–とりあえずメッセージベースの物–アプリケーションが変わっても、あまり変わ
らないネットワーク処理部分を分離させた物–クライアント端末が変わっても大丈夫な様に• PC• スマホ• 家庭用ゲーム機
通信ライブラリ
• 主にこの部分
通信ライブラリ
• 主にこの部分
通信ライブラリ
• 主にこの部分
WindowsWinSock
LinuxUnix Socket
MacBSD Socket
ソケットをラップした何か通信関数
ゲームアプリケーション
通信ライブラリ
• 主にこの部分
WindowsWinSock
LinuxUnix Socket
MacBSD Socket
ソケットをラップした何か通信関数
ゲームアプリケーション
通信ライブラリ
• メッセージ形式を利用したライブラリ–ソケット部分のコード• 幾つかのプラットフォームをサポート• 異なるプラットフォーム間での通信の提供
– RPC ( Remote Procedure Call )の提供• プログラム記述難易度の低減• 通信プロトコルの隠蔽
– HTTP / ゲーム専用プロトコル
– IDL ( Interface Definition Language )の提供
RPC (Remote Procedure Call)
• プログラムから別のマシンで動いているサブルーチン(手続き)を呼び出す仕組み
• 通常はネットワーク越しに、通信対象のマシンで動作するプログラムの手続きを呼び出す
RPC (Remote Procedure Call)
Network.function_call(“ デザイナー” ); void function_call(std::string types){ printf(“%s 募集中 \n” , types);}こんな感じに書くと
コレが実行される
RPC を実現するために
• RPC を実現する為に必要になりそうな情報–どの関数を呼ぶ?–各関数に渡さないといけない引数リスト
• 呼び出し順序の保障• 適切な単位(メッセージ毎)の受信–引数が途中で途切れたりしない様に
• バイトオーダーの吸収–リトルエンディアン / ビッグエンディアン
リトルエンディアン8bit
16bit
• バイトオーダーに影響を受けない数値の受け渡し方
ビッグエンディアン
データの表現
0A 0B 0C 0D
0A0B 0C0D
0D 0C 0B 0A
0C0D 0A0B
8bit
16bit整数値 0x 0A0B0C0D
データの表現
• バイトオーダーが異なる2台で通信すると…?
0D 0C 0B 0A 整数値 0x 0A0B0C0D
リトルエンディアン 8 Bit
リトルエンディアン 16 Bit
0D0C 0B0A 整数値 0x 0B0A0D0C
メモリのイメージをそのまま移すと
データの表現
• バイトオーダーが異なる2台で通信すると…?
0D 0C 0B 0A
リトルエンディアン 8 Bit
リトルエンディアン 16 Bit
0D0C 0B0A
メモリのイメージをそのまま移すと表現したい数値が変わってしまう!!
整数値 0x 0A0B0C0D
整数値 0x 0B0A0D0C
• バイトオーダーに影響を受けない数値の受け渡し方
データの表現
0D 0C 0B 0A 送りたい数値リトルエンディアン 8 Bit
リトルエンディアン 16 Bit
• バイトオーダーに影響を受けない数値の受け渡し方
データの表現
0A 0B 0C 0D
0D 0C 0B 0A 送りたい数値
ネットワーク経路上一度整列
リトルエンディアン 8 Bit
リトルエンディアン 16 Bit
• バイトオーダーに影響を受けない数値の受け渡し方
データの表現
0A 0B 0C 0D
0D 0C 0B 0A
0C0D 0A0B
送りたい数値
受信側受け取り時
リトルエンディアン 8 Bit
リトルエンディアン 16 Bit
ネットワーク経路上一度整列
• 数値をやり取りする際の容量軽減–小さい数値のやり取りが多い場合有効
整数値 0x 00000A0B
データの表現
0A 0B 00 00
整数値 0x 00000A0B
• 数値をやり取りする際の容量軽減–小さい数値のやり取りが多い場合有効
データの表現
0A 0B 00 00
0000_0000 0000_0000 0000_1010 0000_1011
整数値 0x 00000A0B
• 数値をやり取りする際の容量軽減–小さい数値のやり取りが多い場合有効
データの表現
0A 0B 00 00
0000_0000 0000_0000 0000_1010 0000_1011
011000_0101
後続するデータがあるので最上位は1
整数値 0x 00000A0B
• 数値をやり取りする際の容量軽減–小さい数値のやり取りが多い場合有効
データの表現
0A 0B 00 00
0000_0000 0000_0000 0000_1010 0000_1011
0101100_00101000_0101
後続するデータがあるので最上位は1
整数値 0x 00000A0B
• 数値をやり取りする際の容量軽減–小さい数値のやり取りが多い場合有効
データの表現
0A 0B 00 00
0000_0000 0000_0000 0000_1010 0000_1011
0100_00001100_0010 00001000_0101
後続するデータが無いので最上位は0
整数値 0x 00000A0B
• 数値をやり取りする際の容量軽減–小さい数値のやり取りが多い場合有効
データの表現
0A 0B 00 00
0000_0000 0000_0000 0000_1010 0000_1011
0100_00001100_0010 0000_00001000_0101 0000_0---
整数値 0x 00000A0B
• 数値をやり取りする際の容量軽減–小さい数値のやり取りが多い場合有効
データの表現
0A 0B 00 00
0000_0000 0000_0000 0000_1010 0000_1011
0000_00000100_00001100_0010 0000_00001000_0101
この2 Byte は送らなくても良くなる
パケット構造
• RPC を実現する為に必要になる通信内容– プロトコルバージョン番号
• エンコード、デコード形式– 制御用カウンタ
• 何回目の通信なのか?– どの関数を呼ぶ?
• 関数番号( ID )– 関数に合わせた引数
• 関数毎に引数の数、型、長さが変わる– 1手続きを実行するのに必要なデータ総容量
• 正しく受信するために必要
圧縮有
圧縮無
パケット構造
Version(1Byte )
PacketNumber(4 Byte ) PayloadSize(4 Byte )
Compress(1 Byte ) Size(4 Byte ) CompressData( Size )
MessageID(4 Byte ) ParametersCompress(1 Byte )
MessageID(4 Byte ) Parameters
Payload( PayloadSize )
レイヤー構造による処理
• 送信 / 受信で処理する順にレイヤを構築
Compressorデータの圧縮
SplitToContractメッセージ一つを処理
Packetize送受信ヘッダー処理
送信
受信
レイヤー構造による処理
• 各レイヤーで行う処理
必要に応じて圧縮メッセージデータをバイナリストリーム化
ネットワークに流す形式に変換
送信
受信
ネットワークから流れてきたデータの変換必要に応じて伸長バイナリストリームのメッセージ化
OS 低レベル API による送受信処理
データの流れ
Compressorデータの圧縮
SplitToContractメッセージ一つを処理
Packetize送受信ヘッダー処理
送信
受信
MessageID(4Byte ) Parameters
データの流れ
Compressorデータの圧縮
SplitToContractメッセージ一つを処理
Packetize送受信ヘッダー処理
送信
受信
MessageID(4Byte ) ParametersCompress(1 Byte )
データの流れ
Version(1Byte )
PacketNumber(4 Byte )PayloadSize(4
Byte )MessageID(4
Byte ) ParametersCompress(1 Byte )
Compressorデータの圧縮
SplitToContractメッセージ一つを処理
Packetize送受信ヘッダー処理
送信
受信
データの流れ
Compressorデータの圧縮
SplitToContractメッセージ一つを処理
Packetize送受信ヘッダー処理
送信
受信
Version(1Byte )
PacketNumber(4 Byte )PayloadSize(4
Byte )MessageID(4
Byte ) ParametersCompress(1 Byte )
データの流れ
Compressorデータの圧縮
SplitToContractメッセージ一つを処理
Packetize送受信ヘッダー処理
送信
受信
Version(1Byte )
PacketNumber(4 Byte )PayloadSize(4
Byte )MessageID(4
Byte ) ParametersCompress(1 Byte )
データの流れ
Compressorデータの圧縮
SplitToContractメッセージ一つを処理
Packetize送受信ヘッダー処理
送信
受信
MessageID(4Byte ) ParametersCompress(1 Byte )
データの流れ
Compressorデータの圧縮
SplitToContractメッセージ一つを処理
Packetize送受信ヘッダー処理
送信
受信
MessageID(4Byte ) Parameters
データの流れ
Compressorデータの圧縮
SplitToContractメッセージ一つを処理
Packetize送受信ヘッダー処理
送信
受信
関数呼び出し
MessageID(4Byte ) Parameters
実装における問題点
• 通信内容を一つ追加する都度、追加・修正しないといけないコード量が多い–送信関数–受信関数–受信関数の呼び分け部分( Dispatcher)
• これを簡易化する為に IDL ( Interface Definition Language )を用意する
IDL• IDL ( Interface Definition
Language )–通信に載るメッセージ( RPC )の内容定義
を抽象化した物–マルチプラットフォーム向けに変数定義
(型)も抽象化する• この定義を元に、最低限必要になるコー
ドを自動生成する仕組み• 定義ファイルとコードジェネレータ
IDL• 送信側–受信側が識別可能な一意なメッセージ ID–メッセージ毎のパラメータ格納
• 受信側–どのメッセージを受信したのか?(適切な手続
きの呼び出し)–メッセージ毎のパラメータ展開
• 受信後の処理はアプリケーションレベルの実装
IDLサーバ用 受信 クライアント用 送信
メッセージ定義---------------------WANTED string Occupation---------------------
Generator
IDL
メッセージ定義---------------------WANTED string Occupation---------------------
C++
C
C++
C
サーバ用 受信 クライアント用 送信
Generator
メッセージ定義---------------------WANTED string Occupation---------------------
Generator
IDL
C++
C
C++
void send_WANTED(std::string Occupation);
C
void send_WANTED(char* Occupation);
サーバ用 受信
void WANTED(char* Occupation);
クライアント用 送信
void recv_WANTED(std::string Occupation);
IDL• ゲーム内の操作は、こんな感じでメッ
セージ化されています。( MMO の場合)MessageID Message 概要 パラメータ
1 CHAT_SEND チャットに発言 Msg :発言内容2 CHAR_MOVE 移動 X : X 座標
Y : Y 座標3 CHAT_SHOW 誰かがチャットし
たWho :誰が?Msg :何て言った?
4 ATTACK キャラクターが攻撃
Target ;誰に攻撃する?How :どうやって攻撃する?
IDL• 実際の例
<?xml version=“1.0” encoding=“utf-8” standalone=“yes” ?><contract name=“GameMessage”>
<party primary=“Client” secondary=“Server” />
<call name=“CHAT_SEND”> <parameter name=“Msg” type=“String” /> </call>
<typedefine name=“id” type=“int” /> <receive name=“CHAT_SHOW”> <parameter name=“Who” type=“id” /> <parameter name=“Msg” type=“String” /> </receive>
</contract>
<?xml version=“1.0” encoding=“utf-8” standalone=“yes” ?><contract name=“GameMessage”>
<party primary=“Client” secondary=“Server” />
<call name=“CHAT_SEND”> <parameter name=“Msg” type=“String” /> </call>
<typedefine name=“id” type=“int” /> <receive name=“CHAT_SHOW”> <parameter name=“Who” type=“id” /> <parameter name=“Msg” type=“String” /> </receive>
</contract>
IDL• 実際の例このメッセージ群(通信定義)の名前
<?xml version=“1.0” encoding=“utf-8” standalone=“yes” ?><contract name=“GameMessage”>
<party primary=“Client” secondary=“Server” />
<call name=“CHAT_SEND”> <parameter name=“Msg” type=“String” /> </call>
<typedefine name=“id” type=“int” /> <receive name=“CHAT_SHOW”> <parameter name=“Who” type=“id” /> <parameter name=“Msg” type=“String” /> </receive>
</contract>
IDL• 実際の例 以下に記すメッセージ定義の方向指定、このファイルの場合 “ Client to Server”
このメッセージ群(通信定義)の名前
<?xml version=“1.0” encoding=“utf-8” standalone=“yes” ?><contract name=“GameMessage”>
<party primary=“Client” secondary=“Server” />
<call name=“CHAT_SEND”> <parameter name=“Msg” type=“String” /> </call>
<typedefine name=“id” type=“int” /> <receive name=“CHAT_SHOW”> <parameter name=“Who” type=“id” /> <parameter name=“Msg” type=“String” /> </receive>
</contract>
IDL• 実際の例 以下に記すメッセージ定義の方向指定、このファイルの場合 “ Client to Server”
このメッセージ群(通信定義)の名前
call なので Client -> Server
Server -> Client
• クライアント
typedef id int;
class GameMessageReceiver {public: void dispatch(network_data& data) { function_number = data.pop<int>(); switch (function_number) { case 789012: id Who = data.pop<id>(); std::string Msg = data.pop<std::string>(); CHAT_SHOW(Who, Msg); break; } }
virtual void CHAT_SHOW(id Who, std::string Msg) = 0;};
IDL
class GameMessageSender {public: void CHAT_SEND(std::string Msg) { push_args<int>(123456); // function number push_args<std::string>(Msg); }};
• サーバー<Client_RPC.h>
typedef id int;
class GameMessageReceiver {public: void dispatch(network_data& data) { function_number = data.pop<int>(); switch (function_number) { case 123456: std::string Msg = data.pop<std::string>(); CHAT_SEND(Msg); } } virtual void CHAT_SEND(std::string Msg) = 0;};
IDL
class GameMessageSender {public: void CHAT_SHOW(id Who, std::string Msg) { push_args<int>(789012); // function number push_args<id>(Who); push_args<std::string>(Msg); }};
IDLGameMessageSender::CHAT_SEND(“ 募集中” );
IDL
GameMessageReceiver::CHAT_SEND(std::string){ GameMessageSender::CHAT_SHOW(user_id, string);}
IDL
GameMessageReceiver::CHAT_SHOW(id Who, std::string){ std::string name = get_charname(Who); Display(“%s %s”, name, string);}
エンジニア 募集中 エンジニア 募集中
まとめ
• 通信方式( TCP/UDP/ 常時 / 非常時)• 通信内容(メッセージ / キーフレーム)• 通信ライブラリ– RPC• データの表現方法(バイトオーダー)• メッセージの表現(パケット構造)• メッセージを処理するレイヤー構造
– IDL• IDL の例
御清聴ありがとうございました
質疑応答
Top Related