今回の内容
UniTaskに搭載されているAsyncTrigger
という機能について今回紹介します。
AsyncTriggerの概要
AsyncTrigger
とはMonoBehaviour
のイベントをUniTask
/IUniTaskAsyncEnumerable
に変換する機能です。UniRxでいうところのUpdateAsObservable
やOnCollisionEnterAsObservable
に似た機能です。
使い方
AsyncTrigger
は使い方(呼び出し方)が複数あるのでそれぞれ解説します。
A. UniTaskAsyncEnumerableとして扱う(おすすめ)
UniTaskAsyncEnumerable
としてイベントを扱うこともできます。
UniRxライクに使いたい場合はこの方法がおすすめです。
使い方としてはMonoBehaviour
上でGetAsyncXxxTrigger()
を呼び出します。
(Xxx
は任意のイベント名)
using System;
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Linq;
using Cysharp.Threading.Tasks.Triggers;
using UnityEngine;
public class AsyncTriggerSample : MonoBehaviour
{
private void Start()
{
// OnCollisionEnterをUniTaskAsyncEnumerableとして扱う
this.GetAsyncCollisionEnterTrigger()
.Subscribe(collision =>
{
Debug.Log($"Hit:{collision.gameObject.name}");
})
.AddTo(destroyCancellationToken);
}
}
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Triggers;
using UnityEngine;
public class AsyncTriggerSample : MonoBehaviour
{
private async UniTaskVoid Start()
{
await foreach (var collision in this.GetAsyncCollisionEnterTrigger())
{
Debug.Log($"Hit:{collision.gameObject.name}");
}
}
}
UniRxと同様にTrigger
コンポーネントが対象にアタッチされるため、別のGameObjectの衝突イベントを拾ってきたりできます。
B.単発で1回だけawaitする
イベントを1回だけawait
で待つときはGetAsyncXxxTrigger().XxxAsync(CancellationToken)
を使います。
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Triggers;
using UnityEngine;
public class AsyncTriggerSample : MonoBehaviour
{
async UniTaskVoid Start()
{
// Updateを1回だけ待つ
await this.GetAsyncUpdateTrigger()
.UpdateAsync(destroyCancellationToken);
// OnCollisionEnterを1回だけ待つ
var collision = await this.GetAsyncCollisionEnterTrigger()
.OnCollisionEnterAsync(destroyCancellationToken);
}
}
ただしこの記法は繰り返して呼び出すには非効率なので、同じイベントを何度もawait
する場合は次のCの記法を使いましょう。
C.何回も繰り返しawaitする
同じイベントを何度もawaitする場合はAsyncHandler
を取得することで効率的に動作します。
GetAsyncXxxTrigger().GetXxxxAsyncHandler(CancellationToken)
という形でAsyncHandler
を取得できます。
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Triggers;
using UnityEngine;
public class AsyncTriggerSample : MonoBehaviour
{
async UniTaskVoid Start()
{
// 繰り返し何度もawaitするときは、AsyncHandlerを取得する
var asyncHandler = this.GetAsyncCollisionEnterTrigger()
.GetOnCollisionEnterAsyncHandler(destroyCancellationToken);
while (!destroyCancellationToken.IsCancellationRequested)
{
// AsyncHandlerからイベントをUniTaskとして取り出す
var collision = await asyncHandler.OnCollisionEnterAsync();
Debug.Log(collision.gameObject.name);
}
}
}
まとめ
「A」のUniTaskAsyncEnumerable
に変換して扱う方法が一番わかりやすいのでオススメです。
UniRxとほぼ同じ書き心地で扱うことができます。
補足1:Subscribe/ForEachAsync/ForEachAwaitAsync/SubscribeAwaitの違い
// Subscribe
// Observableの書き心地でUniTaskAsyncEnumerableを扱える
this.GetAsyncCollisionEnterTrigger()
.Subscribe(collision =>
{
Debug.Log($"Hit:{collision.gameObject.name}");
})
.AddTo(destroyCancellationToken);
// Subscribe
// エラーを別で扱うこともできる
this.GetAsyncCollisionEnterTrigger()
.Subscribe(collision =>
{
Debug.Log($"Hit:{collision.gameObject.name}");
},
error => Debug.LogError(error))
.AddTo(destroyCancellationToken);
// Subscribe + async/await
// async/awaitを使えるが、この場合はUniTaskVoid扱いで非同期処理が実行される
this.GetAsyncCollisionEnterTrigger()
.Subscribe(async collision =>
{
await UniTask.Delay(TimeSpan.FromSeconds(1));
Debug.Log($"Hit:{collision.gameObject.name}");
})
.AddTo(destroyCancellationToken);
// ForEachAsync
// CancellationTokenの指定方法が異なるだけで挙動はだいたいSubscribeと一緒
this.GetAsyncCollisionEnterTrigger()
.ForEachAsync(collision =>
{
Debug.Log($"Hit:{collision.gameObject.name}");
}, destroyCancellationToken);
// ForEachAwaitAsync
// イベントハンドリング時に非同期処理(async/await)が使える
// ただし非同期処理実行中に到達したイベントは<<無視>>される
// 非同期処理が完了次第、次のイベントを処理できるようになる
this.GetAsyncCollisionEnterTrigger()
.ForEachAwaitAsync(async collision =>
{
await UniTask.Delay(TimeSpan.FromSeconds(1));
Debug.Log($"Hit:{collision.gameObject.name}");
}, destroyCancellationToken);
// ForEachAwaitWithCancellationAsync
// イベントハンドリング時に非同期処理(async/await)が使える + CancellationToken
// ただし非同期処理実行中に到達したイベントは<<無視>>される
// 非同期処理が完了次第、次のイベントを処理できるようになる
this.GetAsyncCollisionEnterTrigger()
.ForEachAwaitWithCancellationAsync(async (collision, ct) =>
{
await UniTask.Delay(TimeSpan.FromSeconds(1), cancellationToken: ct);
Debug.Log($"Hit:{collision.gameObject.name}");
}, destroyCancellationToken);
// SubscribeAwait
// ForEachAwaitAsyncとだいたい同じ処理をSubscribeの記法で書ける
this.GetAsyncCollisionEnterTrigger()
.SubscribeAwait(async collision =>
{
await UniTask.Delay(TimeSpan.FromSeconds(1));
Debug.Log($"Hit:{collision.gameObject.name}");
})
.AddTo(destroyCancellationToken);
// SubscribeAwait
// CancellationTokenを受け取る場合はこう
this.GetAsyncCollisionEnterTrigger()
.SubscribeAwait(async (collision,ct) =>
{
await UniTask.Delay(TimeSpan.FromSeconds(1), cancellationToken: ct);
Debug.Log($"Hit:{collision.gameObject.name}");
})
.AddTo(destroyCancellationToken);
補足2:ForEachAwaitAsyncを使ったときの挙動
UniTaskAsyncEnumerable
でMonoBehaviour
のイベントを扱ったときにForEachAwait
を使うとイベントの取りこぼしが発生します。
this.GetAsyncCollisionEnterTrigger()
.ForEachAwaitAsync(async collision =>
{
await UniTask.Delay(TimeSpan.FromSeconds(1));
Debug.Log($"Hit:{collision.gameObject.name}");
// ここの処理が完遂するまで新たに発行されたイベントは無視される
}, destroyCancellationToken);
この漏れを防ぐにはQueue()
を間に挟みましょう。
// Queueを追加するとイベントの漏れがなくなる
this.GetAsyncCollisionEnterTrigger()
.Queue()
.ForEachAwaitAsync(async collision =>
{
await UniTask.Delay(TimeSpan.FromSeconds(1));
Debug.Log($"Hit:{collision.gameObject.name}");
}, destroyCancellationToken);