1. tatsunoru

    No comment

    tatsunoru
Changes in body
Source | HTML | Preview
@@ -1,280 +1,282 @@
## 動作環境
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インターフェイスを追加して以下のメソッドをかいとけば
+こんな感じの拡張メソッドを用意してな、「目的の型の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();
public void GetResult() { }
public void OnCompleted(System.Action continuation) => tween.OnKill(() => continuation());
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が死んだら大変なことになるでぇ!
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のお好きな方でどうぞ