Unityのコルーチンをasync/awaitで待機できるように変換してみる

# はじめに

Unity 2018からついに.NET 4.6がstableになり、`async/await`を使いこなせるようになっておく必要がでてきました。
今回は個人的な練習として、Unityのコルーチンを`async/await`で待機できるようにしてみました。



# そのまえに：UniRxを導入すれば全部できるよ

実はこれから紹介するような実装をわざわざする必要はありません。
`UniRx`を導入すればいい感じにコルーチンや`AsyncOperation`を`await`できるようになります。

参考:[.NET 4.6時代のUnityでUniRx](https://qiita.com/toRisouP/items/596e42fac1b02f59f271)

```cs:UniRxを使ったAsyncOperationの待機
using System;
using System.Collections;
using UnityEngine;
using UniRx;

class LoadTextureTest : MonoBehaviour
{
    async void Start()
    {
        var request = Resources.LoadAsync<Texture>("texture/player");

        await request; //テクスチャの読み込み待機

        GetComponent<Renderer>().material.mainTexture = request.asset as Texture;
    }
}

```

```cs:UniRxを使ったコルーチンの待機
using System;
using System.Collections;
using UnityEngine;
using UniRx;

class CoroutineAwaitTest: MonoBehaviour
{
    async void Start()
    {
        await MoveCoroutine(Vector3.forward, 1.0f, 1.0f);
        await MoveCoroutine(Vector3.right, 1.5f, 2.0f);
        await MoveCoroutine(Vector3.back, 2.0f, 5.0f);
    }

    /// <summary>
    /// 指定した方向に指定秒数移動する
    /// </summary>
    IEnumerator MoveCoroutine(Vector3 direction, float speed, float seconds)
    {
        var start = Time.time;
        while (Time.time - start <= seconds)
        {
            transform.position += direction * speed * Time.deltaTime;
            yield return null;
        }
    }
}
```


今回の記事は「**自分で`awaitable`なオブジェクトを作るにはどうしたらいいのか**」を紹介するのが目的のため、UniRxは使いません。

#前提「 `await` を使えるようにするためには」

あるオブジェクトを`await`できるようにするためには、次の2つを実装する必要があります。

* `INotifyCompletion`の実装および`IsCompleted`と`GetResult`を定義した「**待機用オブジェクト**」
* 「**待機用オブジェクトを返すGetAwaiter()メソッド**」

逆に言えば、この2つさえ実装してあればどんなものも`await`することができるようになります。



# コルーチンの終了を`await`できるようにしてみる

まずはシンプルな例として、「結果を返さないコルーチン」の終了を`await`で待機できるようにしてみます。

## 1. コルーチンを実行するGameObjectを用意する

コルーチンを処理するためにはなにかしらアクティブな`GameObject`がシーン上に必要になります。
今回は予めシーンにGameObjectを用意しておくことにします。

名前は「`MainThreadDispatcher`」という名前にしておきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/47146/d01072e5-a8b0-5192-439f-cad4d482bad8.png)
（中身は空でいいのでとりあえずGameObjectだけ用意しておく）

## 2. コルーチンを実際に処理する`Component`を定義する

コルーチンを実際に処理して、その終了を通知することができる`Component`を作ります。
名前はGameObject名に合わせて「`MainThreadDispatcher`」にします。

```cs
using System;
using System.Collections;
using UnityEngine;

public class MainThreadDispatcher : MonoBehaviour
{
    private static MainThreadDispatcher instance;

    public static MainThreadDispatcher Instance
    {
        get
        {
            if (instance == null)
            {
                //とりあえず雑に
                instance = GameObject.FindObjectOfType<MainThreadDispatcher>();
            }
            return instance;
        }
    }

    /// <summary>
    /// 登録されたコルーチンを実行する
    /// </summary>
    /// <param name="coroutine">対象のコルーチン</param>
    /// <param name="callback">終了時のコールバック</param>
    public void RegisterCoroutione(IEnumerator coroutine, Action callback)
    {
        StartCoroutine(WorkCoroutine(coroutine, callback));
    }

    /// <summary>
    /// コルーチンを実行し、終了時にコールバック関数を実行する
    /// </summary>
    private IEnumerator WorkCoroutine(IEnumerator target, Action callback)
    {
        yield return target;
        callback();
    }
}

```

そしてこれを先程のGameObjectに貼り付けておきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/47146/afa30b45-2e7f-25b1-6519-66101a41b2d9.png)

## 3. `INotifyCompletion`の実装および`IsCompleted`と`GetResult`を定義した「待機用オブジェクト」を定義する

`await`時に呼び出される待機用のオブジェクトを定義します。

内容としてはコルーチンをコンストラクタで受け取り、`INotifyCompletion.OnCompleted`内でごねごねする形になります。

```cs
/// <summary>
/// コルーチンの待機を補助するオブジェクト
/// </summary>
public class AwaitableEnumerator : INotifyCompletion
{
    /// <summary>
    /// 対象となるコルーチン
    /// </summary>
    private readonly IEnumerator target;

    /// <summary>
    /// 非同期処理が完了しているか
    /// </summary>
    public bool IsCompleted { get; private set; }

    /// <summary>
    /// 結果のオブジェクトを取得する場合に利用する
    /// 今回は結果の値がないのでvoid
    /// </summary>
    public void GetResult()
    {
    }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public AwaitableEnumerator(IEnumerator coroutine)
    {
        target = coroutine;
    }

    /// <summary>
    /// await開始時に実行される関数
    /// </summary>
    /// <param name="continuation">「await以降の処理」を扱うAction</param>
    public void OnCompleted(Action continuation)
    {
        // awaitが開始されたらコルーチンを実行する
        MainThreadDispatcher.Instance.RegisterCoroutione(target, () =>
        {
            // コルーチンが終了したらフラグを立てて、処理の継続を行う
            IsCompleted = true;
            continuation();
        });
    }
}
```

`OnCompleted(Action continuation)`が最も重要なメソッドです。
`await`が実行されたときにこのメソッドが呼び出されるため、ここに「**awaitを終了する条件および継続処理の呼び出し**」を記述します。
`Action continuation`は「await以降の処理」を指し示す`Action`です。そのため、非同期処理が終わったあとに処理を継続する場合は明示的にこの`Action`を実行する必要があります。

今回はこの`OnCompleted()`内でコルーチンを起動し、そのコールバックをもって`await`を終了するという形にします。

## 4. `IEnumerator`から`AwaitableEnumerator`に変換する`GetAwaiter()`を定義する。

コルーチン(`IEnumrator`)を`await`するためには、`IEnumrator`に`GetAwaiter()`メソッドが定義されている必要があります。
今回は拡張メソッドでこれを追加します。

```cs
public static class IEnumeratorExtension
{
    public static AwaitableEnumerator GetAwaiter(this IEnumerator coroutine)
    {
        return new AwaitableEnumerator(coroutine);
    }
}
```

## 4.実際にコルーチンを`await`で待機する

これで下準備は完了したため、実際に使ってみましょう。

次のようなスクリプトをオブジェクトに貼り付けて実行してみます。

```cs
class AwaitCoroutineSample : MonoBehaviour
{

    async void Start()
    {
        // 前へ2秒進む
        await MoveCoroutine(Vector3.forward, 2.0f);

        // 右へ1秒進む
        await MoveCoroutine(Vector3.right, 1.0f);

        // 後ろへ3秒進む
        await MoveCoroutine(Vector3.back, 3.0f);
    }

    /// <summary>
    /// 指定した方向へ、指定した時間[秒]、移動する
    /// </summary>
    IEnumerator MoveCoroutine(Vector3 direction, float seconds)
    {
        var start = Time.time;
        while (Time.time - start <= seconds)
        {
            transform.position += direction * Time.deltaTime;
            yield return null;
        }
    }
}
```

![1.gif](https://qiita-image-store.s3.amazonaws.com/0/47146/defa14c5-2654-4697-1015-cb4834f0617c.gif)

このように、コルーチンを`async/await`で待ち受けることができるようになりました！

# もっと簡単に定義するために「`TaskCompletionSource`」を使う

今回は説明のために`Awaitable`パターンを自前ですべて実装しました。
ですがC#には非同期処理を`Task`にラップしてしまう機能が用意されており、そちらを使うと`AwaitableEnumerator`の定義すら不要になります。

というわけで、先程の`GetAwaiter`メソッドを`TaskCompletionSource`を使って書き換えると次のようになります。

```cs
public static class IEnumeratorExtension
{
        
    public static TaskAwaiter<bool> GetAwaiter(this IEnumerator coroutine)
    {
        // TaskCompletionSourceを生成
        // 非ジェネリック版のTaskCompletionSourceは存在しないので、便宜上boolで代用
        var tcs = new TaskCompletionSource<bool>();

        MainThreadDispatcher.Instance.RegisterCoroutione(coroutine, () =>
        {
            // コルーチン終了時に値を設定
            tcs.TrySetResult(true);
        });

        return tcs.Task.GetAwaiter();
    }
}
```

このように、`TaskCompletionSource`を使えば簡単にコルーチンの処理を`Task`に置き換えて`await`できるようになります。



# コルーチンの実行結果を取り出せるようにする

では続いて、`await`でコルーチンの実行結果を取得できるようにしてみましょう。

## 1. MainThreadDispatcherにメソッドを追加する

`MainThreadDispatcher`に次のメソッドを追加します。
単にコルーチンを実行するだけのメソッドです。

```cs
/// <summary>
/// 登録されたコルーチンを実行する
/// </summary>
/// <param name="coroutine">対象のコルーチン</param>
public void RegisterCoroutione(IEnumerator coroutine)
{
    StartCoroutine(coroutine);
}
```


## 2. statiメソッド経由でコルーチンからTaskに変換できるようにする

コルーチンから結果を取り出そうとした場合、コルーチン内で明示的にコールバックを呼び出す必要があります。
今回はコルーチンに直接`TaskCompletionSource`を渡し、内部で結果を詰めてもらう仕組みにします。

そこで、そのようなコルーチンを簡単に生成できるようにするためのstaticメソッドを用意しておきます。

```cs
public static class Awaitable
{
    public static Task<T> Create<T>(Func<TaskCompletionSource<T>, IEnumerator> creation)
    {
        var tcs = new TaskCompletionSource<T>();

        // コルーチンを生成
        var coroutine = creation(tcs);

        MainThreadDispatcher.Instance.RegisterCoroutione(coroutine);

        return tcs.Task;
    }
}
```

`Func<TaskCompletionSource<T>, IEnumerator>`は「`TaskCompletionSource<T>`を引数にとり、`IEnumerator`を戻り値とする関数」を意味します。

## 3. 実際にコルーチンを生成して利用する

以上で準備が整ったので、コルーチンをawaitしてみます。

さきほどの`Awaitable.Create<string>()`を用いて、結果を返すコルーチンを生成し、それを`Task<string>`に変換します。

```cs
using System;
using System.Collections;
using System.Threading.Tasks;
using Assets;
using UnityEngine;

public class AwaitCoroutineSample2 : MonoBehaviour
{
    async void Start()
    {
        var url = "https://unity3d.com/jp/";

        // コルーチンをawaitableにする
        var awaitableCoroutine = Awaitable.Create<String>(tcs => GetCoroutine(url, tcs));

        // await
        var result = await awaitableCoroutine;

        Debug.Log(result);
    }

    /// <summary>
    /// WWWを使って通信して結果をStringで通知するコルーチン
    /// </summary>
    IEnumerator GetCoroutine(string url, TaskCompletionSource<string> taskCompletionSource)
    {
        var www = new WWW(url);

        yield return www;

        if (!string.IsNullOrEmpty(www.error))
        {
            taskCompletionSource.TrySetException(new Exception(www.error));
        }
        else
        {
            taskCompletionSource.TrySetResult(www.text);
        }
    }
}


```

## (こんなことしなくても)

`UniRx`を導入すれば全部できます。
使い方もだいたい同じです。

参考:[UniRx入門 その5 -コルーチンとの併用-](https://qiita.com/toRisouP/items/c4b9c5701dd6c991b481)

```cs
using System;
using System.Collections;
using UniRx;
using UnityEngine;

public class AwaitCoroutineSample3 : MonoBehaviour
{

    async void Start()
    {
        var url = "https://unity3d.com/jp/";

        // await
        var result = await Observable.FromCoroutine<string>(observer => GetCoroutine(url, observer));

        // 結果がとれる
        Debug.Log(result);
    }

    /// WWWを使って通信して結果をStringで通知するコルーチン
    /// </summary>
    IEnumerator GetCoroutine(string url, IObserver<string> observer)
    {
        var www = new WWW(url);

        yield return www;

        if (!string.IsNullOrEmpty(www.error))
        {
            //失敗
            observer.OnError(new Exception(www.error));
        }
        else
        {
            // 正常系
            observer.OnNext(www.text);
            observer.OnCompleted();
        }
    }
}
```

# 最後に

自前で`Awaitable`パターンのインタフェースを満たしてしまえば、どんなものでも`await`できるようになります。
とくに`TaskCompletionSource`を使うと非同期処理を簡単にTaskに変換して`awaitable`にできるのでおすすめです。


# 参考にしたサイト

[非同期メソッド入門 (9) - Awaitableパターンの自前実装](http://blog.xin9le.net/entry/2012/11/05/203504)