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をしている場合は特に注意しましょう。



