Skip to main content
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