LoginSignup
260

More than 1 year has passed since last update.

UniTask機能紹介

Last updated at Posted at 2018-07-25

UniTaskとは

UniTaskとはUnity向けに最適化されたTask実装を提供するライブラリです。
もとはUniRxに組み込まれていましたが途中で分離し、独立した1つのライブラリとして提供されるようになりました。

もっと詳しい資料

導入

C# 7系の機能に依存しているため、UniTaskを利用する場合はUnity 2018.3以降を推奨します。
導入はGitHubからパッケージをダウンロードし、Unityに取り込むだけでOKです。

UniTaskの機能紹介

Taskより軽量な「UniTask」

UniTaskでは、非同期処理をUniTaskというクラスで扱えるようにしています。
これはTaskをUnityに最適化する形で実装された非同期処理機構です。
(中身はValueTaskに近いです)

Taskの実装を見てもらうとわかると思うのですが、Taskは結構ヘビーな存在です。
しかもしれっとSynchronizationContextに依存してたり、それの有無によって挙動が変わったりと結構作りがヤベーやつでした。

それをよりUnityに最適化する形で再実装したものがUniTaskであり、Taskより軽量になっています。
またSynchronizationContextへの依存もありません。

asyncとUniTask

このUniTaskですが、なんとasyncの戻り値にすることができます。当然、awaitもできます。
つまり、通常のasync/awaitの返り値をTask<T>からUniTask<T>に書き換えるだけで利用することができるようになります。

asyncの戻り値をUniTaskにする
using System.Threading.Tasks;
using UniRx.Async; //必要
using UnityEngine;

public class Sample1 : MonoBehaviour
{
    async void Start()
    {
        // こっちは普通のTaskのawait
        var result = await Work();
        Debug.Log(result);

        // こっちはUniTaskのawait
        var result2 = await Work2();

        // ちゃんとメインスレッドに戻ってくる
        Debug.Log(result2);

        // awaitせずに、非同期処理を投げっぱなしで実行する場合は
        // Forget()をつけるとコンパイラのawaitしろ警告を抑圧できる
        Work2().Forget();
    }

    async Task<string> Work()
    {
        // 適当にマルチスレッド処理
        return await Task.Run(() => "Hello!");
    }

    // 返り値をUniTaskに書き換えただけ
    async UniTask<string> Work2()
    {
        return await Task.Run(() => "Hello!");
    }
}

UniTaskの生成

asyncメソッドを使う以外にも、UniTaskを生成する方法があります。

Func<UniTask>からつくる

UniTaskのコンストラクタはFunc<UniTask>を取るようになっています。
これを用いることで、非同期遅延初期化(AsyncLazy)的なことができるようになります。

UniTaskを使った非同期遅延初期化
using UniRx.Async;
using UnityEngine;

namespace Assets
{
    class ResourceLoadAsyncSample : MonoBehaviour
    {
        /// <summary>
        /// 初回アクセス時のみ非同期的に初期化する
        /// 2度目以降、処理が完了しているならキャッシュを利用する
        /// </summary>
        public UniTask<Texture> PlayerTexture { get; private set; }

        void Awake()
        {
            PlayerTexture = new UniTask<Texture>(() => LoadAsSprite());
        }

        async UniTask<Texture> LoadAsSprite()
        {
            var resource = await Resources.LoadAsync<Texture>("Player");
            return (resource as Texture);
        }
    }
}

UniTaskCompletionSourceからつくる

UniTaskCompletionSourceを使うことで、任意のタイミングで結果を発行するUniTaskを生成できます。
TaskCompletionSource<T>と大体同じです。。Rx的にいうと、Observable.Create<T>()みたいなもんです。

public UniTask<string> WrapMyAsync()
{
    var utcs = new UniTaskCompletionSource<string>();

    // 何かしらの非同期処理
    HogeAsyncAction(result =>
    {
        // 成功
        utcs.TrySetResult(result);
    }, ex =>
    {
        // 失敗
        utcs.TrySetException(ex);
    });

    return utcs.Task; //UniTask<string>を返す
}

/// <summary>
/// 結果をActionで返す非同期処理
/// </summary>
private void HogeAsyncAction(Action<string> onSuccess, Action<Exception> onError)
{
    // Do something.
}

いろんなものをawaitできる

UniRx.Asyncの機能の1つとして、Unityの非同期処理をだいたいawaitすることができるようになっています。

AsyncOperationのawait

ResourceRequestなどの、AsyncOperationを継承したものをawaitすることができます。

ResourceRequestのawait
using UniRx.Async; //必要
using UnityEngine;

public class Sample2 : MonoBehaviour
{
    async void Start()
    {
        var texture = await Resources.LoadAsync<Texture>("Player");

        gameObject.GetComponent<Renderer>().material.mainTexture = texture as Texture;
    }
}
UnityWebRequestAsyncOperationのawait
using UniRx.Async; //必要
using UnityEngine;
using UnityEngine.Networking;

public class Sample3 : MonoBehaviour
{
    async void Start()
    {
        var uri =
            "https://2.bp.blogspot.com/-tcLjNKJqOIQ/WkXHUuSC4qI/AAAAAAABJX0/ArQTS8DS9SEOJI4Mb5tvZg4GXuoED8iIQCLcBGAs/s800/otaku_winter.png";

        //テクスチャをダウンロード
        var texture = await DownloadTexture(uri);

        // テクスチャセット
        gameObject.GetComponent<Renderer>().material.mainTexture = texture;
    }

    // テクスチャをダウンロードする
    async UniTask<Texture> DownloadTexture(string uri)
    {
        // 適当に画像のURL

        var r = UnityWebRequestTexture.GetTexture(uri);

        await r.SendWebRequest(); // UnityWebRequestをawaitできる

        return DownloadHandlerTexture.GetContent(r);
    }
}

また、WWWにも使えます。

WWWのawait
using UniRx.Async; //必要
using UnityEngine;

public class Sample4 : MonoBehaviour
{
    async void Start()
    {
        var uri =
            "https://2.bp.blogspot.com/-tcLjNKJqOIQ/WkXHUuSC4qI/AAAAAAABJX0/ArQTS8DS9SEOJI4Mb5tvZg4GXuoED8iIQCLcBGAs/s800/otaku_winter.png";

        //テクスチャをダウンロード
        var texture = await DownloadTexture(uri);

        // テクスチャセット
        gameObject.GetComponent<Renderer>().material.mainTexture = texture;
    }

    // テクスチャをダウンロードする
    async UniTask<Texture> DownloadTexture(string uri)
    {
        // 適当に画像のURL

        var www = new WWW(uri);

        await www; // WWWをawaitできる

        return www.texture;
    }
}

AsyncOperationのConfigureAwait

AsyncOperationをawaitするときにConfigureAwaitを呼び出すことで、非同期処理の進捗(Progress)を取得することができます。

using System;
using UniRx.Async; //必要
using UnityEngine;
using UnityEngine.Networking;

public class Sample5 : MonoBehaviour
{
    async void Start()
    {
        var request = UnityWebRequest.Get("https://unity3d.com");

        //ConfigureAwaitで途中経過取得
        var result = await request.SendWebRequest().ConfigureAwait(Progress.Create<float>(x => Debug.Log(x)));

        Debug.Log(result.downloadHandler.text);
    }
}

コルーチンのawait

yield return nullのみを使ったコルーチンであれば、直接awaitできます。

詳しくはこちら 【Unity】UniRx.Asyncでコルーチンをawaitするときの注意点

Coroutine
using System.Collections;
using UniRx.Async; //必要
using UnityEngine;

public class Sample6 : MonoBehaviour
{
    async void Start()
    {
        // コルーチン(IEnumerator)をawaitできる
        await MoveCoroutine();

        Debug.Log("Done!");
    }

    //数秒間前に進むコルーチン
    IEnumerator MoveCoroutine()
    {
        var start = Time.time;
        while (Time.time - start < 2)
        {
            transform.position += Vector3.forward * Time.deltaTime;
            yield return null;
        }
    }
}

PlayerLoopに同期する

UniRx.AsyncはUnityのPlayerLoopを内部で利用しており、処理を任意のタイミングから実行させることができるようになっています。
UniTask.Yieldに、指定のPlayerLoopタイムを指定することでタイミングの切り替えが可能です。
コルーチンのWaitForFixedUpdateなどに相当します。

using UniRx.Async; //必要
using UnityEngine;

public class Sample7 : MonoBehaviour
{
    async void Start()
    {
        Debug.Log(Time.time);

        // 次のFixedUpdateのタイミングを待つ
        await UniTask.Yield(PlayerLoopTiming.FixedUpdate);

        //↓↓↓これ以降はFixedUpdateと同期して実行される↓↓↓

        Debug.Log(Time.time);

        // 次のPostLateUpdateを待つ
        await UniTask.Yield(PlayerLoopTiming.PostLateUpdate);

        //↓↓↓これ以降はPostLateUpdate(WaitForEndOfFrame)と同期して実行される↓↓↓

        Debug.Log(Time.time);
    }
}

指定フレーム数待つ

UniTask.DelayFrameを使うと任意のフレーム数待機できます。

using UniRx.Async; //必要
using UnityEngine;

public class Sample8 : MonoBehaviour
{
    async void Start()
    {
        Debug.Log(Time.frameCount);

        // 5フレーム待機
        await UniTask.DelayFrame(5);

        Debug.Log(Time.frameCount);

        // FixedUpdateを5回分待機
        await UniTask.DelayFrame(5, PlayerLoopTiming.FixedUpdate);

        // ↓これ以降はFixedUpdateのタイミングに切り替わる点に注意↓

        Debug.Log("End");
    }
}

Unityコールバックイベントをawaitする

UniRx.Async.Triggersをusingすることで、各種GameObjectのコールバックイベントをUniTaskとして取得することができるようになります。

使い方は、GameObjectまたはComponentに対してGetAsync*Trigger()を呼ぶことで、各種UniTaskを返すAsyncTriggerComponentが取得できます。このコンポーネントから対象のイベントをUniTaskとして取得することができるようになっているため、それawaitすることで、イベント発生を待ち受けることができるようになります。

なお、GetAsync*Trigger()は内部でGetComponentを実行するため、取得したAsyncTriggerComponentはちゃんとキャッシュするようにしておくと良いでしょう。

OnCollisionEnterをawait
using UniRx.Async;
using UniRx.Async.Triggers;
using UnityEngine;

public class AwaitAsyncTrigger : MonoBehaviour
{
    private AsyncCollisionTrigger asyncCollisionTrigger;

    void Start()
    {
        // AsyncTrigger(extends MonoBehaviour)を取得する
        asyncCollisionTrigger = this.GetAsyncCollisionTrigger();

        DoAsync().Forget();
    }

    async UniTask DoAsync()
    {
        Debug.Log("Start");

        // OnCollisionEnterが発生するまで待機する
        var target = await asyncCollisionTrigger.OnCollisionEnterAsync();

        Debug.Log("Hit! " + target.gameObject.name);

        // OnCollisionExitが発生するまで待つ
        await asyncCollisionTrigger.OnCollisionExitAsync();

        Debug.Log("Bye!");
    }
}

UnityEventをawaitする

UnityEventは、UnityEvent.GetAsyncEventHandler()を実行するとAsyncEventHandlerというものを取得できます。
このAsyncEventHandler.OnInvokeAsync()を呼び出すことで、UnityEventのawaitができます。

(一度しかawaitしないなら、UnityEvent.OnInvokeAsync()を直接awaitしても大丈夫です。)

UnityEvent
using UniRx.Async;
using UnityEngine;
using UnityEngine.UI;

public class AwaitUnityEvent : MonoBehaviour
{
    [SerializeField] private Button button;

    void Start()
    {
        DoAsync().Forget();
    }

    async UniTask DoAsync()
    {
        // UnityEventをGetAsyncEventHandlerに変換
        var asyncEventHandler = button.onClick.GetAsyncEventHandler();

        // ボタンが押されるまで待つ
        await asyncEventHandler.OnInvokeAsync();

        Debug.Log("1");

        // ボタンが押されるまで待つ
        await asyncEventHandler.OnInvokeAsync();

        Debug.Log("2");
    }
}

条件を与えてawaitする

UniTask.Wait*シリーズを使うことで、条件を指定してawaitができます。

Predicate
using UniRx.Async;
using UnityEngine;

public class Sample11 : MonoBehaviour
{
    async void Start()
    {
        // 条件を満たしたら待機終了
        await UniTask.WaitUntil(() => transform.position.y < 0);

        // 指定オブジェクトの状態が変化するのを待つ
        await UniTask.WaitUntilValueChanged(transform, x => x.position.x); //x方向に移動した

        // 指定オブジェクトの状態が変化するのを待つ & 対象がDestroyされたら待機終了
        await UniTask.WaitUntilValueChangedWithIsDestroyed(transform, x => x.position.x);

        // 条件を満たさなくなったら待機終了
        await UniTask.WaitWhile(() => transform.position.z < 0);
    }
}

ReactivePropertyをawaitする

ReactivePropertyもawaitできるようになっています。
この場合は「値が変化するまで待機」という挙動になります。

なお、ReactivePropertyは直接awaitした方が軽量かつ安全に機能します。
AsObservableしたり、末尾にオペレータをつなぐとObservableのawaitに切り替わってしまい、直接awaitするのと比べるとパフォーマンスが落ちてしまいます。

using UniRx;
using UniRx.Async;
using UnityEngine;

class AwaitReactiveProperty : MonoBehaviour
{
    [SerializeField] BoolReactiveProperty isDead = new BoolReactiveProperty(false);

    void Start()
    {
        DoAsync().Forget();
    }

    async UniTask DoAsync()
    {
        // 死ぬまで待機
        await isDead;

        Debug.Log("Dead!");
    }

    public void Kill()
    {
        isDead.Value = true;
    }
}

Observableをawaitする

厳密に言うとUniRx.Asyncではなく、UniRx側の機能です。
Observableをawaitで待機できます。

この場合はObseravbleが内部でAsyncSubjectに変換されます。
そのためOnCompletedメッセージが発行されるまでずっとawaitし続けてしまうので注意が必要です。

Observable
using UniRx;
using UnityEngine;

public class Sample12 : MonoBehaviour
{
    async void Start()
    {
        var result = await Observable.Return(1);
        Debug.Log(result);
    }
}

スレッドを切り替える

UniTask.SwitchToThreadPoolまたはUniTask.SwitchToTaskPoolをawaitすることで、以降の処理を別スレッドに切り替えることができます。(ThreadPoolTaskPoolとありますが、ThreadPoolだけ使っていれば問題ないです。Unityの場合はTaskPoolは利用するメリットがありません。)

ChangeThread
using System.Threading;
using UniRx.Async;
using UnityEngine;

public class SampleX : MonoBehaviour
{
    async void Start()
    {
        Debug.Log(Thread.CurrentThread.ManagedThreadId);

        // TaskPoolに切り替え
        await UniTask.SwitchToTaskPool();
        // ↓これ以降はTaskPoolで実行される↓

        Debug.Log(Thread.CurrentThread.ManagedThreadId);

        // メインスレッド(Updateタイミング)に戻す
        await UniTask.Yield();
        // ↓これ以降はメインスレッドで実行される↓

        Debug.Log(Thread.CurrentThread.ManagedThreadId);
    }
}

ObservableとUniTaskの相互変換

Observable.ToUniTask()UniTask.ToObservable()でお互いに相互変換できます。
相互変換できるので、非同期処理はとりあえずasync/awaitで書いておいて、状況に応じてObservableに変換すればOKになります。

ObservableとUniTask
using UniRx;
using UniRx.Async;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class Sample13 : MonoBehaviour
{
    [SerializeField] private Button button;
    [SerializeField] private Text text;

    void Start()
    {
        // ボタンがクリックされたらHTTPで通信する
        // 通信中はボタンを無効化する
        button.BindToOnClick(_ =>
        {
            return GetHtmlAsync().ToObservable()
                .ForEachAsync(x => { text.text = x.Substring(0, 100); });
        });
    }

    // HTTPでHTMLをダウンロードする
    async UniTask<string> GetHtmlAsync()
    {
        var uri = "https://unity3d.com/jp";
        var r = UnityWebRequest.Get(uri);
        var result = await r.SendWebRequest();
        return result.downloadHandler.text;
    }
}

AsyncReactiveCommandと非同期処理をくっつけた例です。

タイムアウト処理

UniTaskにはタイムアウトの機能がついています。
タイムアウトするとTimeoutExceptionがthrowされます。

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

public class Sample14 : MonoBehaviour
{
    async void Start()
    {
        var request = UnityWebRequest.Get("https://unity3d.com");

        // 明示的にUniTaskに変換し、Timeoutをくっつける
        var result = await request.SendWebRequest().ToUniTask().Timeout(TimeSpan.FromSeconds(3));

        Debug.Log(result.downloadHandler.text);
    }
}

並列実行&待機

UniTask.WhenAllを使うと、UniTaskの終了を同時に複数待ち受けることができます。

using UniRx.Async;
using UnityEngine;
using UnityEngine.Networking;

public class Sample15 : MonoBehaviour
{
    async void Start()
    {
        var task1 = GetHtmlAsync("https://unity3d.com/jp");
        var task2 = GetHtmlAsync("https://github.com");
        var task3 = GetHtmlAsync("https://www.yahoo.co.jp");

        // まとめて結果をとれる(便利!)
        var (unity, github, yahoo) = await UniTask.WhenAll(task1, task2, task3);
    }

    // HTTPでHTMLをダウンロードする
    async UniTask<string> GetHtmlAsync(string uri)
    {
        var r = UnityWebRequest.Get(uri);
        var result = await r.SendWebRequest();
        return result.downloadHandler.text;
    }
}

キャンセル処理

UniTaskの実行やawaitをキャンセルしたい場合はCancellationTokenを利用する必要があります。
ぶっちゃけここだけはもとTaskのasync/awaitのころからシンドイ部分ではあります。

キャンセルのやり方は次みたいな感じに。

  1. CancellationTokenSourceを用意する
  2. CancellationTokenSource.TokenCancellationTokenが取得できる
  3. 取得したCancellationTokenを実行するUniTaskに渡していく(シンドイ)
  4. そのとき、定義したasyncメソッド内で逐一キャンセル時の処理を書く(シンドイ)
  5. キャンセルしたくなったタイミングで、CancellationTokenSource.Cancel()を実行する

何度も書きますが、正直めっちゃシンドイです。どうにかならないのかなコレ…。

キャンセル込みの例
using System.Threading;
using UniRx.Async;
using UniRx.Async.Triggers;
using UnityEngine;

public class AwaitAsyncTrigger : MonoBehaviour
{
    private AsyncCollisionTrigger asyncCollisionTrigger;

    private CancellationTokenSource cancellationTokenSource;

    void Start()
    {
        // CancellationTokenSourceを生成
        cancellationTokenSource = new CancellationTokenSource();

        // AsyncTrigger(extends MonoBehaviour)を取得する
        asyncCollisionTrigger = this.GetAsyncCollisionTrigger();

        // 引数にCancellationTokenを渡す
        DoAsync(cancellationTokenSource.Token).Forget();
    }

    void OnDestroy()
    {
        // GameObject破棄時にキャンセル実行
        cancellationTokenSource?.Cancel();
    }

    async UniTask DoAsync(CancellationToken token)
    {
        // OnCollisionEnterが発生するまで待機する
        var (isCanceled, target) = await asyncCollisionTrigger.OnCollisionEnterAsync()
            .WithCancellationWithoutException(token); //tokenでキャンセルできるようにする

        if (isCanceled) return; //Finish

        Debug.Log("Hit! " + target.gameObject.name);

        // OnCollisionExitが発生するまで待つ
        (isCanceled, target) = await asyncCollisionTrigger.OnCollisionExitAsync()
            .WithCancellationWithoutException(token); //tokenでキャンセルできるようにする

        if (isCanceled) return; //Finish

        Debug.Log("Bye! " + target.gameObject.name);
    }
}

キャンセル用の拡張メソッド

さすがにこのままだとツライので、こういう拡張メソッドを生やしておくとちょっとは便利になる…のかな?

拡張メソッド
using System.Threading;
using UniRx.Async;
using UniRx.Async.Triggers;
using UnityEngine;

public static class CancellationTokenSourceExtension
{
    public static void CancelWith(this CancellationTokenSource cts, Component component)
    {
        cts.CancelWith(component.gameObject);
    }

    public static void CancelWith(this CancellationTokenSource cts, GameObject gameObject)
    {
        gameObject.OnDestroyAsync().ContinueWith(() => cts.Cancel()).Forget();
    }
}
使い方
using System.Threading;
using UniRx.Async;
using UniRx.Async.Triggers;
using UnityEngine;

public class AwaitAsyncTrigger : MonoBehaviour
{
    private CancellationTokenSource cancellationTokenSource;

    void Start()
    {
        // CancellationTokenSourceを生成
        cancellationTokenSource = new CancellationTokenSource();
        cancellationTokenSource.CancelWith(this); //このGameObjectが破棄されたらCancel

        // 以下略
    }

UniTaskを使うと何が変わるか

コルーチンをほぼasync/awaitで代用できるようになります。
コルーチンと同等の処理をasync/awaitで記述できるようになる上、コルーチンと違ってUniTask<T>を使えば結果を外に返すことができるようになります。
また、StartCoroutine()GameObjectに依存していたため、MonoBehaviourが無いと実行できないとか、Disableになったらコルーチンごと止まるとかいろいろありましたが、async/await + UniTaskならそういう制約はなくなります。

というわけで、「コルーチンの上位互換」として使える…、かと思いきや途中のキャンセルが結構面倒くさいので一長一短だったりします。 コルーチンの場合はGameObjectDestroyしたら勝手に止まりましたけど、async/awaitの場合は止まりません。
(await対象のオブジェクトがGCで回収されれば勝手にawait処理自体が止まって消え去るので、そこさえGCされてれば最悪大事には至らないかな…?)

Observableとの使い分けですが、非同期処理は基本すべてasync/await+UniTaskで書くことになるかと。
そしてObservableは基本イベント処理(非同期的に複数の値を発行する)への用途がメインになるかと。
とりあえずは思考停止でこの方針で書いてみて、必要に応じて相互変換すればよいかと。

まとめ

すべての機能をまとめきれているわけではないですが、とりあえず目についた部分は書いてみました。
Unityにおける非同期処理がすごいイケてる感じになりそうです!

あ、あと宣伝なのですが、Unity 非同期完全に理解した勉強会を開催します。
大反響で会場のキャパを遥かに超える参加登録が来ており、ちょっとどうするか検討中です。

参考

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

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
260