MagicOnion
〜C#でゲームサーバを開発しよう〜
.NET Conf in Tokyo 2019
#dotnetconfunity
2019/10/27
とりすーぷ
⾃⼰紹介
• 「とりすーぷ」
• @toRisouP
• 株式会社バーチャルキャスト 開発
• xR開発
• 最近はサーバ開発
• Microsoft MVP
for Developer Technologies 2018〜2020
今回の話
• MagicOnionについて解説
• 何のためのフレームワークか
• どんな機能があるのか
• 環境構築
• 実装例の紹介
• 実装時の注意点
• デプロイ
MagicOnionって何?
MagicOnion is 何
• ネットワーク通信のためのフレームワーク
• Unityでのゲーム開発を中⼼に使えるネットワーク通信フレームワーク
• サーバサイド:.NET Core
• クライアントサイド:Unity / .NET Core
• 開発はCysharp社
• MITライセンス
MagicOnionのバックエンド
• C# + .NET Core / Unity
• 通信プロトコル: gRPC
• データフォーマット: MessagePack for C#
• 定義したC#ファイルがそのままDSLになる
• C#インタフェースがAPIのエンドポイント
• C#オブジェクトが通信データ構造(MessagePackObject)
MagicOnionのバックエンド
クライアント サーバ
プロトコルはgRPC
データフォーマットはMessagePack C#ファイルの定義がそのままAPIのエンドポイントに
コード共有
• 定義したインタフェースやオブジェクトを、
サーバとクライアントで共有することで
そのままAPI定義として利⽤できる
コード共有
クライアント サーバ
インタフェース定義
(APIエンドポイント)
インタフェース定義されたC#ファイルを
クライアント・サーバで共有
例:APIエンド先⽃の定義
共有するインタフェースを定義
実装
クライアント サーバ
サーバ側はインタフェースを実装する
実装
サーバ側実装
呼び出し
クライアント サーバ
クライアント側はただ
インタフェースのメソッドを呼び出すだけ
メソッドコール
(async/awiat)
クライアント側の呼び出し
gRPCのコネクションを貼ってからメソッドを呼ぶだけ
インタフェースに定義されたメソッドをコール
裏で通信が⾏われ、結果はasync/awaitで待てる
サーバ処理の呼び出しの流れ
サーバ
メソッド実⾏すると裏でサーバコードが呼び出されて
その結果が返ってくるのをawaitで待てる
共有ファイルを介した通信
クライアント サーバ
共有したインタフェースを介して
クライアント/サーバ間の通信が可能になる
MagicOnion 最⼤の特徴
• 通信レイヤを意識せずにサーバコードを呼び出せる
• サーバとクライアントでC#インタフェースを共有すればOK
• 通信のデータ構造もC#で定義すればOK
• async/awaitでメソッド呼び出しするだけでサーバの処理を実⾏できる
• ただし、gRPCのコネクション管理だけは⾃前で管理しないとダメ(しょうがない)
MagicOnionの機能紹介
機能⼀覧
• 実装できるAPIの種類
• Service、StreamingHub
• Filter機能
• Filter
• その他便利な機能
• Swagger、Telemetry
APIの実装
• ⼤きく2つのAPI実装パターンがある
• Service
• StreamingHub
Service
• Service
• シンプルな単発のAPIを作成するのに使う
• 1Request ‒ 1Response通信(中⾝はUnary RPC)
• リクエストを投げた本⼈にのみレスポンスを返せる
Service
クライアント
サーバRequest
Response
1Requestに対して1Responseのみ返せるのが
「Service」で実装したAPI
StreamingHub
• StreamingHub
• リアルタイム通信向け
• 中⾝はBidirectional Streaming RPC
• 仮想的なコネクションを貼ったままずっと維持する
• クライアントとサーバが⾃由にメッセージを送受信できる
• 全体へブロードキャストも可能
StreamingHub
クライアント
サーバ
Message
クライアント同⼠がサーバを介して
相互通信できるのが「StremingHub」
Broadcast
Broadcast
機能⼀覧
• 実装できるAPIの種類
• Service、StreamingHub
• Filter機能
• Filter
• その他便利な機能
• Swagger、Telemetry
Filter
• 通信の前後に処理を追加する機能
• サーバ側フィルタ
• Service、StreamingHubに適⽤可能
• クライアント側フィルタ
• Serviceのみ対応
Filterの実装例(サーバ側)
Filterを反映する
• Filterを付けたいクラス/メソッドにAttributeをつける
• このメソッド/クラスのRPCが実⾏されるタイミングで、
定義したFilterを通過する
Filterの⽤途
• 使⽤例
• 認証⽤のヘッダーを追加
• メッセージの暗号化/復号化
• 通信ログ出⼒
• 通信をせずにダミーのレスポンスの差し替え
• エラーメッセージのハンドリング
• リトライ処理
機能⼀覧
• 実装できるAPIの種類
• Service、StreamingHub
• Filter機能
• Filter
• その他便利な機能
• Swagger、Telemetry
便利な機能
• Swagger対応
• 「Service」で実装したメソッドをREST APIとして実⾏できる
• Webブラウザ上でAPIの動作確認ができる
• デバッグに便利
• Telemetry対応
• MagicOnionの監視
• メトリクス
今回の話
• MagicOnionについて解説
• 何のためのフレームワークか
• どんな機能があるのか
• 環境構築
• 実装例の紹介
• 実装時の注意点
• デプロイ
MagicOnionの環境構築
環境構築
• MagicOnionは環境構築がちょっとたいへん
• クライアント側(Unity)は⼿動でライブラリ追加が必要
• コード共有
• コードジェネレート
クライアント ライブラリ導⼊
• これらのライブラリを⼿動でUnityに導⼊する必要がある
• MagicOnion.Client.Unity.unitypackage
• https://github.com/Cysharp/MagicOnion/releases
• MessagePack.Unity.x.x.x.x.unitypackage
• https://github.com/neuecc/MessagePack-CSharp/releases
• gRPC Unity plugin
• https://packages.grpc.io/
Player Settingsも設定が必要
• Scripting Define Symbols
• ENABLE_UNSAFE_MSGPACK を書き⾜す
• Allow ʻunsafeʼ Code
• Enable
ついでに導⼊するとよさげ
• UniRx
• Reactive Extensions for Unity
• イベント処理の整理に便利
• UniTask
• ValueTask for Unity
• Unity向けのAwaiter実装の提供
• ⾮同期周りにはコレ
環境構築
• MagicOnionは環境構築がちょっとたいへん
• クライアント側(Unity)は⼿動でライブラリ追加が必要
• コード共有
• コードジェネレート
コード共有について
クライアント サーバ
インタフェース定義
(APIエンドポイント)
インタフェース定義されたC#ファイルを
どうやってクライアント/サーバでコード共有するの?
Unity側の制約
• C#コードはAssets/以下に配置しないといけない
• UnityはAssets/以下のC#ファイルしかコンパイルしてくれない
オススメの⽅法
• サーバ側のcsprojからUnity/Assets以下を参照する
• 実体はUnity側にあり、それをサーバプロジェクトで開いているだけ
参照
Unityサーバプロジェクト
プロジェクト構成の例
Assets/
Assets/Shared/
サーバプロジェクト(ソリューション構成)
Server.Interfaces.csproj Server.Core.csproj
プロジェクト構成の⼀例
プロジェクト構成の例
Assets/
Assets/Shared/
サーバプロジェクト(ソリューション構成)
Server.Interfaces.csproj Server.Core.csproj
共有⽤csprojからUnity/Assets/以下を参照する
参照
ファイルの実体はこっち
プロジェクト構成の例
Assets/
Assets/Shared/
サーバプロジェクト(ソリューション構成)
Server.Interfaces.csproj Server.Core.csproj
ポイント:共有するcsprojと、サーバのコアロジックのcsprojを分離して
コアプロジェクトから共有プロジェクトを参照する
参照
プロジェクト構成の例
Assets/
Assets/Shared/
Server.Interfaces.csproj Server.Core.csproj
git管理するときはこれらをまとめて1つのリポジトリに⼊れる
これで1つの Git Repository
この⽅法のメリット
• 設定が簡単
• csprojに数⾏書き⾜すだけで設定が終わる
• リポジトリをチェックアウトしてくればそのまま動く
• 環境に依存しない
• シンボリックリンクとかそういう⾯倒な作業もいらない
• 相対パスさえ維持されていれば動く
デメリット
• リポジトリが1個にまとまってしまう
• サーバのビルドにクライアントコードのcheckoutも必要になる
• クライアントの容量が⼤きくなってくるとCI/CDがツライことになる
• 開発フローの整理が難しくなる
• クライアント開発とサーバ開発が同じリポジトリで管理される
• GitHubのissue/pr 欄が混ざる
折半案
サーバ側
サーバプロジェクトのポジトリ
Git Repository
Git Repository
ゲーム本体のリポジトリ
折半案
サーバ側
ここのUnityプロジェクトは、
Headlessなクライアント + Debug Scene
が置かれる
Git Repository
Git Repository
折半案
サーバ側
ゲーム本体のプロジェクトは
別のリポジトリとして運⽤する
折半案
サーバ側
接続部分をunitypackageにまとめて
本体側でインポートする
unitypackage
補⾜:UnityPackageはどう管理する?
• Unity Package Managerで管理できる
• Unity標準のパッケージマネージャ機能
• まだ発展途上
• ⾃前でレジストリを⽴てればオレオレライブラリも管理可能
• 余⼒があるならコレで管理すると良さそう
環境構築
• MagicOnionは環境構築がちょっとたいへん
• クライアント側(Unity)は⼿動でライブラリ追加が必要
• コード共有
• コードジェネレート
コードジェネレートの話
• MagicOnionの動作にはコードジェネレートが2回必要
• MagicOnionのインタフェース定義
• MessagePackのオブジェクト定義
• ただし、ジェネレートが必要なのはUnity側のみ(サーバ側は不要)
• ジェネレートしたコードはUnityプロジェクトに配置する
ジェネレータの⽤意
• それぞれGitHubからダウンロードできる
MagicOnion MessagePack
ジェネレータの使い⽅
• 対象のcsprojをコマンドラインで指定して実⾏
• MagicOnnion/MessagePack共に同じコマンド引数で動く
指定するcsproj
• 共有するインタフェースとオブジェクトが含まれたもの
• 共有するファイルだけまとめたcsprojを⽤意しておくと楽
指定するcsprojの例
Assets/
Assets/Shared/
サーバプロジェクト(ソリューション構成)
Server.Interfaces.csproj Server.Core.csproj
これ!
注意点
• ジェネレート対象のcsprojがMagicOnionとMessagePack
に依存するようにしておくこと
• 依存させておかないとコードジェネレートが動かない
Server.Interfaces.csproj
コマンド例
./moc/win-x64/moc.exe
‒i ./Server/ServerSample.Interfaces/ServerSample.Interfaces.csproj
‒o ./Unity/Assets/Scripts/Generated/MagicOnion.Generated.cs
• MagicOnionのジェネレート例
• ServerSample.Interfaces.csprojをもとに、Unity/Assets/以下に⽣成
MessagePackのResolver設定だけ忘れずに
• Unityプロジェクトに次のようなコードを配置
• RuntimeInitializeOnLoadMethodで起動時に実⾏されるようにしておく
Editor拡張でジェネレートできると楽
• MagicOnionのサンプルコードに実装例がある
• https://github.com/Cysharp/MagicOnion/blob/master
/samples/ChatApp/ChatApp.Unity/Assets/Editor/MenuItems
.cs
環境構築 まとめ
• 環境構築がちょっと⼤変
• csprojを編集する必要がある
• コードジェネレートの整備が⼿間
• 開発フローはよく考えよう
• Unity側とサーバ側、2つのプロジェクト管理を同時にやる必要
• gitリポジトリをどうわけるか、どういうフローで開発をすすめるのか
• CI/CDとの相性
今回の話
• MagicOnionについて解説
• 何のためのフレームワークか
• どんな機能があるのか
• 環境構築
• 実装例の紹介
• 実装時の注意点
• デプロイ
実装例の紹介
実装例
• それぞれの実装例を紹介
• Service
• StreamingHub
Serviceの実装例
• サーバサイドで⾜し算する
• シンプルに、引数に与えた数値を⾜して返す
Serviceの実装例
• 実装⼿順
1. 共有するインタフェース/オブジェクトを定義
2. サーバに実装を書く
3. サーバにコネクションする
4. Unity側で呼び出す
Serviceの実装例
• 実装⼿順
1. 共有するインタフェース/オブジェクトを定義
2. サーバに実装を書く
3. サーバにコネクションする
4. Unity側で呼び出す
指定するcsprojの例
Assets/
Assets/Shared/
サーバプロジェクト(ソリューション構成)
Server.Interfaces.csproj Server.Core.csproj
ここに定義する
(どっちに定義しても実体は1個なので同じ)
Service:サーバサイドで計算する
1.サーバ/クライアントで共有するインタフェースを⽤意
IService<T>を継承したインタフェースを定義する
インタフェース内に定義したメソッドがAPIエンドポイントになる
Serviceの実装例
• 実装⼿順
1. 共有するインタフェース/オブジェクトを定義
2. サーバに実装を書く
3. サーバにコネクションする
4. Unity側で呼び出す
指定するcsprojの例
Assets/
Assets/Shared/
サーバプロジェクト(ソリューション構成)
Server.Interfaces.csproj Server.Core.csproj
ここに定義する
例:サーバサイドで計算する
2.サーバ側で実装を⾏う
インタフェースを実装
今回は引数を⾜して返すだけ
Serviceの実装例
• 実装⼿順
1. 共有するインタフェース/オブジェクトを定義
2. サーバに実装を書く
3. サーバにコネクションする
4. Unity側で呼び出す
例:サーバサイドで計算する
3. サーバにコネクションする
gRPCのチャンネルを作って
例:サーバサイドで計算する
4. Unity側で呼び出す
ICalculateServiceへのクライアントを作り
例:サーバサイドで計算する
4. Unity側で呼び出す
あとは普通にメソッドコールするだけ
通信の結果はasync/awaitで待つだけでOK
例:サーバサイドで計算する
サーバ
メソッド実⾏すると裏でサーバコードが呼び出されて
その結果が返ってくるのをawaitで待てる
Serviceの実装は簡単
• リクエストに対して、処理を書いて、結果を返すだけ
• かんたん
• クライアント側は普通にasync/awaitでメソッドを呼ぶだけ
• サーバ側はインタフェースを実装するだけ
StreamingHubの実装例
StreamingHubはムズカシイ
• StreamingHubはリアルタイム通信を実現する機構
• クライアントとサーバが相互にメソッドを呼び出し合える
• Serviceの実装と⽐べて複雑になりやすい
• 「状態」がクライアントとサーバの両⽅で管理することになる
• どのタイミングで何の処理が実⾏されるか、を整理しないとヤバイことになる
StreamingHubの実装例
• サーバサイドでPlayerとその座標を管理する
• Playerは移動量をサーバに送って、移動先を更新する
• Playerが移動したら全員に通知する
• ⼊室時に既存のPlayer⼀覧を返す
• Playerが⼊室/退室したら通知する
• 単⼀のプロセスでのみ動く(プロセス間での協調は考えない)
動作例
クライアントA
サーバ
クライアントがサーバにコネクションしている
クライアントB
動作例
クライアントA
サーバ
サーバ側で座標⼀覧を管理
クライアントB A: (1, 0 ,0 )
B: (0, 0, 2 )
動作例
クライアントA
サーバ
MoveAsync()をクライアントがコールすると
サーバ上の状態が書き換わり
クライアントB A: (1 +1, 0 +0 ,0 +0 )
B: (0, 0, 2 )
MoveAsync( +1, +0, +0)
動作例
クライアントA
サーバ
新しい座標を全員に通知する
クライアントB A: (1, 0 ,0 )
B: (0, 0, 2 )
OnPlayerMoved( id: A, position: (1,0,0) )
OnPlayerMoved( id: A, position: (1,0,0) )
動作例
クライアントA
サーバ
新しいクライアントが接続してきたら
サーバ側で新しいPlayerIdを発⾏して返す
クライアントB A: (1, 0 ,0 )
B: (0, 0, 2 )
C: (0, 0, 0)
クライアントC
Task<int> JoinAsync()
=> id: 12345
動作例
クライアントA
サーバ
既存のクライアントにPlayer⼊室イベントを送信する
クライアントB A: (1, 0 ,0 )
B: (0, 0, 2 )
C: (0, 0, 0)
クライアントC
OnPlayerJoined( id: 12345, position: (0,0,0) )
OnPlayerJoined( id: 12345, position: (0,0,0) )
動作例
クライアントA
サーバ
クライアントCが既存のPlayerの状態⼀覧を
サーバから取得して状態を再現する
クライアントB A: (1, 0 ,0 )
B: (0, 0, 2 )
C: (0, 0, 0)
クライアントC Task<PlayerStatus[]>
FetchCurrentAsync()
A: (1, 0 ,0 )
B: (0, 0, 2 )
C: (0, 0, 0)
動作例
実装順序
• 実装順序
1. インタフェース定義
2. 通信のオブジェクト定義
3. サーバ側実装
4. クライアント側実装
例:座標管理
1. インタフェース定義
• 定義するインタフェースは2つある
• サーバ → クライアント(Receiver)
• クライアント → サーバ(Hub)
1.インタフェース定義(Receiver)
• サーバ→クライアント(Receiver)
• クライアントで実装するメソッド
• サーバから呼び出される
1.インタフェース定義(Hub)
• クライアント→サーバ(Hub)
• サーバで実装するメソッド
• クライアントが呼び出す
2.通信のオブジェクト定義
• 通信に利⽤するデータ構造を定義する
• MessagePackObjectとして定義する
• (定義したあとはMessagePackのコードジェネレートが必要)
2.通信⽤オブジェクトの定義
• 「プレイヤの状態」を扱うデータ構造
2.通信⽤オブジェクトの定義
• 「プレイヤの状態」を扱うデータ構造
サーバサイドは「MessagePack.UnityShims」を導⼊すると
Unityのデータ構造がそのまま使えて便利
3.サーバ側の実装
• StreamingHubの特性を覚えよう
• 1コネクションごとに新しいインスタンスが⽣成して割り当てられる
• コネクションが維持されている限り同じインスタンスが使われる
(インメモリで状態が保持され続ける)
StreamingHubの図
サーバプロセス
StreamingHub
StreamingHub
StreamingHub
クライアント
Application
Application
Service
1コネクションごとにStreamingHubの
インスタンスが⽣成されて、それが維持される
Domain
Application
Domain
Application
Service
StreamingHubの図
サーバプロセス
StreamingHub
StreamingHub
StreamingHub
クライアント
Hub間でデータを共有するなら、
Hubより後ろ側の実装でイイカンジに処理するとヨサソウ
今回の例
サーバプロセス
PlayerHub
PlayerHub
PlayerHub
クライアント
PlayerManager
今回はレイヤ構造を作らずにシンプルな形でつくる
(本当はクリーンアーキテクチャとかに則った⽅がいい)
インメモリでPlayerの状態を
保持するオブジェクト
3.サーバ側の実装
• PlayerManager
• Playerの状態をインメモリで管理するシングルトンなオブジェクト
• 各Hubのインスタンスが同時にアクセスするオブジェクトなので、
中⾝の実装はThread Safeにつくる必要がある
3.サーバ側の実装
• PlayerHub 1/5
• コンストラクタ
3.サーバ側の実装
• PlayerHub 1/5
• コンストラクタ
インメモリで状態を保持できる
3.サーバ側の実装
• PlayerHub 1/5
• コンストラクタ
PlayerManagerはDIで渡される
3.サーバ側の実装
• PlayerHub 2/5
• 新規参加処理「JoinAsync()」
3.サーバ側の実装
• PlayerHub 2/4
• 新規参加処理「JoinAsync()」
1. 新しいPlayerをPlayerManagerに作ってもらう
2. このHubをGroupに登録
3. 既存のクライアントにメッセージ発⾏ & My IDをreturn
3.サーバ側の実装
• PlayerHub 2/4
• 新規参加処理「JoinAsync()」
1. 新しいPlayerをPlayerManagerに作ってもらう
2. このHubをGroupに登録
3. 既存のクライアントにメッセージ発⾏ & My IDをreturn
3.サーバ側の実装
• PlayerHub 2/4
• 新規参加処理「JoinAsync()」
1. 新しいPlayerをPlayerManagerに作ってもらう
2. このHubをGroupに登録
3. 既存のクライアントにメッセージ発⾏ & My IDをreturn
3.サーバ側の実装
• PlayerHub 2/4
• 新規参加処理「JoinAsync()」
1. 新しいPlayerをPlayerManagerに作ってもらう
2. このHubをGroupに登録
3. 既存のクライアントにメッセージ発⾏ & My IDをreturn
めっちゃ⼤事!!!
Group
• Hub同⼠を束ねる機能
• マッチングに合わせてHubをグルーピングする機能
• ⽂字列を指定してGroupに参加できる
• 同じGroup内にメッセージをまとめて発⾏(Broadcast)できる
StreamingHubの「Group」
サーバプロセス
PlayerHub
PlayerHub
PlayerHub
クライアント
メッセージのBroadcastは
同⼀GroupのHubにつないだクライアントに対して送ることができる
Group “A”
Group “A”
Group “B”
同じGroup内なら
メッセージが送れる
Groupはまたげない
Broadcast()
Broadcastの種類
• Broadcast(IGroup)
• 指定Group内の、クライアント全員に送信
• BroadcastToSelf(IGroup) / BroadcastExceptSelf(IGRoup)
• ⾃分に対してのみ送信 / ⾃分以外の全員に送信
• BroadcastExcept(IGRoup, Guid / Guid[])
• 指定Group内の、指定のクライアントを除く全員に送信
• BroadcastTo(IGRoup, Guid / Guid[])
• 指定Group内の、指定のクライアントのみへ送信
3.サーバ側の実装
• PlayerHub 2/5
• 新規参加処理「JoinAsync()」
今回はGroupは1つに固定
(参加したクライアントが全員同じ部屋にマッチングする)
3.サーバ側の実装
• PlayerHub 3/5
• 既存のPlayer情報を取得して返す
3.サーバ側の実装
• PlayerHub 4/5
• 移動処理
• 座標更新をしたあと、OnPlayerMovedメッセージをBroadcast
3.サーバ側の実装
• PlayerHub 5/5
• 切断処理
実装順序
• 実装順序
1. インタフェース定義
2. 通信のオブジェクト定義
3. サーバ側実装
4. クライアント側実装
4.クライアント側
• HubClientの⽣成
4.クライアント側
• 参加して既存Playerの状態復元
4.クライアント側
• 移動処理
• ⼊⼒があったらサーバのMoveAsync()を呼ぶ
4.クライアント側
• 移動コールバック
• サーバからPlayerの座標更新通知がくる
• メッセージをもとに、そのIDのPlayerの座標を更新(描画)
動作例
StreamingHubの実装
• Serviceより実装が複雑になりやすい
• どの状態を、誰が保持して、どこまで再現するのか?の管理が⼤変
• StreamingHubのインスタンスがステートフルである
• 後から接続したクライアントと既存のクライアントの同期をどこまで担保するか
• クライアントとサーバの実装が密結合化する
• データフロー、ロジックの流れを精査しないとグチャグチャになる
• ドメインイベントを起因にしてメッセージを発⾏するスタイルがゴチャらなさそう
「実装例の紹介」のまとめ
• Serviceの実装は、かんたん
• REST APIのControllerを作るのと⼤差ない
• Filterと合わせると便利
• StreamingHubは、表現⼒が⾼いがムズカシイ
• なんでもできそうな代わりに、整えるのが難しい
• バックエンド側のアーキテクチャをかなり考える必要あり
今回の話
• MagicOnionについて解説
• 何のためのフレームワークか
• どんな機能があるのか
• 環境構築
• 実装例の紹介
• 実装時の注意点
• デプロイ
実装時の注意点
覚えておくといいテクニックとか
• クライアント側
• gRPCのコネクション管理ちゃんとやろう
• サーバ側
• Server GC使おう
• ThreadPoolのサイズを増やそう
• Generic Host使おう
クライアント側
• gRPCのコネクション管理を気をつけよう
gRPCのコネクション管理
• C#のgRPC Client(Channel)はUnmanaged
• Channelを利⽤している部分のDisposeを忘れるとすぐリークする
• Dispose漏れがある状態でコネクションを閉じようとすると、やばい
• Unity Editorごと巻き込んでフリーズしたり、アプリが正しく終了できなかったり
MagicOnionの場合は、MagicOnion ClientのDisposeを必ず呼ぶこと
管理する機構を作るとヨサソウ
• マネージャをDisposeしたら関連するClientもまとめて
Disposeする、みたいな機構を⽤意しておくとよさそう
希望の光
• マネージドgRPCクライアント「grpc-dotnet」
• .NET Core 3.0向け
• Unityではまだ使えない
• 今後に期待
サーバ側
• Server GCを使おう
• ThreadPoolのサイズを増やそう
• Generic Hostを使おう
Server GC
• C#のGCモードを「Server」に切り替えよう
• Background garbage collectionが有効になる
• CPUリソースの消費量が増える代わりに、Stop-the-worldが短くなる
設定⽅法
• csprojに下記を追加
• ServerGarbageCollection = true
覚えておくといいこと(サーバ)
• Server GCを使おう
• ThreadPoolのサイズを増やそう
• Generic Hostを使おう
ThreadPoolのサイズ
• C#のThreadPoolの拡張速度は遅い
• スループットを上げるにはあらかじめ⼤きめにしておくと良い
(最⼤1000まで、デフォルト値は25)
覚えておくといいこと(サーバ)
• Server GCを使おう
• ThreadPoolのサイズを増やそう
• Generic Hostを使おう
Generic Host
• ASP.NETから汎⽤機能を分離した公式フレームワーク
• メッセージング
• バックグラウンドサービスの起動
• 依存関係の注⼊(DI)
• ロギング
• 設定ファイル(config)の読み込み
• サーバアプリケーションで必須な機能をまとめた便利パッケージ
MagicOnion on GenericHost
• 「MagicOnion.Hosting」パッケージを追加すればOK
• あとは起動処理をGenericHost⾵にすればOK
Configファイルの読み込み設定
・./config/local.jsonの読み込み
・環境変数のバインド
・起動時のコマンドライン引数の読み込み
DIの設定
・PlayerManager をSingletonとしてBind
ログの設定
・コンソールにログを流す
MagicOnionの起動設定
例:Configの読み込み
• Jsonファイルを配置して読み込み設定を書いておけば
./config/local.json
DIでConfigオブジェクトが渡される
Logging
• ILogger<T>をDIして使う
実装の注意点 まとめ
• gPRCのコネクション管理はマジで気をつけよう
• 切断時の処理とか再接続とか考えると頭痛いけどガンバッテ…
• Generic Hostを使おう
• コレいれておけば雑務な部分は全部やってくれる
• 既存のASP.NETの資産がそのまま流⽤できて便利
今回の話
• MagicOnionについて解説
• 何のためのフレームワークか
• どんな機能があるのか
• 環境構築
• 実装例の紹介
• 実装時の注意点
• デプロイ
MagicOnionのデプロイ
デプロイ
• どういう構成で、どこで、どうやって動かす?
• どこでビルドするの
• どこでどう動かすの
• MagicOnionのサーバ構成
デプロイ
• どういう構成で、どこで、どうやって動かす?
• どこでビルドするの
• どこでどう動かすの
• MagicOnionのサーバ構成
どうするの
• サーバで動かすならDockerビルドしよう
• ビルドするならCircle CIが便利
• Kubernetesでコンテナ管理がよさ
• もちろんコレ以外のやり⽅でもできるけど、多分コレが⼀番無難な選択だと思う
Kubernetes
• Dockerコンテナのオーケストレーションツール
• インフラ構成をyamlファイルで管理できる便利システム
• yamlにどのマシン(Node)に、何のコンテナをどう起動するか(Pod)を書いて
applyすればそのとおりにコンテナが起動してくれる
• 開発元はGoogle
• 現状、Dockerのデファクトスタンダード
• AWS、GCP、Azureももちろん対応している
デプロイ
• どういう構成で、どこで、どうやって動かす?
• どこでビルドするの
• どこでどう動かすの
• MagicOnionのサーバ構成
MagicOnionのサーバ構成
• ⼤きく分けて2つの構成が選べる
• ステートフル構成
• ステートレス構成
• それぞれ⼀⻑⼀短ある
ステートフル構成
• プロセス側で状態を保持する構成
• 簡単にいうと「インメモリでデータを保持する」実装
• あるGroupに参加したいならそのGroupの状態を持っている
インスタンスを選んで接続する必要がある
ステートフル構成
MagicOnionインスタンス(サーバプロセス)
StreamingHub
クライアント
各インスタンスに対応するGroupが決まっている
Group “A”
StreamingHub
StreamingHub
StreamingHub
StreamingHub
Group “B”
Group “C”
状態(インメモリ)
インスタンス X
AとBを担当
インスタンス Y
Cを担当
ステートフル構成
MagicOnionインスタンス(サーバプロセス)
StreamingHub
クライアント
新しいクライアントがつなぐ場合は、
接続したいGroupに合わせてインスタンスを選択する必要がある
StreamingHub
StreamingHub
StreamingHub
StreamingHub
状態(インメモリ)
インスタンス X
AとBを担当
インスタンス Y
Cを担当
新しい
クライアント
「Bに⼊りたいから
Xにつなぐ!」
ステートフル構成
• メリット
• リアルタイム通信に向く
• ⼤量のメッセージのBroadcastがプロセス内で完結する
• データの保持もインメモリで済む
• 外部ストレージへのトランザクション処理が省略できる
• インメモリで済む限りにおいて、外部ストレージを使わないので⾼速に処理可能
ステートフル構成
• デメリット
• オートスケールしにくい、LBが使えない、停⽌メンテが必要になるかも
• ステートフルなのでプロセスをいきなり落とすと死ぬ
• LBの代わりに、サービスディスカバリの⽤意が必要
• Kubernetesと相性が悪い
• External IP / Node Portの設定をがんばる必要がある
• Agonesが使えたらマシになりそう
サービスディスカバリ
• 「ホスト」と「インスタンス」の割当を管理するシステム
インスタンス X インスタンス Y
Service Discovery
インスタンスが保持する
Groupの情報を同期
10.0.0.5:12345 10.0.0.6:12345
サービスディスカバリ
• 「ホスト」と「インスタンス」の割当を管理するシステム
インスタンス X インスタンス Y
Service Discovery
10.0.0.5:12345 10.0.0.6:12345
「 に繋ぎたい」
「10.0.0.5:12345につないでね」
Agones
• GoogleとUbisoftが共同で開発している
マルチプレイヤーゲーム向けのサーバ管理システム
• Kubernetesと協調して動く
• マッチングに合わせてコンテナの起動、接続、停⽌を管理してくれる
• OSS
• 2019年9⽉にv1.0.0が出たばかり
• Ubisoftが使っていた実績はあるが、巷に知⾒があまりない
• 興味がある⼈は⼿を出すとよさそう
ステートレス構成
• プロセス上に状態を持たない構成
• MagicOnionのプロセス同⼠がバックエンドで協調して動く
• どのインスタンスにつないでも好きなGroupに参加できる
ステートレス構成
MagicOnionクライアント
クライアントはMagicOnionのホストがどうなっているかを
意識せずにすきなGroupに参加できる
Group “A”
Group “B”
Group “C”
RedisGroup “B”
Group “A”
Group “A”
Group “C”
Load Balancer
MagicOnionのメッセージは
Redis pub/sub経由でBroadcast
ステートレス構成にするには
• バックエンドにRedisが必要
• Redis Clusterをバックエンドに組む必要がある
• MagicOnionに追加のパッケージが必要
• 「MagicOnion.Redis」を追加
ステートレス構成
• メリット
• オートスケーリングができる
• Load Balancerで負荷分散できる
• ローリングアップデートしやすい
• Kubernetesもニッコリ
ステートレス構成
• デメリット
• リアルタイム通信に向いてない
• メッセージのBroadcastにプロセスをまたぐ必要がある
• データストレージへのトランザクション管理
• パフォーマンスがRedis依存
• MagicOnionがバックエンドにRedis pub/subを利⽤するため
• Redis Clusterを組んでてもpub/subの性能はさほど向上しない
• ⼤規模になるとここが⼀番ツライことになる
構成についてのまとめ
• リアルタイム通信なら「ステートフル」のほうがオススメ
• ただしKubernetes周りが結構キビシイ
• Agonesなんとかしてくれー!!!
• サービスディスカバリは⾃前で作るのがヨサソウ
まとめ
MagicOnionについてのまとめ
• サーバサイドもC#で「いい感じ」にサーバが作れる
• ゲーム向けのリアルタイム通信⽤途にも耐えうる
• クライアントとサーバをC#で統⼀できる
• クライアントエンジニアをそのままサーバ開発に回せる
• 最新のC#を使える!楽しい!
ただし…
• 開発フローの整備が課題
• クライアントとサーバがかなり密結合する
• プロジェクト全体でフローを決めないと混乱しそう
• サーバサイドの開発知識はある程度必要
• クライアントエンジニアを即⽇でサーバ開発に回すのはキビシイ
• ある程度のスイッチングコストは⾒越しておくべき
総評
• 「MagicOnioは、いいぞ。」
• サーバサイドもC#で書けるのは、結構気持ちいい(個⼈の感想)
• クライアントとサーバの開発の境⽬がなくなる
• MagicOnionというか、Kubernetesの運⽤の⽅がムズい
• Agonesに期待

MagicOnion~C#でゲームサーバを開発しよう~