1. tatsunoru

    Posted

    tatsunoru
Changes in title
+DOTweenをasync/await対応にする
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,276 @@
+
+## 動作環境
+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対応にするにはどうすればええんや?
+
+なんかな
+
+```c#
+ public static 目的の型のAwaiter GetAwaiter(this 目的の型 self){ return new 目的の型のAwaiter(); }
+```
+
+こんな感じの拡張メソッドを用意してな、「目的の型のAwaiter」に
+System.Runtime.CompilerServices.ICriticalNotifyCompletionインターフェイスを追加して以下のメソッドをかいとけば
+
+```c#
+ public bool IsCompleted;
+
+ public void GetResult();
+
+ public void OnCompleted(System.Action continuation);
+
+ public void UnsafeOnCompleted(System.Action continuation);
+```
+
+こでもうその型はasync/await出来るようになるらしいで
+なんやこれめっちゃ簡単やな
+
+## 実際にDOTweenをasync/await対応にしようやないけ
+
+```c#
+public static class MyDOTweenExtention
+{
+ public struct TweenAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion
+ {
+ Tween tween;
+
+ public TweenAwaiter(Tween tween) => this.tween = tween;
+
+ public bool IsCompleted => tween.IsComplete();
+
+ public void GetResult() { }
+
+ public void OnCompleted(System.Action continuation) => tween.OnKill(() => continuation());
+
+ public void UnsafeOnCompleted(System.Action continuation) => tween.OnKill(() => continuation());
+ }
+
+ public static TweenAwaiter GetAwaiter(this Tween self)
+ {
+ return new TweenAwaiter(self);
+ }
+}
+```
+
+はい、ドーン!
+
+これをファイルに保存しておいとけばもうTweenに対してasync/await出来るんや!
+
+## 動作確認
+こんなんでホンマに動くんかいな?
+次のテストコード試したろ
+
+```c#
+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](https://qiita-image-store.s3.amazonaws.com/0/330479/e1cf8132-1375-63e3-acf3-f3242a820e8a.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
+```c#
+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でキャンセル時の処理を登録しとるでぇ!
+
+### テストコード
+```c#
+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のお好きな方でどうぞ