1. tatsunoru

    No comment

    tatsunoru
Changes in body
Source | HTML | Preview

動作環境

Unity 2018.3

概要

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が死んだら大変なことになるでぇ!

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 // UniTaskに入れられるようにUniRx.Async.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;
            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 TweenAwaiter GetAwaiter(this Tween self)
    {
        return new TweenAwaiter(self, default);
    }

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

できたでぇ!

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

テストコード

public class TestTween : MonoBehaviour
{
    void Start()
    {
        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が死んでも死ななくても必ずここは通る
        }
    }
}

(ちなみにGameObjectが死んだらCancelされるCancellationTokenは
UniRx.Async.TriggersをusingすればMonoBehaviourのGetCancellationTokenOnDestroy拡張メソッドでゲットできるんや!
便利やなぁ)

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

やったでぇ!

終わり

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

ライセンス

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