Contenu connexe
Similaire à ガールアックス:リアルタイム通信処理の効率的な実装 (20)
ガールアックス:リアルタイム通信処理の効率的な実装
- 1. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
ガールアックス:
リアルタイム通信処理の効率的な実装
第7回DeNAゲーム開発勉強会✕モノビット
株式会社ディー・エヌ・エー
Japanリージョンゲーム事業本部
技術・編成部 開発基盤グループ
堀米 智彦 tomohiko.horigome@dena.com
- 2. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
自己紹介
堀米 智彦(ほりごめ ともひこ)
DeNA 入社5年目(2011年8月 中途入社)
⁃ 前職は組み込み機器向けブラウザ開発
入社後の業務経歴
⁃ ゲーム向けライブラリ開発
⁃ Ninja Royale エンジニア
⁃ D.O.T. エンジニア/リードエンジニア
⁃ 三国志ロワイヤル エンジニア/リードエンジニア
⁃ ガールアックス エンジニア
2
- 3. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
アジェンダ
3
「ガールアックス」でのリアルタイム通信処理の実装の
詳細について、ご紹介いたします
⁃ 1. リアルタイム通信処理プラットフォーム「IRIS」
⁃ 2. サーバ/クライアント構成
⁃ 3. リアルタイム通信ゲームとして必要な処理
⁃ 4. シリアライズ/デシリアライズ処理について
⁃ 5. 効率よく実装するための工夫
⁃ 6. パフォーマンスチューニングのアイデア
⁃ 7. デバッグ効率化のためのアイデア
- 4. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
本題に入る前に:ガールアックスとは?
4
5vs5対戦 カジュアルMOBAゲーム
iOS/Android向け
- 6. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
1. リアルタイム通信処理プラットフォーム「IRIS」
6
DeNA内製のリアルタイム通信プラットフォーム
IRISサーバとIRIS C++ Client SDKが用意されている
ゲーム側からSDKの各種APIを呼び出し、通信処理を行う
使用しているAPIはざっくり以下
⁃ 接続処理
⁃ 切断処理
⁃ 部屋への参加(指定した部屋名の部屋に入る or 無かったら作って入る)
⁃ 部屋からの退出
⁃ ミューテックス取得(排他制御)
⁃ ユニキャスト(特定にユーザにだけ送信)
⁃ ブロードキャスト(部屋に参加中の全ユーザに送信)
⁃ 受信データ取得
- 8. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
2. ガールアックスのサーバ/クライアント構成
8
ざっくりとした図
ゲームとしての基本的な認証やデータ保存/取得等はSakashoサーバ
クライアント間のバトルデータ同期イベントはIRISサーバを経由
クライアントのうち1人だけ、「ホストクライアント」として動作
調停が必要な動作は、ホストだけが責任をもって処理する方針
ホストが抜けたら切り替わる処理も必要
(*1) Sakashoについては以下を参照。
第4回DeNAゲーム開発勉強会: Rubyで作るGame Backend as a Service
http://www.slideshare.net/dena_study/game-baas
認証、データ保存/取得、etc. バトルデータ
同期イベント
Sakashoサーバ(*1) IRISサーバ
クライアント1(ホスト) クライアント2(非ホスト) クライアント3(非ホスト)
- 10. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
3. リアルタイム通信ゲームとして必要な処理(1/2)
10
IRISサーバへの接続/切断
⁃ 特に難しいことはない
部屋への参加/退出
⁃ 部屋名のルールはゲーム側で決める必要がある
⁃ 違うアプリバージョンで同じ部屋に入らないようにする工
夫などが必要
ホストクライアントの決定
⁃ 基本的には最初に部屋に入ったユーザがホストになるが、
ほぼ同時に接続した場合を考慮し、排他制御が必要
⁃ ミューテックス取得APIを使って決定
⁃ 最初にミューテックスを取得できたユーザがホストになる
⁃ ホストが抜けた場合も、再度ミューテックスの取り合いで
次のホストクライアントを決定
- 11. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
3. リアルタイム通信ゲームとして必要な処理(2/2)
11
バトル中の状態の同期
⁃ 同期が必要な情報をイベントとして定義しておく
• 例: バトル開始、バトル終了、移動通知、拠点奪取通知、...
⁃ このイベントデータをクライアント間で送受信する
⁃ SDKの用意するユニキャスト/ブロードキャストAPIは汎用
的なものなので、「バイナリ列」しか扱えない
⁃ ゲーム側で定義したイベントをバイナリ列に変換する必要
がある(シリアライズ)
⁃ イベント数が多いのでうまく実装するには工夫が必要
- 13. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
4. シリアライズ/デシリアライズ処理について(1/2)
13
ゲーム側で定義したイベントデータとバイナリ列を相互
に変換する処理
ガールアックスではProtocol Buffersを使用
⁃ Google製のシリアライザ
• https://developers.google.com/protocol-buffers/
⁃ C++の実装がある
⁃ プロトコル定義ファイル foo.proto を用意し、
protoc コマンドでコンパイルする
⁃ コンパイルの結果、C++ コードがfoo.pb.cc、
foo.pb.h に生成される
⁃ ゲーム側では、生成コードで定義されるクラスを使
用してシリアライズ/デシリアライズを行う
- 14. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
message BattleProtocol {
enum EventId {
ID_SYNC_PLAYER = 1;
}
message SyncPlayer {
required int32 x = 1;
required int32 y = 2;
}
required EventId event_id = 1;
optional SyncPlayer sync_player_data = 2;
}
4. シリアライズ/デシリアライズ処理について(2/2)
14
ゲーム側のイベント定義を、このプロトコル定義ファイ
ルで記述してやればよい
例
battle.proto # include "battle.pb.h"
// プレイヤー位置同期イベントのシリアライズ関数
void SerializePlayerSync(int x, int y, std::string& data) {
// protoc で生成されたAPIを使ってシリアライズ
BattleProtocol* pProto = new BattleProtocol();
pProto->set_event_id(ID_SYNC_PLAYER);
BattleProtocol::SyncPlayer* pSync
= pProto->mutable_sync_player();
pSync->set_x(x);
pSync->set_y(y);
pProto->SerializeToString(data);
}
BattleScene.cpp
battle.pb.h
battle.pb.cc
protocコマンドで
コンパイルして
ソースを生成
BattleProtocolクラス
の定義が含まれる
- 16. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
5. 効率よく実装するための工夫(1/6)
16
IRIS SDKのラッパークラスを作成
⁃ SDKで定義される型がゲーム側コードに混ざるとい
ろいろ面倒
• 担当者によって使い方が異なると統一性が無くなる
• SDK側の変更を取り込む時の影響範囲が増える
• ゲーム側/SDKで命名規則が違うので可読性が下がる
⁃ SDKのAPIをラップしたクラスを用意し、ゲーム側か
らはラッパークラスのみ使用
• SDKで定義される型は、ラッパクラス側で再定義
⁃ SDK側の変更の影響を受けづらくなる
• ラッパークラスだけSDK変更に追従させればよい
⁃ 内部実装を差し替えて、オフライン版も作成
- 17. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
5. 効率よく実装するための工夫(2/6)
17
処理をコマンドパターンで記述
⁃ 通信関連の処理をコードのあちこちに埋め込むと、
見通しが悪くなる
• 定型的な処理が多いので、処理の流れを分断しがち
⁃ コマンドパターンで実装することに決定
⁃ ひとまとまりの処理をコマンドクラスとして実装
• パラメータはコマンドクラスのメンバに持たせる
⁃ 処理が必要なタイミングでコマンドインスタンスを
生成してキューイング
⁃ キューイングされたコマンドは、フレーム更新処理
でまとめて処理
- 18. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
5. 効率よく実装するための工夫(3/6)
18
コマンドクラスの自動生成
⁃ 結構な数のコマンドクラスを作ることになる
⁃ 自動生成ツールCommandGeneratorを用意
⁃ JSONとコマンド処理のコードを書くだけでよい
コマンドのClass定義や
定型処理を自動生成
void FooCommand::Update(float dt)
{
// コマンドの処理
...
if (処理終了)
{
End();
}
}
コマンド処理のコードは
手動で書く
ス
ク
リ
プ
ト
{
"class": "FooCommand",
"parameters": [
{
"name": "bar",
"type": "int"
}
]
}
.h
.cpp
FooCommand.json
- 19. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
5. 効率よく実装するための工夫(4/6)
19
プロトコル定義ファイルの自動生成
⁃ イベントの種類が多いので、それなりの分量になる
⁃ 担当者によって書き方がバラバラだとメンテしづら
くなる
• 命名規則、フィールドの順序、使う型など
⁃ とはいえ、書き方の統一のためにルールを作ると覚
えることが増えて大変
⁃ 自動生成ツールEventGeneratorを用意
⁃ これのおかげで.protoの書き方を覚える必要がなく
なった
- 20. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
5. 効率よく実装するための工夫(5/6)
20
イベント自動生成ツールEventGenerator(1/2)
⁃ JSONでイベントを定義し、スクリプトで .proto フ
ァイルを自動生成
⁃ 前述のCommandGeneratorとも連携し、以下のコ
ード群も自動生成
• EventHandlerクラスのコード
⁃ シリアライズ/デシリアライズ処理、送信処理、受信処
理、etc.
• 受信処理用のコマンドクラス
⁃ イベント定義JSONとイベント受信コマンドのコード
を書くだけでよい
⁃ 送信処理はEventHandlerクラスの関数を呼ぶだけ
- 21. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
......
5. 効率よく実装するための工夫(6/6)
21
イベント自動生成ツールEventGenerator(2/2)
0
void HandleSyncPlayerEventCommand::Update(float dt)
{
// 受信したイベントの処理
}
イベント受信処理は
コマンドとして実装
.h
.cppス
ク
リ
プ
ト
イベント処理の大半
のコードを自動生成
{
"events": [
{
"name": "SyncPlayer",
"event_id": "ID_SYNC_PLAYER",
"parameters": [
{ "name": "x", "type": "int" },
{ "name": "y", "type": "int" }
]
}
...
]
}
EventProtocol.json
EventHandler.h
EventHandler.cppス
ク
リ
プ
ト
コ
ン
パ
イ
ル
.pb.h
.pb.cc
// イベントの送信
EventHandler::SendSyncPlayerEvent(x, y);
イベント送信処理は
EventHandlerの
メンバ関数で一発
event.proto
コマンド定義JSON
- 23. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
6. パフォーマンスチューニングのアイデア(1/4)
23
通信回数の削減(1/2)
⁃ 通信回数が多いと電池を食うので回数を減らす必要がある
⁃ 送信間隔を0.1秒(=6フレーム)にし、その間に発生した
データはキューに溜めておき、まとめて送信
• ゲームの仕様と遅延時間を考えると、これ以上の小さい間隔にす
る必要性はないと判断
⁃ 複数イベントのデータを保持するイベントを定義し、そこ
にデータを詰めて送信
⁃ UnicastとBroadcastが混ざるとまとめられない
• 例えば以下がキューにある場合、順序を維持して送信しないとい
けないので、個別に3回送信する必要がある
⁃ 1. 全員宛の移動通知メッセージ(Broadcast)
⁃ 2. 特定のプレイヤー宛の攻撃通知メッセージ(Unicast)
⁃ 3. 全員宛の移動通知メッセージ(Broadcast)
- 24. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
6. パフォーマンスチューニングのアイデア(2/4)
24
通信回数の削減(2/2)
⁃ 順序を維持しないといけないメッセージはあえて
Broadcast(全員宛)で送信
• メッセージ内に宛先を入れておく
• 受信側は、自分宛じゃないものは読み捨てる
⁃ メッセージサイズは増えるし不要な相手にも送ることにな
って無駄だが、送信回数が減るメリットのほうが大きい
⁃ 送信回数を約四分の一に削減できた
• 対応前: 0.1秒間に約4回 → 対応後: 0.1秒間に1回
- 25. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
6. パフォーマンスチューニングのアイデア(3/4)
25
通信データ量の削減(1/2)
⁃ Protocol Buffers のシリアライズ処理に任せっきり
だと無駄がある
• 例えば0~3しか値を取らない変数であれば2ビットで表現できる
はずだが、Protocol Buffers では1バイト使う
⁃ ビットレベルで最適化してパッキングを行う
⁃ メッセージデータの構造体のデータをもとに、パッ
キング用の構造体を定義
• パッキングデータ用のイベントも定義
⁃ ビット演算でパッキングデータに変換してから送信
⁃ 受信側でもパッキングデータを元に戻してから処理
- 26. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
6. パフォーマンスチューニングのアイデア(4/4)
26
通信データ量の削減(2/2)
struct PlayerData {
uint8_t playerType; // 値域: 0~3 : 2ビット
uint8_t jobType; // 値域: 0~7 : 3ビット
uint16_t posX; // 値域: 0~4000 : 12ビット
uint16_t posY; // 値域: 0~1000 : 10ビット
uint16_t hp; // 値域: 0~15000 : 14ビット
uint16_t maxHp; // 値域: 0~15000 : 14ビット
}
struct PlayerDataPack {
uint32_t data1; // ZERO埋め(6ビット), hp(14ビット), posY(10ビット), playerType(2ビット)
uint32_t data2; // ZERO埋め(3ビット), posX(12ビット), maxHp(14ビット), jobType(3ビット)
}
ガールアックスでは送信頻度が上位のイベントに適用
⁃ 総送信データ量で10%程度の削減ができた
⁃ ロジック変更なしで削減出来るのが大きなメリット
例
ビットレベルで並べ替えるための構造体を定義、送信時に変換。
Protocol Buffersのシリアライズ処理を考慮し、
上位ビットに0が並びやすい形にする。
- 28. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
7. デバッグ効率化のためのアイデア(1/2)
28
確認用のログを充実させる
⁃ リアルタイム通信ゲーム特有のバグは厄介
• 特定のケースでイベントを受信できない、イベント順序がおかしい、等
⁃ 複数のクライアントがいるので、デバッガで追うのは困難
⁃ ログから解析する以外に調査方法がない事が多い
⁃ 送信側の送信データと受信側の受信データを突き合わせる等ができ
るような形でログを埋め込む
• シリアライズされたデータの16進ダンプ、パラメータ値など
⁃ 例:
パラメータを出力しておく
■送信側ログ
AddCommand: SyncPlayerCommand{x=14, y=112}
EventHandler::SendSyncPlayerEvent(): <0013de32 22f90a> (7 bytes)
■受信側ログ
EventHandler::HandleSyncPlayerEvent(): Recv <0013de32 22f90a> (7 bytes)
AddCommand: HandleSyncPlayerEventCommand{x=14, y=112}
このダンプ値で送信側/受信側の
ログの突き合わせができる
- 29. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
7. デバッグ効率化のためのアイデア(2/2)
29
統計情報の取得
⁃ 各イベントごとの送受信数、送受信データサイズ等の統計
情報を取得するようにしておく
⁃ 統計情報を取るためのコードは自動生成に組み込む
• 前述のEventGeneratorで生成されるEventHandlerで処理
⁃ これを見てパフォーマンス改善を行う
⁃ 改善の結果、効果があったのか無かったのか確認をすぐに
行えるようにしておくのが大事
⁃ 良く発生するイベントについてはデバッグ情報として表示
しておくと良い
- 30. Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
まとめ
30
定型的な処理にはコードの自動生成が効果的
パフォーマンス改善は細かいチューニングの積み
重ねが大事
デバッグのためのログを充実させておくと楽