C#
Unity
UniRx
gRPC
MagicOnion

MagicOnion v2を使ってUnity IL2CPPでgRPC通信をする

こんにちは、ブラストエッジゲームズでエンジニアをしているきみかです。
Qiita詳しくないのでやさしくしてください。

さて弊社には自主制作支援制度というものがあり、それを使って業後や休日などの時間で行っている個人開発をサポートしてもらっているのですが、その開発でMagicOnion v2を使うことに決めました。

https://github.com/cysharp/MagicOnion

MagicOnionを使いたい理由としてはいくつかあるのですが、やはり個人開発ということで生産効率を一番高めるにはUnityとサーバサイドをC#で統一しVisualStudioにサポートを受けること、Streamingな通信とUnaryな通信(リアルタイム通信とWebAPIみたいな単発の通信)を共通で扱えるものが欲しいこと、自身は業務でUnrealEngineを使っているので個人開発ではUnityを使っておきたいこと、などです。

基本的な部分は

http://tech.cygames.co.jp/archives/3181/
MagicOnion – C#による .NET Core/Unity 用のリアルタイム通信フレームワーク

https://qiita.com/shiena/items/6c3b34a8d8f1bb938470
Unity2018.2 + il2cppでgRPCアプリをAndroid/iOS向けにビルドする

https://qiita.com/mitchydeath/items/cecf01493d1efeb4ae55
Unity+MagicOnionで超絶手軽にリアルタイム通信を実装してみた

をご覧ください。
MagicOnion v1の記事も検索をすれば出てくるかと思うのですが、gRPCのバージョンが変わっていること、MagicOnion自体のコンセプトもv1からv2でかなり変わっているので、注意してください。

なお今回はVisualStudio 2017を使っています。
Macなどでやる場合でも似たようにやればできると思いますので、参考程度にご覧ください。

プロジェクト構成

まず、MagicOnionやその内部で使われているMessagePackには、自動でシリアライザのResolverを実行時に作成してくれます。
ようはMessagePackObjectな自前の型をbyte[]に、またはその逆を処理する部分です。
が、IL2CPPではそのような実行時に新たなものを生成するのは認められません。
そのため、あらかじめUnityで使うすべてのMessagePackObjectな型に対して自動生成したスクリプトを用意しておく必要があります。
そのため、プロジェクト構成をこのようにします。

image.png

Minamoはプロジェクトのコードネームです。
Assembly-Csarpなどの説明は省略します。
注目してほしいのはMinamo.SharedというSharedプロジェクトです。
これはHubの定義や通信で使うMessagePackObjectを入れておくプロジェクトになります。
なぜSharedProjectなのかというと、サーバサイドを.NET Standard(.NET Core 2.X)、Unity側を.NET 4.Xにしているせいで、その両方から扱われるものをdllの形で共有すと困るからです。
SharedプロジェクトはMinamo.ModelとMinamo.ClientLibraryから参照されています。
Minamo.Modelはサーバ側のロジック、つまり.NET StandardとしてShardプロジェクトはふるまい、Minamo.ClientLibraryでは.NET 4.Xとして、コードファイルがそのまんま参照先のプロジェクトでコピーされているかのようにふるまいます。

今回はIL2CPPの話なので、Minamo.ClientLibraryについてお話します。

image.png

ClientLibraryの中には、MagicOnion、MessagePack、UniRxの各ソースコードを、先ほどのSharedへの参照があります。
これらを直接Unityプロジェクトに入れている理由は、ClientLibraryをビルドする時のイベントを使ってアレコレしたいからです。
やってることは
・MagicOnion.UniversalCodeGeneratorを使ってHubの自動生成
・MessagePack.UniversalCodeGeneratorを使ってResolverの自動生成
になります。
ひょっとしたらUnityのイベントでいい感じに生成できる気はするので、お好みでいろんなやり方を試してみてください。
私は今のところこの形がしっくり来ています。

$(SolutionDir)..\Externals\MagicOnion.UniversalCodeGenerator\bin\MagicOnionCodeGenerator\win-x64\moc.exe -i $(SolutionDir)..\Minamo.Service\Minamo.Service.csproj -n Minamo -o $(SolutionDir)..\Minamo.Client\Assets\Generated\MagicOnion.Generated.cs
$(SolutionDir)..\Externals\MessagePack.UniversalCodeGenerator\bin\MessagePackUniversalCodeGenerator\win-x64\mpc.exe -i $(SolutionDir)..\Minamo.ClientLibrary\Minamo.ClientLibrary.csproj -o $(SolutionDir)..\Minamo.Client\Assets\Generated\MessagePack.Generated.cs

この2行をMinamo.ClientLibraryのPre-build event command lineに書くことで、Assets\Generated\にそれぞれの自動生成ファイルが吐き出されます。
ここでエラーが出たり吐き出されたりしない場合は、各UniversalCodeGeneratorにあるpublish.batをたたいてmoc.exeとmpc.exeを作ってあげてください。

ただこのClientLibrary方式には、Conditional compilation symbolsが複雑になるという欠点があります。
Minamo.ClientLibraryの場合、今は

ENABLE_UNSAFE_MSGPACK UNITY_2018_3_OR_NEWER NET_4_6 CSHARP_7_OR_LATER ENABLE_IL2CPP

となっています。
UniRxをUnityに直接配置をしていないので、現状UniTaskTrackerWindowが正常に動いていない状態になっちゃっています。

再接続と汎用通信クライアントの作成

再接続はgRPCのTryWaitForStateChangedAsyncを使ってやっています。

https://qiita.com/mxProject/items/ae8cee146a1fe19d52e3
C# gRPC チャネルの状態と自動再接続

https://gist.github.com/yKimisaki/93c12b661bc1188ddf3f8ae0fe1aac46
シンプルにこういう形のものを使っています。

ApplicationEntryPoint

Unity側でMagicOnionを使う際にやらなければいけないことがあります。
それは↑でせっかく作ったMessagePackのResolverを、ちゃんとRegisterしないと使えないことです。

Unityでは様々な開発のスタイルがあると思いますが、どのシーンからでもまず統一的に初期化したいという話はよくあることだと思います。
そのためMinamoでは

https://gist.github.com/yKimisaki/9fb2ffacbe64012fe1bbf2a983cac1b6

こういうものを作っています。
RuntimeInitializeOnLoadMethodについてはこちらをご覧ください。

http://tsubakit1.hateblo.jp/entry/2016/07/29/073000
【Unity】ゲームの起動後 Awakeより前にメソッドを実行する

これを使うと以下のような雑い感じのことができます。

HogeBehaviour.cs
class HogeBehaviour : MonoBehaviour
{
    void Start() => StartCoreAsync().Forget();

    async UniTask StartCoreAsync()
    {
        await ApplicationEntryPoint.WaitInitializationAsync();

        // 初期化後の処理
    }
}

本当はこれをMVRP的なシーンの初期化システムと融合をさせたりするわけですが、今回は割愛します。

さいごに

実際なにも考えずにとりあえず動かそうとすると、はまりポイントは結構あるんですが、個々の問題は上記のことでほぼ解決できると思います。
ちなみにサーバ側はAWSの.NET Core AMIのt2.nanoインスタンス、Tokyoリージョンで動かしていたのですが、ほぼわからないくらいの遅延で通信がAndroid↔iOS間でできていました。
ここから進めていくともっとディープな問題が出てくるとは思うのですが、それはまたまとまったら…

ちなみにMinamoについてですがどんなものかというと、

https://twitter.com/kimika127/status/1075886582006075393

という感じのもので、私も左のピンク髪の子のキャラデザをしています。
私がママになるんだよ。
ということで、今後この子たちが何らかの形で公開できるように頑張ります。
半分実務のノウハウという形でできるだけ共有していきたいと思っていますので、みなさんもぜひぜひ、強力なUnity+gRPC+サーバサイドC#の世界への一歩を踏み出してみてください。