PhotonCloudのイベント処理をTaskで扱えるようにした

# 今回の話

[【PUN】PhotonCloudのコールバック処理をUniRxでスマートに書く「PhotonRx」](https://qiita.com/toRisouP/items/10d9112eda30a0ba9278) の続きの話です。

# やったこと

PhotonCloudのサーバに接続する処理や、部屋に参加する処理を`Task`で管理できるようにしました。

PUNのイベントをHookするコンポーネントを用意して、そこから`TaskCompletionSource`を介して`Task`を取得できるようにしています。

```cs:ログインイベントの取得を行うコンポーネント
public class ConnectionEventHook : MonoBehaviour
{
    private object gate = new object();

    private List<TaskCompletionSource<IResult<DisconnectCause, bool>>> observers
        = new List<TaskCompletionSource<IResult<DisconnectCause, bool>>>();

    public Task<IResult<DisconnectCause, bool>> Connect(Action connectAction)
    {
        var tcs = new TaskCompletionSource<IResult<DisconnectCause, bool>>();
        lock (gate)
        {
            observers.Add(tcs);
        }
        connectAction();
        return tcs.Task;
    }

    private void OnConnectedToPhoton()
    {
        if (PhotonNetwork.autoJoinLobby) return;
        lock (gate)
        {
            var targets = observers.ToArray();
            observers.Clear();
            foreach (var t in targets)
            {
                t.SetResult(Success.Create<DisconnectCause, bool>(true));
            }
        }
    }

    private void OnJoinedLobby()
    {
        if (!PhotonNetwork.autoJoinLobby) return;
        lock (gate)
        {
            var targets = observers.ToArray();
            observers.Clear();
            foreach (var t in targets)
            {
                t.SetResult(Success.Create<DisconnectCause, bool>(true));
            }
        }
    }

    private void OnFailedToConnectToPhoton(DisconnectCause cause)
    {
        lock (gate)
        {
            var targets = observers.ToArray();
            observers.Clear();
            foreach (var t in targets)
            {
                t.SetResult(Failure.Create<DisconnectCause, bool>(cause));
            }
        }
    }
}
```

これをstaticメソッド経由で呼び出せるようにしています。

```cs

public static class PhotoTask
{

    public static Task<IResult<DisconnectCause, bool>> ConnectToBestCloudServer(string gameVersion)
    {
        return Connect(() => PhotonNetwork.ConnectToBestCloudServer(gameVersion));
    }

    public static Task<IResult<DisconnectCause, bool>> ConnectToMaster(string masterServerAddress, int port, string appID, string gameVersion)
    {
        return Connect(() => PhotonNetwork.ConnectToMaster(masterServerAddress, port, appID, gameVersion));
    }

    public static Task<IResult<DisconnectCause, bool>> ConnectToRegion(CloudRegionCode region, string gameVersion)
    {
        return Connect(() => PhotonNetwork.ConnectToRegion(region, gameVersion));
    }

    public static Task<IResult<DisconnectCause, bool>> ConnectUsingSettings(string gameVersion)
    {
        return Connect(() => PhotonNetwork.ConnectUsingSettings(gameVersion));
    }

    private static Task<IResult<DisconnectCause, bool>> Connect(Action connectAction)
    {
        var eventHook = GetOrAddComponent<ConnectionEventHook>(PhotonEventManager.Instance.gameObject);
        return eventHook.Connect(connectAction);
    }

    //以下略
}

```

# 導入方法

【[PhotonRx](https://github.com/TORISOUP/PhotonRx)】に追加機能としていれたので、こちらのライブラリを導入してください。

# 使用例

```cs
using UnityEngine;
using System.Threading.Tasks;
using PhotonRx;

public class TaskSample : MonoBehaviour
{
    async void Start()
    {
        PhotonNetwork.autoJoinLobby = true;

        var isConnected = await Connect();

        if (!isConnected) return;

        var isJoined = await JoinRoom();

        Debug.Log(isJoined);

    }

    private async Task<bool> Connect()
    {
        // サーバに接続
        var connect = await PhotoTask.ConnectUsingSettings("v1");

        if (connect.IsFailure)
        {
            // 失敗
            Debug.LogError(connect.ToFailure.Value);
        }

        return connect.IsSuccess;
    }

    private async Task<bool> JoinRoom()
    {
        // 適当な部屋に参加する
        var randomJoined = await PhotoTask.JoinRandomRoom();

        // 成功なら終わり
        if (randomJoined.IsSuccess) return true;

        // 部屋を作って参加する
        var created = await PhotoTask.CreateRoom("test", null, null, null);

        if (!created.IsSuccess)
        {
            //失敗
            Debug.LogError(created.ToFailure.Value);
        }

        return created.IsSuccess;
    }
}

```

# Taskの結果として利用しているIResult型について

`Task`の返り値としてオリジナルの`IResult`型を利用しています。

```cs
public static Task<IResult<DisconnectCause, bool>> ConnectUsingSettings(string gameVersion)

public static Task<IResult<FailureReason, bool>> JoinRoom(string roomName)
```

こちらは関数型プログラミング言語における`Either`型っぽいものを目指して作ったものです。
成功または失敗のどちらかを取る型であり、`IsSuccess`または`IsFailure`で成否の判定が行なえます。

```cs
/// <summary>
/// 成功か失敗かどちらかの状態を表す
/// </summary>
/// <typeparam name="L">失敗時の型</typeparam>
/// <typeparam name="R">成功時の型</typeparam>
public interface IResult<L, R>
{
    bool IsSuccess { get; }
    bool IsFailure { get; }

    Success<L, R> ToSuccess { get; }
    Failure<L, R> ToFailure { get; }

    IResult<L2, R2> Bind<L2, R2>(Func<L, IResult<L2, R2>> fl, Func<R, IResult<L2, R2>> fr);

    IResult<L, R> AsResult { get; }
}
```

あとなんとなく`map`と`faltMap`も実装してみましたけど、たぶん使うことは無いでしょう。

参考

* [Either と Option による関数型のエラー処理](https://www.ibm.com/developerworks/jp/java/library/j-ft13/index.html)

# まとめ

PUNのイベントはさすがに公式でTask化してほしい。