Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
ガールアックス:
リアルタイム通信処理の効率的な実装
第7回DeNAゲーム開発勉強会✕モノビット
株式会社ディー・エヌ・エー
Japanリージョンゲーム事業本部
技術・編成部 開発基盤グループ
堀米 智彦 tomohiko.horigome@dena.com
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
自己紹介
 堀米 智彦(ほりごめ ともひこ)
 DeNA 入社5年目(2011年8月 中途入社)
⁃ 前職は組み込み機器向けブラウザ開発
 入社後の業務経歴
⁃ ゲーム向けライブラリ開発
⁃ Ninja Royale エンジニア
⁃ D.O.T. エンジニア/リードエンジニア
⁃ 三国志ロワイヤル エンジニア/リードエンジニア
⁃ ガールアックス エンジニア
2
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
アジェンダ
3
 「ガールアックス」でのリアルタイム通信処理の実装の
詳細について、ご紹介いたします
⁃ 1. リアルタイム通信処理プラットフォーム「IRIS」
⁃ 2. サーバ/クライアント構成
⁃ 3. リアルタイム通信ゲームとして必要な処理
⁃ 4. シリアライズ/デシリアライズ処理について
⁃ 5. 効率よく実装するための工夫
⁃ 6. パフォーマンスチューニングのアイデア
⁃ 7. デバッグ効率化のためのアイデア
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
本題に入る前に:ガールアックスとは?
4
 5vs5対戦 カジュアルMOBAゲーム
 iOS/Android向け
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
1. リアルタイム通信処理プラットフォーム「IRIS」
5
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
1. リアルタイム通信処理プラットフォーム「IRIS」
6
 DeNA内製のリアルタイム通信プラットフォーム
 IRISサーバとIRIS C++ Client SDKが用意されている
 ゲーム側からSDKの各種APIを呼び出し、通信処理を行う
 使用しているAPIはざっくり以下
⁃ 接続処理
⁃ 切断処理
⁃ 部屋への参加(指定した部屋名の部屋に入る or 無かったら作って入る)
⁃ 部屋からの退出
⁃ ミューテックス取得(排他制御)
⁃ ユニキャスト(特定にユーザにだけ送信)
⁃ ブロードキャスト(部屋に参加中の全ユーザに送信)
⁃ 受信データ取得
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
2. ガールアックスのサーバ/クライアント構成
7
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(非ホスト)
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
3. リアルタイム通信ゲームとして必要な処理
9
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
3. リアルタイム通信ゲームとして必要な処理(1/2)
10
 IRISサーバへの接続/切断
⁃ 特に難しいことはない
 部屋への参加/退出
⁃ 部屋名のルールはゲーム側で決める必要がある
⁃ 違うアプリバージョンで同じ部屋に入らないようにする工
夫などが必要
 ホストクライアントの決定
⁃ 基本的には最初に部屋に入ったユーザがホストになるが、
ほぼ同時に接続した場合を考慮し、排他制御が必要
⁃ ミューテックス取得APIを使って決定
⁃ 最初にミューテックスを取得できたユーザがホストになる
⁃ ホストが抜けた場合も、再度ミューテックスの取り合いで
次のホストクライアントを決定
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
3. リアルタイム通信ゲームとして必要な処理(2/2)
11
 バトル中の状態の同期
⁃ 同期が必要な情報をイベントとして定義しておく
• 例: バトル開始、バトル終了、移動通知、拠点奪取通知、...
⁃ このイベントデータをクライアント間で送受信する
⁃ SDKの用意するユニキャスト/ブロードキャストAPIは汎用
的なものなので、「バイナリ列」しか扱えない
⁃ ゲーム側で定義したイベントをバイナリ列に変換する必要
がある(シリアライズ)
⁃ イベント数が多いのでうまく実装するには工夫が必要
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
4. シリアライズ/デシリアライズ処理について
12
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 に生成される
⁃ ゲーム側では、生成コードで定義されるクラスを使
用してシリアライズ/デシリアライズを行う
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クラス
の定義が含まれる
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
5. 効率よく実装するための工夫
15
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
5. 効率よく実装するための工夫(1/6)
16
 IRIS SDKのラッパークラスを作成
⁃ SDKで定義される型がゲーム側コードに混ざるとい
ろいろ面倒
• 担当者によって使い方が異なると統一性が無くなる
• SDK側の変更を取り込む時の影響範囲が増える
• ゲーム側/SDKで命名規則が違うので可読性が下がる
⁃ SDKのAPIをラップしたクラスを用意し、ゲーム側か
らはラッパークラスのみ使用
• SDKで定義される型は、ラッパクラス側で再定義
⁃ SDK側の変更の影響を受けづらくなる
• ラッパークラスだけSDK変更に追従させればよい
⁃ 内部実装を差し替えて、オフライン版も作成
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
5. 効率よく実装するための工夫(2/6)
17
 処理をコマンドパターンで記述
⁃ 通信関連の処理をコードのあちこちに埋め込むと、
見通しが悪くなる
• 定型的な処理が多いので、処理の流れを分断しがち
⁃ コマンドパターンで実装することに決定
⁃ ひとまとまりの処理をコマンドクラスとして実装
• パラメータはコマンドクラスのメンバに持たせる
⁃ 処理が必要なタイミングでコマンドインスタンスを
生成してキューイング
⁃ キューイングされたコマンドは、フレーム更新処理
でまとめて処理
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
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
5. 効率よく実装するための工夫(4/6)
19
 プロトコル定義ファイルの自動生成
⁃ イベントの種類が多いので、それなりの分量になる
⁃ 担当者によって書き方がバラバラだとメンテしづら
くなる
• 命名規則、フィールドの順序、使う型など
⁃ とはいえ、書き方の統一のためにルールを作ると覚
えることが増えて大変
⁃ 自動生成ツールEventGeneratorを用意
⁃ これのおかげで.protoの書き方を覚える必要がなく
なった
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
5. 効率よく実装するための工夫(5/6)
20
 イベント自動生成ツールEventGenerator(1/2)
⁃ JSONでイベントを定義し、スクリプトで .proto フ
ァイルを自動生成
⁃ 前述のCommandGeneratorとも連携し、以下のコ
ード群も自動生成
• EventHandlerクラスのコード
⁃ シリアライズ/デシリアライズ処理、送信処理、受信処
理、etc.
• 受信処理用のコマンドクラス
⁃ イベント定義JSONとイベント受信コマンドのコード
を書くだけでよい
⁃ 送信処理はEventHandlerクラスの関数を呼ぶだけ
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
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
6. パフォーマンスチューニングのアイデア
22
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)
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
6. パフォーマンスチューニングのアイデア(2/4)
24
 通信回数の削減(2/2)
⁃ 順序を維持しないといけないメッセージはあえて
Broadcast(全員宛)で送信
• メッセージ内に宛先を入れておく
• 受信側は、自分宛じゃないものは読み捨てる
⁃ メッセージサイズは増えるし不要な相手にも送ることにな
って無駄だが、送信回数が減るメリットのほうが大きい
⁃ 送信回数を約四分の一に削減できた
• 対応前: 0.1秒間に約4回 → 対応後: 0.1秒間に1回
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
6. パフォーマンスチューニングのアイデア(3/4)
25
 通信データ量の削減(1/2)
⁃ Protocol Buffers のシリアライズ処理に任せっきり
だと無駄がある
• 例えば0~3しか値を取らない変数であれば2ビットで表現できる
はずだが、Protocol Buffers では1バイト使う
⁃ ビットレベルで最適化してパッキングを行う
⁃ メッセージデータの構造体のデータをもとに、パッ
キング用の構造体を定義
• パッキングデータ用のイベントも定義
⁃ ビット演算でパッキングデータに変換してから送信
⁃ 受信側でもパッキングデータを元に戻してから処理
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が並びやすい形にする。
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
7. デバッグ効率化のためのアイデア
27
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}
このダンプ値で送信側/受信側の
ログの突き合わせができる
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
7. デバッグ効率化のためのアイデア(2/2)
29
 統計情報の取得
⁃ 各イベントごとの送受信数、送受信データサイズ等の統計
情報を取得するようにしておく
⁃ 統計情報を取るためのコードは自動生成に組み込む
• 前述のEventGeneratorで生成されるEventHandlerで処理
⁃ これを見てパフォーマンス改善を行う
⁃ 改善の結果、効果があったのか無かったのか確認をすぐに
行えるようにしておくのが大事
⁃ 良く発生するイベントについてはデバッグ情報として表示
しておくと良い
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
まとめ
30
 定型的な処理にはコードの自動生成が効果的
 パフォーマンス改善は細かいチューニングの積み
重ねが大事
 デバッグのためのログを充実させておくと楽
Copyright (C) DeNA Co.,Ltd. All Rights Reserved.
ご清聴ありがとうございました
31

ガールアックス:リアルタイム通信処理の効率的な実装

  • 1.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. ガールアックス: リアルタイム通信処理の効率的な実装 第7回DeNAゲーム開発勉強会✕モノビット 株式会社ディー・エヌ・エー Japanリージョンゲーム事業本部 技術・編成部 開発基盤グループ 堀米 智彦 tomohiko.horigome@dena.com
  • 2.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. 自己紹介  堀米 智彦(ほりごめ ともひこ)  DeNA 入社5年目(2011年8月 中途入社) ⁃ 前職は組み込み機器向けブラウザ開発  入社後の業務経歴 ⁃ ゲーム向けライブラリ開発 ⁃ Ninja Royale エンジニア ⁃ D.O.T. エンジニア/リードエンジニア ⁃ 三国志ロワイヤル エンジニア/リードエンジニア ⁃ ガールアックス エンジニア 2
  • 3.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. アジェンダ 3  「ガールアックス」でのリアルタイム通信処理の実装の 詳細について、ご紹介いたします ⁃ 1. リアルタイム通信処理プラットフォーム「IRIS」 ⁃ 2. サーバ/クライアント構成 ⁃ 3. リアルタイム通信ゲームとして必要な処理 ⁃ 4. シリアライズ/デシリアライズ処理について ⁃ 5. 効率よく実装するための工夫 ⁃ 6. パフォーマンスチューニングのアイデア ⁃ 7. デバッグ効率化のためのアイデア
  • 4.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. 本題に入る前に:ガールアックスとは? 4  5vs5対戦 カジュアルMOBAゲーム  iOS/Android向け
  • 5.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. 1. リアルタイム通信処理プラットフォーム「IRIS」 5
  • 6.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. 1. リアルタイム通信処理プラットフォーム「IRIS」 6  DeNA内製のリアルタイム通信プラットフォーム  IRISサーバとIRIS C++ Client SDKが用意されている  ゲーム側からSDKの各種APIを呼び出し、通信処理を行う  使用しているAPIはざっくり以下 ⁃ 接続処理 ⁃ 切断処理 ⁃ 部屋への参加(指定した部屋名の部屋に入る or 無かったら作って入る) ⁃ 部屋からの退出 ⁃ ミューテックス取得(排他制御) ⁃ ユニキャスト(特定にユーザにだけ送信) ⁃ ブロードキャスト(部屋に参加中の全ユーザに送信) ⁃ 受信データ取得
  • 7.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. 2. ガールアックスのサーバ/クライアント構成 7
  • 8.
    Copyright (C) DeNACo.,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(非ホスト)
  • 9.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. 3. リアルタイム通信ゲームとして必要な処理 9
  • 10.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. 3. リアルタイム通信ゲームとして必要な処理(1/2) 10  IRISサーバへの接続/切断 ⁃ 特に難しいことはない  部屋への参加/退出 ⁃ 部屋名のルールはゲーム側で決める必要がある ⁃ 違うアプリバージョンで同じ部屋に入らないようにする工 夫などが必要  ホストクライアントの決定 ⁃ 基本的には最初に部屋に入ったユーザがホストになるが、 ほぼ同時に接続した場合を考慮し、排他制御が必要 ⁃ ミューテックス取得APIを使って決定 ⁃ 最初にミューテックスを取得できたユーザがホストになる ⁃ ホストが抜けた場合も、再度ミューテックスの取り合いで 次のホストクライアントを決定
  • 11.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. 3. リアルタイム通信ゲームとして必要な処理(2/2) 11  バトル中の状態の同期 ⁃ 同期が必要な情報をイベントとして定義しておく • 例: バトル開始、バトル終了、移動通知、拠点奪取通知、... ⁃ このイベントデータをクライアント間で送受信する ⁃ SDKの用意するユニキャスト/ブロードキャストAPIは汎用 的なものなので、「バイナリ列」しか扱えない ⁃ ゲーム側で定義したイベントをバイナリ列に変換する必要 がある(シリアライズ) ⁃ イベント数が多いのでうまく実装するには工夫が必要
  • 12.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. 4. シリアライズ/デシリアライズ処理について 12
  • 13.
    Copyright (C) DeNACo.,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) DeNACo.,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クラス の定義が含まれる
  • 15.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. 5. 効率よく実装するための工夫 15
  • 16.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. 5. 効率よく実装するための工夫(1/6) 16  IRIS SDKのラッパークラスを作成 ⁃ SDKで定義される型がゲーム側コードに混ざるとい ろいろ面倒 • 担当者によって使い方が異なると統一性が無くなる • SDK側の変更を取り込む時の影響範囲が増える • ゲーム側/SDKで命名規則が違うので可読性が下がる ⁃ SDKのAPIをラップしたクラスを用意し、ゲーム側か らはラッパークラスのみ使用 • SDKで定義される型は、ラッパクラス側で再定義 ⁃ SDK側の変更の影響を受けづらくなる • ラッパークラスだけSDK変更に追従させればよい ⁃ 内部実装を差し替えて、オフライン版も作成
  • 17.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. 5. 効率よく実装するための工夫(2/6) 17  処理をコマンドパターンで記述 ⁃ 通信関連の処理をコードのあちこちに埋め込むと、 見通しが悪くなる • 定型的な処理が多いので、処理の流れを分断しがち ⁃ コマンドパターンで実装することに決定 ⁃ ひとまとまりの処理をコマンドクラスとして実装 • パラメータはコマンドクラスのメンバに持たせる ⁃ 処理が必要なタイミングでコマンドインスタンスを 生成してキューイング ⁃ キューイングされたコマンドは、フレーム更新処理 でまとめて処理
  • 18.
    Copyright (C) DeNACo.,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) DeNACo.,Ltd. All Rights Reserved. 5. 効率よく実装するための工夫(4/6) 19  プロトコル定義ファイルの自動生成 ⁃ イベントの種類が多いので、それなりの分量になる ⁃ 担当者によって書き方がバラバラだとメンテしづら くなる • 命名規則、フィールドの順序、使う型など ⁃ とはいえ、書き方の統一のためにルールを作ると覚 えることが増えて大変 ⁃ 自動生成ツールEventGeneratorを用意 ⁃ これのおかげで.protoの書き方を覚える必要がなく なった
  • 20.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. 5. 効率よく実装するための工夫(5/6) 20  イベント自動生成ツールEventGenerator(1/2) ⁃ JSONでイベントを定義し、スクリプトで .proto フ ァイルを自動生成 ⁃ 前述のCommandGeneratorとも連携し、以下のコ ード群も自動生成 • EventHandlerクラスのコード ⁃ シリアライズ/デシリアライズ処理、送信処理、受信処 理、etc. • 受信処理用のコマンドクラス ⁃ イベント定義JSONとイベント受信コマンドのコード を書くだけでよい ⁃ 送信処理はEventHandlerクラスの関数を呼ぶだけ
  • 21.
    Copyright (C) DeNACo.,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
  • 22.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. 6. パフォーマンスチューニングのアイデア 22
  • 23.
    Copyright (C) DeNACo.,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) DeNACo.,Ltd. All Rights Reserved. 6. パフォーマンスチューニングのアイデア(2/4) 24  通信回数の削減(2/2) ⁃ 順序を維持しないといけないメッセージはあえて Broadcast(全員宛)で送信 • メッセージ内に宛先を入れておく • 受信側は、自分宛じゃないものは読み捨てる ⁃ メッセージサイズは増えるし不要な相手にも送ることにな って無駄だが、送信回数が減るメリットのほうが大きい ⁃ 送信回数を約四分の一に削減できた • 対応前: 0.1秒間に約4回 → 対応後: 0.1秒間に1回
  • 25.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. 6. パフォーマンスチューニングのアイデア(3/4) 25  通信データ量の削減(1/2) ⁃ Protocol Buffers のシリアライズ処理に任せっきり だと無駄がある • 例えば0~3しか値を取らない変数であれば2ビットで表現できる はずだが、Protocol Buffers では1バイト使う ⁃ ビットレベルで最適化してパッキングを行う ⁃ メッセージデータの構造体のデータをもとに、パッ キング用の構造体を定義 • パッキングデータ用のイベントも定義 ⁃ ビット演算でパッキングデータに変換してから送信 ⁃ 受信側でもパッキングデータを元に戻してから処理
  • 26.
    Copyright (C) DeNACo.,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が並びやすい形にする。
  • 27.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. 7. デバッグ効率化のためのアイデア 27
  • 28.
    Copyright (C) DeNACo.,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) DeNACo.,Ltd. All Rights Reserved. 7. デバッグ効率化のためのアイデア(2/2) 29  統計情報の取得 ⁃ 各イベントごとの送受信数、送受信データサイズ等の統計 情報を取得するようにしておく ⁃ 統計情報を取るためのコードは自動生成に組み込む • 前述のEventGeneratorで生成されるEventHandlerで処理 ⁃ これを見てパフォーマンス改善を行う ⁃ 改善の結果、効果があったのか無かったのか確認をすぐに 行えるようにしておくのが大事 ⁃ 良く発生するイベントについてはデバッグ情報として表示 しておくと良い
  • 30.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. まとめ 30  定型的な処理にはコードの自動生成が効果的  パフォーマンス改善は細かいチューニングの積み 重ねが大事  デバッグのためのログを充実させておくと楽
  • 31.
    Copyright (C) DeNACo.,Ltd. All Rights Reserved. ご清聴ありがとうございました 31