はじめに
こちらは「Applibot Advent Calendar 2022」7日目の記事になります。
DOTween + UniTask環境で、ToUniTask()する際にCancellationToken以外にも渡すべきものがあるという話を聞き、どういったものなのか、どういった挙動になるのか気になったため記事にしてみました!
TweenCancelBehaviourとは?
DOTween + UniTask 環境で ToUniTask()の引数に入れるもので、キャンセルされたときの挙動について指定します。
await transform.DOLocalMoveX(10, 1).ToUniTask(TweenCancelBehaviour.Kill, cancellationToken: ct);
デフォルトでTweenCancelBehaviour.Kill
が入るので指定しなくても動くことは動くのですが、バグに繋がりそうな挙動なので実際にコードと一緒に見ていこうと思います!
今回はシンプルなKill
,KillAndCancelAwait
,Complete
, CompleteAndCancelAwait
の比較を行っていきます!
実行するコード
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(デフォルト)の場合
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);
}
この場合、gifのようになり、1つ目のawait中にキャンセルされたのにも関わらず、例外を出さずに関数2行目print(ct.IsCancellationRequested);
が実行されてしまっています。その後、2つ目のAsync関数実行のエントリー時に例外がスローされているため、print(e.ToString());
が実行されるという流れ。
ToUniTask()
内のコードを見てみるとCreate
時にキャンセルのチェックがされて例外を出している感じですね。
// 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の場合
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);
}
こちらはキャンセル時にちゃんと例外を出してくれているので単純に途中で止める、という挙動の場合はKillAndCancelAwait
が想定通りの挙動になっています。良さそう。
Completeの場合
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);
}
Completeを入れた場合もKillと同じくキャンセルされた後に例外を出さず、2行目のコードprint(ct.IsCancellationRequested);
が実行されていますね。さらに、2つ目のAsync関数に入りCompleteされてから例外を出しています。
仕組みをしっかりとは把握できていませんが中を見てみると、TweenCancelBehaviour.Complete
の場合TweenConfiguredSource
のOnUpdate
でキャンセルフラグが立たないため、OnCompleteCallbackDelegate()
でoriginalCompleteAction
が呼ばれているのが原因っぽいです。詳しく知りたい方はDOTweenの中を見てみることをお勧めします。
動きの話に戻りますが、これも意図した挙動ではないことの方が多いと思うのであまり使い道はなさそう。
CompleteAndCancelAwaitの場合
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);
}
CompleteAndCancelAwait
の場合はComplete
と違い、ちゃんとすぐに例外を出してくれているのでこちらの方が良さそう。
まとめ
ToUniTask()を使う際はTweenCancelBehaviourも意識しよう!
大体の場合はKillAndCancelAwait
かCompleteAndCancelAwait
で問題なさそう!
おしまい!