1. tatsunoru

    No comment

    tatsunoru
Changes in body
Source | HTML | Preview

2019/01/22 張り付けるべきテストコードが間違ってたので修正しました

動作環境

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渡せばええんや?

CancellationToken対応のAwaiterを書こう!

public struct TweenAwaiter : ICriticalNotifyCompletion
{
    Tween tween;
    CancellationToken cancellationToken;

    public TweenAwaiter(Tween tween, CancellationToken cancellationToken)
    {
        this.tween = tween;
        this.cancellationToken = cancellationToken;
    }

    public bool IsCompleted => !tween.IsPlaying();

    public void GetResult() => cancellationToken.ThrowIfCancellationRequested();

    public void OnCompleted(Action continuation) => UnsafeOnCompleted(continuation);

    public void UnsafeOnCompleted(Action continuation)
    {
        CancellationTokenRegistration regist = new CancellationTokenRegistration();
        var tween = this.tween;

        // Tweenが死んだら続きを実行
        tween.OnKill(() =>
        {
            regist.Dispose(); // CancellationTokenRegistrationを破棄する
            continuation(); // 続きを実行
        });

        // tokenが発火したらTweenをKillする
        regist = cancellationToken.Register(()=>{
            tween.Kill(true);
        });
    }

    public TweenAwaiter GetAwaiter() => this;
}

public static class TweenAwaiterEx
{
    // TweenにToAwaiter拡張メソッドを追加
    public static TweenAwaiter ToAwaiter(this Tween self, CancellationToken cancellationToken = default)
    {
        return new TweenAwaiter(self, cancellationToken);
    }
}

できたでぇ!

DOTweenはKill時のコールバック設定が出来るから、それで成功時の処理を
CanncellationToken.Registerでキャンセル時の処理を登録しとるでぇ!

テストコード

public class TestTween : MonoBehaviour
{
    CancellationTokenSource source;

    void Start()
    {
        // CancellationTokenSourceを準備
        source = new CancellationTokenSource();
        Go(source.Token).Forget();
    }

    private void OnDestroy()
    {
        // CancellationTokenSourceを破棄する
        source.Cancel();
        source.Dispose();
    }

    async UniTask Go(CancellationToken token)
    {
        try
        {
            // 上に移動
            await transform.DOMove(new Vector3(0, 5, 0), 3).ToAwaiter(token);
            Debug.Log("Step1");

            // 右斜め上に移動
            await transform.DOMove(new Vector3(5, 5, 0), 3).ToAwaiter(token);
            Debug.Log("Step2");
        }
        finally
        {
            Debug.Log("Finish");
        }
    }
}
public class TestTween : MonoBehaviour
{
    CancellationTokenSource source;

    void Start()
    {
        // CancellationTokenSourceを準備
        source = new CancellationTokenSource();
        Go(source.Token).Forget();
    }

    private void OnDestroy()
    {
        // CancellationTokenSourceを破棄する
        source.Cancel();
        source.Dispose();
    }

    async UniTask Go(CancellationToken token)
    {
        try
        {
            // 上に移動、ToAwaiter拡張メソッドにCancellationTokenを渡す
            await transform.DOMove(new Vector3(0, 5, 0), 3).ToAwaiter(token);
            Debug.Log("Step1");

            // 右斜め上に移動
            await transform.DOMove(new Vector3(5, 5, 0), 3).ToAwaiter(token);
            Debug.Log("Step2");
        }
        finally
        {
            Debug.Log("Finish");
        }
    }
}

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

実際にトゥイーン移動中に、UnityEditorのヒエラルキーから強制的に削除してみると
Finishという文字列が即座にコンソールに出力されるようになっとる!

やったでぇ!


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

終わり

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

ライセンス

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