1. tatsunoru

    No comment

    tatsunoru
Changes in body
Source | HTML | Preview

動作環境

Unity 2018.3

想定している読者ターゲット

async/awaitが何なのか既に分かっている人
CancellationTokenが何なのか既に分かっている人

概要

DOTweenをシリアル実行したい場合、普通はDOTweenが提供しているSequenceを使うやろ?
それが世の中の常識っつーもんや

せやけどな、ワシはどないしてもasync/awaitでやってみたいんじゃ

async/awaitは男の夢じゃけいのぅ…

ちゅーわけで、DOTweenのTweenのasync/await対応をしてみようやないか?

これ何番煎じの記事なんや?

おまえさんの言いたいことはわかるで
DOTween asyncでちょろっと検索しただけで何個かでるわな

【Unity】DOTween で async / await を使用する
http://baba-s.hatenablog.com/entry/2018/05/08/085900

Unity async/awaitで非同期を書く
https://qiita.com/unity_ganbaru/items/b0d837ef1baea5b8bd21

せやけどワシasync/awaitを Unity 2018.3から始めたばかりの赤ちゃんやからな
簡単な記事から始める必要があるんや

それにちょっと上記ページとは記述方法違うし、上記記事の方法ではキャンセル時の問題があるよってに
ワシのこの記事の存在を許したってつかーさい

async/await対応にするにはどうすればええんや?

なんかな

    public static 目的の型のAwaiter GetAwaiter(this 目的の型 self){ return new 目的の型のAwaiter(); }

こんな感じの拡張メソッドを用意してな、「目的の型のAwaiter」の定義に
ICriticalNotifyCompletionインターフェイスを追加して、以下のメソッドをかいとけば

    public bool IsCompleted;

    public void GetResult();

    public void OnCompleted(System.Action continuation);

    public void UnsafeOnCompleted(System.Action continuation);

こでもうその型はasync/await出来るようになるらしいで
なんやこれめっちゃ簡単やな

実際にDOTweenをasync/await対応にしようやないけ

public static class MyDOTweenExtention
{
    // TweenのAwaiter
    public struct TweenAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion
    {
        Tween tween;

        public TweenAwaiter(Tween tween) => this.tween = tween;

        // 最初にすでに終わってるのか終わってないのかの判定のために呼び出されるメソッドらしい
        public bool IsCompleted => tween.IsComplete();

        // Tweenは値を返さないので特に処理がいらないと思う
        public void GetResult() { }

        // このAwaiterの処理が終わったらcontinuationを呼び出してほしいって感じのメソッドらしい
        public void OnCompleted(System.Action continuation) => tween.OnKill(() => continuation());

        // OnCompletedと同じでいいっぽい?
        public void UnsafeOnCompleted(System.Action continuation) => tween.OnKill(() => continuation());
    }

    // Tweenに対する拡張メソッド
    public static TweenAwaiter GetAwaiter(this Tween self)
    {
        return new TweenAwaiter(self);
    }
}

はい、ドーン!

これをファイルに保存しておいとけばもうTweenに対してasync/await出来るんや!

動作確認

こんなんでホンマに動くんかいな?
次のテストコード試したろ

public class TestTween : MonoBehaviour
{
    async void Start()
    {
        await transform.DOMove(new Vector3(0, 2, 0), 5);
        await transform.DOMove(new Vector3(2, 4, 0), 5);
    }
}

簡単やな、5秒掛けて上に動いて、次にまた5秒掛けて右斜め上に動く感じや

jkjk45vv45ddd.gif

で、できたぁ~!
なんやこれ、ホンマめっちゃ簡単やん!

ちょっとまてやぁ!キャンセルどないすんねん

このコード、常に最後まで実行されるならなんの問題もないわ
せやけどもし一つ目のawait中に、このGameObjectが死んだら大変なことになるでぇ!
GameObjectが死んでもこのasyncの処理は動き続けてしまうんや!

コルーチンを使っとる感覚でasync/awaitを使うと
このGameObjectが死んでも処理が続行してしまう問題にハマってまう!

Unityでのゲーム層におけるプログラミングはキャンセルできるのは割と必須や!
キャンセル対応はしっかりと気を付けてせなあかん!

...
しかし、async/awaitのキャンセルには、「CancellationToken」っちゅーもん使うんやが
このAwaiterって一体いつCancellationToken渡せばええんや?

UniRx.Asyncを見よう

わからん時は、UniRx.Async様のソースコードを読むんじゃぁ!
ありがてぇありがてぇ

...
...
ほほーん、わかったでぇ!

UniRx.AsyncではIEnumeratorのawaitにはEnumeratorAwaiterというのを用意しておるんやが
IEnumeratorにConfigureAwait拡張メソッドを用意して
そこでEnumeratorAwaiterをUniTaskに渡してそのUniTaskを返しとるんやな

EnumeratorAwaiterのソースコードはここから見れるでぇ
https://github.com/neuecc/UniRx/blob/master/Assets/Plugins/UniRx/Scripts/Async/EnumeratorAsyncExtensions.cs

よっしゃこのUniRx.AsyncのEnumeratorAwaiterをめっちゃ参考にして、さっきのコード書き直してみよか

UniRx.AsyncのEnumeratorAwaiterを参考に改造したTweenAwaiter

using UnityEngine;
using UniRx.Async;
using DG.Tweening;
using System.Threading;
using System;
using UniRx.Async.Triggers;

    public static class MyDOTweenExtention
    {
        public class TweenAwaiter : IAwaiter
        {
            Tween tween;
            AwaiterStatus status;
            CancellationToken cancellationToken;
            Action continuation;

            public TweenAwaiter(Tween tween, CancellationToken cancellationToken)
            {
                if (cancellationToken.IsCancellationRequested)
                {
                    status = AwaiterStatus.Canceled;
                    return;
                }

                this.tween = tween;
                this.cancellationToken = cancellationToken;
            }

            public bool IsCompleted => status.IsCompleted();

            public AwaiterStatus Status => status;

            public void GetResult()
            {
                switch (status)
                {
                    case AwaiterStatus.Succeeded:
                        break;
                    case AwaiterStatus.Pending:
                        throw new InvalidOperationException("Not yet completed.");
                    case AwaiterStatus.Canceled:
                        throw new OperationCanceledException();
                    default:
                        break;
                }
            }

            void InvokeContinuation(AwaiterStatus status)
            {
                if (this.status == AwaiterStatus.Canceled)
                {
                    return;
                }

                this.status = status;
                cancellationToken = CancellationToken.None;

                if (tween != null && status == AwaiterStatus.Canceled)
                {
                    // キャンセルならtweenをkillしておく
                    tween.Kill(false); 
                }

                tween = null;

                var cont = continuation;
                continuation = null;
                if (cont != null) cont.Invoke();
            }

            public void OnCompleted(Action continuation)
            {
                UnsafeOnCompleted(continuation);
            }

            public void UnsafeOnCompleted(Action continuation)
            {
                if (this.continuation != null) throw new InvalidOperationException("continuation is already registered.");
                this.continuation = continuation;

                // tweenが終わったら成功
                tween.OnKill(() => InvokeContinuation(AwaiterStatus.Succeeded));

                // tokenが発火したらキャンセル
                cancellationToken.Register(() => InvokeContinuation(AwaiterStatus.Canceled));
            }
        }

        public static UniTask ConfigureAwait(this Tween self, CancellationToken cancellationToken = default)
        {
            var awaiter = new TweenAwaiter(self, cancellationToken);
            return new UniTask(awaiter);
        }
    }

できたでぇ!

EnumeratorAwaiterはPlayerLoopに登録して逐次実行で毎ループ判定するタイプやけどEnumeratorAwaiterはPlayerLoopに登録して毎ループ判定するタイプやけど
今回の対象のTweenは、Kill時のコールバック設定が出来るからそれで成功時の処理を
CanncellationToken.Registerでキャンセル時の処理を登録しとるでぇ!

テストコード

using UnityEngine;
using UniRx.Async;
using DG.Tweening;
using System.Threading;
using System;
using UniRx.Async.Triggers;

public class TestTween : MonoBehaviour
{
    void Start()
    {
        // UniRx.Async.Triggersをusingすれば使えるGetCancellationTokenOnDestroy拡張メソッドは
        // そのGameObjectが死んだら発火するCancellationTokenをゲットできる
        Sequence(this.GetCancellationTokenOnDestroy()).Forget();
    }

    async UniTask Sequence(CancellationToken cancellationToken)
    {
        try
        {
            await transform.DOMove(new Vector3(0, 2, 0), 5).ConfigureAwait(cancellationToken: cancellationToken);
            Debug.Log("Step 1"); // 上のDOMove中にGameObjectが削除されたらここから下の行にはいかない

            await transform.DOMove(new Vector3(2, 4, 0), 5).ConfigureAwait(cancellationToken: cancellationToken);
            Debug.Log("Step 2");
        }
        finally
        {
            Debug.Log("End"); // 途中でGameObjectが死んでも死ななくても必ずここは通る
        }
    }
}

これでDOTweenのawait中にGameObjectが死んでも大丈夫になったわ!

やったでぇ!


まぁでも、DOWeenのシリアル実行の実用には、やっぱりDOTweenのSequence使った方がいいんやけどな
DOTweenに関していえばそっちの方が色々やっぱ楽やねん

終わり

何度も言うけど、ワシasync/await赤ちゃんやから
このページがあってるかあってないかはまったく保証しないし
間違ってたら辛らつにコメントして正解を教えてほしい!

ライセンス

ソースコードのライセンスはMIT LicenseかApache 2.0 Licenseのお好きな方でどうぞ