Unity+.NET Core+MagicOnion v2 の環境構築ハンズオン

本記事は MagicOnion v2 の環境構築の方法を紹介しています。
MagicOnion v3 の環境構築については以下の別記事で書き直しています。

[Unity + .NET Core + MagicOnion v3 環境構築ハンズオン](https://qiita.com/_y_minami/items/db7b19eb5979ef1d6fe9)

## 更新履歴（2019/10/19）
MagicOnion v2.6.3 が公開されました。
それに合わせて gRPC のパッケージへのリンクの修正や、スクリーンショットの更新を行いました。

動作確認は下記の環境で行っています。
- Windows 10
- Unity 2019.1.14f1
- Visual Studio 2019 v16.3.5
- MagicOnion 2.6.3

## はじめに
MagicOnion は一度環境を構築してしまえば、触り心地が良くてとても使いやすいフレームワークだと思います。

しかし初期構築がやや複雑で、特に Unity と サーバー(.NET Core) の両方の経験が無い方が挑戦した場合はどこかでつまづいてしまうことも多いのではないかと思いました。

そこで、どちらかの経験が無い方でもつまずかずにポチポチと環境構築できる資料を目指して書いたのがこの記事になります。

もし途中でつまづくところがありましたら [Twitter](https://twitter.com/_y_minami) で教えていただけると喜びます。

ハンズオンの所要時間は Unity と VisualStudio をインストール済みの状態から開始して、30分～1時間程度です。

## 目次
- 1. 環境や事前準備について
- 2. Unity 側の構築
- 3. サーバー側の構築
- 4. 動作確認
- 5. IL2CPP対応（コードジェネレーターによるコード生成）
- 6. マルチスタートアッププロジェクトについて
- 7. 後書きと参考にさせていただいた記事などへのリンク

## 1. 環境や事前準備について
### 1.1. OS について
MagicOnion は Windows と Mac のどちらの環境でも基本的には動作します。
ただし Mac は IL2CPP に対応するためのコードジェネレーターの不具合があるため注意が必要です。

参考Issue
https://github.com/neuecc/MessagePack-CSharp/issues/355

### 1.2. Unity のインストール
2018.3 以降の Unity をインストールしてください。

### 1.3. Visual Studio のインストール
UnityHub からインストールすると Visual Studio 2017 がインストールされますが、最新版の 2019 を推奨します。

Visual Studio 2019 は下記のサイトからダウンロードできます。
https://visualstudio.microsoft.com/ja/?rr=https%3A%2F%2Fwww.google.com%2F

エディションは無料で利用できる Community を選びます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/ffc6b562-4900-2ea5-bdd5-52e2d50c5406.png)

インストール時に下記のオプションを有効にしてください。

  - Unity によるゲーム開発
  - .NET Core クロスプラットフォームの開発
  - .NET Core 2.2 開発ツール
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/4c0d8a49-f5e0-782e-0708-7b335d9164e8.png)

既に Visual Studio をインストール済みで、の各種オプションがインストールされているか不明な場合は Visual Studio Installer を起動して現状の確認や、追加のインストールが可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/0efa59ac-d0f9-54cf-021a-6944a51512ea.png)

Mac の方は [Visual Studio For Mac](https://visualstudio.microsoft.com/ja/vs/mac/) を利用してください。
こちらもバージョンは 2019 をおすすめします。

### 1.4. .NET Core 2.2 SDK のインストール
[こちらのページ](https://dotnet.microsoft.com/download)からダウンロードが可能です。
Visual Studio が 2019 の場合は Download .NET Core SDK のリンクからダウンロードしてください。
Visual Studio が 2017 の場合は Compatible with Visual Studio 2017 と書かれたリンクからダウンロードしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/82cc99c3-87cd-2a7a-a9df-d591878489ad.png)
※VS2019 なら上の赤枠、VS2017 なら下の赤枠のものをダウンロードしてください。

## 2. Unity 側の構築
### 2.1. プロジェクトの新規作成、PlayerSettings の変更、各種フォルダの準備
今回は 3D プロジェクトを作成します。
プロジェクト名は任意ですが、サーバーサイドのプロジェクトと見分けをつけやすくするために Sample.Unity としてみました。

保存先は任意の保存先を入力してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/021e4073-babc-932a-55bf-f43b82203beb.png)

Unity が起動したら PlayerSettings を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/eb9bc078-68e4-7e55-e0b3-1b4f591b60fe.png)

Player を選択して、下記の2箇所を変更します。

- APICompatibilityLevel を .NET4.x に変更する
- Allow unsafe Code にチェックをいれる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/7dd35fb6-6d62-2d38-534a-32a9a0d48b8a.png)

次に下記のフォルダを作成します。

- Sample.Unity\Assets\Editor
- Sample.Unity\Assets\Plugins
- Sample.Unity\Assets\Scripts
- Sample.Unity\Assets\Scripts\Generated
- Sample.Unity\Assets\Scripts\ServerShared
- Sample.Unity\Assets\Scripts\ServerShared\Hubs
- Sample.Unity\Assets\Scripts\ServerShared\MessagePackObjects
- Sample.Unity\Assets\Scripts\ServerShared\Services
- GeneratorTools

※Sample.Unity は自分のプロジェクト名に読み替えてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/e67f92b9-89d0-7c6e-f0e5-18db5e63a502.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/d0213d6d-69f7-226c-f1ea-8c55881b15b4.png)

### 2.2. MagicOnion のインストール
[こちらのページ](https://github.com/Cysharp/MagicOnion/releases)から下記の2つをダウンロードします。

- MagicOnion.Client.Unity.unitypackage
- moc.zip

Version はダウンロード可能な最新のものを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/a388de3c-3a8d-3992-c0b0-74e3739bd86a.png)

ダウンロードが終わったら MagicOnion.Client.Unity.unitypackage をダブルクリックしてインポートします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/12f3a8e5-dd3a-1e3d-b306-df45279b7a3f.png)

次に moc.zip の中身の MagicOnion.UniversalCodeGenerator を GeneratorTools フォルダにコピーします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/873f969c-10b9-a205-e7bc-370a971785f8.png)

### 2.3. MessagePack for C# のインストール
[こちらのページ](https://github.com/neuecc/MessagePack-CSharp/releases)から下記の2つをダウンロードします。

- MessagePack.Unity.1.7.3.5.unitypackage
- MessagePackUniversalCodeGenerator.zip

バージョンは UnityPackage は 1.7.3.5、CodeGenerator は 1.7.3.6 です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/ceead4f9-e76d-3e01-4d46-bae6ec06300e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/4d27f64e-91d5-a265-8076-38caa9cf63d6.png)

ダウンロードが終わったら MessagePack.Unity.1.7.3.5.unitypackage をダブルクリックしてインポートします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/e764f150-c9c3-e17d-005a-d6208ba7a318.png)

次に MessagePackUniversalCodeGenerator.zip の中身の MessagePackUniversalCodeGenerator を GeneratorTools フォルダにコピーします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/c8753d77-0014-d584-e2f1-3604f914a486.png)

### 2.4. Unity 用の gRPC パッケージの入手
gRPC の Unity 用のパッケージを以下のサイトからダウンロードします。
https://packages.grpc.io/

画面右側の BuildID をクリックすると、そのビルドのパッケージをダウンロードできます。

現在は 2.24.0 を使えばサーバー側と同じバージョンになりますので、下記のリンクからダウンロードします。
https://packages.grpc.io/archive/2019/09/f3cd878e0b6d11cd134be27d0ead7352a527f6de-9c60f3e0-20ab-4d7b-8441-930e1d5588cb/index.xml

ダウンロードの際は grpc_unity_package.2.24.0-dev.zip を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/402c18ca-5523-069f-6b1e-47f83200258d.png)

ダウンロードが終わったら、Asset\Plugins 以下に中身をコピーします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/355c80c2-8681-e006-974a-cc2a8e19bfa2.png)

### 2.5. サーバーへ接続するための Script の用意
Assets\Scenes 以下に SampleController スクリプトを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/f967f6d8-e006-c362-7521-d0bbb8889bec.png)

SampleController のコードは以下をコピペしてください。
Start() でサーバーへ接続し、OnDestroy() で切断するようになっています。

※その他の処理は動作確認の章で書き加えます。

```C#
using Grpc.Core;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SampleController : MonoBehaviour
{
    private Channel channel;

    void Start()
    {
        this.channel = new Channel("localhost:12345", ChannelCredentials.Insecure);
    }

    async void OnDestroy()
    {
        await this.channel.ShutdownAsync();
    }
}
```

SampleScene へ空の GameObject を追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/5b9401c8-7ff3-5d84-682e-7e3684e6b110.png)

追加した GameObject に SampleController を AddComponent します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/82572cc1-59fb-d56a-d50e-858ffe149936.png)

### 2.6. Unity ⇔ サーバー間のコード共有の動作確認用の class の用意
MagicOnion を利用する際は Unity 側で作成した class などをサーバーサイドと共有して使うことが一般的です。
今回は Assets\Scripts\ServerShared 以下に作成した script をすべて共有する設定を行います。
この設定作業自体は後でサーバー側の設定で行いますが、先に動作確認用の class を用意しておきます。

Assets\ServerShared\MessagePackObjects 以下に Player スクリプトを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/92ec3dc6-6574-83ce-0e20-e5d3f9886665.png)

Player のコードは以下をコピペしてください。

```C#
using MessagePack;
using UnityEngine;

namespace Sample.Shared.MessagePackObjects
{
    [MessagePackObject]
    public class Player
    {
        [Key(0)]
        public string Name { get; set; }
        [Key(1)]
        public Vector3 Position { get; set; }
        [Key(2)]
        public Quaternion Rotation { get; set; }
    }
}
```

MagicOnion を使用する場合、Client ⇔ サーバー間の通信で使用する class はこのような MessagePackObject として定義します。

MessagePackObject として定義するために必要なことは下記の2つだけですので覚えておきましょう。

- class に MessagePackObjectAttribute を付与する
- 各プロパティに KeyAttribute を付与して番号を順番にふる

Unity 側の構築はここまでです。

## 3. サーバー側の構築
続いてサーバー側の構築作業を進めます。

### 3.1. ソリューションへサーバー側のプロジェクトを追加

ソリューションを右クリックして、新しいプロジェクトを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/34a61487-3945-0090-cbaf-ba1d266b7bd5.png)

コンソールアプリ（.NET Core）を選択し、次へをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/e4d95dbc-946b-695b-0d91-9a9c8ab53af5.png)

任意のプロジェクト名を入力します。（ここでは Sample.Server としました）
場所はプロジェクトのルートを指定してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/fadf8380-ef64-a9a2-ab3f-a6464515adf3.png)

これによってルート直下のフォルダ構成はこのようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/3093dc70-726d-965a-1d7a-36f166af38f9.png)

### 3.2. MagicOnion のインストール

NuGet から MagicOnion をインストールします。
ツール -> NuGet パッケージマネージャー -> ソリューションの NuGet パッケージの管理 を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/9f5780b8-0c3d-2ddc-ff83-717d59942371.png)

参照 をクリックし、 MagicOnion を検索します。
検索結果から MagicOnion.Hosting を選択し、Sample.Server にチェックをいれてインストールをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/535decad-5eee-4482-83bc-fd54b72e2d45.png)

ライセンスに同意します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/36e97831-de0a-fca2-7790-ca9f2d2a24fe.png)

### 3.3. MessagePack.UnityShims のインストール
Unity の Vector3 などをサーバー側で扱えるようにするために、MessagePack.UnityShims をインストールします。
先ほどの MagicOnion のインストールと同じ要領で、NuGet から MessagePack.UnityShims を検索してインストールします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/2098f803-05c8-597f-414c-eab7f76f4786.png)

### 3.4. Unity側のコードを読み込む設定
Sample.Server をダブルクリックして、Sample.Server.csproj を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/cddf91d8-1eb6-b824-8e4f-4ac6b6ed6ea4.png)

Sample.Server.crproj へ下記の設定を追加します。
※Sample.Unity の部分は自分の Unity 側のプロジェクト名に読み替えてください。

```C#
  <ItemGroup>
    <Compile Include="..\Sample.Unity\Assets\Scripts\ServerShared\**\*.cs" LinkBase="LinkFromUnity" />
  </ItemGroup>
```

ファイル全体はこのようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/f21501c6-926b-d01c-5f71-e923199e1a87.png)

この状態でソリューションエクスプローラーを見てみると、Unity 側で作った ServerShared フォルダ以下のファイルなどが、サーバー側のプロジェクトと共有できていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/46bcdd6d-656e-26b9-a030-7c073b03b66b.png)

### 3.5. Program.cs の編集
Program.cs を下記の内容で上書き保存します。
これでサーバー側のプロジェクトを起動すると MagicOnion が起動するようになります。

``` C#
using Grpc.Core;
using MagicOnion.Hosting;
using MagicOnion.Server;
using Microsoft.Extensions.Hosting;
using System.Threading.Tasks;

namespace Sample.Server
{
    class Program
    {
        static async Task Main(string[] args)
        {
            GrpcEnvironment.SetLogger(new Grpc.Core.Logging.ConsoleLogger());

            await MagicOnionHost.CreateDefaultBuilder()
                .UseMagicOnion(
                    new MagicOnionOptions(isReturnExceptionStackTraceInErrorDetail: true),
                    new ServerPort("0.0.0.0", 12345, ServerCredentials.Insecure))
                .RunConsoleAsync();
        }
    }
}
```
※ServerPort として指定している IP アドレスは 0.0.0.0 のままで大丈夫です。
※TCP の 12345 ポートが利用できない環境の場合は 80 などの一般的なポートを使用してください。

## 4. 動作確認
ここからは動作確認を行います。
MagicOnion は普通の API 通信とリアルタイム通信の2種類の通信が利用できますので、それぞれをテストしてみます。

### 4.1. 普通の API通信
まずは普通の API 通信から試してみます。

#### 4.1.1. Unity側で API の定義を作る
Assets\Scripts\ServerShared\Services 以下に SampleService スクリプトを作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/0c801dc8-70b7-566c-a552-5c949d55f30e.png)

SampleService の中身は以下をコピペして保存してください。
今回は足し算をしてくれる API と掛け算をしてくれる API を定義してみます。

```C#
using MagicOnion;

namespace Sample.Shared.Services
{
    public interface ISampleService : IService<ISampleService>
    {
        UnaryResult<int> SumAsync(int x, int y);
        UnaryResult<int> ProductAsync(int x, int y);
    }
}
```

#### 4.1.2. サーバー側で API を実装する
Sample.Server を右クリックして、新しいフォルダを追加します。
名前は Services とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/1463b7d4-1896-2e5e-f519-0d8ceeaa1dfb.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/b8ebfeca-d12b-9c26-06f4-ff3d7d237b49.png)

次に Services フォルダ内にクラスを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/92961ab7-6dd9-9023-787a-b8c68dcb48f1.png)

名前は SampleService.cs とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/4f362dbe-f87c-bcff-2612-6dd14c371d27.png)

SampleService.cs の中身は以下をコピペして保存してください。

```C#
using MagicOnion;
using MagicOnion.Server;
using Sample.Shared.Services;

namespace Sample.Server.Services
{
    public class SampleService : ServiceBase<ISampleService>, ISampleService
    {
        public async UnaryResult<int> SumAsync(int x, int y)
        {
            Logger.Debug($"SumAsync Received:{x}, {y}");
            return x + y;
        }

        public async UnaryResult<int> ProductAsync(int x, int y)
        {
            Logger.Debug($"ProductAsync Received:{x}, {y}");
            return x * y;
        }
    }
}
```

#### 4.1.3. Unity側で API を呼ぶコードを実装する
SampleController に SampleService を呼び出すコードを追加します。
下記のコードをコピペして上書きしてください。

```C#
using Grpc.Core;
using MagicOnion.Client;
using Sample.Shared.Services;
using UnityEngine;
public class SampleController : MonoBehaviour
{
    private Channel channel;
    private ISampleService sampleService;

    void Start()
    {
        this.channel = new Channel("localhost:12345", ChannelCredentials.Insecure);
        this.sampleService = MagicOnionClient.Create<ISampleService>(channel);

        this.SampleServiceTest(1, 2);
    }

    async void OnDestroy()
    {
        await this.channel.ShutdownAsync();
    }

    async void SampleServiceTest(int x, int y)
    {
        var sumReuslt = await this.sampleService.SumAsync(x, y);
        Debug.Log($"{nameof(sumReuslt)}: {sumReuslt}");

        var productResult = await this.sampleService.ProductAsync(2, 3);
        Debug.Log($"{nameof(productResult)}: {productResult}");
    }
}
```

#### 4.1.4. API 通信の動作確認
まずはサーバーを起動します。

ですが、その前にソリューションのスタートアッププロジェクトをサーバー側のプロジェクトにしておきます。
Sample.Server を右クリックして、スタートアッププロジェクトに設定をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/6c228778-c632-a4d0-caed-97c0f4c5e04d.png)

これによって、普段は `Unity にアタッチ` と表示されていたボタンが `Sample.Server` の表示に代わります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/e4b31ac5-b946-2714-b5e8-4758253887ce.png)

このボタンを押すとサーバーを起動することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/66657295-a5f4-9abf-1251-be615823ccb2.png)
※スタートアッププロジェクトを元に戻す場合は Assembly-CSharp を右クリックしてスタートアッププロジェクトに指定します。
※Unity にアタッチしつつサーバーを起動したい場合はマルチスタートアッププロジェクトを使用します。（後述）

続いて、サーバーを起動した状態で Unity の Scene を再生します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/27354997-aa3a-4a93-55ca-5c286a99c114.png)

Unity の Console にログが表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/6fe5137e-f5a0-b385-87ef-46ee41afb5df.png)

サーバー側も API が呼ばれたログが表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/edbacd6a-cc01-0cd0-d150-53c3288802c3.png)

### 4.2. リアルタイム通信
続いてリアルタイム通信を試してみます。

#### 4.2.1. Unity 側で API の定義を作る
普通の API 通信と同じく、まずは API の定義から作ります。
Assets\Scripts\ServerShared\Hubs 以下に SampleHub スクリプトを作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/d31530af-3b0b-8001-0529-adab3a7a3e04.png)
 
SampleHub の中身は以下をコピペして保存してください。
今回はゲームにログイン、チャットで発言、位置情報を更新、ゲームから切断、という4つの API を作ります。

```C#
using MagicOnion;
using Sample.Shared.MessagePackObjects;
using System.Threading.Tasks;
using UnityEngine;

namespace Sample.Shared.Hubs
{
    /// <summary>
    /// CLient -> ServerのAPI
    /// </summary>
    public interface ISampleHub : IStreamingHub<ISampleHub, ISampleHubReceiver>
    {
        /// <summary>
        /// ゲームに接続することをサーバに伝える
        /// </summary>
        Task JoinAsync(Player player);
        /// <summary>
        /// ゲームから切断することをサーバに伝える
        /// </summary>
        Task LeaveAsync();
        /// <summary>
        /// メッセージをサーバに伝える
        /// </summary>
        Task SendMessageAsync(string message);
        /// <summary>
        /// 移動したことをサーバに伝える
        /// </summary>
        Task MovePositionAsync(Vector3 position);
    }

    /// <summary>
    /// Server -> ClientのAPI
    /// </summary>
    public interface ISampleHubReceiver
    {
        /// <summary>
        /// 誰かがゲームに接続したことをクライアントに伝える
        /// </summary>
        void OnJoin(string name);
        /// <summary>
        /// 誰かがゲームから切断したことをクライアントに伝える
        /// </summary>
        void OnLeave(string name);
        /// <summary>
        /// 誰かが発言した事をクライアントに伝える
        /// </summary>
        void OnSendMessage(string name, string message);
        /// <summary>
        /// 誰かが移動した事をクライアントに伝える
        /// </summary>
        void OnMovePosition(Player player);
    }
}
```

#### 4.2.2 サーバー側で API を実装する
普通の API の実装の時と同じ要領で、Sample.Server 以下に Hubs フォルダを作り、その中に SampleHub.cs を作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/34d8b60e-4f0a-ae16-a7d1-7dca7567ae23.png)

SampleHub.cs の中身は以下をコピペして保存してください。

```C#
using MagicOnion.Server.Hubs;
using Sample.Shared.Hubs;
using Sample.Shared.MessagePackObjects;
using System.Threading.Tasks;
using UnityEngine;

public class SampleHub : StreamingHubBase<ISampleHub, ISampleHubReceiver>, ISampleHub
{
    IGroup room;
    Player me;

    public async Task JoinAsync(Player player)
    {
        //ルームは全員固定
        const string roomName = "SampleRoom";
        //ルームに参加&ルームを保持
        this.room = await this.Group.AddAsync(roomName);
        //自分の情報も保持
        me = player;
        //参加したことをルームに参加している全メンバーに通知
        this.Broadcast(room).OnJoin(me.Name);
    }

    public async Task LeaveAsync()
    {
        //ルーム内のメンバーから自分を削除
        await room.RemoveAsync(this.Context);
        //退室したことを全メンバーに通知
        this.Broadcast(room).OnLeave(me.Name);
    }

    public async Task SendMessageAsync(string message)
    {
        //発言した内容を全メンバーに通知
        this.Broadcast(room).OnSendMessage(me.Name, message);
    }

    public async Task MovePositionAsync(Vector3 position)
    {
        // サーバー上の情報を更新
        me.Position = position;

        //更新したプレイヤーの情報を全メンバーに通知
        this.Broadcast(room).OnMovePosition(me);
    }

    protected override ValueTask OnDisconnected()
    {
        //nop
        return CompletedTask;
    }
}
```

#### 4.2.3 Unity側で API を呼ぶコードを実装する
SampleController に SampleHub の各 API を呼び出すコードを追加します。
下記のコードをコピペして上書きしてください。

```C#
using Grpc.Core;
using MagicOnion.Client;
using Sample.Shared.Hubs;
using Sample.Shared.MessagePackObjects;
using Sample.Shared.Services;
using UnityEngine;

public class SampleController : MonoBehaviour, ISampleHubReceiver
{
    private Channel channel;
    private ISampleService sampleService;
    private ISampleHub sampleHub;

    async void Start()
    {
        this.channel = new Channel("localhost:12345", ChannelCredentials.Insecure);
        this.sampleService = MagicOnionClient.Create<ISampleService>(channel);
        this.sampleHub = StreamingHubClient.Connect<ISampleHub, ISampleHubReceiver>(this.channel, this);

        // 普通の API の呼び出しはコメントアウトしておきます
        // 残しておいても問題はないです（リアルタイム通信と両方動きます）
        //this.SampleServiceTest(1, 2);

        this.SampleHubTest();
    }

    async void OnDestroy()
    {
        await this.sampleHub.DisposeAsync();
        await this.channel.ShutdownAsync();
    }

    /// <summary>
    /// 普通のAPI通信のテスト用のメソッド
    /// </summary>
    async void SampleServiceTest(int x, int y)
    {
        var sumReuslt = await this.sampleService.SumAsync(x, y);
        Debug.Log($"{nameof(sumReuslt)}: {sumReuslt}");

        var productResult = await this.sampleService.ProductAsync(2, 3);
        Debug.Log($"{nameof(productResult)}: {productResult}");
    }

    /// <summary>
    /// リアルタイム通信のテスト用のメソッド
    /// </summary>
    async void SampleHubTest()
    {
        // 自分のプレイヤー情報を作ってみる
        var player = new Player
        {
            Name = "Minami",
            Position = new Vector3(0, 0, 0),
            Rotation = new Quaternion(0, 0, 0, 0)
        };

        // ゲームに接続する
        await this.sampleHub.JoinAsync(player);

        // チャットで発言してみる
        await this.sampleHub.SendMessageAsync("こんにちは！");

        // 位置情報を更新してみる
        player.Position = new Vector3(1, 0, 0);
        await this.sampleHub.MovePositionAsync(player.Position);

        // ゲームから切断してみる
        await this.sampleHub.LeaveAsync();
    }

    #region リアルタイム通信でサーバーから呼ばれるメソッド群

    public void OnJoin(string name)
    {
        Debug.Log($"{name}さんが入室しました");
    }

    public void OnLeave(string name)
    {
        Debug.Log($"{name}さんが退室しました");
    }

    public void OnSendMessage(string name, string message)
    {
        Debug.Log($"{name}: {message}");
    }

    public void OnMovePosition(Player player)
    {
        Debug.Log($"{player.Name}さんが移動しました: {{ x: {player.Position.x}, y: {player.Position.y}, z: {player.Position.z} }}");
    }

    #endregion
}
```

#### 4.2.4 リアルタイム通信の動作確認
普通の API 通信の動作確認と同じ要領でサーバーを起動し、その後で Unity で Scene を再生します。

Unity の Console にログが表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/17bb9e3b-6281-4484-b7bc-b7efe0294080.png)

これで普通の API 通信とリアルタイム通信の両方の動作確認ができました。

## 5. IL2CPP対応（コードジェネレーターによるコード生成）
※この方法は現在 Windows にのみ対応しています。
　Mac の場合は bash でコードジェネレーターを叩くなどの別の方法を使用してください。

UnityEditor 上で動かすなら今のままでも問題ないのですが、IL2CPP を使う場合（例えば Platform を iOS にしたとき）はこのようなエラーが発生します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/e724ff4d-bb71-2d2e-ddb3-2d196bae6b27.png)

IL2CPP は動的なコード生成に対応していないため、コードジェネレーターを使用して事前に必要なコードを生成する必要があります。

MagicOnion と MessagePack for C# のコードジェネレーターは入手済みでしたので、それを UnityEditor 上からワンぽちで実行できるようにします。

Assets\Editor 以下に MenuItems スクリプトを作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/51c8d8ce-7f78-6319-29b8-aec407c9fef3.png)

MenuItems の中身は以下をコピペして保存します。
プロジェクトの Path をハードコードしている箇所がありますので、そちらは自分の環境にあわせて書き換えてください。

``` C#
#if UNITY_EDITOR
using System.Diagnostics;
using UnityEditor;
using UnityEngine;

public class MenuItems : MonoBehaviour
{
    [MenuItem("MagicOnion/CodeGenerate")]
    private static void GenerateFormatters()
    {
        // Generate MagicOnion code.
        ExecuteMagicOnionCodeGenerator();

        // Generate MessagePack code.
        ExecuteMessagePackCodeGenerator();
    }


    private static void ExecuteMagicOnionCodeGenerator()
    {
        UnityEngine.Debug.Log($"{nameof(ExecuteMagicOnionCodeGenerator)} : start");

        var exProcess = new Process();

        var rootPath = Application.dataPath + "/../..";
        var filePath = rootPath + "/GeneratorTools/MagicOnion.UniversalCodeGenerator/bin/moc";
        var exeFileName = "";
#if UNITY_EDITOR_WIN
        exeFileName = "/win-x64/moc.exe";
#elif UNITY_EDITOR_OSX
        exeFileName = "/osx-x64/moc";
#elif UNITY_EDITOR_LINUX
        exeFileName = "/linux-x64/moc";
#else
        return;
#endif

        var psi = new ProcessStartInfo()
        {
            CreateNoWindow = true,
            WindowStyle = ProcessWindowStyle.Hidden,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            FileName = filePath + exeFileName,
            Arguments = $@"-i ""{rootPath}/Sample.Server/Sample.Server.csproj"" -o ""{Application.dataPath}/Scripts/Generated/MagicOnion.Generated.cs""",
            // Pathの「Sample.Server」と「Sample.Server.csproj」は実際のものに書き換えてください
        };

        var p = Process.Start(psi);

        p.EnableRaisingEvents = true;
        p.Exited += (object sender, System.EventArgs e) =>
        {
            var data = p.StandardOutput.ReadToEnd();
            UnityEngine.Debug.Log($"{data}");
            UnityEngine.Debug.Log($"{nameof(ExecuteMagicOnionCodeGenerator)} : end");
            p.Dispose();
            p = null;
        };
    }


    private static void ExecuteMessagePackCodeGenerator()
    {
        UnityEngine.Debug.Log($"{nameof(ExecuteMessagePackCodeGenerator)} : start");

        var exProcess = new Process();

        var rootPath = Application.dataPath + "/../..";
        var filePath = rootPath + "/GeneratorTools/MessagePackUniversalCodeGenerator";
        var exeFileName = "";
#if UNITY_EDITOR_WIN
        exeFileName = "/win-x64/mpc.exe";
#elif UNITY_EDITOR_OSX
        exeFileName = "/osx-x64/mpc";
#elif UNITY_EDITOR_LINUX
        exeFileName = "/linux-x64/mpc";
#else
        return;
#endif

        var psi = new ProcessStartInfo()
        {
            CreateNoWindow = true,
            WindowStyle = ProcessWindowStyle.Hidden,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            FileName = filePath + exeFileName,
            Arguments = $@"-i ""{Application.dataPath}/../Assembly-CSharp.csproj"" -o ""{Application.dataPath}/Scripts/Generated/MessagePack.Generated.cs""",
        };

        var p = Process.Start(psi);

        p.EnableRaisingEvents = true;
        p.Exited += (object sender, System.EventArgs e) =>
        {
            var data = p.StandardOutput.ReadToEnd();
            UnityEngine.Debug.Log($"{data}");
            UnityEngine.Debug.Log($"{nameof(ExecuteMessagePackCodeGenerator)} : end");
            p.Dispose();
            p = null;
        };
    }
}
#endif
```

すると Unity Editor のメニューに `MagicOnion` が追加されます。
`CodeGenerate` ボタンを押してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/2b704be1-016f-69a6-d9fd-90d8ec27e342.png)

コード生成が完了すると以下のようなログが出力されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/227a57bb-c4a9-1fe3-0f4a-365b17a72d11.png)

実際に生成されたコードは Generated フォルダに保存されています。
※これを触る必要はありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/e2761b67-82ce-b429-8e9f-fbe6774aae43.png)

最後にこのコードが Scene 起動時に Register されるようにします。

Scripts フォルダに C# Script を作り、名前を InitialSettings とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/4766ff5e-cf06-1aa3-7cde-95dcf9285d2e.png)

InitialSettings の中身は下記をコピペして保存します。

```C#
using MagicOnion.Resolvers;
using MessagePack.Resolvers;
using MessagePack.Unity;
using UnityEngine;

namespace Assets.Scripts
{
    
    class InitialSettings
    {
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        static void RegisterResolvers()
        {
            CompositeResolver.RegisterAndSetAsDefault
            (
                UnityResolver.Instance,
                MagicOnionResolver.Instance,
                GeneratedResolver.Instance,
                BuiltinResolver.Instance,
                PrimitiveObjectResolver.Instance
            );
        }
    }
}
```

これで IL2CPP の環境でも動くようになります。

## 6. マルチスタートアッププロジェクトについて
途中で説明を割愛したマルチスタートアッププロジェクトの利用方法です。

`ソリューション` を右クリックして、`スタートアッププロジェクト` の設定をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/9e18809b-0dcb-c4d1-8da4-65f9a42d15d8.png)

`マルチスタートアッププロジェクト` にチェックをいれ、`Assembly-CSharp` と `Sample.Server` のアクションを `開始` にして `OK` を押します。 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/e27c9c37-7fb8-a57e-7f6b-d8be2de3212c.png)

この状態で `開始` を押すと、Unityにアタッチしながらサーバーを起動することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/1ebbb27c-f76d-7d70-9f3e-4accfcc55740.png)

## 7. 後書きと参考にさせていただいた記事などへのリンク
こんなに長い記事を最後まで読んでいただいてありがとうございます。
少しでも役に立つことがあれば幸いです。

環境構築に成功して、より技術的な内容や実践的なコードが必要になった際は下記の記事などがおすすめです。

- [GitHub の MagicOnion の README](https://github.com/Cysharp/MagicOnion)
- [公式の MagicOnion の Sample](https://github.com/Cysharp/MagicOnion/tree/master/samples)
- [Cygames Engineers' Blog MagicOnion – C#による .NET Core/Unity 用のリアルタイム通信フレームワーク](http://tech.cygames.co.jp/archives/3181/)
- [Unity+MagicOnionで超絶手軽にリアルタイム通信を実装してみた](https://qiita.com/mitchydeath/items/cecf01493d1efeb4ae55)
- [MagicOnion v2を使ってUnity IL2CPPでgRPC通信をする](https://qiita.com/yKimisaki/items/1d55b08f3e7bcae46585)