はなちるのマイノート

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

【Unity】UniTaskを初めて触ってみる

はじめに

最近UniTaskという単語を耳にしますが、async/awaitを使えばいいし学習コストも高そうなのでずっと手を出していませんでした。

ただUniTaskにはWebGLのようなマルチスレッドができない場合にはシングルスレッドにしてくれる機能があるらしいです。

実は以前既存のパソコンのゲームをWebGLに移植してほしいとお願いされ、めちゃくちゃ苦労した覚えがあります。(もはや断念しましたが・・・)

加えて以下の記事はUniRXUniTaskの作者さんのブログの記事なのですが、結構面白いことが書かれていました。
neue.cc

Rx vs Coroutine vs async/await

もう結論が出ていて、async/await一本でOK、です。まずRxには複数の側面があって、代表的にはイベントと非同期。そのうち非同期はasync/awaitのほうがハンドリングが用意です。そしてコルーチンによるフレームベースの処理に関してはUniTask.DelayやYieldが解決しました。ので、コルーチン→出番減る, async/await → 非同期, Rx → イベント処理 というように分離されていくと思われます。

neue cc - UniTask - Unity + async/awaitの完全でハイパフォーマンスな統合


私はまだまだ初心者なので深くは分かりませんが、もうこの文章だけでUniTaskを学ぶ意欲がみるみる上がってきました。

非同期はasync/await,イベントはRx・・・覚えておこうと思います。

というわけで実際に触っていきましょう。

環境

Unity2019.3.0f6
UniTask v1.2.0

導入方法

GitHubからUniRx.Async.unitypackageをダウンロードしてUnityにドラッグ&ドロップします。

github.com

名前がUniRx.Asyncとなっているのは、以前UniRxの一部として公開されていた名残らしいです。

コルーチンを置き換える

冒頭でも紹介したように、コルーチンはもうUniTaskに置き換えた方が良さそうです。

昔作ったネットから画像を拾ってくる処理の比較をしてみたと思います。
【Unity】ネット上から画像を取得して表示してみる - はなちるのマイノート


まずはコルーチンを使った例

using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class CoroutineTest : MonoBehaviour
{
    private const string URI = "https://4.bp.blogspot.com/-4xxTe_qeV1E/Vd7FkNUlwjI/AAAAAAAAxFc/8u9MNKtg7gg/s800/syachiku.png";

    private void Start()
    {
        var image = GetComponent<RawImage>();
        StartCoroutine(GetTexture2D(t => image.texture = t));
    }

    private IEnumerator GetTexture2D(Action<Texture2D> onResult)
    {
        UnityWebRequest www = UnityWebRequestTexture.GetTexture(URI);

        //画像を取得できるまで待つ
        yield return www.SendWebRequest();

        if (www.isNetworkError || www.isHttpError)
        {
            Debug.Log(www.error);
        }
        else
        {
            var texture = ((DownloadHandlerTexture)www.downloadHandler).texture;
            // 結果はコールバックで取得する
            onResult(texture);
        }
    }
}


これをUniTaskに入れ替えた例

using System;
using UniRx.Async;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class CoroutineTest : MonoBehaviour
{
    private const string URI = "https://4.bp.blogspot.com/-4xxTe_qeV1E/Vd7FkNUlwjI/AAAAAAAAxFc/8u9MNKtg7gg/s800/syachiku.png";

    private async void Start()
    {
        var texture = await GetImageAsync();
        GetComponent<RawImage>().texture = texture;
    }

    private async UniTask<Texture2D> GetImageAsync()
    {
        UnityWebRequest www = UnityWebRequestTexture.GetTexture(URI);
        await www.SendWebRequest();
        if (www.isNetworkError || www.isHttpError) throw new Exception(www.error);
        return ((DownloadHandlerTexture)www.downloadHandler).texture;
    }
}

コルーチンを待つ

もはやUniTaskが提供するAwaiterによりコルーチンをawaitすることもできます。

using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using UniRx.Async;

public class CoroutineTest : MonoBehaviour
{
    private const string URI = "https://4.bp.blogspot.com/-4xxTe_qeV1E/Vd7FkNUlwjI/AAAAAAAAxFc/8u9MNKtg7gg/s800/syachiku.png";

    private async void Start()
    {
        var image = GetComponent<RawImage>();
        await GetTexture2D(t => image.texture = t);
        Debug.Log("取得完了");
    }

    private IEnumerator GetTexture2D(Action<Texture2D> onResult)
    {
        UnityWebRequest www = UnityWebRequestTexture.GetTexture(URI);

        //画像を取得できるまで待つ
        yield return www.SendWebRequest();

        if (www.isNetworkError || www.isHttpError)
        {
            Debug.Log(www.error);
        }
        else
        {
            var texture = ((DownloadHandlerTexture)www.downloadHandler).texture;
            // 結果はコールバックで取得する
            onResult(texture);
        }
    }
}

よく使いそうなstaticメソッド

コルーチンをUniTaskに変換するにあたって、以下のstaticメソッドは重宝しそうなものをまとめてみました。

using System;
using UniRx.Async;
using UniRx.Async.Triggers;
using UnityEngine;

public class Test : MonoBehaviour
{

    private void Start()
    {
        _ = Hoge();
       Debug.Log("先に実行される");
    }

    private async UniTaskVoid Hoge()
    {
        // 1秒待つ
        await UniTask.Delay(TimeSpan.FromSeconds(1));

        // 1フレーム待つ
        await UniTask.DelayFrame(1);

        // FixedUpdateで1フレーム待つ
        await UniTask.DelayFrame(1, PlayerLoopTiming.FixedUpdate);

        // 1フレーム待ってUpdate()のタイミングまで待機
        await UniTask.Yield();

        // 次のFixedUpdate()のタイミングまで待機
        await UniTask.Yield(PlayerLoopTiming.FixedUpdate);

        var flag = true;

        // 条件がtrueになるまで待つ
        await UniTask.WaitUntil(() => flag);

        flag = false;

        // 条件がfalseになるまで待つ
        await UniTask.WaitWhile(() => flag);

        // Awakeが終わるまで待つ
        await gameObject.AwakeAsync();

        // Startが終わるまで待つ
        await gameObject.StartAsync();
        
        Debug.Log("いっぱい待っているので後で実行される");
    }
}


またfire and forget「呼ぶだけ呼んであとは放置」といった使い方の場合はasync voidを使います。

ただしこれではC#標準のTaskでも使えてしまうので、UniTask専用のasync UniTaskVoidを使うとなお良いでしょう。

マルチスレッド化

次はマルチスレッド化をしてみましょう。

ここは結構感動するかも。

using UniRx.Async;
using UnityEngine;

public class CoroutineTest : MonoBehaviour
{

    private async void Start()
    {
        await Hoge();
    }

    private async UniTask Hoge()
    {
        // メインスレッドのスレッドidを調べる
        int id = System.Threading.Thread.CurrentThread.ManagedThreadId;
        Debug.Log("ThreadID : " + id);

        // これ移行はスレッドプールで実行させるというマルチスレッド化
        await UniTask.SwitchToThreadPool();

        // スレッドidを調べる
        id = System.Threading.Thread.CurrentThread.ManagedThreadId;
        Debug.Log("ThreadID : " + id);

        // これ移行はメインスレッド上で実行される 
        await UniTask.SwitchToMainThread();

        id = System.Threading.Thread.CurrentThread.ManagedThreadId;
        Debug.Log("ThreadID : " + id);

        // もう一回別スレッドに
        await UniTask.SwitchToThreadPool();

        id = System.Threading.Thread.CurrentThread.ManagedThreadId;
        Debug.Log("ThreadID : " + id);

        // これもメインスレッドに戻るが、1フレーム待つ
        await UniTask.Yield();

        id = System.Threading.Thread.CurrentThread.ManagedThreadId;
        Debug.Log("ThreadID : " + id);
    }
}

f:id:hanaaaaaachiru:20200224000640p:plain


またTask.Runと同様にUniTask.Runを使うことでも別スレッドで実行することができます。

using UniRx.Async;
using UnityEngine;

public class Test : MonoBehaviour
{

    private void Start()
    {
        _ = UniTask.Run(() => SomeMethod());
    }

    private async UniTaskVoid SomeMethod()
    {
        await UniTask.Delay(10);
    }
}


ただしUnityAPIはメインスレッド上でしか動かないなどの制約があるので、メインスレッドに戻したいときはSynchronizationContextを使わなければならなかったりと、Task.Run特有の地獄が脳裏をよぎります。

上手に使いこなせないなら、上の方法で別スレッドを使ったほうが良いかもしれません。

さいごに

これまで紹介してきたものだけでも結構すごさがありますよね。

これからは重宝しそうな予感がします。

さらにまだまだ紹介しきれていない機能もたくさんあるので、是非参考に貼ったサイトをみてみると面白いと思います。

また今回触れていませんが、非同期処理のキャンセルはかなり大事なテーマなのでそちらも参照してみてください。

ではまた。