LoginSignup
22
9

UniTaskでMonoBehaviourのイベントを扱う

Last updated at Posted at 2024-02-11

今回の内容

UniTaskに搭載されているAsyncTriggerという機能について今回紹介します。

AsyncTriggerの概要

AsyncTriggerとはMonoBehaviourのイベントをUniTask/IUniTaskAsyncEnumerableに変換する機能です。UniRxでいうところのUpdateAsObservableOnCollisionEnterAsObservableに似た機能です。

使い方

AsyncTriggerは使い方(呼び出し方)が複数あるのでそれぞれ解説します。

A. UniTaskAsyncEnumerableとして扱う(おすすめ)

UniTaskAsyncEnumerableとしてイベントを扱うこともできます。
UniRxライクに使いたい場合はこの方法がおすすめです。

使い方としてはMonoBehaviour上でGetAsyncXxxTrigger()を呼び出します。
Xxxは任意のイベント名)

UniRxライクな書き方の場合
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);
    }
}
await_foreachを使う書き方の場合
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を使ったときの挙動

UniTaskAsyncEnumerableMonoBehaviourのイベントを扱ったときにForEachAwaitを使うとイベントの取りこぼしが発生します。

this.GetAsyncCollisionEnterTrigger()
    .ForEachAwaitAsync(async collision =>
    {
        await UniTask.Delay(TimeSpan.FromSeconds(1));
        Debug.Log($"Hit:{collision.gameObject.name}");
        // ここの処理が完遂するまで新たに発行されたイベントは無視される
    }, destroyCancellationToken);

1.png

この漏れを防ぐにはQueue()を間に挟みましょう。

// Queueを追加するとイベントの漏れがなくなる
this.GetAsyncCollisionEnterTrigger()
    .Queue()
    .ForEachAwaitAsync(async collision =>
    {
        await UniTask.Delay(TimeSpan.FromSeconds(1));
        Debug.Log($"Hit:{collision.gameObject.name}");
    }, destroyCancellationToken);

2.png

22
9
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
22
9