他の資料
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と同じ挙動です。
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()を利用しましょう。
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);
}
}
AsyncOperationをawaitするときのConfigureAwait廃止
AsyncOperationをawaitするときに利用できていた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.DeferとUniTask.Lazyがあるのでこちらを使いましょう。
UniTask.Lazyの戻り値の変更
上記の多重await禁止の変更をうけて、UniTask.Lazyの戻り値がAsyncLazyに変換されました。
public static AsyncLazy Lazy(Func<UniTask> factory)
public static AsyncLazy<T> Lazy<T>(Func<UniTask<T>> factory)
AsyncLazyはawaitableなので、そのまま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に変わりました。
既存機能への追加機能とか
全体的なパフォーマンスの向上
全体的にパフォーマンスが向上しています。
内部でUniTaskとAsyncMethodBuilder、Runnerの再利用が自動的に行われるようになり、ゼロアロケーションで動作するようになりました。
UniTaskCompletionSource
UniTaskCompletionSourceにReset()メソッドが追加されました。
こちらを利用することで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の生成を遅延させる |
生成されるものはAsyncLazy。AsyncLazy.Taskは何回でもawaitできる。Deferよりはコストが大きい。 |
DeferとLazyの違いは直接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
JobSystemのJobHandleにWaitAsyncが追加されました。
こちらを利用することで、任意の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.ToUniTaskでUniTaskを生成することもできます。
このUniTaskはCancellationTokenがキャンセルされると正常終了(Succeeded)状態となります。
DoTweenのawaitに対応
DoTweenのawaitにも対応しました。
ただし動作させるためには次の設定が必要です。
- 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に対応
AsyncOperationHandleのawaitも可能になりました。
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で便利だったAddToのCancellationToken版も追加されました。
新機能
UniTaskAsyncEnumerable/IUniTaskAsyncEnumerable
UniTaskAsyncEnumerableはUniTask2の目玉機能です。
IUniTaskAsyncEnumerable<T>はC# 8.0のIAsyncEnumerable<T>をUniTaskとして実装したものです。
なんとこちらはC# 7.x系のUnityでも利用可能になっています。
何をするためのものかというと、「非同期処理を複数個まとめて扱う」ことができるようになる機能です。
Observable(IObservable<T>)と似ていますが、こちらはPull型として機能するという違いがあります。
Observableとの使い分け
ObservableはPush型なのに対して、UniTaskAsyncEnumerableはPull型です。
そのため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;
}
}
対するObservableはPush型のため、多数のObserverに対してメッセージをブロードキャストするのに向いています。
複数個の非同期処理を管理する場合は基本にはUniTaskAsyncEnumerableを使う。
イベント駆動を制御する場合にはObservableを使う。
という使い方をするとよいでしょう。
UniTaskAsyncEnumerableの消費
ForEachAsync
C# 8.0であればawait foreachが使えるのですが、それが使えないUnityバージョンでは代替としてForEachAsyncを利用します。
感覚としては、IObservable<T>に対するSubscribe()に似ています。
ですがこちらは「ForEachAsync()の完了をさらにawaitで待つ」といったことが可能となります。
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が利用でき、この非同期処理が完了するまで次のメッセージを取りに行きません。
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の違い
それぞれの大きな違いは登録したデリゲートの呼び出し方にあります。
ForEachAwaitAsyncとSubscribeは非同期処理を用いたときに挙動が異なるので注意が必要です。
while (await e.MoveNextAsync())
{
action(e.Current); // 常に同期
}
while (await e.MoveNextAsync())
{
await action(e.Current); // awaitしてから次へいく
}
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コンポーネントから変換
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
ChannelはGoにおけるChannelと同義です。
挙動としてはObservableにおけるSubjectに相当します。
実装としてはCreateSingleConsumerUnboundedが用意されています。
CreateSingleConsumerUnboundedは内部でメッセージをキューイングしているため、メッセージの取りこぼしは発生しません。
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
AsyncReactivePropertyはUniRxのReactivePropertyのUniTask版です。
ベースとしてIUniTaskAsyncEnumerable<T>が利用されています。
(ReactivePropertyはベースがIObservable<T>)
基本的な使い方はReactivePropertyと変わりません。
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.ReactivePropertyとAsyncReactivePropertyはそれぞれ次の点が異なります。
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になるため、取り扱いが面倒くさいことになります。
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ではメッセージ発行がPushなIUniTaskAsyncEnumerable<T>も存在します(ややこしい)
UniTask.EveryUpdate()AsyncReactiveProperty-
AsyncTriggerより生成したもの -
uGUIのイベントなどから変換したもの
これらはPushされてきたイベントをIUniTaskAsyncEnumerable<T>として提供します。
そのためMoveNextAsync()とタイミングが合わなかった場合は、その間に発行されたイベントは取りこぼされるという点に注意が必要です。
逆に、このイベントの取りこぼしを利用して処理を組むこともできます。
// uGUIのボタンの連打防止
// 1回ボタンを押したら2秒間無反応になる
// ForEachAwaitAsyncが待機中に発行されたメッセージは無視するという
// 性質を利用している
_button.OnClickAsAsyncEnumerable()
.ForEachAwaitWithCancellationAsync(async (_, ct) =>
{
Debug.Log("Clicked!");
await UniTask.Delay(2000, cancellationToken: ct);
}, token);
取りこぼしが嫌なら Queue() を使おう
QueueはUniTask.Linqが提供するLINQメソッドの1つです。
IUniTaskAsyncEnumerable<T>に対して先にMoveNextAsync()を実行し、その結果をキューに詰めて再度IUniTaskAsyncEnumerable<T>として提供します。
(ObservableでいうところのPublish()に相当します)
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.0のIAsyncEnumerable相当の処理をいち早くUnityでも利用できるため、かなり期待が高いアップデートとなりました。
UniTaskAsyncEnumerableとAsyncReactivePropertyが刺さる人にはかなり刺さる機能でしょう(自分はさっそく使いたい)
UniRx(Observable)との使い分け
ぶっちゃけ、非同期処理の用途としてUniRx(Observable)が選択肢に上がってくることがなくなりました。
async/await + UniTask / UniTaskAsyncEnumerable でほとんどの非同期処理のシチュエーションはカバーできてしまいます。
じゃあObservableはお役御免かというとそうではなく、「イベント駆動」として使う方ではまだまだ活用が可能です。
特にイベントメッセージが飛び交うゲーム開発ではObservableが活躍するシチュエーションは多いでしょう。
とくにReactivePropertyとAsyncReactivePropertyの使い分けが特に今後は重要になってくるかなと思います。

