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

31
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. ガールアックス: リアルタイム通信処理の効率的な実装 第7回DeNAゲーム開発勉強会✕モノビット 株式会社ディー・エヌ・エー Japanリージョンゲーム事業本部 技術・編成部 開発基盤グループ 堀米 智彦 [email protected]

Upload: denastudy

Post on 07-Jan-2017

2.526 views

Category:

Technology


1 download

TRANSCRIPT

Copyright (C) DeNA Co.,Ltd. All Rights Reserved.

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

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

株式会社ディー・エヌ・エー Japanリージョンゲーム事業本部 技術・編成部 開発基盤グループ 堀米 智彦 [email protected]

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