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の結果
WaitForSeconds
が正しく稼働し、期待したとおりにコルーチンが実行されています。
2018.3の結果
WaitForSeconds
が無視され、コルーチンが正しく起動しませんでした。
なぜ?
それぞれのバージョンで呼び出されるAwaiter
が異なっているためです。
IEnumerator
に対してawaitを行うと、
- 2018.2では、CoroutineAsyncBridgeの
GetAwaiter
が呼び出され、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}");
}
}
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}");
}
}
まとめ
2018.2でUniRxをバリバリ使っている状況で、2018.3にアップデートするとここでハマる可能性があります。
コルーチンのawaitをしている場合は特に注意しましょう。