DOTween + UniTask環境でのTweenCancelBehaviourについて

# はじめに
こちらは「[Applibot Advent Calendar 2022](https://qiita.com/advent-calendar/2022/applibot)」7日目の記事になります。

DOTween + UniTask環境で、ToUniTask()する際にCancellationToken以外にも渡すべきものがあるという話を聞き、どういったものなのか、どういった挙動になるのか気になったため記事にしてみました！

# TweenCancelBehaviourとは？
DOTween + UniTask 環境で ToUniTask()の引数に入れるもので、キャンセルされたときの挙動について指定します。
```C#
await transform.DOLocalMoveX(10, 1).ToUniTask(TweenCancelBehaviour.Kill, cancellationToken: ct);
```

デフォルトで`TweenCancelBehaviour.Kill`が入るので指定しなくても動くことは動くのですが、バグに繋がりそうな挙動なので実際にコードと一緒に見ていこうと思います！
今回はシンプルな`Kill`,`KillAndCancelAwait`,`Complete`, `CompleteAndCancelAwait`の比較を行っていきます！

# 実行するコード
```C#
public class CancellationTokenTest : MonoBehaviour
{
    private CancellationTokenSource _cts;

    async void Start()
    {
        _cts = new CancellationTokenSource();
        await UniTask.Delay(1000, cancellationToken: this.GetCancellationTokenOnDestroy());
        // ここで実行するコードを変えていく
        NanikaAsync(_cts.Token).Forget(e => { print(e.ToString()); });
        await UniTask.Delay(500, cancellationToken: this.GetCancellationTokenOnDestroy());
        _cts.Cancel();
    }
}
```
今回上記のコードを実行します。見ての通り、Delayで1秒後に`NanikaAsync`を実行し、さらに0.5秒後にキャンセルする、といった処理になっています。
`NanikaAsync`にあたるコードをこれから変更していき、どう違うのか見ていきます。

# Kill(デフォルト)の場合
```C#
private async UniTask KillAsync(CancellationToken ct)
{
    await transform.DOLocalMoveX(10, 1).ToUniTask(cancellationToken: ct);
    print(ct.IsCancellationRequested);
    await transform.DOLocalMoveX(-10, 1).ToUniTask(cancellationToken: ct);
    print(ct.IsCancellationRequested);
}
```
![KillAsync.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2998005/e3106064-e931-1272-169f-53427c086800.gif)
この場合、gifのようになり、1つ目のawait中にキャンセルされたのにも関わらず、例外を出さずに関数２行目`print(ct.IsCancellationRequested);`が実行されてしまっています。その後、２つ目のAsync関数実行のエントリー時に例外がスローされているため、`print(e.ToString());`が実行されるという流れ。
`ToUniTask()`内のコードを見てみると`Create`時にキャンセルのチェックがされて例外を出している感じですね。
```C#
// DOTween内ToUniTaskのコード
public static UniTask ToUniTask(this Tween tween, TweenCancelBehaviour tweenCancelBehaviour = TweenCancelBehaviour.Kill, CancellationToken cancellationToken = default)
{
    Error.ThrowArgumentNullException(tween, nameof(tween));
    if (!tween.IsActive()) return UniTask.CompletedTask;
    return new UniTask(TweenConfiguredSource.Create(tween, tweenCancelBehaviour, cancellationToken, CallbackType.Kill, out var token), token);
}

public static IUniTaskSource Create(Tween tween, TweenCancelBehaviour cancelBehaviour, CancellationToken cancellationToken, CallbackType callbackType, out short token)
{
    if (cancellationToken.IsCancellationRequested)
    {
        DoCancelBeforeCreate(tween, cancelBehaviour);
        return AutoResetUniTaskCompletionSource.CreateFromCanceled(cancellationToken, out token);
    }
    ...
```
キャンセルされた時点で例外を出してほしいことがほとんどだと思うので、これだと意図した挙動じゃない場合が多いと思います。

# KillAndCancelAwaitの場合
```C#
private async UniTask KillAndCancelAwaitAsync(CancellationToken ct)
{
    await transform.DOLocalMoveX(10, 1).ToUniTask(TweenCancelBehaviour.KillAndCancelAwait, cancellationToken: ct);
    print(ct.IsCancellationRequested);
    await transform.DOLocalMoveX(-10, 1).ToUniTask(TweenCancelBehaviour.KillAndCancelAwait, cancellationToken: ct);
    print(ct.IsCancellationRequested);
}
```
![KillAndCancelAwaitAsync.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2998005/05dfbb59-bd7f-01fc-9a8e-3399ac72b942.gif)
こちらはキャンセル時にちゃんと例外を出してくれているので単純に途中で止める、という挙動の場合は`KillAndCancelAwait`が想定通りの挙動になっています。良さそう。

# Completeの場合
```C#
private async UniTask CompleteAsync(CancellationToken ct)
{
    await transform.DOLocalMoveX(10, 1).ToUniTask(TweenCancelBehaviour.Complete, cancellationToken: ct);
    print(ct.IsCancellationRequested);
    await transform.DOLocalMoveX(-10, 1).ToUniTask(TweenCancelBehaviour.Complete, cancellationToken: ct);
    print(ct.IsCancellationRequested);
}
```
![CompleteAsync.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2998005/13d0b5a9-3cb9-6848-d084-12b503faf40e.gif)
Completeを入れた場合もKillと同じくキャンセルされた後に例外を出さず、2行目のコード`print(ct.IsCancellationRequested);`が実行されていますね。さらに、2つ目のAsync関数に入りCompleteされてから例外を出しています。
仕組みをしっかりとは把握できていませんが中を見てみると、`TweenCancelBehaviour.Complete`の場合`TweenConfiguredSource`の`OnUpdate`でキャンセルフラグが立たないため、`OnCompleteCallbackDelegate()`で`originalCompleteAction`が呼ばれているのが原因っぽいです。詳しく知りたい方はDOTweenの中を見てみることをお勧めします。

動きの話に戻りますが、これも意図した挙動ではないことの方が多いと思うのであまり使い道はなさそう。

# CompleteAndCancelAwaitの場合
```C#
private async UniTask CompleteAndCancelAwaitAsync(CancellationToken ct)
{
    await transform.DOLocalMoveX(10, 1)
        .ToUniTask(TweenCancelBehaviour.CompleteAndCancelAwait, cancellationToken: ct);
    print(ct.IsCancellationRequested);
    await transform.DOLocalMoveX(-10, 1)
        .ToUniTask(TweenCancelBehaviour.CompleteAndCancelAwait, cancellationToken: ct);
    print(ct.IsCancellationRequested);
}
```
![CompleteAndCancelAwaitAsync.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2998005/5c4d0fcf-38e5-8287-c295-625fc76de0f2.gif)
`CompleteAndCancelAwait`の場合は`Complete`と違い、ちゃんとすぐに例外を出してくれているのでこちらの方が良さそう。




# まとめ
ToUniTask()を使う際はTweenCancelBehaviourも意識しよう！
大体の場合は`KillAndCancelAwait`か`CompleteAndCancelAwait`で問題なさそう！

おしまい！