MagicOnion のストリーミング接続を複数シーンで持ち越す場合のTips

# はじめに

MagicOnion については[こちら](http://tech.cygames.co.jp/archives/3181/)をご覧ください。
一般的なマルチプレイゲームでは、マッチング→メインのゲーム→リザルト といったふうに、複数のシーンでストリーミング接続を繋ぎっぱなしにする事が多いと思います。
その場合、1シーンで使う場合に比べ気をつける必要がある点がいくつかあるので、それぞれに対して自分がやっている対応を紹介します。

### Receiver

接続全体を1つのインターフェースにまとめることになるので、シーンごとに一部のメソッドしか使わないことになります。
一方、IHogeHubReceiver を実装するクラスでは当然すべてのメソッドを定義しなければいけません。
このままでは非常に扱いづらいのですべてのメソッドを UniRx の Observable に変換してしまうと良さそうです。
インターフェイスの明示的実装を使えば同名のPublicなプロパティが生やせます。

```csharp:例
public interface IHogeHubReceiver
{
    void OnHoge();
    void OnFuga(int i);
    void OnPiyo(int i, int j);
}

public class HogeHubReceiver : IHogeHubReceiver
{
    // 引数なしは UniRx.Unit に
    public IObservable<Unit> OnHoge => _onHoge;
    private Subject<Unit> _onHoge = new Subject<Unit>();
    void IHogeHubReceiver.OnHoge() => _onHoge.OnNext(Unit.Default);

    // 1引数はそのまま
    public IObservable<int> OnFuga => _onFuga;
    private Subject<int> _onFuga = new Subject<int>();
    void IHogeHubReceiver.OnFuga(int i) => _onFuga.OnNext(i);

    // 2引数以上はタプル化
    public IObservable<(int i, int j)> OnPiyo => _onPiyo;
    private Subject<(int i, int j)> _onPiyo = new Subject<(int i, int j)>();
    void IHogeHubReceiver.OnPiyo(int i, int j) => _onPiyo.OnNext((i, j));
}
```

これを手動で書くのはめんどくさくてしょうがないのでエディタ拡張で自動生成しましょう。

```csharp:StreamingHubReceiverCreator.cs
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.IO;
using UnityEditor;

/// <summary>
/// StreamingHubReceiverのインターフェースから具象型を生成するエディタ拡張
/// </summary>
public static class StreamingHubReceiverCreator
{
    /// <summary>
    /// StreamingHubReceiverのインターフェースから具象型を生成する
    /// すべてのメソッドはIObservableに変換される
    /// </summary>
    [MenuItem("Assets/Create/StreamingHubReceiver")]
    private static void CreateStreamingHubReceiver()
    {
        var interfaceName = Selection.activeObject.name;

        var source = File.ReadAllText(AssetDatabase.GetAssetPath(Selection.activeObject));

        // using追加
        if (!source.Contains("using System;")) source = "using System;\r\n" + source;
        if (!source.Contains("using UniRx;")) source = "using UniRx;\r\n" + source;

        // クラス名変換
        source = Regex.Replace(source, "interface I(.*Receiver)", "class $1 : I$1");

        // 各メソッド変換
        source = Regex.Replace(source, "( *)void (.*)\\((.*)\\);", m =>
        {
            var space = m.Groups[1].Value;
            var name = m.Groups[2].Value;
            var privateName = "_" + Char.ToLower(name[0]) + name.Substring(1);

            var args = m.Groups[3].Value;
            var genericArgs = "Unit";
            var argNames = "Unit.Default";

            // 引数の個数に応じてデータ構造を変更
            var x = Regex.Matches(args, "(?:(.+?[^,]) (.+?)(?:,|$))").Cast<Match>().ToArray();
            if (x.Length == 1)
            {
                genericArgs = x[0].Groups[1].Value;
                argNames = x[0].Groups[2].Value;
            }
            else if (x.Length > 1)
            {
                // タプル化
                genericArgs = "(" + args + ")";
                argNames = "(" + string.Join(", ", x.Select(y => y.Groups[2].Value)) + ")";
            }

            return $"{space}public IObservable<{genericArgs}> {name} => {privateName};\r\n" +
                $"{space}private Subject<{genericArgs}> {privateName} = new Subject<{genericArgs}>();\r\n" +
                $"{space}void {interfaceName}.{name}({args}) => {privateName}.OnNext({argNames});";
        });

        var fileName = Selection.activeObject.name.Substring(1) + ".cs";
        var classPath = Directory.EnumerateFiles(".", fileName, SearchOption.AllDirectories).FirstOrDefault();
        File.WriteAllText(classPath ?? "Assets/" + fileName, source);
        AssetDatabase.Refresh();
    }

    /// <summary>
    /// 選択しているアイテムがスクリプトファイルかどうかを判別する
    /// </summary>
    /// <returns>スクリプトファイルかどうか</returns>
    [MenuItem("Assets/Create/StreamingHubReceiver", isValidateFunction: true)]
    private static bool ValidateCreateStreamingHubReceiver() => Selection.activeObject is MonoScript;
}
```

### 切断

接続が不要になったら切断するというは大事で、これをしないと Unity エディタがすぐフリーズします。
単独のシーンで使うならシーンで使うスクリプトの OnDestroy で切断すれば良いですが、
複数シーンの場合は、接続を管理するオブジェクトを作り DontDestroyOnLoad に設定しておくのが良いでしょう。

```csharp
public class ConnectionHolder : MonoBehaviour
{
    public IHogeHub Client { get; private set; }
    public HogeHubReceiver Receiver { get; } = new HogeHubReceiver(); // さっき作ったやつ

    private Channel _channel;

    public static ConnectionHolder Create()
    {
        // DontDestroy化
        var gameObject = new GameObject("ConnectionHolder");
        DontDestroyOnLoad(gameObject);

        // 接続
        var holder = gameObject.AddComponent<ConnectionHolder>();
        holder._channel = new Channel("host:port", ChannelCredentials.Insecure);
        holder.Client = StreamingHubClient.Connect<IHogeHub, IHogeHubReceiver>(holder._channel, holder.Receiver);

        return holder;
    }

    private void OnDestroy()
    {
        Client.DisposeAsync();
        _channel.ShutdownAsync();
    }
}
```

1. はじめのシーンでは Create を呼ぶ
1. 以降のシーンでは Find で探してくる
1. 最後のシーンで Destroy する

というイメージです。
ついでに参加者の保持管理もこのクラスでやってしまうと後のシーンで使いやすくて便利です。