LoginSignup
150
102

More than 1 year has passed since last update.

UniTask Ver2 、UniTaskAsyncEnumerableまとめ

Last updated at Posted at 2020-05-19

他の資料

C# Tokyo オンライン「Unity 祭り」で登壇した資料も追加しておきます。あわせて御覧ください。

UniTask2

UniTaskが大きくアップデートされて、Ver2となりました!
今回はその変更点についてまとめてみました。

Ver.2.0.25準拠で書いています。

破壊的変更

まずは1系からの破壊的変更。導入時に気をつける必要があるところです。

必要な最低Unityバージョン

2018.4.13f1 が最低サポートバージョンとなりました。

名前空間の変更

名前空間がUniRx.AsyncからCysharp.Threading.Tasksに変更されました。
もともとUniRxのライブラリの一部だった名残なのですが、今回のアップデートで完全に名残がなくなりました。

UniTaskのawait2度漬け禁止

同じUniTaskに対しての2回以上のawaitが明確に禁止となりました。
IValueTaskSource準拠の挙動なので、これは本家ValueTaskと同じ挙動です。

UniTaskの2回以上のawait禁止
private async UniTaskVoid DoAsync(CancellationToken token)
{
    try
    {
        var uniTask = GetAsync("https://unity.com/ja", token);

        // 1回目のawaitは問題ない
        await uniTask;

        // 同じオブジェクトに対して2回以上のawaitはできない
        // (InvalidOperationExceptionが発行される)
        await uniTask;
    }
    catch (InvalidOperationException e)
    {
        Debug.LogException(e);
    }
}

private async UniTask<string> GetAsync(string uri, CancellationToken token)
{
    var uwr = UnityWebRequest.Get(uri);
    await uwr.SendWebRequest().WithCancellation(token);
    return uwr.downloadHandler.text;
}

Preserve()

もし同じUniTaskを2回以上awaitする必要があるならば、Preserve()を利用しましょう。

Preserve()
private async UniTaskVoid DoAsync(CancellationToken token)
{
    try
    {
        var uniTask = GetAsync("https://unity.com/ja", token);

        // Preserve()で何回でもawait可能なUniTaskに変換
        var reusable = uniTask.Preserve();

        await reusable;
        await reusable;
    }
    catch (InvalidOperationException e)
    {
        Debug.LogException(e);
    }
}

AsyncOperationawaitするときのConfigureAwait廃止

AsyncOperationawaitするときに利用できていたConfigureAwaitが廃止となりました。

IProgress<float>CancellationTokenを指定したい場合はToUniTask()またはWithCancellation()を利用する必要があります。

private async UniTask<string> GetAsync(string uri, CancellationToken token)
{
    using (var uwr = UnityWebRequest.Get(uri))
    {
        // CancellationTokenを指定
        await uwr.SendWebRequest().WithCancellation(token);
        return uwr.downloadHandler.text;
    }
}

// IProgress<float>を指定したい場合(あとついでにCancellationToken)
var urw2 = UnityWebRequest.Get("https://unity.com/ja");
await urw2.SendWebRequest()
    .ToUniTask(
        Progress.Create<float>(x => Debug.Log(x)),
        cancellationToken: token);

UniTaskの一部コンストラクタ廃止

UniTaskの一部コンストラクタが廃止されました。
こちらはUniTaskの起動を遅延実行するために利用できたコンストラクタです。
UniTask.Lazyに相当していた)

廃止されたコンストラクタ
public UniTask(Func<UniTask<T>> factory)

これ相当のファクトリメソッドとして、UniTask.DeferUniTask.Lazyがあるのでこちらを使いましょう。

UniTask.Lazyの戻り値の変更

上記の多重await禁止の変更をうけて、UniTask.Lazyの戻り値がAsyncLazyに変換されました。

public static AsyncLazy Lazy(Func<UniTask> factory)
public static AsyncLazy<T> Lazy<T>(Func<UniTask<T>> factory)

AsyncLazyawaitableなので、そのままawaitすることができます。
もしUniTaskに変換したい場合はAsyncLazy.Taskを使いましょう。

var asyncLazy = UniTask.Lazy(Factory);
await asyncLazy; // 直接待てる
await asyncLazy.Task; // UniTaskに変換

// UniTask.Lazyの結果は何回awaitしてもOK

UniTask.WhenAnyの戻り値変更

WhenAnyの戻り値も変更されました。

public static async UniTask<(int winArgumentIndex, (bool hasResult, T0 result0), (bool hasResult, T1 result1), (bool asResult, T2 result2))> 
WhenAny<T0, T1, T2>()
public static UniTask<(int winArgumentIndex, T1 result1, T2 result2, T3 result3)> WhenAny<T1, T2, T3>()

UniTask.DelayFrameの戻り値変更

UniTask.DelayFrameの戻り値がUniTask<int>からUniTaskに変更されました。

IObservable.ToUniTask()の引数の順序が変更

CancellationToken, bool useFirstValue の順序が bool useFirstValue, CancellationTokenと逆になりました。

public static UniTask<T> ToUniTask<T>(this IObservable<T> source, CancellationToken cancellationToken = default(CancellationToken), bool useFirstValue = false)
public static UniTask<T> ToUniTask<T>(this IObservable<T> source, bool useFirstValue = false, CancellationToken cancellationToken = default)

UniTask.Result/IsCompletedが削除

UniTaskから同期的に結果を取得するResultプロパティが削除されました。
あと完了したかを表すIsCompletedも削除されました。

もし同等の機能が必要な場合はGetAwaiter()を経由することで一応取得はできます。

// uniTask.Result相当
var result = uniTask.GetAwaiter().GetResult();

// IsCompleted相当
var isCompleted = uniTask.GetAwaiter().IsCompleted;

AwaiterStatus -> UniTaskStatus

UniTaskの状態を表すenumの名前がAwaiterStatusからUniTaskStatusに変わりました。

既存機能への追加機能とか

全体的なパフォーマンスの向上

全体的にパフォーマンスが向上しています。
内部でUniTaskAsyncMethodBuilderRunnerの再利用が自動的に行われるようになり、ゼロアロケーションで動作するようになりました。

UniTaskCompletionSource

UniTaskCompletionSourceReset()メソッドが追加されました。
こちらを利用することでUniTaskCompletionSourceを再利用することができるようになりました。

UniTaskCompletionSource.Reset()2.0.19で廃止されました。
かわりにUniTaskCompletionSourceの挙動がTaskCompletionSourceに揃えられました。

すなわちUniTaskCompletionSourceから生成したUniTaskは何回でもawaitができるという仕様に変更されました。
UniTaskCompletionSourceをフィールドに定義して、そこから生成したUniTaskをプロパティとして公開しても問題なくなった)

AutoResetUniTaskCompletionSource追加

AutoResetUniTaskCompletionSourceも追加されました。
こちらはUniTaskCompletionSourceと破棄したときに内部のインスタンスが自動的に再利用される仕組みになっています。

なお、こちらから生成したUniTaskは1回しかawaitできません。
メソッド内でUniTaskを生成する、みたいな局所的な場所でUniTaskCompletionSourceを生成してすぐ使い捨てるみたいな場合はこちらを使ったほうがよいでしょう。

追記

AutoResetUniTaskCompletionSource で生成したインスタンスについては、2回TrySet~メソッドを呼び出すと状態が壊れ、次に生成したインスタンスの挙動がおかしくなってしまいます。

参考: UniTask2のAutoResetUniTaskCompletionSourceでは2回TrySetしてはいけない

ファクトリメソッドが追加

ファクトリメソッドがいくつか追加されました。

  • UniTask.Create
  • UniTask.Defer
  • UniTask.Action
  • UniTask.UnityAction
  • UniTask.WaitUntilCanceled
  • UniTask.NextFrame
  • UniTask.WaitForEndOfFrame
  • UniTask.WaitForFixedUpdate

UniTask.Create, UniTask.Defer, UniTask.Delay

UniTaskをデリゲートから生成するファクトリメソッドとして、次の3つがあります。

  • UniTask.Create
  • UniTask.Defer
  • UniTask.Lazy
// 引数が同じで挙動も似てる3つのファクトリメソッド
public static UniTask<T> Create<T>(Func<UniTask<T>> factory)
public static UniTask<T> Defer<T>(Func<UniTask<T>> factory)
public static AsyncLazy<T> Lazy<T>(Func<UniTask<T>> factory)

これら3つは引数がどれも同じですが、微妙に挙動に違いがあります。

名前 効果 備考
UniTask.Create メソッドが呼び出されたタイミングで新しいUniTask即座に生成する Create()を呼んだ瞬間にUniTaskが起動する。
UniTask.Defer awaitされるタイミングまでUniTaskの生成を遅延させる 1回しかawaitできない代わりにLazyより軽量。
UniTask.Lazy 最初にawaitされるタイミングまでUniTaskの生成を遅延させる 生成されるものはAsyncLazyAsyncLazy.Taskは何回でもawaitできる。Deferよりはコストが大きい。

DeferLazyの違いは直接UniTaskを生成するか、AsyncLazyを生成するかの違いです。

Deferから生成されるUniTaskは2回awaitできません。
Lazyから生成されるUniTask(AsyncLazy.UniTask)は何回でもawaitが可能です。

Lazyと比べてDeferの方がより軽量になっています。
そのためawait回数が1度のみならDeferを使いましょう。

一方で何度もawaitする必要がある場合はLazyを使いましょう。

PlayerLoopTiming追加

PlayerLoopTimingが追加され、次の種類となりました。

  • Initialization
  • LastInitialization
  • EarlyUpdate
  • LastEarlyUpdate
  • FixedUpdate
  • LastFixedUpdate
  • PreUpdate
  • LastPreUpdate
  • Update
  • LastUpdate
  • PreLateUpdate
  • LastPreLateUpdate
  • PostLateUpdate
  • LastPostLateUpdate

とくにLastPostLateUpdateが重要で、こちらはコルーチンにおけるWaitForEndOfFrameに相当します。
いままでUniTaskではyield return new WaitForEndOfFrame()ができなかったのですが、今回のアップデートから可能となりました。

JobHandle.WaitAsync

JobSystemJobHandleWaitAsyncが追加されました。
こちらを利用することで、任意のPlayerLoopのタイミングに切り替えてから完了待ちができます。

// WaitForEndOfFrame相当の待機をしてからComplete()
await jobHandle.WaitAsync(PlayerLoopTiming.LastPostLateUpdate);

AsyncTriggerメソッドのIUniTaskAsyncEnumerable対応

AsyncTrigger系のメソッド(this.GetAsyncCollisionEnterTriggerとか)がIUniTaskAsyncEnumerableに対応しました。

CancellationToken.WaitUntilCanceled

CancellationToken.WaitUntilCanceledという拡張メソッドが追加されました。
これを用いると、CancellationTokenが完了するのをawaitできるようになります。

private void Start()
{
    var token = this.GetCancellationTokenOnDestroy();
    WaitForCanceledAsync(token).Forget();
}

private async UniTaskVoid WaitForCanceledAsync(CancellationToken token)
{
    await token.WaitUntilCanceled();
    Debug.Log("Canceled!");
}

CancellationToken.ToUniTask

同様に、CancellationToken.ToUniTaskUniTaskを生成することもできます。
このUniTaskCancellationTokenがキャンセルされると正常終了(Succeeded)状態となります。

DoTweenのawaitに対応

DoTweenawaitにも対応しました。

ただし動作させるためには次の設定が必要です。

  • OpenUPMからDOTweenを導入する、またはScripting Define Symbolsに「UNITASK_DOTWEEN_SUPPORT」を定義する
  • asmdef「UniTask.DOTweeen」への参照を追加する
private void Start()
{
    MoveAsync().Forget();
}

private async UniTaskVoid MoveAsync()
{
    // 直列実行
    await transform.DOMove(transform.position + Vector3.up, 1.0f);
    await transform.DOScale(Vector3.one * 2.0f, 1.0f);

    // UniTask.WhenAllで並行実行して終了待機
    await
    (
        transform.DOMove(Vector3.zero, 1.0f).ToUniTask(),
        transform.DOScale(Vector3.one, 1.0f).ToUniTask()
    );
}

Addressable Asset Systemのawaitに対応

AsyncOperationHandleawaitも可能になりました。

using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.UI;

public class AsyncOperationHandleAwaitSample : MonoBehaviour
{
    /// <summary>
    /// 読み込む対象のAssetReference
    /// </summary>
    [SerializeField] AssetReference _target;

    [SerializeField] private RawImage _image;

    private void Start()
    {
        var token = this.GetCancellationTokenOnDestroy();
        InitializeAsync(_target, token).Forget();
    }

    private async UniTaskVoid InitializeAsync(
        AssetReference target,
        CancellationToken token)
    {
        // Addressables.LoadAssetAsyncをawaitで待ち受ける
        var texture = await Addressables.LoadAssetAsync<Texture>(target)
            .WithCancellation(token);

        _image.texture = texture;
    }
}

IDisposable.AddTo(CancellationToken)

UniRxで便利だったAddToCancellationToken版も追加されました。

新機能

UniTaskAsyncEnumerable/IUniTaskAsyncEnumerable

UniTaskAsyncEnumerableUniTask2の目玉機能です。

IUniTaskAsyncEnumerable<T>C# 8.0IAsyncEnumerable<T>UniTaskとして実装したものです。
なんとこちらはC# 7.x系のUnityでも利用可能になっています。

何をするためのものかというと、「非同期処理を複数個まとめて扱う」ことができるようになる機能です。
ObservableIObservable<T>)と似ていますが、こちらはPull型として機能するという違いがあります。

image.png

Observableとの使い分け

ObservablePush型なのに対して、UniTaskAsyncEnumerablePull型です。
そのためUniTaskAsyncEnumerableでは非同期処理の実行タイミングを受信側でコントロールできるというメリットがあります。

UniTaskAsyncEnumerableで複数の非同期処理を扱う
private async void Start()
{
    var token = this.GetCancellationTokenOnDestroy();

    var uris = new[]
    {
        "https://www.google.com/",
        "https://unity.com/ja",
        "https://github.com/"
    };


    // URIのリストに対してアクセスしてデータをとってくる
    // ただし常に実行される通信は同時に1つであり、
    // 前回のものが完了しないと次の通信に進まない
    await uris.ToUniTaskAsyncEnumerable()
        // 通信完了を待機してメッセージを発行する
        .SelectAwait(async x => await FetchAsync(x))
        .ForEachAsync(x => Debug.Log(x), token);
}


private async UniTask<string> FetchAsync(string uri)
{
    using (var uwr = UnityWebRequest.Get(uri))
    {
        await uwr.SendWebRequest();
        if (uwr.isNetworkError || uwr.isHttpError)
        {
            throw new Exception($"Error>{uwr.error}");
        }

        return uwr.downloadHandler.text;
    }
}

対するObservablePush型のため、多数のObserverに対してメッセージをブロードキャストするのに向いています。

複数個の非同期処理を管理する場合は基本にはUniTaskAsyncEnumerableを使う。
イベント駆動を制御する場合にはObservableを使う。

という使い方をするとよいでしょう。

UniTaskAsyncEnumerableの消費

ForEachAsync

C# 8.0であればawait foreachが使えるのですが、それが使えないUnityバージョンでは代替としてForEachAsyncを利用します。
感覚としては、IObservable<T>に対するSubscribe()に似ています。
ですがこちらは「ForEachAsync()の完了をさらにawaitで待つ」といったことが可能となります。

ForEachAsync
private async void Start()
{
    var token = this.GetCancellationTokenOnDestroy();

    // EveryUpdate()は毎フレームのタイミングで完了するUniTaskを返す
    await UniTaskAsyncEnumerable.EveryUpdate()
        .Select((_, x) => x)
        // 5回まで実行する
        .Take(5)
        // ForEachAsyncで待機する
        .ForEachAsync(_ => Debug.Log(Time.frameCount), token);

    Debug.Log("Done!");
}

ForEachAwaitAsync

また、ForEachAsyncの他にForEachAwaitAsyncもあります。
こちらはデリゲートの内部でasync/awaitが利用でき、この非同期処理が完了するまで次のメッセージを取りに行きません。

ForEachAwaitAsync
await UniTaskAsyncEnumerable.EveryUpdate()
    .Select((_, x) => x)
    // 5回まで実行する
    .Take(5)
    // ForEachAwaitAsyncで待機する
    .ForEachAwaitAsync(async _ =>
    {
        // 10フレーム待ってから次のメッセージを取りに行く
        await UniTask.DelayFrame(10, cancellationToken: token);
    }, token);

Subscribe

また、Subscribeというメソッドも用意されています。
こちらはIObservable.Subscribe()と似た感覚で利用できます。

なお、こちらはデリゲート内部でasync/awaitが利用できますが、その結果を待機しません(Forgetする)

var token = this.GetCancellationTokenOnDestroy();
UniTaskAsyncEnumerable.EveryUpdate()
    .Select((_, x) => x)
    .Take(5)
    .Subscribe(async (_, ct) =>
    {
        await UniTask.Delay(100, cancellationToken: ct);
        Debug.Log("Do!");
    }, token);

ForEachAsync/ForEachAwaitAsync/Subscribeの違い

それぞれの大きな違いは登録したデリゲートの呼び出し方にあります。
ForEachAwaitAsyncSubscribeは非同期処理を用いたときに挙動が異なるので注意が必要です。

ForEachAsync
while (await e.MoveNextAsync())
{
    action(e.Current); // 常に同期
}
ForEachAwaitAsync
while (await e.MoveNextAsync())
{
    await action(e.Current); // awaitしてから次へいく
}
Subscribe
while (await e.MoveNextAsync())
{
    action(e.Current).Forget(); // Forget()して次へいく
}

UniTaskAsyncEnumerableの作り方

UniTaskAsyncEnumerableはさまざまな方法で生成することができます。

ファクトリメソッド

  • Return
  • Repeat
  • Empty
  • Throw
  • Never
  • Range
  • EveryUpdate
  • Timer
  • TimerFrame
  • Interval
  • IntervalFrame
  • EveryValueChanged
UniTaskAsyncEnumerable.Create

C#8の非同期イテレータに相当するものとして、UniTaskAsyncEnumerable.Createがあります。

// 整数のカウントダウン
private IUniTaskAsyncEnumerable<int> CountDownAsync(int startCount, TimeSpan timeSpan)
{
    return UniTaskAsyncEnumerable.Create<int>(async (writer, token) =>
    {
        var currentCount = startCount;
        while (currentCount >= 0)
        {
            // writer.YieldAsync を使うと UniTaskAsyncEnumerable に値が書き込まれる
            await writer.YieldAsync(currentCount--);
            await UniTask.Delay(timeSpan, cancellationToken: token);
        }
    });
}

他のデータ構造から変換

  • IEnumerable<T>
  • IObservable<T>
  • Task<T>
  • UniTaskT>

uGUIコンポーネントから変換

uGUIイベントからの変換
public class FromUGui : MonoBehaviour
{
    [SerializeField] private Button _button;

    private void Start()
    {
        var token = this.GetCancellationTokenOnDestroy();

        // 連打防止ボタン
        // 1回ボタンを押したら2秒間無反応になる
        _button.OnClickAsAsyncEnumerable()
            .ForEachAwaitWithCancellationAsync(async (_, ct) =>
            {
                Debug.Log("Clicked!");
                await UniTask.Delay(2000, cancellationToken: ct);
            }, token);
    }
}

AsyncTriggerを利用する

this.GetAsyncCollisionEnterTrigger()など。

Channel

ChannelGoにおけるChannelと同義です。
挙動としてはObservableにおけるSubjectに相当します。

実装としてはCreateSingleConsumerUnboundedが用意されています。
CreateSingleConsumerUnboundedは内部でメッセージをキューイングしているため、メッセージの取りこぼしは発生しません。

Channelの例
 private void Start()
{
    // Channel作成
    var channel = Channel.CreateSingleConsumerUnbounded<int>();

    // Channelを読み取るときはReaderを使う
    var reader = channel.Reader;

    // メッセージの待受
    WaitForChannelAsync(reader, this.GetCancellationTokenOnDestroy()).Forget();

    // 書き込むときはWriteを使う
    var writer = channel.Writer;

    // IObserver<T>.OnNext() に相当
    writer.TryWrite(1);
    writer.TryWrite(2);
    writer.TryWrite(3);

    // IObserver<T>.OnCompleted() に相当
    writer.TryComplete();

    // TryComplete()に例外を渡すと IObserver<T>.OnError() に相当
    // writer.TryComplete(new Exception(""));
}

private async UniTaskVoid WaitForChannelAsync(ChannelReader<int> reader, CancellationToken token)
{
    try
    {
        // 1回だけ読み取るならReadAsync
        var result1 = await reader.ReadAsync(token); // UniTask<int>
        Debug.Log(result1);

        // 完了するまで読み続けるなら ReadAllAsync
        // IObservable<T>.Subscribe() に相当
        await reader.ReadAllAsync() // IUniTaskAsyncEnumerable<int>
            .ForEachAsync(x => Debug.Log(x), token);

        Debug.Log("Done");
    }
    catch (Exception e)
    {
        Debug.LogException(e);
    }
}

なお、CreateSingleConsumerUnboundedは同時に1箇所でしかメッセージの消費が行なえません。
2箇所以上でForEachAsync等を行った場合は正しく動作しません。

もし2箇所以上で消費したい(値を分配したい)場合はPublish()を利用しましょう。

private void Start()
{
    // Channel作成
    var channel = Channel.CreateSingleConsumerUnbounded<int>();

    // ReadAsync + Publish で何回も待受可能になる
    var connectable = channel.Reader.ReadAllAsync().Publish();

    using (connectable.Connect())
    {
        // 複数回待受
        WaitForChannelAsync(connectable, this.GetCancellationTokenOnDestroy()).Forget();
        WaitForChannelAsync(connectable, this.GetCancellationTokenOnDestroy()).Forget();
        WaitForChannelAsync(connectable, this.GetCancellationTokenOnDestroy()).Forget();

        var writer = channel.Writer;

        writer.TryWrite(1);
        writer.TryWrite(2);
        writer.TryWrite(3);
        writer.Complete();
    }
}

AsyncReactiveProperty

AsyncReactivePropertyUniRxReactivePropertyUniTask版です。
ベースとしてIUniTaskAsyncEnumerable<T>が利用されています。
ReactivePropertyはベースがIObservable<T>

基本的な使い方はReactivePropertyと変わりません。

AsyncReactiveProperty
private void Start()
{
    var token = this.GetCancellationTokenOnDestroy();

    // AsyncReactiveProperty生成
    var asyncReactiveProperty = new AsyncReactiveProperty<string>(null);

    // 待受開始
    WaitForAsync(asyncReactiveProperty, token).Forget();

    // Valueプロパティに値をセットすると
    // MoveNextAsync() が次に進む
    asyncReactiveProperty.Value = "Hello!";

    // Dispose()するとこのAsyncReactivePropertyが完了する
    asyncReactiveProperty.Dispose();
}

private async UniTaskVoid WaitForAsync(
    IReadOnlyAsyncReactiveProperty<string> asyncReadOnlyReactiveProperty,
    CancellationToken token)
{
    // Valueプロパティで現在値を取得可能
    var current = asyncReadOnlyReactiveProperty.Value;
    Debug.Log(current);

    // IUniTaskAsyncEnumerable<T>として扱える
    var result = await asyncReadOnlyReactiveProperty
        // null以外の値がセットされるのを待つ
        .FirstOrDefaultAsync(x => x != null, cancellationToken: token);

    Debug.Log(result);
}

UniRx.ReactivePropertyとAsyncReactivePropertyの使い分け

結論から先にいうと次になります。

  • イベントメッセージの通知として使う方が多いなら UniRx.ReactiveProperty を使う
  • awaitで変化を待ち受けることの方が多いなら AsyncReactiveProperty を使う

UniRx.ReactivePropertyAsyncReactivePropertyはそれぞれ次の点が異なります。

UniRx.ReactiveProperty<T> AsyncReactiveProperty<T>
ベースの型 IObservable<T> IUniTaskAsyncEnumerable<T>
ふるまい Push Pull
直接awaitした時の挙動 次の値に更新されるまで待つ(現在値は無視) WaitAsyncまたはUniTask.Linqを経由する必要あり
Operator/LINQをつないだときの挙動 IObservable<T>扱いになるので、ToUniTask()などが必要 LINQの挙動による
得意なこと メッセージのブロードキャスト 条件を指定しての柔軟なawait

UniRx.ReactiveProperty<T>は直接awaitできますが、そのときの挙動がコントロールできません。
たとえば「特定のパラメータに変化するまで待つ」といったことがしたい場合、Operatorを経由する必要があります。

ただOperatorを使ってしまうとIObservable<T>に対するawaitになるため、取り扱いが面倒くさいことになります。

UniRx.ReactivePropertyのawait
private async UniTaskVoid WaitForPropertyAsync(ReactiveProperty<int> p, CancellationToken ct)
{
    // IObservableに対するawaitになる
    // CancellationTokenを指定するにはToUniTaskが必要でいろいろめんどくさい
    var result = await p.Where(x => x == 10).ToUniTask(useFirstValue: true, ct);
    Debug.Log(result);
}

UniRx.ReactiveProperty<T>awaitとは相性があまり良くないですが、複数の購読者に対して一斉にメッセージを送ることについては抜群に得意です。


一方のAsyncReactiveProperty<T>IUniTaskAsyncEnumerable<T>ベースのため、awaitと非常に相性がよいです。
素直にそのまま書くことができ、CancellationTokenの指定も簡単です。

private async UniTaskVoid WaitForPropertyAsync(AsyncReactiveProperty<int> p, CancellationToken ct)
{
    // そのままLINQを書いてawaitできる
    // CancellationTokenも渡しやすい
    var result = await p.FirstAsync(x => x == 10, ct);

    // 非同期処理を間で挟むこともできる
    await p
        .SelectAwaitWithCancellation(async (x, t) =>
        {
            // 与えられた値のミリ秒分だけ待つ(何がしたいんだこの例)
            await UniTask.Delay(x, cancellationToken: t);
            return x;
        })
        .FirstAsync(cancellationToken: ct);
}

ただしAsyncReactiveProperty<T>awaitのタイミングによっては値の取りこぼしが発生してしまいます。
そのため確実にメッセージをブロードキャストして相手に確実に届けるという用途には向いていません。
(その点はUniRx.ReactiveProperty<T>のほうが有利です)

取りこぼしが起きる例
private async UniTaskVoid WaitForPropertyAsync(AsyncReactiveProperty<int> p, CancellationToken ct)
{
    // UniRxのSubscribe感覚でForEachAwaitAsyncを使うと
    // メッセージの取りこぼしが起きうる
    await p.ForEachAwaitWithCancellationAsync(async (x, ct) =>
    {
        Debug.Log(x);
        // 1秒待ってから次の値を取りに行く
        // このawait中に変動した値は取りこぼす
        await UniTask.Delay(1000, cancellationToken: ct);
    }, ct);
}

取りこぼしの防止策として、後述するQueue()を使う方法もあります。

UniTask.Linq

IUniTaskAsyncEnumerable<T>にはLINQメソッドが用意されています。
利用するためにはasmdef「UniTask.Linq」への参照を追加する必要があります。

使えるメソッドは次です。

同期型

  • AggregateAsync
  • AllAsync
  • AnyAsync
  • AsUniTaskAsyncEnumerable
  • AverageAsync
  • Buffer
  • Append
  • Prepend
  • Cast
  • Concat
  • ContainsAsync
  • CountAsync
  • DefaultIfEmpty
  • Distinct
  • DistinctUntilChanged
  • Do
  • ElementAtAsync
  • ElementAtOrDefaultAsync
  • Except
  • FirstAsync
  • FirstOrDefaultAsync
  • ForEachAsync
  • GroupBy
  • GroupJoin
  • Intersect
  • Join
  • LastAsync
  • LastOrDefaultAsync
  • LongCountAsync
  • MaxAsync
  • MinAsync
  • OfType
  • OrderBy
  • OrderByDescending
  • Reverse
  • Select
  • SelectMany
  • SequenceEqualAsync
  • SingleAsync
  • SingleOrDefaultAsync
  • Skip
  • SkipLast
  • SkipWhile
  • SkipUntil
  • SumAsync
  • Take
  • TakeLast
  • TakeWhile
  • TakeUntil
  • ToArrayAsync
  • ToDictionaryAsync
  • ToHashSetAsync
  • ToListAsync
  • ToLookupAsync
  • ToObservable
  • Union
  • Where
  • Zip
  • CombineLatest
  • Pairwise
  • Queue
  • SkipUntilCanceled
  • TakeUntilCanceled
  • Publish

非同期型

AwaitAsyncがついているものはasync/awaitを内部で利用できます。

  • AggregateAwaitAsync
  • AllAwaitAsync
  • AnyAwaitAsync
  • AverageAwaitAsync
  • CountAwaitAsync
  • DistinctAwait
  • DistinctUntilChangedAwait
  • DoAwait
  • FirstAwaitAsync
  • FirstOrDefaultAwaitAsync
  • ForEachAwaitAsync
  • GroupByAwait
  • GroupJoinAwait
  • JoinAwait
  • LastAwaitAsync
  • LastOrDefaultAwaitAsync
  • LongCountAwaitAsync
  • MaxAwaitAsync
  • MinAwaitAsync
  • OrderByAwait
  • OrderByDescendingAwait
  • SelectAwait
  • SelectManyAwait
  • SingleAwaitAsync
  • SingleOrDefaultAwaitAsync
  • SkipWhileAwait
  • SumAwaitAsync
  • TakeWhileAwait
  • ToDictionaryAwaitAsync
  • ToLookupAwaitAsync
  • WhereAwait
  • ZipAwait

こちらはCancellationTokenを内部で必要とするときに使います。

  • AggregateAwaitWithCancellationAsync
  • AnyAwaitWithCancellationAsync
  • AverageAwaitWithCancellationAsync
  • CountAwaitWithCancellationAsync
  • DistinctAwaitWithCancellation
  • DistinctUntilChangedAwaitWithCancellation
  • DoAwaitWithCancellation
  • FirstAwaitWithCancellationAsync
  • FirstOrDefaultAwaitWithCancellationAsync
  • ForEachAwaitWithCancellationAsync
  • GroupByAwaitWithCancellation
  • GroupJoinAwaitWithCancellation
  • JoinAwaitWithCancellation
  • LastAwaitWithCancellationAsync
  • LastOrDefaultAwaitWithCancellationAsync
  • LongCountAwaitWithCancellationAsync
  • MaxAwaitWithCancellationAsync
  • MinAwaitWithCancellationAsync
  • OrderByAwaitWithCancellation
  • OrderByDescendingAwaitWithCancellation
  • SelectAwaitWithCancellation
  • SelectManyAwaitWithCancellation
  • SingleAwaitWithCancellationAsync
  • SingleOrDefaultAwaitWithCancellationAsync
  • SkipWhileAwaitWithCancellation
  • SumAwaitWithCancellationAsync
  • TakeWhileAwaitWithCancellation
  • ToDictionaryAwaitWithCancellationAsync
  • ToLookupAwaitWithCancellationAsync
  • WhereAwaitWithCancellation
  • ZipAwaitWithCancellation

補足: PushベースのUniTaskAsyncEnumerableの注意点

IUniTaskAsyncEnumerable<T>Pull型として動作します。
そのためメッセージの受信準備が整い、内部でイテレータのMoveNextAsync()が実行されたタイミングで次のメッセージを取りに行きます。

ですが、UniTaskAsyncEnumerableではメッセージ発行がPushIUniTaskAsyncEnumerable<T>も存在します(ややこしい)

  • UniTask.EveryUpdate()
  • AsyncReactiveProperty
  • AsyncTriggerより生成したもの
  • uGUIのイベントなどから変換したもの

これらはPushされてきたイベントをIUniTaskAsyncEnumerable<T>として提供します。
そのためMoveNextAsync()とタイミングが合わなかった場合は、その間に発行されたイベントは取りこぼされるという点に注意が必要です。

image.png

逆に、このイベントの取りこぼしを利用して処理を組むこともできます。

イベントの取りこぼしを利用した例
// uGUIのボタンの連打防止
// 1回ボタンを押したら2秒間無反応になる

// ForEachAwaitAsyncが待機中に発行されたメッセージは無視するという
// 性質を利用している
_button.OnClickAsAsyncEnumerable()
    .ForEachAwaitWithCancellationAsync(async (_, ct) =>
    {
        Debug.Log("Clicked!");
        await UniTask.Delay(2000, cancellationToken: ct);
    }, token);

取りこぼしが嫌なら Queue() を使おう

QueueUniTask.Linqが提供するLINQメソッドの1つです。

IUniTaskAsyncEnumerable<T>に対して先にMoveNextAsync()を実行し、その結果をキューに詰めて再度IUniTaskAsyncEnumerable<T>として提供します。
ObservableでいうところのPublish()に相当します)

Queueを使えばイベントの取りこぼしを防ぐことができるため、必要に応じて利用しましょう。

Queue
private void Start()
{
    var token = this.GetCancellationTokenOnDestroy();

    // AsyncReactiveProperty生成
    var asyncReactiveProperty = new AsyncReactiveProperty<string>("Initialize!");

    // 待受開始
    WaitForAsync(asyncReactiveProperty, token).Forget();

    // 値を設定
    asyncReactiveProperty.Value = "Hello!";
    asyncReactiveProperty.Value = "World!";
    asyncReactiveProperty.Value = "Thank you!";

    asyncReactiveProperty.Dispose();
}

private async UniTaskVoid WaitForAsync(
    IReadOnlyAsyncReactiveProperty<string> asyncReadOnlyReactiveProperty,
    CancellationToken token)
{
    // AsyncReactiveProperty はキューイングしてくれないので
    // タイミングによってはメッセージの取りこぼしが置きうる
    // 取りこぼしが嫌ならQueueを併用する
    await asyncReadOnlyReactiveProperty
        .Queue() // Queueを挟む
        .ForEachAwaitWithCancellationAsync(async (x, ct) =>
        {
            Debug.Log(x);
            // 1秒待って次の値を取りに行く
            await UniTask.Delay(1000, cancellationToken: ct);
        }, token);
}

まとめ

UniTask2の感想

C# 8.0IAsyncEnumerable相当の処理をいち早くUnityでも利用できるため、かなり期待が高いアップデートとなりました。
UniTaskAsyncEnumerableAsyncReactivePropertyが刺さる人にはかなり刺さる機能でしょう(自分はさっそく使いたい)

UniRx(Observable)との使い分け

ぶっちゃけ、非同期処理の用途としてUniRx(Observable)が選択肢に上がってくることがなくなりました。
async/await + UniTask / UniTaskAsyncEnumerable でほとんどの非同期処理のシチュエーションはカバーできてしまいます。

じゃあObservableはお役御免かというとそうではなく、「イベント駆動」として使う方ではまだまだ活用が可能です。
特にイベントメッセージが飛び交うゲーム開発ではObservableが活躍するシチュエーションは多いでしょう。

とくにReactivePropertyAsyncReactivePropertyの使い分けが特に今後は重要になってくるかなと思います。

150
102
0

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
150
102