ASP.NET CoreとiOS（Unity）間でgRPC通信を行う with MagicOnion

 簡単な足し算関数をgRPCでunity-iOSクライアントから呼ぶサンプルを作成しました。iOS×MagicOnionの環境構築に色々手こずったので備忘録として一連の流れを書いておくことにしました。基本的には[公式ドキュメント](https://github.com/Cysharp/MagicOnion)と[Qiita記事](https://qiita.com/KyoheiOkawa/items/63a68bbcc3caaaac581e)をなぞっているため、不具合が出たときはそちらも参照してください。ソースコード全体は[ここ](https://github.com/yumarun/MagicOnionSample-iOS)から見れます。

# 環境
- Unity： 2022.3.15f1
- ASP.NET Core： .NET 6.0
- MagicOnion： Ver.5.1.8
- gRPC unityプラグイン: 2.47.0-dev202204190851
- MessagePack: v2.5.140
- Visual Studio 2022
- Windows 11

# 手順
1. サーバー側(ASP.NET Core)の準備
2. クライアント側(Unity)の準備
3. ソースコード生成
4. iOS用その他の設定
5. 実行

# 1. サーバー側(ASP.NET Core)の準備

Visual Studio 2022の新規プロジェクト生成で「ASP.NET Core(空)」を選択 → 作成

![スクリーンショット (960).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421961/d0dca920-cfdc-83a5-9a73-163b1d9bfc3f.png)

![スクリーンショット (961).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421961/f34d8743-221e-75c1-36a2-3266229f86d2.png)

Nugetで「Grpc.AspNetCore」・「MagicOnion.Server」をインストール。
Program.csを以下に変更。
```cs
using MagicOnion;
using MagicOnion.Server;


var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseKestrel(option =>
{
    option.ConfigureEndpointDefaults(endpointOptions =>
    {
        endpointOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http2;
    });
});


builder.Services.AddGrpc();
builder.Services.AddMagicOnion();


var app = builder.Build();

app.MapMagicOnionService();

app.Run();

```
ソリューションエクスプローラからC#ファイルを追加し、「IMyFirstService.cs」に名前変更
![スクリーンショット (962).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421961/6d995a2f-c6d3-ca31-b867-2d1fa5501e9c.png)

IMyFirstService.csの中身を以下に変更
```cs
using MagicOnion;

namespace YourProjectName
{
    public interface IMyFirstService: IService<IMyFirstService>
    {
        UnaryResult<int> SumAsync(int x, int y);
    }
}
```

再びソリューションエクスプローラからc#ファイルを追加、名前を「MyFirstService.cs」に変更、中身を以下に変更
```cs
using MagicOnion.Server;
using MagicOnion;

namespace MagicOnionSampleiOSServer
{
    public class MyFirstService : ServiceBase<IMyFirstService>, IMyFirstService
    {
        public async UnaryResult<int> SumAsync(int x, int y)
        {
            Console.WriteLine($"Reveiced: {x}, {y}");
            return x + y;
        }
    }
}
```

ソリューションエクスプローラからappsettings.jsonを開き、中身を以下に変更（Urlの中身は各自自身のPCのローカルIPアドレスにしてください）。
```json
{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://192.168.2.107:7000"
      },
      "Https": {
        "Url": "https://192.168.2.107:7001"
      }
    }
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}
```

# 2. クライアント側(Unity)の準備
## 2-1. Unityのプロジェクト作成、設定変更
Unityのプロジェクトを作成し、Player Settings/Player/Api Compatibility Level を「.NET Framework」に変更
![スクリーンショット (964).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421961/f29d8578-4783-ba00-cf45-c2571c5c1ed1.png)

## 2-2. gRPC unityプラグインの導入
[ここ](https://packages.grpc.io/archive/2022/04/67538122780f8a081c774b66884289335c290cbe-f15a2c1c-582b-4c51-acf2-ab6d711d2c59/index.xml)から「grpc_unity_package.2.47.0-dev202204190851.zip」をダウンロード、解凍。UnityのAssets直下にPluginsフォルダを作成し、先ほど解凍したフォルダ内の「Google.Protobuf」・「Grpc.Core」・「Grpc.Core.Api」の3つのフォルダをコピー。
ここでエラーが出るが一旦放置。![スクリーンショット (966).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421961/be2c9ffd-ccfe-123d-f00a-c2da0ebb094b.png)

## 2-3. MessagePackの導入
[ここ](https://github.com/MessagePack-CSharp/MessagePack-CSharp/releases)から「MessagePack.Unity.unitypackage」をダウンロード、unity上でインポート。

## 2-4. MagicOnionの導入
[ここ](https://github.com/Cysharp/MagicOnion/releases)から「MagicOnion.Client.Unity.unitypackage」をダウンロード、同様にunity上でインポート。

## 2-5. サーバー側で作成したIMyFirstService.csをunityに持ってくる
Assets/ScriptsにIMyFirstService.csをコピー&ペーストします。

## 2-6. iOSデバイスにテキストを表示する機構の作成。
Hierarchyで右クリックしUI->Legacy->Textからテキストを作成。
Hierarchyで右クリックし空のGameObjectを作成。
Assets/ScriptsにClientTest.csを作成。内容を以下に変更（ipアドレスとIMyFirstServiceの名前空間は自身の環境に合わせてください）。

```cs
using Grpc.Core;
using MagicOnion.Client;
using UnityEngine;
using UnityEngine.UI;

public class ClientTest : MonoBehaviour
{
    [SerializeField]
    Text _text;

    async void Start()
    {
        var channel = new Channel("192.168.2.107", 7000, ChannelCredentials.Insecure);
        var client = MagicOnionClient.Create<MagicOnionSampleiOSServer.IMyFirstService>(channel);
        var result = await client.SumAsync(1, 2);
        _text.text = $"Result: {result}";
        await channel.ShutdownAsync();
    }
}
```


編集後上で作ったGameObjectにアタッチ。inspectorのClientTestのTextに先ほど作成したテキストをアタッチ。

# 3. ソースコード生成
ここまでの手順で、クライアントがUnity EditorであればASP.NET Coreプロジェクト → Unityプロジェクトの順に起動することでgRPCが動作します(1+2の結果がテキストに表示される)。

![スクリーンショット (968).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421961/6bd609f8-f01c-c58f-342e-9a29f9ebc377.png)


これはMagicOnionの動的コード生成が走ってくれているためです。この動的コード生成はiOS上では走らないので今回は手動でコード生成を行う必要があります

## 3-1. MessagePack用のコード生成
unityの上のWindow/MessagePack/CodeGeneratorを選択し（ここで必要ならインストール）、出てきたタブ内の空欄2行を以下のように埋めます。
![スクリーンショット (967).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421961/249089a9-43a9-5c4d-8ec4-2f98c5da19b3.png)

埋めたあとはGenerateボタンを押します。少し待つとAssets/Scripts以下にc#ファイルが作成されていると思います。

## 3-2. MagicOnion用のコード生成

MagicOnion用のコード生成はUnity Editor上で行えないので専用のツール（moc）を使用します。自身のWindowsでdotnetコマンドが使えるようにして、任意のディレクトリで以下のコマンドを入力してmocをインストールします。
```
dotnet new tool-manifest
dotnet tool install MagicOnion.Generator
```

以下のコマンドが使用できればmocのインストールは完了です。
```
dotnet moc --help
```

次にコード生成を行います。{}の部分は自身の環境に合わせて変更してください。

```
dotnet moc -i {サーバー側ASP.NET Coreプロジェクト内の.csprojファイルへの絶対パス} -o {unityプロジェクトのAssetsの親ディレクトリの絶対パス}\Assets\Scripts\MagicOnion.Generated.cs
```

# 3-3. 生成コードを登録
UnityのAssets/Scriptsの下にStartup.csファイルを作成し、以下のコードをコピペ。
```cs
using MessagePack;
using MessagePack.Resolvers;
using UnityEngine;

public class Startup
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    static void RegisterResolvers()
    {
        // NOTE: Currently, CompositeResolver doesn't work on Unity IL2CPP build. Use StaticCompositeResolver instead of it.
        StaticCompositeResolver.Instance.Register(
            // This resolver is generated by MagicOnion's code generator.
            MagicOnion.Resolvers.MagicOnionResolver.Instance,
            // This resolver is generated by MessagePack's code generator.
            MessagePack.Resolvers.GeneratedResolver.Instance,
            StandardResolver.Instance
        );

        MessagePackSerializer.DefaultOptions = MessagePackSerializer.DefaultOptions
            .WithResolver(StaticCompositeResolver.Instance);
    }
}
```

# 4. iOS用その他の設定

UnityのAssets/Editorの下にBuildeIos.csファイルを作成し、以下のコードをコピペ。

```cs
#if UNITY_IPHONE
using System.IO;
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;

public class BuildIos
{
    /// <summary>
    /// Handle libgrpc project settings.
    /// </summary>
    /// <param name="target"></param>
    /// <param name="path"></param>
    [PostProcessBuild(1)]
    public static void OnPostProcessBuild(BuildTarget target, string path)
    {
        var projectPath = PBXProject.GetPBXProjectPath(path);
        var project = new PBXProject();
        project.ReadFromString(File.ReadAllText(projectPath));
#if UNITY_2019_3_OR_NEWER
        var targetGuid = project.GetUnityFrameworkTargetGuid();
#else
        var targetGuid = project.TargetGuidByName(PBXProject.GetUnityTargetName());
#endif

        // libz.tbd for grpc ios build
        project.AddFrameworkToProject(targetGuid, "libz.tbd", false);

        // libgrpc_csharp_ext missing bitcode. as BITCODE exand binary size to 250MB.
        project.SetBuildProperty(targetGuid, "ENABLE_BITCODE", "NO");
        
        File.WriteAllText(projectPath, project.WriteToString());
    }
}
#endif
```

理由については[公式doc](https://github.com/Cysharp/MagicOnion?tab=readme-ov-file#ios-build-with-grpc)参照

# 5. 実行
## 5-1. サーバープログラム実行
サーバープログラムを開いているVisual Studioの「ビルド」→「ソリューションのビルド」で.exeを生成、起動

## 5-2. クライアントの実行
UnityプロジェクトをiOSビルドし、Xcodeで実機にビルド

以下の画面がiOS端末に表示されればおｋ。
![IMG_0019.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421961/da79da42-7682-6234-fa94-4ab822864f25.png)