UniTask機能紹介

# UniTaskとは

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

# もっと詳しい資料

* [UniTask入門](https://learning.unity3d.jp/2974/)

# 導入

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

* [UniTask](https://github.com/Cysharp/UniTask/releases)


# UniTaskの機能紹介

## Taskより軽量な「UniTask」

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

[Taskの実装](https://referencesource.microsoft.com/#mscorlib/system/threading/tasks/Task.cs)を見てもらうとわかると思うのですが、Taskは結構ヘビーな存在です。
しかもしれっと`SynchronizationContext`に依存してたり、それの有無によって挙動が変わったりと結構作りがヤベーやつでした。

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

## asyncとUniTask

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

```cs: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<T>](https://blogs.msdn.microsoft.com/pfxteam/2011/01/15/asynclazyt/))的なことができるようになります。

```cs: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>()`みたいなもんです。

```cs
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することができます。

```cs: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;
    }
}
```

```cs: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`にも使えます。

```cs: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)を取得することができます。

```cs:
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するときの注意点](https://qiita.com/toRisouP/items/5dd497723ac7c2ffb822)

```cs: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`などに相当します。

```cs
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`を使うと任意のフレーム数待機できます。

```cs
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`はちゃんとキャッシュするようにしておくと良いでしょう。

```cs: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しても大丈夫です。）

```cs: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ができます。

```cs: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するのと比べるとパフォーマンスが落ちてしまいます。

```cs
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し続けてしまうので注意が必要です。

```cs: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することで、以降の処理を別スレッドに切り替えることができます。（`ThreadPool`と`TaskPool`とありますが、`ThreadPool`だけ使っていれば問題ないです。Unityの場合は`TaskPool`は利用するメリットがありません。）

```cs: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になります。

```cs: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](https://qiita.com/toRisouP/items/c6fba9f01e6d15dabd79)と非同期処理をくっつけた例です。

## タイムアウト処理

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

```cs: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`の終了を同時に複数待ち受けることができます。

```cs
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.Token`で`CancellationToken`が取得できる
 3. 取得した`CancellationToken`を実行する`UniTask`に渡していく（シンドイ）
 4. そのとき、定義したasyncメソッド内で逐一キャンセル時の処理を書く（シンドイ）
 5. キャンセルしたくなったタイミングで、`CancellationTokenSource.Cancel()`を実行する

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

```cs:キャンセル込みの例
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);
    }
}
```

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

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

```cs:拡張メソッド
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();
    }
}
```

```cs:使い方
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`ならそういう制約はなくなります。

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

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

# まとめ

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

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

# 参考

[UniTask - Unity + async/awaitの完全でハイパフォーマンスな統合](http://neue.cc/2018/07/12_567.html)