はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】UniTask-Supplementを用いてUniTaskのCancellationTokenを渡す記述を簡素化する

はじめに

今回はUniTask-SupplementというUniTaskCancellationTokenを渡す記述をより簡単にしてくれるライブラリについて紹介をしたいと思います。

github.com
github.com

導入方法

PackageManagerAdd package from git url...を選択し、以下の文字列を打ち込みます。

https://github.com/su10/UniTask-Supplement.git#upm
PackageManagerからインストールする

OpenUPMにも対応しているので、そちらのやり方がいい場合はGitHubをチェックしてください。
github.com

使い方

基本

Cysharp/UniTaskではCancellationToken名前付き引数として渡さなければならなかったものを、省略できるようになります。

var cancellationToken = this.GetCancellationTokenOnDestroy();

// 以下コメントアウトされている書き方がUniTask Supplementを利用しない書き方

//await UniTask.DelayFrame(1, cancellationToken: cancellationToken);
await UniTask.DelayFrame(1, cancellationToken);

//await UniTask.Delay(1, cancellationToken: cancellationToken);
await UniTask.Delay(1, cancellationToken);

//await UniTask.Delay(TimeSpan.FromMilliseconds(1), cancellationToken: cancellationToken);
await UniTask.Delay(TimeSpan.FromMilliseconds(1), cancellationToken);
        
//await UniTask.WaitUntil(() => true, cancellationToken: cancellationToken);
await UniTask.WaitUntil(() => true, cancellationToken);

//await UniTask.WaitWhile(() => false, cancellationToken: cancellationToken);
await UniTask.WaitWhile(() => false, cancellationToken);

//await UniTask.WaitUntilValueChanged(transform, x => x.position, cancellationToken: cancellationToken);
await UniTask.WaitUntilValueChanged(transform, x => x.transform, cancellationToken);

実装としてはCancellationToken = defaultのようにデフォルト値が設定されていないオーバーロードを増やしています。

ちゃんとAssembly Definition Referenceを利用してUniTask(後で紹介するがUniTask.DoTweenも)のアセンブリにコードを入れてくれていますね。助かります。

新しく実装されたメソッド

UniTask.DelayMillisecondsUniTask.DelaySecondというメソッドが新しく追加されました。

// UniTask.DelayMillisecondsはUniTaskSupplementの中に実装されている(Cysharp/UniTaskには実装されていない)
await UniTask.DelayMilliseconds(1, cancellationToken: cancellationToken);
await UniTask.DelayMilliseconds(1, cancellationToken);
        
// UniTask.DelaySecondはUniTaskSupplementの中に実装されている(Cysharp/UniTaskには実装されていない)
await UniTask.DelaySeconds(1, cancellationToken: cancellationToken);
await UniTask.DelaySeconds(1, cancellationToken);

またシンボル定義を行うことによって、これらのメソッドを利用させないようにすることもできます。

// UniTask.DelayMillisecondsを消す場合
UNITASK_SUPPLEMENT_DISABLE_DELAY_MILLISECONDS
// UniTask.DelaySecondsを消す場合
UNITASK_SUPPLEMENT_DISABLE_DELAY_SECONDS

Unityでプロジェクト全体にシンボル定義を行う場合はProjectSettings/Player/OtherSettings/Script Compilation/Scripting DefineSymbolsに記述してあげればOKです。

シンボル定義

またアセンブリ単位で定義したい場合はAssembly Definition FileVersion Definesで記述してあげます。

Version Defines

【Unity】asmdefのVersion Definesを利用して特定のパッケージ(の特定バージョン)がある場合にのみシンボル定義を行う - はなちるのマイノート

WhenAnyでCancellationTokenを渡す

WhenAnyにもCanellationTokenを渡せるようになります。

private async void Start()
{
    var token = this.GetCancellationTokenOnDestroy();
        
    // 非同期メソッドの返り値の型(UniTask<int>とUniTask<int>)が同じ場合
    var (winArgumentIndex, result) = await UniTask.WhenAny<int>(
        cancel => HogeAsync(cancel), 
        cancel => HogeAsync(cancel), 
        token
    );

    // 非同期メソッドの返り値の型(UniTask<int>とUniTask<bool>)が異なる場合
    var (winArgumentIndex2, result1, result2) = await UniTask.WhenAny(
        cancel => HogeAsync(cancel),
        cancel => FugaAsync(cancel),
        token
    );
}
    
private async UniTask<int> HogeAsync(CancellationToken token)
{
    await UniTask.DelayMilliseconds(10, token);
    return 1;
}

private async UniTask<bool> FugaAsync(CancellationToken token)
{
    await UniTask.DelayMilliseconds(10, token);
    return true;
}

ただし挙動についてちゃんと理解しながら利用した方が良いでしょう。
どんな挙動をするのか実装を見てみます。

public static async UniTask<(int winArgumentIndex, T result)> WhenAny<T>(
    Func<CancellationToken, UniTask<T>> taskFunc1,
    Func<CancellationToken, UniTask<T>> taskFunc2,
    CancellationToken cancellationToken
)
{
    var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
    (int winArgumentIndex, T result) result = default;

    try
    {
        result = await WhenAny<T>(
            taskFunc1(cts.Token),
            taskFunc2(cts.Token)
        );

        cts.Cancel();
    }
    catch (OperationCanceledException ex) when (ex.CancellationToken == cts.Token)
    {
        if (cancellationToken.IsCancellationRequested)
        {
            throw new OperationCanceledException(ex.Message, ex, cancellationToken);
        }

        throw;
    }
    finally
    {
        cts.Dispose();
    }

    return result;
}

UniTask-Supplement/UniTask.WhenAny.Generated.cs at main · su10/UniTask-Supplement · GitHub

エラーが発生しなければ以下のステップを踏みます。

  1. 引数のCancelltionTokenを紐づけながらCancellationTokenSourceを生成
  2. 引数のUniTask<T>を生成したCancellationTokenSource.Tokenを渡しながら実行
  3. エラーなく終了すればCancellationTokenSource.Cancellを呼び出す

つまり引数で渡したUniTask達は、どれか一つのUniTaskが終われば他もキャンセルされるようになっています。(ちゃんとUniTaskの中で止まるように書いてあればであるが)

使わないのに処理が続いているなんてことが起こらないようになるので、助かる機能だと思います。

またキャンセル周りの実装について、以下の記事が参考になりました。
neue.cc

DoTween関係

UniTask.DoTweenの機能を拡張する機能も実装されています。

UniTaskDoTweenの機能を有効にするには少し作業が必要でした。
【Unity】DoTweenでUniTaskを対応させてawaitできるようにする - はなちるのマイノート

具体的にはUNITASK_DOTWEEN_SUPPORTをシンボル定義する必要があったわけですが、UniTask-Supplementの機能を有効にする場合は代わりにUNITASK_SUPPLEMENT_DOTWEEN_SUPPORTを定義します。

TweenerCore<float, float, FloatOptions> tween = DOTween.To(
    () => 0f,
    x => Debug.Log(x),
    1f,
    1f
);
        
CancellationToken cancellationToken = this.GetCancellationTokenOnDestroy();
        
// UniTask
await tween.ToUniTask(cancellationToken: cancellationToken);
await tween.AwaitForComplete(cancellationToken: cancellationToken);
await tween.AwaitForPause(cancellationToken: cancellationToken);
await tween.AwaitForPlay(cancellationToken: cancellationToken);
await tween.AwaitForRewind(cancellationToken: cancellationToken);
await tween.AwaitForStepComplete(cancellationToken: cancellationToken);

// with UniTask Supplement
await tween.ToUniTask(cancellationToken);
await tween.AwaitForComplete(cancellationToken);
await tween.AwaitForPause(cancellationToken);
await tween.AwaitForPlay(cancellationToken);
await tween.AwaitForRewind(cancellationToken);
await tween.AwaitForStepComplete(cancellationToken);

実装としてCysharp/UniTaskに同梱されていたDOTweenAsyncExtensions.csがまるっとUniTask-Supplementに含まれており、完全上位互換になっているので安心してください。

新しく追加された機能はDoTweenAsyncExtensions.Supplement.csの中に入っています。

#if UNITASK_SUPPLEMENT_DOTWEEN_SUPPORT
using System.Threading;
using DG.Tweening;

namespace Cysharp.Threading.Tasks
{
    public static partial class DOTweenAsyncExtensions
    {
        private const TweenCancelBehaviour DefaultTweenCancelBehaviour =
#if UNITASK_SUPPLEMENT_DOTWEEN_SUPPORT_USE_ORIGINAL_DEFAULT_TWEEN_CANCEL_BEHAVIOUR
            TweenCancelBehaviour.Kill;
#else
            TweenCancelBehaviour.KillAndCancelAwait;
#endif

        public static UniTask ToUniTask(this Tween tween, CancellationToken cancellationToken)
        {
            return ToUniTask(tween, DefaultTweenCancelBehaviour, cancellationToken);
        }

        public static UniTask AwaitForComplete(this Tween tween, CancellationToken cancellationToken)
        {
            return AwaitForComplete(tween, DefaultTweenCancelBehaviour, cancellationToken);
        }

        public static UniTask AwaitForPause(this Tween tween, CancellationToken cancellationToken)
        {
            return AwaitForPause(tween, DefaultTweenCancelBehaviour, cancellationToken);
        }

        public static UniTask AwaitForPlay(this Tween tween, CancellationToken cancellationToken)
        {
            return AwaitForPlay(tween, DefaultTweenCancelBehaviour, cancellationToken);
        }

        public static UniTask AwaitForRewind(this Tween tween, CancellationToken cancellationToken)
        {
            return AwaitForRewind(tween, DefaultTweenCancelBehaviour, cancellationToken);
        }

        public static UniTask AwaitForStepComplete(this Tween tween, CancellationToken cancellationToken)
        {
            return AwaitForStepComplete(tween, DefaultTweenCancelBehaviour, cancellationToken);
        }
    }
}
#endif

UniTask-Supplement/DOTweenAsyncExtensions.Supplement.cs at main · su10/UniTask-Supplement · GitHub

またDoTweenAsyncExtensions.Supplement.csではTweenCancelBehaviourなるものがUniTask.DoTweenのデフォルトのものと異なっています。

        private const TweenCancelBehaviour DefaultTweenCancelBehaviour =
#if UNITASK_SUPPLEMENT_DOTWEEN_SUPPORT_USE_ORIGINAL_DEFAULT_TWEEN_CANCEL_BEHAVIOUR
            TweenCancelBehaviour.Kill;
#else
            TweenCancelBehaviour.KillAndCancelAwait;
#endif

こちらをデフォルトと同じ挙動にしたい場合はUNITASK_SUPPLEMENT_DOTWEEN_SUPPORT_USE_ORIGINAL_DEFAULT_TWEEN_CANCEL_BEHAVIOURを定義してとのことです。
GitHub - su10/UniTask-Supplement: Supplemental codes for UniTask.

具体的にどう異なるかは以下のコードの箇所が関係しそうです。

void OnUpdate()
{
    originalUpdateAction?.Invoke();

    if (!cancellationToken.IsCancellationRequested)
    {
        return;
    }

    switch (this.cancelBehaviour)
    {
        case TweenCancelBehaviour.Kill:
        default:
            this.tween.Kill(false);
            break;
        case TweenCancelBehaviour.KillAndCancelAwait:
            this.canceled = true;
            this.tween.Kill(false);
            break;
        // 以下省略
    }
}

github.com