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

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

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

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

もし途中でつまづくところがありましたら Twitter で教えていただけると喜びます。

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

## 目次


## 1. 環境について
本記事を書くにあたって使用した OS、ツール、ソフトウェアのバージョンです。

- Windows 10
- Unity 2019.4.7f1
- Visual Studio 2019 16.7
- [MagicOnion 3.0.12](https://github.com/Cysharp/MagicOnion/releases/tag/3.0.12)
- [MessagePack 2.1.152](https://github.com/neuecc/MessagePack-CSharp/releases/tag/v2.1.152)
- [gRPC 2.23.0](https://packages.grpc.io/archive/2019/08/e14154481baf7e23710b60b78a1e91299985ec80-41fb322e-6416-4509-9a6f-2b17085b6272/index.xml)

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

保存先は任意の保存先を入力してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/5e78f543-60e6-6fe2-f284-2623f3639e92.png)

Unity が起動したら `PlayerSettings` を開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/ab9cd62e-3d6f-654a-b39e-a7b9d6ca3a3c.png)

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

- APICompatibilityLevel を `.NET 4.x` に変更する
- Allow unsafe Code にチェックをいれる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/f0cdb36b-cfc7-d26d-21d1-9f78a3503d1b.png)

次に下記のフォルダを作成します。
※Sample.Unity は自分のプロジェクト名に読み替えてください。

- 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

フォルダ構成が下記のようになっていることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/e04a9108-7770-978d-0345-c03bfffef60c.png)


### 2.2. MagicOnion のインストール
[GitHub](https://github.com/Cysharp/MagicOnion/releases/tag/3.0.12) から `MagicOnion.Client.Unity.unitypackage` をダウンロードします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/cffe111d-6fe9-0baf-13ef-a23f7059e489.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/40100b52-113c-7f0c-98ee-291a812450e8.png)

### 2.3. MessagePack for C# のインストール
[GitHub](https://github.com/neuecc/MessagePack-CSharp/releases/tag/v2.1.152) から `MessagePack.Unity.2.1.152.unitypackage` をダウンロードします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/dcb41b11-efb2-0763-6397-1f029c1dcf7c.png)

ダウンロードが終わったらダブルクリックしてインポートします。
MagicOnion と重複するファイルがあるため警告が表示されますがこのままインポートします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/2a854967-51bd-2598-06ac-08ab53c643cb.png)

### 2.4. gRPC のインストール
gRPC の [Daily Builds(2019/08/01)](https://packages.grpc.io/archive/2019/08/e14154481baf7e23710b60b78a1e91299985ec80-41fb322e-6416-4509-9a6f-2b17085b6272/index.xml) から `grpc_unity_package.2.23.0-dev.zip` をダウンロードします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/9f80e565-f57d-2123-c965-bc0cc6da31fe.png)

ダウンロードが終わったら展開し、以下のフォルダを `Sample.Unity\Assets\Plugins` にコピーします。

- Google.Protobuf
- Grpc.Core
- Grpc.Core.Api

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/67d2d124-f7a7-e355-9959-821cec355002.png)

### 2.5. サーバーへ接続するスクリプトの用意
`Assets\Scenes` に `SampleController` スクリプトを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/aa1f4d13-bc13-ded7-031f-2978561f11fd.png)

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

```csharp
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/d7637231-7d42-650e-ed58-1fcf1eedb02a.png)

追加した GameObject に SampleController を AddComponent します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/a3174908-5c06-9095-5392-207db5a1f8a0.png)

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

`Assets\ServerShared\MessagePackObjects` に `Player` スクリプトを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/dc26b825-6de0-7b5c-33a4-9ef44e9a3b23.png)

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

```csharp
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 ⇔ サーバー間の通信で使用するクラスはこのような MessagePackObject として定義します。

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

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

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

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

### 3.1. ソリューションへサーバー側のプロジェクトを追加
ソリューションを右クリックして、新しいプロジェクトを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/9f9bdee5-a8e4-d771-8487-99e13c7da3cf.png)

コンソールアプリ（.NET Core）を選択し、次へをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/ef0261b0-81be-ca59-58c5-7624d2a3df26.png)

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

### 3.2. MagicOnion のインストール
NuGet から MagicOnion をインストールします。
ツール -> NuGet パッケージマネージャー -> ソリューションの NuGet パッケージの管理 を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/5aaae6a0-46e6-577c-d450-498930d0eb26.png)

参照 をクリックし、 MagicOnion を検索します。
検索結果から MagicOnion.Hosting を選択し、Sample.Server にチェックをいれます。
バージョンは 3.0.12 を選択し、インストールします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/a3377fb6-4363-5b50-0cbb-19600b712dd2.png)

変更のプレビューが表示されるので OK を押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/cbfd1001-2adf-9211-141e-03cbd4bc9392.png)

ライセンスへの同意を求められるので同意します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/013e6ea7-8645-f3a6-0766-eebcb7d923f0.png)

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

```csharp
using MagicOnion.Hosting;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading.Tasks;

namespace Sample.Server
{
    class Program
    {
        static async Task Main(string[] args)
        {
            await MagicOnionHost.CreateDefaultBuilder()
                .UseMagicOnion()
                .RunConsoleAsync();
        }
    }
}
```

### 3.4. ソリューションへクラスライブラリのプロジェクトを追加
このクラスライブラリを使って Unity 側とサーバー側のコード共有を行います。

ソリューションを右クリックして、新しいプロジェクトを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/a643b0a0-711d-be98-7de9-532117f8a64d.png)

クラスライブラリ(C# .NET Standard)を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/88c0b165-8b41-418a-de05-050787abb3ba.png)

任意のプロジェクト名を入力します。（ここでは Sample.Shared としました）
場所はプロジェクトのルートを指定してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/3e1e52b7-42a9-092c-855c-0524ca373a18.png)

自動的に作成される Class1.cs は不要なので削除します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/90f198c9-3fe0-a6da-bd25-a8b94c41f837.png)

### 3.5. クラスライブラリへ MagicOnion.Abstractions をインストール
先ほどの MagicOnion のインストールと同じ要領で、NuGet から MagicOnion.Abstractions を検索してインストールします。
対象のプロジェクトは Sample.Shared を選択し、バージョンは 3.0.12 を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/4e72f61b-0603-1ca4-d652-c2ef8451072f.png)

### 3.6. クラスライブラリへ MessagePack.UnityShims をインストール
同じ要領で Sample.Shared へ MessagePack.UnityShims をインストールします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/cc6227e2-7e96-ef79-2d53-8c4a557990bf.png)

### 3.7. クラスライブラリから Unity 側のコードを参照する
Sample.Shared をダブルクリックして Sample.Shared.csproj を開き、赤枠部分の設定を追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/b74975ce-bea2-4403-1677-04c23a0cc3c6.png)

追加する設定はこちらをコピペしてください。
※Sample.Unity の部分は自分の Unity 側のプロジェクト名に読み替えてください。

```csharp
<ItemGroup>
    <Compile Include="..\Sample.Unity\Assets\Scripts\ServerShared\**\*.cs" />
</ItemGroup>
```

この状態でソリューションエクスプローラーを見てみると、Unity 側で用意した ServerShared フォルダ以下のファイルがクラスライブラリに読み込めたことがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/08679d04-a703-6cb8-3ac5-22c1e15f4df0.png)

### 3.8. サーバー側のプロジェクトからクラスライブラリを参照する
Sample.Server を右クリックし、追加、プロジェクト参照を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/2856cacc-3eac-dc63-1b05-ca3d863d0b5d.png)

Sample.Shared を選択して OK を押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/48f51b13-8bbf-1d4f-7239-e37525b96937.png)

これで Unity とコード共有したサーバーの用意ができました。

## 4. API の実装と動作確認
ここからは API の実装と動作確認を行います。
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/b990f8c5-0a49-a501-2b55-9cd99bf74462.png)

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

```csharp
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/978c87e1-64d7-e7dc-206a-41835b7986d7.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/ebc760af-f3d3-93d0-7562-23fd7b40cfc0.png)

次に Services フォルダ内にクラスを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/23733d35-e834-804c-828c-4407ca5a4b88.png)

名前は SampleService.cs とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/a7ea3baf-2689-4ccb-3bf3-e7aa4da0da88.png)

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

```csharp
using MagicOnion;
using MagicOnion.Server;
using Sample.Shared.Services;

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

        public UnaryResult<int> ProductAsync(int x, int y)
        {
            return UnaryResult(x * y);
        }
    }
}
```

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

```csharp
using Grpc.Core;
using MagicOnion.Client;
using Sample.Shared.Services;
using System.Threading.Tasks;
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/5da88f1e-5978-107d-95b0-fe65763e36db.png)

これによって、普段は `Unity にアタッチ` と表示されていたボタンが `Sample.Server` の表示に変わります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/d52dd0c3-9f91-49e5-1f1a-c73dc117f06c.png)

このボタンを押すとサーバーを起動することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/f0931384-4d5d-5ca0-299e-52aca2b81f60.png)

※スタートアッププロジェクトを元に戻す場合は `Assembly-CSharp` を右クリックしてスタートアッププロジェクトに指定します。
※Unity にアタッチしつつサーバーを起動したい場合は `マルチスタートアッププロジェクト` を使用します。（後述）

続いてサーバーを起動した状態で Unity の Scene を再生します。

Unity の Console にログが表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/326e7461-6aa1-3426-e838-2c81710dd6bb.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/d5005bce-174d-555f-8cd2-4ad6f0044f94.png)

SampleHub の中身は以下をコピペして保存してください。
今回はゲームにログイン、チャットで発言、位置情報を更新、ゲームから切断、という4つの API を作ります。

```csharp
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/5071954e-a49d-956c-87a0-b68b2a855841.png)

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

```csharp
using MagicOnion.Server.Hubs;
using Sample.Shared.Hubs;
using Sample.Shared.MessagePackObjects;
using System;
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);

        await Task.CompletedTask;
    }

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

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

        await Task.CompletedTask;
    }

    protected override ValueTask OnConnecting()
    {
        // handle connection if needed.
        Console.WriteLine($"client connected {this.Context.ContextId}");
        return CompletedTask;
    }

    protected override ValueTask OnDisconnected()
    {
        // handle disconnection if needed.
        // on disconnecting, if automatically removed this connection from group.
        return CompletedTask;
    }
}
```

#### 4.2.3. Unity 側で API を呼ぶコードを実装する

SampleController に SampleHub の各 API を呼び出すコードを追加します。
下記のコードをコピペして上書きしてください。

```csharp
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;

    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/cea08e3e-6134-7e58-db81-88570deae8d9.png)

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

## 5. IL2CPP 対応（コードジェネレーターによるコード生成）
UnityEditor 上で動かすなら今のままでも問題ないのですが、IL2CPP を使う場合（例えば Platform を iOS にしたとき）はこのようなエラーが発生します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/e6bc2c0e-9023-ffac-e418-d8c30077ae7c.png)

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

### 5.1. MagicOnion.MSBuild.Tasks のインストール
NuGet で MagicOnion.MSBuild.Tasks をインストールします。
プロジェクトは Sample.Shared を選択し、バージョンは 3.0.12 を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/213b051c-8595-2728-7576-c8e09429bbac.png)

### 5.2. MessagePack.MSBuild.Tasks のインストール
同じ要領で MessagePack.MSBuild.Tasks もインストールします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/c39f95c0-cb41-2c00-d249-bed154ed6521.png)

### 5.3. Sample.Shared.csproj の編集
Sample.Shared をダブルクリックし、Sample.Shared.csproj を開きます。
赤枠部分のコードを追加して保存します。
コードは以下をコピペしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/ee67efb1-9d27-293a-1d1e-1c183b593013.png)

```csharp
<Target Name="GenerateMessagePack" AfterTargets="Compile">
  <MessagePackGenerator Input=".\Sample.Shared.csproj" Output="..\Sample.Unity\Assets\Scripts\Generated\MessagePack.Generated.cs" />
</Target>
<Target Name="GenerateMagicOnion" AfterTargets="Compile">
  <MagicOnionGenerator Input=".\Sample.Shared.csproj" Output="..\Sample.Unity\Assets\Scripts\Generated\MagicOnion.Generated.cs" />
</Target>
```

この状態で Sample.Server をビルドすると Sample.Unity\Assets\Scripts\Generated 以下にコードジェネレーターによってコードが生成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/55ad19d5-d5e8-c044-f6a5-81ab4b39c639.png)

### 5.4. 生成されたコードの使用

次にこのコードを使用する設定を行います。
Scripts フォルダに C# Script を作り、名前を InitialSettings とします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/63bd5dcf-76b9-f069-43b6-48cd8f6af8e9.png)

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

```csharp
using MessagePack;
using MessagePack.Resolvers;
using UnityEngine;

namespace Assets.Scripts
{
    class InitialSettings
    {
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        static void RegisterResolvers()
        {
            // NOTE: Currently, CompositeResolver doesn't work on Unity IL2CPP build. Use StaticCompositeResolver instead of it.
            StaticCompositeResolver.Instance.Register(
                MagicOnion.Resolvers.MagicOnionResolver.Instance,
                MessagePack.Resolvers.GeneratedResolver.Instance,
                BuiltinResolver.Instance,
                PrimitiveObjectResolver.Instance,
                MessagePack.Unity.UnityResolver.Instance
            );

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

これで IL2CPP の環境でも動作するようになりました。

## 6. iOS ビルド対応
iOS 用のビルドではさらに以下の追加作業が必要です。

- Disable Bitcode
- Add libz.tbd

Assets\Editor に C# Script を追加し、名前を BuildIos とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/f884497d-ec0d-5fb5-0f9b-1cede7cd1381.png)

BuildIos のコードは以下をコピペします。

```csharp
#if UNITY_IPHONE
using System.IO;
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));
        var targetGuid = project.GetUnityFrameworkTargetGuid();

        // 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
```

環境構築は以上で終了です。お疲れさまでした。

## 7. マルチスタートアッププロジェクトについて
途中で説明を割愛したマルチスタートアッププロジェクトの利用方法です。
ソリューションを右クリックして、`スタートアッププロジェクトの設定` をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/dc429b06-8433-6c6b-2762-8cded7b8ba14.png)

`マルチスタートアッププロジェクト` にチェックをいれ、`Assembly-CSharp` と `Sample.Server` のアクションを `開始` にして OK を押します。

この状態で `開始` を押すとデバッガーを Unity にアタッチしながらサーバーを起動することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/042cd368-c176-d72c-a705-bd84d77a5b7a.png)

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

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

- [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)