UniTaskでMonoBehaviourのイベントを扱う

# 今回の内容

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

# AsyncTriggerの概要

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

# 使い方

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

## A. UniTaskAsyncEnumerableとして扱う（おすすめ）

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

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

```cs: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);
    }
}
```

```cs: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)`を使います。

```cs
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`を取得できます。

```cs
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の違い

```cs
// 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`を使うとイベントの取りこぼしが発生します。

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

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/44675668-08c9-c105-216c-63a4f002bb2b.png)



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

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

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/15b41544-5cff-c85c-0623-c61852f30433.png)