1. tatsunoru

    No comment

    tatsunoru
Changes in body
Source | HTML | Preview
@@ -1,304 +1,309 @@
## 動作環境
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対応にするにはどうすればええんや?
なんかな
```c#
public static 目的の型のAwaiter GetAwaiter(this 目的の型 self){ return new 目的の型のAwaiter(); }
```
こんな感じの拡張メソッドを用意してな、「目的の型のAwaiter」の定義に
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
{
// 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出来るんや!
### 動作確認
こんなんでホンマに動くんかいな?
次のテストコード試したろ
```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が死んだら大変なことになるでぇ!
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
```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
{
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に登録して逐次実行で毎ループ判定するタイプやけど
今回の対象のTweenは、Kill時のコールバック設定が出来るからそれで成功時の処理を
CanncellationToken.Registerでキャンセル時の処理を登録しとるでぇ!
### テストコード
```c#
+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が死んでも死ななくても必ずここは通る
}
}
}
```
-(ちなみにGameObjectが死んだらCancelされるCancellationTokenは
-UniRx.Async.TriggersをusingすればMonoBehaviourのGetCancellationTokenOnDestroy拡張メソッドでゲットできるんや!
-便利やなぁ)
-
これでDOTweenのawait中にGameObjectが死んでも大丈夫になったわ!
やったでぇ!
まぁでも、実用にはDOTweenのSequence使った方がいいけどな
そっちの方が色々楽やし
## 終わり
何度も言うけど、ワシasync/await赤ちゃんやから
このページがあってるかあってないかはまったく保証しないし
間違ってたら辛らつにコメントして正解を教えてほしい!
##ライセンス
ソースコードのライセンスはMIT LicenseかApache 2.0 Licenseのお好きな方でどうぞ