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.

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

https://vsuc.connpass.com/event/146588/

  • Login to see the comments

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

  1. 1. MagicOnion 〜C#でゲームサーバを開発しよう〜 .NET Conf in Tokyo 2019 #dotnetconfunity 2019/10/27 とりすーぷ
  2. 2. ⾃⼰紹介 • 「とりすーぷ」 • @toRisouP • 株式会社バーチャルキャスト 開発 • xR開発 • 最近はサーバ開発 • Microsoft MVP for Developer Technologies 2018〜2020
  3. 3. 今回の話 • MagicOnionについて解説 • 何のためのフレームワークか • どんな機能があるのか • 環境構築 • 実装例の紹介 • 実装時の注意点 • デプロイ
  4. 4. MagicOnionって何?
  5. 5. MagicOnion is 何 • ネットワーク通信のためのフレームワーク • Unityでのゲーム開発を中⼼に使えるネットワーク通信フレームワーク • サーバサイド:.NET Core • クライアントサイド:Unity / .NET Core • 開発はCysharp社 • MITライセンス
  6. 6. MagicOnionのバックエンド • C# + .NET Core / Unity • 通信プロトコル: gRPC • データフォーマット: MessagePack for C# • 定義したC#ファイルがそのままDSLになる • C#インタフェースがAPIのエンドポイント • C#オブジェクトが通信データ構造(MessagePackObject)
  7. 7. MagicOnionのバックエンド クライアント サーバ プロトコルはgRPC データフォーマットはMessagePack C#ファイルの定義がそのままAPIのエンドポイントに
  8. 8. コード共有 • 定義したインタフェースやオブジェクトを、 サーバとクライアントで共有することで そのままAPI定義として利⽤できる
  9. 9. コード共有 クライアント サーバ インタフェース定義 (APIエンドポイント) インタフェース定義されたC#ファイルを クライアント・サーバで共有
  10. 10. 例:APIエンド先⽃の定義 共有するインタフェースを定義
  11. 11. 実装 クライアント サーバ サーバ側はインタフェースを実装する 実装
  12. 12. サーバ側実装
  13. 13. 呼び出し クライアント サーバ クライアント側はただ インタフェースのメソッドを呼び出すだけ メソッドコール (async/awiat)
  14. 14. クライアント側の呼び出し gRPCのコネクションを貼ってからメソッドを呼ぶだけ インタフェースに定義されたメソッドをコール 裏で通信が⾏われ、結果はasync/awaitで待てる
  15. 15. サーバ処理の呼び出しの流れ サーバ メソッド実⾏すると裏でサーバコードが呼び出されて その結果が返ってくるのをawaitで待てる
  16. 16. 共有ファイルを介した通信 クライアント サーバ 共有したインタフェースを介して クライアント/サーバ間の通信が可能になる
  17. 17. MagicOnion 最⼤の特徴 • 通信レイヤを意識せずにサーバコードを呼び出せる • サーバとクライアントでC#インタフェースを共有すればOK • 通信のデータ構造もC#で定義すればOK • async/awaitでメソッド呼び出しするだけでサーバの処理を実⾏できる • ただし、gRPCのコネクション管理だけは⾃前で管理しないとダメ(しょうがない)
  18. 18. MagicOnionの機能紹介
  19. 19. 機能⼀覧 • 実装できるAPIの種類 • Service、StreamingHub • Filter機能 • Filter • その他便利な機能 • Swagger、Telemetry
  20. 20. APIの実装 • ⼤きく2つのAPI実装パターンがある • Service • StreamingHub
  21. 21. Service • Service • シンプルな単発のAPIを作成するのに使う • 1Request ‒ 1Response通信(中⾝はUnary RPC) • リクエストを投げた本⼈にのみレスポンスを返せる
  22. 22. Service クライアント サーバRequest Response 1Requestに対して1Responseのみ返せるのが 「Service」で実装したAPI
  23. 23. StreamingHub • StreamingHub • リアルタイム通信向け • 中⾝はBidirectional Streaming RPC • 仮想的なコネクションを貼ったままずっと維持する • クライアントとサーバが⾃由にメッセージを送受信できる • 全体へブロードキャストも可能
  24. 24. StreamingHub クライアント サーバ Message クライアント同⼠がサーバを介して 相互通信できるのが「StremingHub」 Broadcast Broadcast
  25. 25. 機能⼀覧 • 実装できるAPIの種類 • Service、StreamingHub • Filter機能 • Filter • その他便利な機能 • Swagger、Telemetry
  26. 26. Filter • 通信の前後に処理を追加する機能 • サーバ側フィルタ • Service、StreamingHubに適⽤可能 • クライアント側フィルタ • Serviceのみ対応
  27. 27. Filterの実装例(サーバ側)
  28. 28. Filterを反映する • Filterを付けたいクラス/メソッドにAttributeをつける • このメソッド/クラスのRPCが実⾏されるタイミングで、 定義したFilterを通過する
  29. 29. Filterの⽤途 • 使⽤例 • 認証⽤のヘッダーを追加 • メッセージの暗号化/復号化 • 通信ログ出⼒ • 通信をせずにダミーのレスポンスの差し替え • エラーメッセージのハンドリング • リトライ処理
  30. 30. 機能⼀覧 • 実装できるAPIの種類 • Service、StreamingHub • Filter機能 • Filter • その他便利な機能 • Swagger、Telemetry
  31. 31. 便利な機能 • Swagger対応 • 「Service」で実装したメソッドをREST APIとして実⾏できる • Webブラウザ上でAPIの動作確認ができる • デバッグに便利 • Telemetry対応 • MagicOnionの監視 • メトリクス
  32. 32. 今回の話 • MagicOnionについて解説 • 何のためのフレームワークか • どんな機能があるのか • 環境構築 • 実装例の紹介 • 実装時の注意点 • デプロイ
  33. 33. MagicOnionの環境構築
  34. 34. 環境構築 • MagicOnionは環境構築がちょっとたいへん • クライアント側(Unity)は⼿動でライブラリ追加が必要 • コード共有 • コードジェネレート
  35. 35. クライアント ライブラリ導⼊ • これらのライブラリを⼿動で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/
  36. 36. Player Settingsも設定が必要 • Scripting Define Symbols • ENABLE_UNSAFE_MSGPACK を書き⾜す • Allow ʻunsafeʼ Code • Enable
  37. 37. ついでに導⼊するとよさげ • UniRx • Reactive Extensions for Unity • イベント処理の整理に便利 • UniTask • ValueTask for Unity • Unity向けのAwaiter実装の提供 • ⾮同期周りにはコレ
  38. 38. 環境構築 • MagicOnionは環境構築がちょっとたいへん • クライアント側(Unity)は⼿動でライブラリ追加が必要 • コード共有 • コードジェネレート
  39. 39. コード共有について クライアント サーバ インタフェース定義 (APIエンドポイント) インタフェース定義されたC#ファイルを どうやってクライアント/サーバでコード共有するの?
  40. 40. Unity側の制約 • C#コードはAssets/以下に配置しないといけない • UnityはAssets/以下のC#ファイルしかコンパイルしてくれない
  41. 41. オススメの⽅法 • サーバ側のcsprojからUnity/Assets以下を参照する • 実体はUnity側にあり、それをサーバプロジェクトで開いているだけ 参照 Unityサーバプロジェクト
  42. 42. プロジェクト構成の例 Assets/ Assets/Shared/ サーバプロジェクト(ソリューション構成) Server.Interfaces.csproj Server.Core.csproj プロジェクト構成の⼀例
  43. 43. プロジェクト構成の例 Assets/ Assets/Shared/ サーバプロジェクト(ソリューション構成) Server.Interfaces.csproj Server.Core.csproj 共有⽤csprojからUnity/Assets/以下を参照する 参照 ファイルの実体はこっち
  44. 44. プロジェクト構成の例 Assets/ Assets/Shared/ サーバプロジェクト(ソリューション構成) Server.Interfaces.csproj Server.Core.csproj ポイント:共有するcsprojと、サーバのコアロジックのcsprojを分離して コアプロジェクトから共有プロジェクトを参照する 参照
  45. 45. プロジェクト構成の例 Assets/ Assets/Shared/ Server.Interfaces.csproj Server.Core.csproj git管理するときはこれらをまとめて1つのリポジトリに⼊れる これで1つの Git Repository
  46. 46. この⽅法のメリット • 設定が簡単 • csprojに数⾏書き⾜すだけで設定が終わる • リポジトリをチェックアウトしてくればそのまま動く • 環境に依存しない • シンボリックリンクとかそういう⾯倒な作業もいらない • 相対パスさえ維持されていれば動く
  47. 47. デメリット • リポジトリが1個にまとまってしまう • サーバのビルドにクライアントコードのcheckoutも必要になる • クライアントの容量が⼤きくなってくるとCI/CDがツライことになる • 開発フローの整理が難しくなる • クライアント開発とサーバ開発が同じリポジトリで管理される • GitHubのissue/pr 欄が混ざる
  48. 48. 折半案 サーバ側 サーバプロジェクトのポジトリ Git Repository Git Repository ゲーム本体のリポジトリ
  49. 49. 折半案 サーバ側 ここのUnityプロジェクトは、 Headlessなクライアント + Debug Scene が置かれる Git Repository Git Repository
  50. 50. 折半案 サーバ側 ゲーム本体のプロジェクトは 別のリポジトリとして運⽤する
  51. 51. 折半案 サーバ側 接続部分をunitypackageにまとめて 本体側でインポートする unitypackage
  52. 52. 補⾜:UnityPackageはどう管理する? • Unity Package Managerで管理できる • Unity標準のパッケージマネージャ機能 • まだ発展途上 • ⾃前でレジストリを⽴てればオレオレライブラリも管理可能 • 余⼒があるならコレで管理すると良さそう
  53. 53. 環境構築 • MagicOnionは環境構築がちょっとたいへん • クライアント側(Unity)は⼿動でライブラリ追加が必要 • コード共有 • コードジェネレート
  54. 54. コードジェネレートの話 • MagicOnionの動作にはコードジェネレートが2回必要 • MagicOnionのインタフェース定義 • MessagePackのオブジェクト定義 • ただし、ジェネレートが必要なのはUnity側のみ(サーバ側は不要) • ジェネレートしたコードはUnityプロジェクトに配置する
  55. 55. ジェネレータの⽤意 • それぞれGitHubからダウンロードできる MagicOnion MessagePack
  56. 56. ジェネレータの使い⽅ • 対象のcsprojをコマンドラインで指定して実⾏ • MagicOnnion/MessagePack共に同じコマンド引数で動く
  57. 57. 指定するcsproj • 共有するインタフェースとオブジェクトが含まれたもの • 共有するファイルだけまとめたcsprojを⽤意しておくと楽
  58. 58. 指定するcsprojの例 Assets/ Assets/Shared/ サーバプロジェクト(ソリューション構成) Server.Interfaces.csproj Server.Core.csproj これ!
  59. 59. 注意点 • ジェネレート対象のcsprojがMagicOnionとMessagePack に依存するようにしておくこと • 依存させておかないとコードジェネレートが動かない Server.Interfaces.csproj
  60. 60. コマンド例 ./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/以下に⽣成
  61. 61. MessagePackのResolver設定だけ忘れずに • Unityプロジェクトに次のようなコードを配置 • RuntimeInitializeOnLoadMethodで起動時に実⾏されるようにしておく
  62. 62. Editor拡張でジェネレートできると楽 • MagicOnionのサンプルコードに実装例がある • https://github.com/Cysharp/MagicOnion/blob/master /samples/ChatApp/ChatApp.Unity/Assets/Editor/MenuItems .cs
  63. 63. 環境構築 まとめ • 環境構築がちょっと⼤変 • csprojを編集する必要がある • コードジェネレートの整備が⼿間 • 開発フローはよく考えよう • Unity側とサーバ側、2つのプロジェクト管理を同時にやる必要 • gitリポジトリをどうわけるか、どういうフローで開発をすすめるのか • CI/CDとの相性
  64. 64. 今回の話 • MagicOnionについて解説 • 何のためのフレームワークか • どんな機能があるのか • 環境構築 • 実装例の紹介 • 実装時の注意点 • デプロイ
  65. 65. 実装例の紹介
  66. 66. 実装例 • それぞれの実装例を紹介 • Service • StreamingHub
  67. 67. Serviceの実装例 • サーバサイドで⾜し算する • シンプルに、引数に与えた数値を⾜して返す
  68. 68. Serviceの実装例 • 実装⼿順 1. 共有するインタフェース/オブジェクトを定義 2. サーバに実装を書く 3. サーバにコネクションする 4. Unity側で呼び出す
  69. 69. Serviceの実装例 • 実装⼿順 1. 共有するインタフェース/オブジェクトを定義 2. サーバに実装を書く 3. サーバにコネクションする 4. Unity側で呼び出す
  70. 70. 指定するcsprojの例 Assets/ Assets/Shared/ サーバプロジェクト(ソリューション構成) Server.Interfaces.csproj Server.Core.csproj ここに定義する (どっちに定義しても実体は1個なので同じ)
  71. 71. Service:サーバサイドで計算する 1.サーバ/クライアントで共有するインタフェースを⽤意 IService<T>を継承したインタフェースを定義する インタフェース内に定義したメソッドがAPIエンドポイントになる
  72. 72. Serviceの実装例 • 実装⼿順 1. 共有するインタフェース/オブジェクトを定義 2. サーバに実装を書く 3. サーバにコネクションする 4. Unity側で呼び出す
  73. 73. 指定するcsprojの例 Assets/ Assets/Shared/ サーバプロジェクト(ソリューション構成) Server.Interfaces.csproj Server.Core.csproj ここに定義する
  74. 74. 例:サーバサイドで計算する 2.サーバ側で実装を⾏う インタフェースを実装 今回は引数を⾜して返すだけ
  75. 75. Serviceの実装例 • 実装⼿順 1. 共有するインタフェース/オブジェクトを定義 2. サーバに実装を書く 3. サーバにコネクションする 4. Unity側で呼び出す
  76. 76. 例:サーバサイドで計算する 3. サーバにコネクションする gRPCのチャンネルを作って
  77. 77. 例:サーバサイドで計算する 4. Unity側で呼び出す ICalculateServiceへのクライアントを作り
  78. 78. 例:サーバサイドで計算する 4. Unity側で呼び出す あとは普通にメソッドコールするだけ 通信の結果はasync/awaitで待つだけでOK
  79. 79. 例:サーバサイドで計算する サーバ メソッド実⾏すると裏でサーバコードが呼び出されて その結果が返ってくるのをawaitで待てる
  80. 80. Serviceの実装は簡単 • リクエストに対して、処理を書いて、結果を返すだけ • かんたん • クライアント側は普通にasync/awaitでメソッドを呼ぶだけ • サーバ側はインタフェースを実装するだけ
  81. 81. StreamingHubの実装例
  82. 82. StreamingHubはムズカシイ • StreamingHubはリアルタイム通信を実現する機構 • クライアントとサーバが相互にメソッドを呼び出し合える • Serviceの実装と⽐べて複雑になりやすい • 「状態」がクライアントとサーバの両⽅で管理することになる • どのタイミングで何の処理が実⾏されるか、を整理しないとヤバイことになる
  83. 83. StreamingHubの実装例 • サーバサイドでPlayerとその座標を管理する • Playerは移動量をサーバに送って、移動先を更新する • Playerが移動したら全員に通知する • ⼊室時に既存のPlayer⼀覧を返す • Playerが⼊室/退室したら通知する • 単⼀のプロセスでのみ動く(プロセス間での協調は考えない)
  84. 84. 動作例 クライアントA サーバ クライアントがサーバにコネクションしている クライアントB
  85. 85. 動作例 クライアントA サーバ サーバ側で座標⼀覧を管理 クライアントB A: (1, 0 ,0 ) B: (0, 0, 2 )
  86. 86. 動作例 クライアントA サーバ MoveAsync()をクライアントがコールすると サーバ上の状態が書き換わり クライアントB A: (1 +1, 0 +0 ,0 +0 ) B: (0, 0, 2 ) MoveAsync( +1, +0, +0)
  87. 87. 動作例 クライアントA サーバ 新しい座標を全員に通知する クライアントB A: (1, 0 ,0 ) B: (0, 0, 2 ) OnPlayerMoved( id: A, position: (1,0,0) ) OnPlayerMoved( id: A, position: (1,0,0) )
  88. 88. 動作例 クライアントA サーバ 新しいクライアントが接続してきたら サーバ側で新しいPlayerIdを発⾏して返す クライアントB A: (1, 0 ,0 ) B: (0, 0, 2 ) C: (0, 0, 0) クライアントC Task<int> JoinAsync() => id: 12345
  89. 89. 動作例 クライアント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) )
  90. 90. 動作例 クライアント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)
  91. 91. 動作例
  92. 92. 実装順序 • 実装順序 1. インタフェース定義 2. 通信のオブジェクト定義 3. サーバ側実装 4. クライアント側実装
  93. 93. 例:座標管理 1. インタフェース定義 • 定義するインタフェースは2つある • サーバ → クライアント(Receiver) • クライアント → サーバ(Hub)
  94. 94. 1.インタフェース定義(Receiver) • サーバ→クライアント(Receiver) • クライアントで実装するメソッド • サーバから呼び出される
  95. 95. 1.インタフェース定義(Hub) • クライアント→サーバ(Hub) • サーバで実装するメソッド • クライアントが呼び出す
  96. 96. 2.通信のオブジェクト定義 • 通信に利⽤するデータ構造を定義する • MessagePackObjectとして定義する • (定義したあとはMessagePackのコードジェネレートが必要)
  97. 97. 2.通信⽤オブジェクトの定義 • 「プレイヤの状態」を扱うデータ構造
  98. 98. 2.通信⽤オブジェクトの定義 • 「プレイヤの状態」を扱うデータ構造 サーバサイドは「MessagePack.UnityShims」を導⼊すると Unityのデータ構造がそのまま使えて便利
  99. 99. 3.サーバ側の実装 • StreamingHubの特性を覚えよう • 1コネクションごとに新しいインスタンスが⽣成して割り当てられる • コネクションが維持されている限り同じインスタンスが使われる (インメモリで状態が保持され続ける)
  100. 100. StreamingHubの図 サーバプロセス StreamingHub StreamingHub StreamingHub クライアント Application Application Service 1コネクションごとにStreamingHubの インスタンスが⽣成されて、それが維持される Domain
  101. 101. Application Domain Application Service StreamingHubの図 サーバプロセス StreamingHub StreamingHub StreamingHub クライアント Hub間でデータを共有するなら、 Hubより後ろ側の実装でイイカンジに処理するとヨサソウ
  102. 102. 今回の例 サーバプロセス PlayerHub PlayerHub PlayerHub クライアント PlayerManager 今回はレイヤ構造を作らずにシンプルな形でつくる (本当はクリーンアーキテクチャとかに則った⽅がいい) インメモリでPlayerの状態を 保持するオブジェクト
  103. 103. 3.サーバ側の実装 • PlayerManager • Playerの状態をインメモリで管理するシングルトンなオブジェクト • 各Hubのインスタンスが同時にアクセスするオブジェクトなので、 中⾝の実装はThread Safeにつくる必要がある
  104. 104. 3.サーバ側の実装 • PlayerHub 1/5 • コンストラクタ
  105. 105. 3.サーバ側の実装 • PlayerHub 1/5 • コンストラクタ インメモリで状態を保持できる
  106. 106. 3.サーバ側の実装 • PlayerHub 1/5 • コンストラクタ PlayerManagerはDIで渡される
  107. 107. 3.サーバ側の実装 • PlayerHub 2/5 • 新規参加処理「JoinAsync()」
  108. 108. 3.サーバ側の実装 • PlayerHub 2/4 • 新規参加処理「JoinAsync()」 1. 新しいPlayerをPlayerManagerに作ってもらう 2. このHubをGroupに登録 3. 既存のクライアントにメッセージ発⾏ & My IDをreturn
  109. 109. 3.サーバ側の実装 • PlayerHub 2/4 • 新規参加処理「JoinAsync()」 1. 新しいPlayerをPlayerManagerに作ってもらう 2. このHubをGroupに登録 3. 既存のクライアントにメッセージ発⾏ & My IDをreturn
  110. 110. 3.サーバ側の実装 • PlayerHub 2/4 • 新規参加処理「JoinAsync()」 1. 新しいPlayerをPlayerManagerに作ってもらう 2. このHubをGroupに登録 3. 既存のクライアントにメッセージ発⾏ & My IDをreturn
  111. 111. 3.サーバ側の実装 • PlayerHub 2/4 • 新規参加処理「JoinAsync()」 1. 新しいPlayerをPlayerManagerに作ってもらう 2. このHubをGroupに登録 3. 既存のクライアントにメッセージ発⾏ & My IDをreturn めっちゃ⼤事!!!
  112. 112. Group • Hub同⼠を束ねる機能 • マッチングに合わせてHubをグルーピングする機能 • ⽂字列を指定してGroupに参加できる • 同じGroup内にメッセージをまとめて発⾏(Broadcast)できる
  113. 113. StreamingHubの「Group」 サーバプロセス PlayerHub PlayerHub PlayerHub クライアント メッセージのBroadcastは 同⼀GroupのHubにつないだクライアントに対して送ることができる Group “A” Group “A” Group “B” 同じGroup内なら メッセージが送れる Groupはまたげない Broadcast()
  114. 114. Broadcastの種類 • Broadcast(IGroup) • 指定Group内の、クライアント全員に送信 • BroadcastToSelf(IGroup) / BroadcastExceptSelf(IGRoup) • ⾃分に対してのみ送信 / ⾃分以外の全員に送信 • BroadcastExcept(IGRoup, Guid / Guid[]) • 指定Group内の、指定のクライアントを除く全員に送信 • BroadcastTo(IGRoup, Guid / Guid[]) • 指定Group内の、指定のクライアントのみへ送信
  115. 115. 3.サーバ側の実装 • PlayerHub 2/5 • 新規参加処理「JoinAsync()」 今回はGroupは1つに固定 (参加したクライアントが全員同じ部屋にマッチングする)
  116. 116. 3.サーバ側の実装 • PlayerHub 3/5 • 既存のPlayer情報を取得して返す
  117. 117. 3.サーバ側の実装 • PlayerHub 4/5 • 移動処理 • 座標更新をしたあと、OnPlayerMovedメッセージをBroadcast
  118. 118. 3.サーバ側の実装 • PlayerHub 5/5 • 切断処理
  119. 119. 実装順序 • 実装順序 1. インタフェース定義 2. 通信のオブジェクト定義 3. サーバ側実装 4. クライアント側実装
  120. 120. 4.クライアント側 • HubClientの⽣成
  121. 121. 4.クライアント側 • 参加して既存Playerの状態復元
  122. 122. 4.クライアント側 • 移動処理 • ⼊⼒があったらサーバのMoveAsync()を呼ぶ
  123. 123. 4.クライアント側 • 移動コールバック • サーバからPlayerの座標更新通知がくる • メッセージをもとに、そのIDのPlayerの座標を更新(描画)
  124. 124. 動作例
  125. 125. StreamingHubの実装 • Serviceより実装が複雑になりやすい • どの状態を、誰が保持して、どこまで再現するのか?の管理が⼤変 • StreamingHubのインスタンスがステートフルである • 後から接続したクライアントと既存のクライアントの同期をどこまで担保するか • クライアントとサーバの実装が密結合化する • データフロー、ロジックの流れを精査しないとグチャグチャになる • ドメインイベントを起因にしてメッセージを発⾏するスタイルがゴチャらなさそう
  126. 126. 「実装例の紹介」のまとめ • Serviceの実装は、かんたん • REST APIのControllerを作るのと⼤差ない • Filterと合わせると便利 • StreamingHubは、表現⼒が⾼いがムズカシイ • なんでもできそうな代わりに、整えるのが難しい • バックエンド側のアーキテクチャをかなり考える必要あり
  127. 127. 今回の話 • MagicOnionについて解説 • 何のためのフレームワークか • どんな機能があるのか • 環境構築 • 実装例の紹介 • 実装時の注意点 • デプロイ
  128. 128. 実装時の注意点
  129. 129. 覚えておくといいテクニックとか • クライアント側 • gRPCのコネクション管理ちゃんとやろう • サーバ側 • Server GC使おう • ThreadPoolのサイズを増やそう • Generic Host使おう
  130. 130. クライアント側 • gRPCのコネクション管理を気をつけよう
  131. 131. gRPCのコネクション管理 • C#のgRPC Client(Channel)はUnmanaged • Channelを利⽤している部分のDisposeを忘れるとすぐリークする • Dispose漏れがある状態でコネクションを閉じようとすると、やばい • Unity Editorごと巻き込んでフリーズしたり、アプリが正しく終了できなかったり MagicOnionの場合は、MagicOnion ClientのDisposeを必ず呼ぶこと
  132. 132. 管理する機構を作るとヨサソウ • マネージャをDisposeしたら関連するClientもまとめて Disposeする、みたいな機構を⽤意しておくとよさそう
  133. 133. 希望の光 • マネージドgRPCクライアント「grpc-dotnet」 • .NET Core 3.0向け • Unityではまだ使えない • 今後に期待
  134. 134. サーバ側 • Server GCを使おう • ThreadPoolのサイズを増やそう • Generic Hostを使おう
  135. 135. Server GC • C#のGCモードを「Server」に切り替えよう • Background garbage collectionが有効になる • CPUリソースの消費量が増える代わりに、Stop-the-worldが短くなる
  136. 136. 設定⽅法 • csprojに下記を追加 • ServerGarbageCollection = true
  137. 137. 覚えておくといいこと(サーバ) • Server GCを使おう • ThreadPoolのサイズを増やそう • Generic Hostを使おう
  138. 138. ThreadPoolのサイズ • C#のThreadPoolの拡張速度は遅い • スループットを上げるにはあらかじめ⼤きめにしておくと良い (最⼤1000まで、デフォルト値は25)
  139. 139. 覚えておくといいこと(サーバ) • Server GCを使おう • ThreadPoolのサイズを増やそう • Generic Hostを使おう
  140. 140. Generic Host • ASP.NETから汎⽤機能を分離した公式フレームワーク • メッセージング • バックグラウンドサービスの起動 • 依存関係の注⼊(DI) • ロギング • 設定ファイル(config)の読み込み • サーバアプリケーションで必須な機能をまとめた便利パッケージ
  141. 141. MagicOnion on GenericHost • 「MagicOnion.Hosting」パッケージを追加すればOK • あとは起動処理をGenericHost⾵にすればOK
  142. 142. Configファイルの読み込み設定 ・./config/local.jsonの読み込み ・環境変数のバインド ・起動時のコマンドライン引数の読み込み
  143. 143. DIの設定 ・PlayerManager をSingletonとしてBind
  144. 144. ログの設定 ・コンソールにログを流す
  145. 145. MagicOnionの起動設定
  146. 146. 例:Configの読み込み • Jsonファイルを配置して読み込み設定を書いておけば ./config/local.json
  147. 147. DIでConfigオブジェクトが渡される
  148. 148. Logging • ILogger<T>をDIして使う
  149. 149. 実装の注意点 まとめ • gPRCのコネクション管理はマジで気をつけよう • 切断時の処理とか再接続とか考えると頭痛いけどガンバッテ… • Generic Hostを使おう • コレいれておけば雑務な部分は全部やってくれる • 既存のASP.NETの資産がそのまま流⽤できて便利
  150. 150. 今回の話 • MagicOnionについて解説 • 何のためのフレームワークか • どんな機能があるのか • 環境構築 • 実装例の紹介 • 実装時の注意点 • デプロイ
  151. 151. MagicOnionのデプロイ
  152. 152. デプロイ • どういう構成で、どこで、どうやって動かす? • どこでビルドするの • どこでどう動かすの • MagicOnionのサーバ構成
  153. 153. デプロイ • どういう構成で、どこで、どうやって動かす? • どこでビルドするの • どこでどう動かすの • MagicOnionのサーバ構成
  154. 154. どうするの • サーバで動かすならDockerビルドしよう • ビルドするならCircle CIが便利 • Kubernetesでコンテナ管理がよさ • もちろんコレ以外のやり⽅でもできるけど、多分コレが⼀番無難な選択だと思う
  155. 155. Kubernetes • Dockerコンテナのオーケストレーションツール • インフラ構成をyamlファイルで管理できる便利システム • yamlにどのマシン(Node)に、何のコンテナをどう起動するか(Pod)を書いて applyすればそのとおりにコンテナが起動してくれる • 開発元はGoogle • 現状、Dockerのデファクトスタンダード • AWS、GCP、Azureももちろん対応している
  156. 156. デプロイ • どういう構成で、どこで、どうやって動かす? • どこでビルドするの • どこでどう動かすの • MagicOnionのサーバ構成
  157. 157. MagicOnionのサーバ構成 • ⼤きく分けて2つの構成が選べる • ステートフル構成 • ステートレス構成 • それぞれ⼀⻑⼀短ある
  158. 158. ステートフル構成 • プロセス側で状態を保持する構成 • 簡単にいうと「インメモリでデータを保持する」実装 • あるGroupに参加したいならそのGroupの状態を持っている インスタンスを選んで接続する必要がある
  159. 159. ステートフル構成 MagicOnionインスタンス(サーバプロセス) StreamingHub クライアント 各インスタンスに対応するGroupが決まっている Group “A” StreamingHub StreamingHub StreamingHub StreamingHub Group “B” Group “C” 状態(インメモリ) インスタンス X AとBを担当 インスタンス Y Cを担当
  160. 160. ステートフル構成 MagicOnionインスタンス(サーバプロセス) StreamingHub クライアント 新しいクライアントがつなぐ場合は、 接続したいGroupに合わせてインスタンスを選択する必要がある StreamingHub StreamingHub StreamingHub StreamingHub 状態(インメモリ) インスタンス X AとBを担当 インスタンス Y Cを担当 新しい クライアント 「Bに⼊りたいから Xにつなぐ!」
  161. 161. ステートフル構成 • メリット • リアルタイム通信に向く • ⼤量のメッセージのBroadcastがプロセス内で完結する • データの保持もインメモリで済む • 外部ストレージへのトランザクション処理が省略できる • インメモリで済む限りにおいて、外部ストレージを使わないので⾼速に処理可能
  162. 162. ステートフル構成 • デメリット • オートスケールしにくい、LBが使えない、停⽌メンテが必要になるかも • ステートフルなのでプロセスをいきなり落とすと死ぬ • LBの代わりに、サービスディスカバリの⽤意が必要 • Kubernetesと相性が悪い • External IP / Node Portの設定をがんばる必要がある • Agonesが使えたらマシになりそう
  163. 163. サービスディスカバリ • 「ホスト」と「インスタンス」の割当を管理するシステム インスタンス X インスタンス Y Service Discovery インスタンスが保持する Groupの情報を同期 10.0.0.5:12345 10.0.0.6:12345
  164. 164. サービスディスカバリ • 「ホスト」と「インスタンス」の割当を管理するシステム インスタンス X インスタンス Y Service Discovery 10.0.0.5:12345 10.0.0.6:12345 「 に繋ぎたい」 「10.0.0.5:12345につないでね」
  165. 165. Agones • GoogleとUbisoftが共同で開発している マルチプレイヤーゲーム向けのサーバ管理システム • Kubernetesと協調して動く • マッチングに合わせてコンテナの起動、接続、停⽌を管理してくれる • OSS • 2019年9⽉にv1.0.0が出たばかり • Ubisoftが使っていた実績はあるが、巷に知⾒があまりない • 興味がある⼈は⼿を出すとよさそう
  166. 166. ステートレス構成 • プロセス上に状態を持たない構成 • MagicOnionのプロセス同⼠がバックエンドで協調して動く • どのインスタンスにつないでも好きなGroupに参加できる
  167. 167. ステートレス構成 MagicOnionクライアント クライアントはMagicOnionのホストがどうなっているかを 意識せずにすきなGroupに参加できる Group “A” Group “B” Group “C” RedisGroup “B” Group “A” Group “A” Group “C” Load Balancer MagicOnionのメッセージは Redis pub/sub経由でBroadcast
  168. 168. ステートレス構成にするには • バックエンドにRedisが必要 • Redis Clusterをバックエンドに組む必要がある • MagicOnionに追加のパッケージが必要 • 「MagicOnion.Redis」を追加
  169. 169. ステートレス構成 • メリット • オートスケーリングができる • Load Balancerで負荷分散できる • ローリングアップデートしやすい • Kubernetesもニッコリ
  170. 170. ステートレス構成 • デメリット • リアルタイム通信に向いてない • メッセージのBroadcastにプロセスをまたぐ必要がある • データストレージへのトランザクション管理 • パフォーマンスがRedis依存 • MagicOnionがバックエンドにRedis pub/subを利⽤するため • Redis Clusterを組んでてもpub/subの性能はさほど向上しない • ⼤規模になるとここが⼀番ツライことになる
  171. 171. 構成についてのまとめ • リアルタイム通信なら「ステートフル」のほうがオススメ • ただしKubernetes周りが結構キビシイ • Agonesなんとかしてくれー!!! • サービスディスカバリは⾃前で作るのがヨサソウ
  172. 172. まとめ
  173. 173. MagicOnionについてのまとめ • サーバサイドもC#で「いい感じ」にサーバが作れる • ゲーム向けのリアルタイム通信⽤途にも耐えうる • クライアントとサーバをC#で統⼀できる • クライアントエンジニアをそのままサーバ開発に回せる • 最新のC#を使える!楽しい!
  174. 174. ただし… • 開発フローの整備が課題 • クライアントとサーバがかなり密結合する • プロジェクト全体でフローを決めないと混乱しそう • サーバサイドの開発知識はある程度必要 • クライアントエンジニアを即⽇でサーバ開発に回すのはキビシイ • ある程度のスイッチングコストは⾒越しておくべき
  175. 175. 総評 • 「MagicOnioは、いいぞ。」 • サーバサイドもC#で書けるのは、結構気持ちいい(個⼈の感想) • クライアントとサーバの開発の境⽬がなくなる • MagicOnionというか、Kubernetesの運⽤の⽅がムズい • Agonesに期待

×