LoginSignup
17
8

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-05-10

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が有効であるかどうかで挙動が異なります

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

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

2018.3の結果

image.png

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

なぜ?

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

IEnumeratorに対してawaitを行うと、

  • 2018.2では、CoroutineAsyncBridgeGetAwaiterが呼び出され、IEnumeratorコルーチンとみなしてawaitする
  • 2018.3では、EnumeratorAsyncExtensionsが呼び出され、IEnumeratorただのイテレータとみなしてawaitする

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

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

対策

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

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

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

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

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

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

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

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

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

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

まとめ

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

17
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
8