LoginSignup
6
3

More than 5 years have passed since last update.

Unityのコルーチンをawaitできるようにする(CancellationToken対応)

Last updated at Posted at 2018-12-28

環境

Unity 2018.3
UniRx - Reactive Extensions for Unity / Ver 6.2.2

想定している読者ターゲット

async/awaitを理解している人
UniTaskを少し理解している人
Awaiterの仕組みをある程度理解している人

await CoroutineFunction()
がUnityのコルーチンの動作と異なるのが気になる人

Unityのコルーチンをawaitしたい!

2018/12/29 追記

この件、@toRisouPさんに教えていただきました!

実際に試してみましたところ
UniRxをusingすると、IEnumeratorの拡張メソッドとして追加されるToYieldInstructionを使うと
期待通りの動作をしましたので、この記事のモノを使うより、そっちをお使いください

以下の記事は記念に残しておきます

本来UniRx.Asyncの機能でIEnumeratorもawait出来るのだけども

自分は今、UniRx.Async.UniTaskの力を使い、コルーチンからasync/awaitに移行している途中
現在コルーチンとasync/awaitが混在している状態

そんななかコルーチンで書いた処理をawaitしようとすると
普通にUnityのコルーチンで実行したときと
UniTaskの機能を使いawaitで待った時とで挙動が違うことに気が付いた

簡単な例


public class TestCoroutineAwait : MonoBehaviour
{
    void Start()
    {
        // コルーチンで実行
        StartCoroutine(CoroutineTest());

        // async/awaitで実行
        Go(this.GetCancellationTokenOnDestroy()).Forget();
    }

    async UniTask Go(CancellationToken token)
    {
        try
        {
            // UniRxの機能のConfigureAwaitでCancellationTokenを渡す
            await CoroutineTest().ConfigureAwait(cancellationToken: token);
        }
        finally
        {
            Debug.Log("End");
        }
    }

    IEnumerator CoroutineTest()
    {
        // 10カウントする
        for (int i=0; i<10; ++i)
        {
            Debug.Log(i);
            yield return new WaitForSeconds(1);
        }
    }
}

どう挙動が違う?

上記コードは、コルーチンで実行するなら1秒に1つづつ10秒かけてカウントアップするが、
async/awaitの場合、瞬時に出力されて終わるようだ(1フレームに1つ出力されてる?)

また、WaitForEndOfFrameのタイミングもスルーするのか
以下のコードをasync/awaitで実行しようとすると
「このタイミングではスクリーンバッファはReadPixelできない」というエラーとなる

    IEnumerator CoroutineTest()
    {
        { // スクリーンショットをとる
            // 描画が終わったタイミングで処理が戻るWaitForEndOfFrameを使う
            yield return new WaitForEndOfFrame();

            var texture = new Texture2D(Screen.width, Screen.height, TextureFormat.ARGB32, false);
            RenderTexture.active = null;
            texture.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
            texture.Apply();
            GameObject.DestroyImmediate(texture);
        }
    }

どうするか

今後Coroutineは使わない方向になるだろうし
とりあえずの間に合わせに簡単に自前でコルーチンを待つAwaiterを書いてみた

もちろん、CancellationTokenへの対応は個人的に必須なのでそこは外さずいれてある

自前のコルーチンAwaiter


// (2018/12/31 17:02追記) CancellationToken.Registerの登録周りで問題があったので書き直した

// コルーチン用Awaiter
public struct CoroutineAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion
{
    CancellationToken token;
    IEnumerator e;

    public CoroutineAwaiter(IEnumerator e, CancellationToken token)
    {
        this.e = e;
        this.token = token;
    }

    // falseを返すべきなんだ
    public bool IsCompleted => false;

    // GetResultでThrowIfCancellationRequestedを呼び出し、キャンセルされてたら例外
    public void GetResult() => token.ThrowIfCancellationRequested();

    // UnsafeOnCompletedを呼び出す
    public void OnCompleted(Action continuation) => UnsafeOnCompleted(continuation);

    public void UnsafeOnCompleted(Action continuation)
    {
        CancellationTokenRegistration regist = new CancellationTokenRegistration();

        Coroutine c = CoroutineExecutor.Begin(e, () =>
        {
            // CancellationToken.Registerで登録したのを解除
            regist.Dispose();
            continuation();
        });

        regist = token.Register(() =>
        {
            CoroutineExecutor.End(c);
            continuation();
        });
    }

    // 我はawaiterでありながらawaitableでもあるのだ
    public CoroutineAwaiter GetAwaiter() => this;
}

// コルーチンを実行するためのシングルトン
public class CoroutineExecutor : MonoBehaviour
{
    static CoroutineExecutor _instance;

    static CoroutineExecutor instance
    {
        get
        {
            if (_instance == null)
            {
                Init();
            }
            return _instance;
        }
    }

    static void Init()
    {
        var g = new GameObject();
        g.name = "CoroutineExecutor";
        GameObject.DontDestroyOnLoad(g);
        _instance = g.AddComponent<CoroutineExecutor>();
    }

    public static Coroutine Begin(IEnumerator e, System.Action continuation)
    {
        return instance.StartCoroutine(Exec(e, continuation));
    }

    public static void End(Coroutine c)
    {
        instance.StopCoroutine(c);
    }

    static IEnumerator Exec(IEnumerator e, System.Action continuation)
    {
        yield return e;
        continuation();
    }
}

自前のコルーチンAwaiterの使用方法

上記のコードをファイルに保存し以下のように記述する

await new CoroutineAwaiter(CoroutineTest(), token);

自前のコルーチンAwaiterの使用例

public class TestTween : MonoBehaviour
{
    void Start()
    {
        Go(this.GetCancellationTokenOnDestroy()).Forget();
    }

    async UniTask Go(CancellationToken token)
    {
        try
        {
            // CoroutineAwaiterにIEnumeratorとCancellationTokenを渡しAwaiterとして包み込む
            await new CoroutineAwaiter(CoroutineScreenShot(), token);

            await new CoroutineAwaiter(CoroutineTest(), token);
        }
        finally
        {
            Debug.Log("End");
        }
    }

    IEnumerator CoroutineScreenShot()
    {
        yield return new WaitForEndOfFrame();

        var texture = new Texture2D(Screen.width, Screen.height, TextureFormat.ARGB32, false);
        RenderTexture.active = null;
        texture.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
        texture.Apply();
        GameObject.DestroyImmediate(texture);
    }

    IEnumerator CoroutineTest()
    {
        // 10カウント
        for (int i=0; i<10; ++i)
        {
            Debug.Log(i);
            yield return new WaitForSeconds(1);
        }
    }
}

終わりに

これで正しいのかいまいち自信が無いのですが、とりあえず期待通りに動いてるので
コルーチンを駆逐するまでこれでごまかしていきたいという所信表明

また、自分がUniRx.Asyncの使い方をそもそも間違えてる可能性が大きいので
間違ってたらご指摘お願いします

ライセンス

この記事の私の書いた部分のソースコードのライセンスはMIT LicenseかApache 2.0 Licenseのお好きな方でどうぞ

6
3
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
6
3