他の資料
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
の使い分けが特に今後は重要になってくるかなと思います。