のりまき日記

Unityなどの活用リファレンスブログ。「こうしたい時どうする」をまとめたい

unity)Resources.LoadAsyncみたいに独自の非同期処理を書きたい

僕がUnityでコードを書く時はUnityっぽい書き方に寄せたいな、と思っています。非同期処理を書こうとすると毎回うまくできないのでまとめてみます。

処理落ちせずに画像を変換できている様子

memo

ややこしい処理をすれば実現できるのですが、できるだけややこしい処理をせずに実現してみます。UniRxも存在しない世界です。

はじめに非同期処理と並行処理

yieldやasyncやスレッドを使ってると、いろいろな非同期が発生します。いろいろ端折ってますが以下のような感じです。

yield

IEnumerable Start()
{
    yield return Run();
}

IEnumerator Run(string url)
{
    for (int i = 0; i < 1000000; i++)
    {
        // なにか処理
        ...

        if (i % 1000 == 999)
        {
            yield return null;
        }
    }
}

メインスレッドのまま処理を数フレームに分けて処理落ちを軽減する非同期。

async(Task.Runなし)

async void Start()
{
    await Run();
}

async Task Run()
{
    for (int i = 0; i < 1000000; i++)
    {
        // なにか処理
        ...
        
        if (i % 1000 == 999)
        {
            await Task.Yield();
        }
    }
}

メインスレッドのまま処理を数フレームに分けて処理落ちを軽減する非同期。awaitの先は実装次第で非同期だったり並行だったり別スレッドだったりします。

async(Task.Runあり)

async void Start()
{
    await Task.Run(() => Run());
}

void Run()
{
    for (int i = 0; i < 1000000; i++)
    {
        // なにか処理
        ...
    }
}

別スレッドなのでメインスレッドの処理落ちを考えずぶん回す非同期。

目指したい書き方

Resources.LoadAsyncっぽい書き方です。

public class AsyncSample : MonoBehaviour
{
    [SerializeField]
    RawImage m_Photo;

    IEnumerator Start()
    {
        var request = Resources.LoadAsync<Texture2D>("cat");
        yield return request;
        m_Photo.texture = request.asset as Texture2D;
    }
}

今回はフルHDの画像をCPUで反転する処理を非同期にしたい、という内容で進めます。

void Run(Texture2D tex)
{
    var result = new Texture2D(tex.width, tex.height, tex.format, false);
    var cols = tex.GetPixels();
    for (int y = 0; y < tex.height; y++)
    {
        for (int x = 0; x < tex.width; x++)
        {
            var idx = x + y * tex.width;
            var col = cols[idx];
            cols[idx] = new Color(1f - col.r, 1f - col.g, 1f - col.b, col.a);
        }
    }
    result.SetPixels(cols);
    result.Apply();
}

目指す書き方

public class AsyncSample : MonoBehaviour
{
    [SerializeField]
    RawImage m_Photo;

    IEnumerator Start()
    {
        var request = TextureUtility.InvertAsync((Texture2D)m_Photo.texture);
        yield return request;
        m_Photo.texture = request.result;
    }
}

Resources.LoadAsyncの中身

var request = Resources.LoadAsync<Texture2D>("cat");

この時点で非同期処理が動いています。requestはAsyncOperationを継承したオブジェクトで、結果の保持と処理がおわったかどうかを保持しています。呼び出した時点で非同期処理が動き出すのがポイントです。

yield return request;

ここは処理がおわるのを待っているだけでStartCoroutineしているわけではなく、この記述がなくても裏では非同期処理が進行しています。

AsyncOperationを実装する

public abstract class MyAsyncOperation<T> : CustomYieldInstruction
{
    protected T m_Result;
    public T result => m_Result;

    public override bool keepWaiting => m_Result == null;

    // publicになってしまうのは気持ちわるい
    public void Complete(T resule)
    {
        m_Result = resule;
    }
}

CustomYieldInstructionを使ってそれっぽいものを作りました。

public class TextureConvertRequest : MyAsyncOperation<Texture2D>
{
}

結果として画像を加工した結果を返すMyAsyncOperationです。

Coroutineを使ったパターン

async / awaitが使えなかった頃は別スレッドに逃すかCoroutineを使っていました。

public static class TextureUtility
{
    public static TextureConvertRequest InvertAsync(Texture2D tex, MonoBehaviour mono)
    {
        var req = new TextureConvertRequest();
        mono.StartCoroutine(Run(tex, req));
        return req;
    }

    static IEnumerator Run(Texture2D tex, TextureConvertRequest req)
    {
        var result = new Texture2D(tex.width, tex.height);
        var cols = tex.GetPixels();
        for (int x = 0; x < tex.width; x++)
        {
            for (int y = 0; y < tex.height; y++)
            {
                var idx = x + y * tex.width;
                var col = cols[idx];
                cols[idx] = new Color(1f - col.r, 1f - col.g, 1f - col.b, col.a);
            }

            // 一列ごとに処理を戻す
            yield return null;
        }
        result.SetPixels(cols);
        result.Apply();

        req.Complete(result);
    }
}

この場合は「目指す書き方」は不可能です。誰かがStartCoroutineを呼ばなければいけないので、MonoBehaviourを渡したりグローバルなコルーチンを回すだけの存在を作ったりしていました。

note

MoveNext()とかを駆使すればできるかも。

asyncを使ったパターン

public static class TextureUtility
{
    public static TextureConvertRequest InvertAsync(Texture2D tex)
    {
        var req = new TextureConvertRequest();
        // なげっぱなし
        var _ = Run(tex, req);
        return req;
    }

    static async Task Run(Texture2D tex, TextureConvertRequest req)
    {
        var result = new Texture2D(tex.width, tex.height);
        var cols = tex.GetPixels();
        for (int x = 0; x < tex.width; x++)
        {
            for (int y = 0; y < tex.height; y++)
            {
                var idx = x + y * tex.width;
                var col = cols[idx];
                cols[idx] = new Color(1f - col.r, 1f - col.g, 1f - col.b, col.a);
            }

            // 一列ごとに処理を戻す
            await Task.Yield();
        }
        result.SetPixels(cols);
        result.Apply();

        req.Complete(result);
    }
}

asyncが使えるようになったのでStartCoroutineなしに非同期処理を回せます。「目指す書き方」が出来ました。

スレッドを使ったパターン

public static class TextureUtility
{
    public static TextureConvertRequest InvertAsync(Texture2D tex)
    {
        var req = new TextureConvertRequest();
        // なげっぱなし
        var _ = Run(tex, req);
        return req;
    }

    static async Task Run(Texture2D tex, TextureConvertRequest req)
    {
        var result = new Texture2D(tex.width, tex.height);

        // メインスレッド以外ではUnityのAPIが使えないのでここで取得する
        var cols = tex.GetPixels();
        var width = tex.width;
        var height = tex.height;

        try
        {
            await Task.Run(() => _Run(cols, width, height));
        }
        catch (System.Exception e)
        {
            Debug.LogException(e);
            throw e;
        }

        result.SetPixels(cols);
        result.Apply();

        req.Complete(result);
    }

    static void _Run(Color[] cols, int width, int height)
    {
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                var idx = x + y * width;
                var col = cols[idx];
                cols[idx] = new Color(1f - col.r, 1f - col.g, 1f - col.b, col.a);
            }
        }
    }
}

メインスレッドの処理をほとんど止めることなく実現できました。

おまけ

以下のクラスを実装しておくとコルーチンasync / awaitも同居できると思います。UnityWebRequestをawaiしたい場面や、プラグインasyncを使っていてyield returnしたい時に使えると思います。

AsyncOperationをawaitする

拡張メソッドでGetAwaiterを定義することでawaitできるようになります。

public static class AsyncOperationExtension
{
    public static TaskAwaiter<bool> GetAwaiter(this AsyncOperation ope)
    {
        var source = new TaskCompletionSource<bool>();
        ope.completed += (_) =>
        {
            source.TrySetResult(true);
        };
        return source.Task.GetAwaiter();
    }
}
public class AsyncSample : MonoBehaviour
{
    [SerializeField]
    RawImage m_Photo;

    private async void Start()
    {
        var request = Resources.LoadAsync<Texture2D>("cat");
        await request;
        m_Photo.texture = request.asset as Texture2D;
    }
}

Taskをyield returnする

class WaitForTaskCompletion : CustomYieldInstruction
{
    Task m_Task;

    public override bool keepWaiting => !m_Task.IsCompleted;

    public WaitForTaskCompletion(Task task)
    {
        m_Task = task;
    }
}

これはfirebaseのunityプラグインでも使われていました。

public class AsyncSample : MonoBehaviour
{
    [SerializeField]
    RawImage m_Photo;

    private IEnumerator Start()
    {
        var task = TextureUtility.InvertAsyncByTask();
        yield return new WaitForTaskCompletion(task);
        m_Photo.texture = task.Result;
    }
}

他に参考にさせていただいた記事

tsubakit1.hateblo.jp

github.com

light11.hatenadiary.com