【Unity】UniRx.Asyncでコルーチンをawaitするときの注意点

# 2019/06/24 追記

ライブラリが`UniTask`に分離されたタイミングで、**下記の挙動は修正されました。**
コルーチンをawaitしたときに、正しくコルーチンとして処理される方に統一されました。

以下は過去ログとして残しておきます。


------

# はじめに

Unity 2018.3以降と、それUnity 2018.2とで、UniRxでコルーチンのawaitをしたときの挙動が異なってしまっています。

* Unity 2018.3以降で、**UniRx.Asyncを用いてコルーチンのawaitを行うとコルーチンが正しく機能しない可能性がある**
* Unity 2018.2以前で、UniRxを用いてコルーチンのawaitを行う分には問題はない

何も考えずにUnityのバージョンを2018.2から2018.3に上げると、この問題を踏む可能性があります。

# どういう現象が起きているのか

UniRxにはコルーチンをawaitする機能がついています。
が、これの挙動がUnity2018.2と2018.3とで異なります。
正確にいうと、**UniRx.Asyncが有効であるかどうかで挙動が異なります**。

```cs
using System.Collections;
using System.Threading.Tasks;
using UniRx;
using UnityEngine;
using UniRx.Async; // 2018.3のときのみ有効化される

public class CoroutineAwaitSample : MonoBehaviour
{
    void Start()
    {
        Debug.Log(Application.unityVersion);

        AwaitCoroutineAsync();
    }

    private async Task AwaitCoroutineAsync()
    {
        Debug.Log($"Await前:{Time.frameCount}");

        await Coroutine();

        Debug.Log($"Await後:{Time.frameCount}");
    }

    private IEnumerator Coroutine()
    {
        Debug.Log($"====コルーチン開始:{Time.frameCount}");
        
        yield return new WaitForSeconds(1.0f);
        
        Debug.Log($"====コルーチン終了:{Time.frameCount}");
    }
}

```

## 2018.2の結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/03bfa4f2-b9b6-f700-0949-a234060bcc60.png)

`WaitForSeconds`が正しく稼働し、期待したとおりにコルーチンが実行されています。

## 2018.3の結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/d1065462-1999-a1b0-c924-14a8a00150ac.png)

`WaitForSeconds`が無視され、**コルーチンが正しく起動しませんでした。**

# なぜ？

それぞれのバージョンで呼び出される`Awaiter`が異なっているためです。

`IEnumerator`に対してawaitを行うと、

* 2018.2では、[CoroutineAsyncBridge](https://github.com/neuecc/UniRx/blob/master/Assets/Plugins/UniRx/Scripts/UnityEngineBridge/CoroutineAsyncBridge.cs#L116)の`GetAwaiter`が呼び出され、`IEnumerator`を**コルーチンとみなして**awaitする
* 2018.3では、[EnumeratorAsyncExtensions](https://github.com/neuecc/UniRx/blob/master/Assets/Plugins/UniRx/Scripts/Async/EnumeratorAsyncExtensions.cs#L14)が呼び出され、`IEnumerator`を**ただのイテレータとみなして**awaitする

といった違いがあります。

この呼び出される`Awaiter`が異なるために、このような挙動の違いがでてきてしまうのです。

# 対策

2018.3以降でコルーチンのawaitを行いたい場合は、次の方法で対策ができます。

## 方法1. IEnumeratorをコルーチンとして起動してからawaitする

`StartCoroutine`でコルーチンを明示的に起動してからawaitすることで正しく動作します

```cs
using System.Collections;
using System.Threading.Tasks;
using UniRx;
using UnityEngine;
using UniRx.Async; // 2018.3のときのみ有効化される

public class CoroutineAwaitSample : MonoBehaviour
{
    void Start()
    {
        Debug.Log(Application.unityVersion);

        AwaitCoroutineAsync();
    }

    private async Task AwaitCoroutineAsync()
    {
        Debug.Log($"Await前:{Time.frameCount}");

        // StartCoroutineでコルーチンを起動して、それをawaitする
        await StartCoroutine(Coroutine());

        Debug.Log($"Await後:{Time.frameCount}");
    }

    private IEnumerator Coroutine()
    {
        Debug.Log($"====コルーチン開始:{Time.frameCount}");

        yield return new WaitForSeconds(1.0f);

        Debug.Log($"====コルーチン終了:{Time.frameCount}");
    }
}

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/67f78aaf-aa8b-7b3c-be65-cbbd7b787cb5.png)

`Observable.FromCoroutine`などを使っても問題ありません。

```cs
// コルーチンをObservableに変換してしまう
await Observable.FromCoroutine(Coroutine);

// Observable.FromCoroutineの省略記法
await Coroutine().ToObservable();
```

## 方法2.マイクロコルーチンと同等の使い方をする

UniRxには、`MicroCoroutine`という`yield return null`のみを許容する軽量なコルーチン機構が実装されています。
それにならい、`yield return null`のみを使うコルーチンに書き直し、それをawaitしてしまうという方法です。

```cs
using System.Collections;
using System.Threading.Tasks;
using UniRx;
using UnityEngine;
using UniRx.Async; // 2018.3のときのみ有効化される

public class CoroutineAwaitSample : MonoBehaviour
{
    void Start()
    {
        Debug.Log(Application.unityVersion);

        AwaitCoroutineAsync();
    }

    private async Task AwaitCoroutineAsync()
    {
        Debug.Log($"Await前:{Time.frameCount}");

        await Coroutine();

        Debug.Log($"Await後:{Time.frameCount}");
    }

    private IEnumerator Coroutine()
    {
        Debug.Log($"====コルーチン開始:{Time.frameCount}");

        var startTime = Time.time;
        while (Time.time - startTime < 1.0f)
        {
            yield return null; // WaitForSecondsを使わない
        }

        Debug.Log($"====コルーチン終了:{Time.frameCount}");
    }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/af8c5bc5-5040-e9eb-0470-b7ae834aede6.png)

# まとめ

2018.2でUniRxをバリバリ使っている状況で、2018.3にアップデートするとここでハマる可能性があります。
コルーチンのawaitをしている場合は特に注意しましょう。