はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】コルーチンでタスクをyiled returnしようとすると1フレームだけ待つという罠(UniTask.ToCorouine推奨)

はじめに

コルチーンの中でTaskを待つために以下のようなコードがあったとします。

private IEnumerator SampleCoroutine()
{
    Debug.Log("Start Task");
        
    // 1フレームだけ待つことに注意
    yield return SampleAsync();
        
    Debug.Log("EndTask");
}

private async Task SampleAsync()
{
    await Task.Delay(TimeSpan.FromSeconds(10));
}

一見ちゃんと動作するように見えますが、実はyield return SampleAsync()は1フレームだけ待った後、続きが実行されます。

コンパイルエラーにならないので要注意です。

対応案1

簡単に考えられる対応としてTask.IsCompletedtrueにまで、UnityEngine.WaitUntilを利用することが挙げられます。
Task.IsCompleted プロパティ (System.Threading.Tasks) | Microsoft Learn
UnityEngine.WaitUntil - Unity スクリプトリファレンス

private IEnumerator SampleCoroutine()
{
    Debug.Log("Start Task");
        
    // 対応案1
    Task task = SampleAsync();
    yield return new WaitUntil(() => task.IsCompleted);
        
    Debug.Log("EndTask");
}

private async Task SampleAsync()
{
    await Task.Delay(TimeSpan.FromSeconds(10));
}

ただ大きな問題点として、SampleAsync内でエラーが発生したとき、エラー出力されることなくtask.IsCompletedtrueになってしまいます。

private async Task SampleAsync()
{
    await UniTask.Yield();
    // この時点でTask.IsCompletedがtrueになる、またエラーが伝播されない
    throw new NotImplementedException();
    await UniTask.Yield();
}

一応エラーの情報はTask内部に入っているので、調べることは可能ではあります。

var task = SampleAsync();
yield return new WaitUntil(() => task.IsCompleted);
if(!task.IsCompletedSuccessfully)
    if (task.Exception != null)
        throw task.Exception;

対応案2

UniTaskを利用することで、asyncからコルーチンに変換できます。

If you want to convert async to coroutine, you can use .ToCoroutine(), this is useful if you want to only allow using the coroutine system.

https://github.com/Cysharp/UniTask#install-via-git-url

private IEnumerator SampleCoroutine()
{
    Debug.Log("Start Task");
        
    // 対応案2
    yield return SampleAsync().ToCoroutine();
        
    Debug.Log("EndTask");
}

private async UniTask SampleAsync()
{
    await UniTask.Delay(TimeSpan.FromSeconds(2));
}


ToCoroutineを利用した場合はちゃんとエラーが伝播されます。
またコールバックを追加することでエラーが発生したときの処理を追加することができます。

private IEnumerator SampleCoroutine()
{
    Debug.Log("Start Task");
        
    // SampleAsync内でエラーがあった場合、ログ出力をする
    yield return SampleAsync().ToCoroutine(exception => Debug.Log(exception.Message));
        
    Debug.Log("EndTask");
}

private async UniTask SampleAsync()
{
    await UniTask.Yield();
    throw new NotImplementedException();
}