NinaLabo

個人ゲーム開発者の技術メモ

【Unity】async/awaitのフレーム消費

C# 6.0から async/await が使えるようになり、コルーチンでは解決できなかった「何もしてないのにフレーム消費されてしまう」問題が解決できそうです。

まずは今までのコルーチン処理です。IEnumeratorを返すメソッドでは下記のように非同期処理を上から順に実行される形で書くことができました。下記の処理では、coroutine1が終わるのを待ってからcoroutine2が実行されます。コールバック処理を書くよりも見通しの良いコードを書くことができました。

yield return coroutine1;

yield return coroutine2;

もちろん、下記のようにMoveNextを使って書くこともできますが、 上記のほうが1行で書けるためスッキリ書けました。

while (coroutine1.MoveNext())

{

    yield return null;

}

while (coroutine2.MoveNext())

{

    yield return null;

}

 

ただし、yield return coroutine; の書き方は問題もありました。メソッド内部で仮に何も処理していなくても、必ず1フレーム消費されてしまうことです。 

public class AsyncFrameTest1 : MonoBehaviour
{
    private int mFrameNum;
    private bool mIsPlaying;

    private IEnumerator Start()
    {
        // 念のため1フレーム待って計測
        yield return null;

        mIsPlaying = true;
        mFrameNum = 0;

        var previousFrame = mFrameNum;
        yield return TestCoroutine1();
        Debug.LogFormat("TestCoroutine1: (+{0}フレーム)",  mFrameNum - previousFrame);

        previousFrame = mFrameNum;
        yield return TestCoroutine2();
        Debug.LogFormat("TestCoroutine2: (+{0}フレーム)", mFrameNum - previousFrame);
    }

    public IEnumerator TestCoroutine1()
    {
        yield break;
    }
    public IEnumerator TestCoroutine2()
    {
        yield return null;
    }

    private void Update()
    {
        if (mIsPlaying)
        {
            mFrameNum++;
        }
    }
}

 実行結果はご覧の通りです。TestCoroutine1は内部でyield breakしているだけですが、yield return nullしているTestCoroutine2と同様に処理が終わるまで1フレームかかっています。これが結構厄介で、例えばコルーチンメソッド内でキャッシュがある場合はロードせずにyield breakするような処理を書いても最低1フレームはかかってしまうということになります。

f:id:ninagreen:20190207030834p:plain

もちろんMoveNextで書いておけば余計なフレームは消費せずに済みます。

       previousFrame = mFrameNum;
        var testCoroutine1 = TestCoroutine1();
        while (testCoroutine1.MoveNext())
        {
            yield return null;
        }
        Debug.LogFormat("MoveNext(TestCoroutine1): (+{0}フレーム)", mFrameNum - previousFrame);


        var testCoroutine2 = TestCoroutine2();
        while (testCoroutine2.MoveNext())
        {
            yield return null;
        }
        Debug.LogFormat("MoveNext(TestCoroutine2): (+{0}フレーム)"

, mFrameNum - previousFrame); 

f:id:ninagreen:20190207031620p:plain

 

1行でスッキリ書けて、かつ余計なフレームを消費しない書き方があればいいのになあとずっと思っていましたが、async/awaitで実現できるようです。

public class AsyncFrameTest2 : MonoBehaviour
{
    private int mFrameNum;
    private bool mIsPlaying;

    private async void Start()
    {
        mIsPlaying = true;
        mFrameNum = 0;

        var previousFrame = mFrameNum;
        await TestAsync1();
        Debug.LogFormat("TestAsync1: (+{0}フレーム)",  mFrameNum - previousFrame);

        previousFrame = mFrameNum;
        await TestAsync2();
        Debug.LogFormat("TestAsync2: (+{0}フレーム)", mFrameNum - previousFrame);

        mIsPlaying = false;
    }

    public async UniTask TestAsync1()
    {
        // 何もしない
    }

    public async UniTask TestAsync2()
    {
        await UniTask.DelayFrame(1); // 1フレーム待つ
    }

    private void Update()
    {
        if (mIsPlaying)
        {
            mFrameNum++;
        }
    }
}

 実行結果です

f:id:ninagreen:20190207032657p:plain


便利ですね!