Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

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

第7回DeNAゲーム開発勉強会×モノビット

  • Login to see the comments

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

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

×