この記事は Akatsuki Advent Calendar 2019 - Adventar 5日目の記事です。
UniTaskとは🤔🤔🤔
Cysharpさんが開発された、Unityでメインスレッドを使ってasync/awaitを使えるようにするプラグインです。
ちなみにUniRxは事前に入れる必要があるとか、そういうことはありませんでした。
UniRxなしでも単体で扱うことができます。
tech.cygames.co.jp
今まではIEnumeratorを使って書いていた箇所をasync/awaitで置き換えることができるようになります。
やりたいこと🤔
通信などの非同期処理などが多い場合、「通信が成功した(or失敗した)」のコールバック関数だらけになってしまいます。
例えばログインのフローでこんなコードがあったとします。
// ログインフロー // 例えばログインボタンがタップされてログインするとする場合 void OnLoginButtonClick() { // 利用規約が読まれていないなら利用規約UIを出したい if (!HasReadTermOfUse) { OpenTermOfUseDialog(onAgreed: OnAgreedEvent); return; } // ログインAPIを叩く NetworkManager.Login(onLoginSuccess: OnLoginSuccessEvent); } // 利用規約が同意された void OnAgreedEvent() { // ログインAPIを叩く NetworkManager.Login(onLoginSuccess: OnLoginSuccessEvent); } // ログイン完了 void OnLoginSuccessEvent() { LoadNextScene(); }
ぶっちゃけこのレベルなら個人的には許容できるのですが、私が実際に経験したコードだとログイン一つでこれを遥かに超える複雑な作りになっていました。
なのですが、あまり具体的なことは社外秘のためとりあえずこの範囲で...
上記のような処理を、できるなら一つの関数の中で何をやっているのかすべて理解できるようにしたいですね。
まず最初に考えたやり方🤔
最初はコルーチンでやってしまおうと思いました。
IEnumerator LoginFlow() { if (!HasReadTermOfUse) { var dialog = OpenTermOfUseDialog(); // ダイアログを表示している間ずっと待機し続ける yield return dialog; // 終了したら承認されたかどうかの判定を行う // 利用規約に同意してもらえなかったらそこで終了 if (!dialog.Agreed) { yield break; } } // ログインAPIを叩く var login = NetworkManager.Login(); yield return login; // ログインに成功したのかどうか // 失敗したなら終了 if (!login.Success) { yield break; } LoadNextScene(); }
これで一つの関数内に収めることができます
が、いちいちAsyncOperation的なやつを取らないといけないのが面倒くさいです。
UniRxを使えばFromCoroutineでどうにかなるものの、StartCoroutineを呼び出す元になるスクリプトがないと動かせないのも欠点です。
みたいなことを会社の同僚と話していて「そういう事やりたいならUniTaskってのがあるから、それ使ってみては?」
という案が出ました。
というわけで早速導入してみました。
参考になった記事
UniRx.Async(UniTask)機能紹介 - Qiita
当初、Incremental Compilerなるものが必要で、
しかもそれはPreview版とのことだったので若干UniTaskは避けていたのですが、どうやらそれは昔の話で最近は違うらしいと知りました。
Incremental Compilerが不要に!😇
neue cc - UniTask(UniRx.Async)から見るasync/awaitの未来
ということでこれで安心して導入できます!!
async UniTask LoginFlow() { // ダイアログを表示して結果が返ってくるまで待機する。 if (!HasReadTermOfUse) { var agreed = await OpenTermOfUseDialog(); if (!agreed) { return; } } // ログインAPIを叩く var success = await NetworkManager.Login(); // エラーだったら終了 if (!success) { return; } LoadNextScene(); }
よりスッキリしました!
戻り値をawaitで待機しながら受け取れるのは便利ですね!
とはいえ全部をいきなりUniTaskに置き換えるのは難しくない?
私がUniTaskを導入したのはプロジェクトのかなり終盤でした。
そのため、全てをUniTaskに置き換えることはできないのですが、それでもやりようはあります。
例えばダイアログ関係はこのように書き換えることができます。
// 元のコード public void ShowDialog(string title, string message, Action onClose) { var prefab = Resources.Load("HogeHogeDialog"); var dialog = Instantiate(prefab); dialog.Initialize(title, message, onClose); } // これを元に改造 public UniTask ShowDialogAsync(string title, string message) { var source = new UniTaskCompletionSource(); var prefab = Resources.Load("HogeHogeDialog"); var dialog = Instantiate(prefab); dialog.Initialize(title, message, () => { source.TrySetResult(); }); return source.Task; }
UniTaskCompletionSource
UniTaskCompletionSourceというやつで終了時にTrySetResultすれば元の実装をいじくる必要なくUniTaskで待てるように改造できます!
これも最初は自分はまったく知らなかった機能で、知る前までは毎回
var isDone = false; dialog.Initialize(title, message, () => { isDone = true; }); await UniTask.WaitUntil(() => isDone);
こんな風に書いてました。
UniTaskの中身を解析してみよう🤔
どう動いているのか非常に気になったので、中身を解析してみました。
PlayerLoopHelper
コルーチンの場合、StartCoroutineを走らせるためのMonoBehaviourがどうしても必要でした。
でしたが、PlayerLoopを上書きできるようになったので、この仕組みで直接UniTaskを走らせる用のUpdate関数を登録しているようでした。
PlayerLoopに関しての参考リンク
tsubakit1.hateblo.jp
PlayerLoopHelper.csの中身
public static class PlayerLoopHelper { public static SynchronizationContext UnitySynchronizationContext => unitySynchronizationContetext; public static int MainThreadId => mainThreadId; static int mainThreadId; static SynchronizationContext unitySynchronizationContetext; static ContinuationQueue[] yielders; static PlayerLoopRunner[] runners;
yieldersとrunnersがどうにも気になります。
PlayerLoopHelperのInitというメソッドがRuntimeInitializeOnLoadMethodによって起動時に呼び出されているようです。
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] static void Init() { // capture default(unity) sync-context. unitySynchronizationContetext = SynchronizationContext.Current;
Initialize内では以下のような感じになっていました。
InsertRunnerでループ処理を追加しています。
public static void Initialize(ref PlayerLoopSystem playerLoop) { yielders = new ContinuationQueue[7]; runners = new PlayerLoopRunner[7]; var copyList = playerLoop.subSystemList.ToArray(); copyList[0].subSystemList = InsertRunner(copyList[0], typeof(UniTaskLoopRunners.UniTaskLoopRunnerYieldInitialization), yielders[0] = new ContinuationQueue(), typeof(UniTaskLoopRunners.UniTaskLoopRunnerInitialization), runners[0] = new PlayerLoopRunner()); copyList[1].subSystemList = InsertRunner(copyList[1], typeof(UniTaskLoopRunners.UniTaskLoopRunnerYieldEarlyUpdate), yielders[1] = new ContinuationQueue(), typeof(UniTaskLoopRunners.UniTaskLoopRunnerEarlyUpdate), runners[1] = new PlayerLoopRunner()); copyList[2].subSystemList = InsertRunner(copyList[2], typeof(UniTaskLoopRunners.UniTaskLoopRunnerYieldFixedUpdate), yielders[2] = new ContinuationQueue(), typeof(UniTaskLoopRunners.UniTaskLoopRunnerFixedUpdate), runners[2] = new PlayerLoopRunner()); copyList[3].subSystemList = InsertRunner(copyList[3], typeof(UniTaskLoopRunners.UniTaskLoopRunnerYieldPreUpdate), yielders[3] = new ContinuationQueue(), typeof(UniTaskLoopRunners.UniTaskLoopRunnerPreUpdate), runners[3] = new PlayerLoopRunner()); copyList[4].subSystemList = InsertRunner(copyList[4], typeof(UniTaskLoopRunners.UniTaskLoopRunnerYieldUpdate), yielders[4] = new ContinuationQueue(), typeof(UniTaskLoopRunners.UniTaskLoopRunnerUpdate), runners[4] = new PlayerLoopRunner()); copyList[5].subSystemList = InsertRunner(copyList[5], typeof(UniTaskLoopRunners.UniTaskLoopRunnerYieldPreLateUpdate), yielders[5] = new ContinuationQueue(), typeof(UniTaskLoopRunners.UniTaskLoopRunnerPreLateUpdate), runners[5] = new PlayerLoopRunner()); copyList[6].subSystemList = InsertRunner(copyList[6], typeof(UniTaskLoopRunners.UniTaskLoopRunnerYieldPostLateUpdate), yielders[6] = new ContinuationQueue(), typeof(UniTaskLoopRunners.UniTaskLoopRunnerPostLateUpdate), runners[6] = new PlayerLoopRunner());
InsertRunnerの中身を見たところ、yieldLoopというのとrunnerLoopというループがあるのですが、
これをUnityの各PlayerLoopに2つずつ挿入しています。
このループの中で実際にやっている処理はContinuationQueueとPlayerLoopRunnerというやつをそれぞれ走らせています。
yieldLoop内でContinuationQueueのRunを走らせて
runnerLoop内でPlayerLoopRunnerのRunを走らせています。
では次にContinuationQueueとPlayerLoopRunnerを見てみます。
ContinuationQueue
内部的にはactionListというActionの配列を管理しているようです。
そしてこれをRun実行時に呼び出している。
Enqueueというメソッドが用意されていて、これを呼び出すことでActionが登録できそうです。
ではこのEnqueueはどこから呼び出されているのだろう?
と思って追ってみたら、PlayerLoopHelperで管理しているようでした。
イメージとしては以下の図のような感じでしょうか。
では、次にこのAddActionとAddContinautionがどこから呼び出されているか調べてみます。
ReusablePromise.csが気になるので見てみます。
PlayerLoopReusablePromiseBaseというクラスのIsCompletedというメソッドで呼び出されていました。
PlayerLoopReusablePromiseBase
public override bool IsCompleted { get { if (Status == AwaiterStatus.Canceled || Status == AwaiterStatus.Faulted) return true; if (!isRunning) { isRunning = true; ResetStatus(false); OnRunningStart(); #if UNITY_EDITOR TaskTracker.TrackActiveTask(this, capturedStackTraceForDebugging); #endif PlayerLoopHelper.AddAction(timing, this); } return false; } }
PlayerLoopReusablePromiseBaseの実装を見ればわかりますが、IPlayerLoopというやつを実装していました。
public abstract class PlayerLoopReusablePromiseBase : ReusablePromise, IPlayerLoopItem {
これはMoveNextというメソッドを提供するインターフェースのようで、
MoveNextメソッドはPlayerLoopReusablePromiseBaseを継承した先で実装されていました。
WaitUntilPromise
WaitWhilePromise
YieldPromise
PlayerLoopRunnerは基本的にこのIPlayerLoopItemを配列で管理して、毎ループ時に各IPlayerLoopItemのMoveNextを呼び出しているようです。
次はUniTaskを見ます。
AddActionメソッドにログを仕込んでみました。
で、下記のようなサンプルコードを書いてみました。とりあえず3秒間待つタスクです。
bool isDone = false; private void Awake() { Run().Forget(); StartCoroutine(WaitCoroutine()); } private IEnumerator WaitCoroutine() { yield return new WaitForSeconds(3.0f); isDone = true; } private async UniTask Run() { Debug.Log("Run Start!!"); await UniTask.WaitUntil(() => isDone); Debug.Log("Run End!!"); }
呼ばれています!
呼び出し先を見てみると、PlayerLoopReusablePromiseBaseのIsCompletedというプロパティで呼び出されているのがわかります。
PlayerLoopReusablePromiseBaseのIsCompletedを呼び出しているのはUniTaskのIsCompletedプロパティのようです。
UniTask
IsCompleted
[DebuggerHidden] public bool IsCompleted { get { return awaiter == null ? true : awaiter.IsCompleted; } }
UniTaskがawaiterというフィールドを持っていることがわかります。
このawaiterにPlayerLoopReusablePromiseBaseのインスタンスを設定しているということになります
そして、これを呼び出しているのはAwaiterというやつのようですAwaiterはUniTask内に定義されています。
Awaiter
Awaiter
public struct Awaiter : IAwaiter { readonly UniTask task; [DebuggerHidden] public Awaiter(UniTask task) { this.task = task; }
こいつの中にUniTaskが内包されているようです。
で、実際はAwaiterのIsCompletedからUniTaskのプロパティにアクセスしているようです。
IAwaiter
IAwaiterの定義自体はこんな感じです。
public interface IAwaiter : ICriticalNotifyCompletion { AwaiterStatus Status { get; } bool IsCompleted { get; } void GetResult(); }
ICriticalNotifyCompletion
ICriticalNotifyCompletionはちょっとよくわからないので調べます。
どうもTaskでも使用されているインターフェースのようです。
.NET Framework標準で提供されているTaskAwaiter/TaskAwaiter型はINotifyCompletionインターフェースを継承するICriticalNotifyCompletionインターフェースを実装しています。
参考記事
このIsCompletedはどこで呼ばれているのか?
IsCompleted内部にログを仕込んでみます。
どうやらAsyncUniTaskMethodBuilderとかいうやつが居るようで、そいつが呼び出しているようです。
では今度はこいつのStartメソッドに同じようなログを仕込んでみましょう。
これ以上先はもう何もありません。
このAsyncUniTaskMethodBuilderが何者なのか?といろいろ調べていたら、これはどうもコンパイラ側が使用するクラスのようです
www.atmarkit.co.jp
IAsyncStateMachineというやつが何か関連がありそうです。
コイツについて調べてみましょう。
docs.microsoft.com
非同期メソッドに生成されるステート マシンを表します。 この型はコンパイラでのみ使用されます。
他にもこのような記事がありました。
blog.xin9le.net
・async/awaitは何かの糖衣構文で、実際はコンパイラがもっと複雑な形に変換している
・exeやdllには変換後の形で格納されている
とのことなので、試しにILSpyで中身を開いてみます。
.method /* 06000006 */ private hidebysig instance valuetype [UniRx.Async]UniRx.Async.UniTask Run () cil managed { .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = ( 01 00 12 54 65 73 74 43 6f 64 65 2b 3c 52 75 6e 3e 64 5f 5f 36 00 00 ) .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x2080 // Code size 59 (0x3b) .maxstack 2 .locals /* 11000001 */ init ( [0] class TestCode/'<Run>d__6', [1] valuetype [UniRx.Async]UniRx.Async.CompilerServices.AsyncUniTaskMethodBuilder ) IL_0000: newobj instance void TestCode/'<Run>d__6'::.ctor() /* 06000012 */ IL_0005: stloc.0 IL_0006: ldloc.0 IL_0007: ldarg.0 IL_0008: stfld class TestCode TestCode/'<Run>d__6'::'<>4__this' /* 04000008 */ IL_000d: ldloc.0 IL_000e: call valuetype [UniRx.Async]UniRx.Async.CompilerServices.AsyncUniTaskMethodBuilder [UniRx.Async]UniRx.Async.CompilerServices.AsyncUniTaskMethodBuilder::Create() /* 0A000012 */ IL_0013: stfld valuetype [UniRx.Async]UniRx.Async.CompilerServices.AsyncUniTaskMethodBuilder TestCode/'<Run>d__6'::'<>t__builder' /* 04000007 */ IL_0018: ldloc.0 IL_0019: ldc.i4.m1 IL_001a: stfld int32 TestCode/'<Run>d__6'::'<>1__state' /* 04000006 */ IL_001f: ldloc.0 IL_0020: ldfld valuetype [UniRx.Async]UniRx.Async.CompilerServices.AsyncUniTaskMethodBuilder TestCode/'<Run>d__6'::'<>t__builder' /* 04000007 */ IL_0025: stloc.1 IL_0026: ldloca.s 1 IL_0028: ldloca.s 0 IL_002a: call instance void [UniRx.Async]UniRx.Async.CompilerServices.AsyncUniTaskMethodBuilder::Start<class TestCode/'<Run>d__6'>(!!0&) /* 2B000001 */ IL_002f: ldloc.0 IL_0030: ldflda valuetype [UniRx.Async]UniRx.Async.CompilerServices.AsyncUniTaskMethodBuilder TestCode/'<Run>d__6'::'<>t__builder' /* 04000007 */ IL_0035: call instance valuetype [UniRx.Async]UniRx.Async.UniTask [UniRx.Async]UniRx.Async.CompilerServices.AsyncUniTaskMethodBuilder::get_Task() /* 0A000014 */ IL_003a: ret } // end of method TestCode::Run
AsyncUniTaskMethodBuilder::Create()とかAsyncUniTaskMethodBuilder::Start()とか呼ばれてます!
ILコードがよくわかりませんが、callとか呼ばれてますね。
(この辺もそのうち勉強してみたい)
[参考リンク]
www5b.biglobe.ne.jp
次に気になったこと
どうやってAsyncUniTaskMethodBuilderを呼び出している?🤔
これがちょっと謎でしたが、UniTask.csの定義をよく見るとAsyncMethodBuilderというやつがあります!
[AsyncMethodBuilder(typeof(AsyncUniTaskMethodBuilder))] // ←これ!!!! public partial struct UniTask : IEquatable<UniTask>
AsyncMethodBuilderに関して参考リンク
qiita.com
つまりawaitが呼ばれたとき、その戻り値の型に対応されたAsyncMethodBuilderが呼び出される
ということでしょうか?
ここ、調べてもあまり出てこないのでちょっとわからない。
おそらくこんな感じ
試しに
UnsafeOnCompleted(Action act)
で受け取るデリゲートを最後に呼び出さないようにしてみたら
永遠にawaitから戻ってこれない状態になったので、おそらくこれは処理が終了したあとに呼び出さないといけないやつ。
GetAwaiterが何者なのか気になったけど、これは戻り値の型が独自定義型であろうとなんだろうと関係なく呼ばれるように見えます。
docs.microsoft.com
ちなみにGetAwaiterをコメントアウトするとawaitする箇所全てでコンパイルエラーになって死にます。
要約
全体のライフサイクルはこんな感じ?だと思われる🤔
オレオレTaskを作ってみよう!
試しに自分でawaitできるタスクを作ってみます。
UniTaskはstructで定義されていましたが、structだとデフォルトコンストラクタが定義できないので今回はclassで定義しています。
using System.Runtime.CompilerServices; using System; namespace Bz.Brotherhood { [AsyncMethodBuilder(typeof(BzTaskBuilder))] public class BzTask { IAwaiter awaiter; public BzTask() { UnityEngine.Debug.Log($"BzTask.Constructor"); } public BzTask(IAwaiter awaiter) { this.awaiter = awaiter; } public IAwaiter GetAwaiter() { UnityEngine.Debug.Log($"BzTask.GetAwaiter"); return new Awaiter(awaiter); } private struct Awaiter : IAwaiter { IAwaiter awaiter; public Awaiter(IAwaiter awaiter) { this.awaiter = awaiter; } public bool IsCompleted { get { var isCompleted = true; if (awaiter != null) { isCompleted = awaiter.IsCompleted; } UnityEngine.Debug.Log($"Awaiter.IsCompleted :{isCompleted}"); return isCompleted; } } public void OnCompleted(Action moveNext) { UnityEngine.Debug.Log("Awaiter OnCompleted"); awaiter?.OnCompleted(moveNext); } public void UnsafeOnCompleted(Action moveNext) { UnityEngine.Debug.Log("Awaiter UnsafeOnCompleted"); awaiter?.UnsafeOnCompleted(moveNext); } public void GetResult() { UnityEngine.Debug.Log("Awaiter GetResult"); awaiter?.GetResult(); } } }
AsyncMethodBuilderAttributeは存在しない?
コードを書いてみたのですがエラーが出ます。
UniTaskはAsyncMethodBuilderAttributeを定義しているようでした。
blog.meilcli.net
また、現時点でほとんどのプラットフォームではAsyncMethodBuilderAttribute属性も自分で定義する必要があります。
理由はよくわからないのですが、とにかくこっちで定義しないと駄目らしい。
以下のようなオレオレタスクビルダーを作ってみました(といってもほぼUniTaskBuilderのコピペ)
namespace Bz.Brotherhood { public struct BzTaskBuilder { IAwaiter awaiter; // 1. Static Create method. public static BzTaskBuilder Create() { UnityEngine.Debug.Log("BzTaskBuilder.Create"); var builder = new BzTaskBuilder(); return builder; } // 2. TaskLike Task property. public BzTask Task { get { UnityEngine.Debug.Log("BzTaskBuilder.Task"); return new BzTask(awaiter); } } // 3. SetException public void SetException(Exception exception) { UnityEngine.Debug.Log("BzTaskBuilder.SetException"); } // 4. SetResult public void SetResult() { UnityEngine.Debug.Log("BzTaskBuilder.SetResult"); } // 5. AwaitOnCompleted public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine { UnityEngine.Debug.Log("BzTaskBuilder.AwaitOnCompleted"); this.awaiter = awaiter as IAwaiter; this.awaiter.OnCompleted(stateMachine.MoveNext); } // 6. AwaitUnsafeOnCompleted [SecuritySafeCritical] public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine { UnityEngine.Debug.Log("BzTaskBuilder.AwaitUnsafeOnCompleted"); this.awaiter = awaiter as IAwaiter; this.awaiter.UnsafeOnCompleted(stateMachine.MoveNext); } // 7. Start public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine { UnityEngine.Debug.Log("BzTaskBuilder.Start"); stateMachine.MoveNext(); } // 8. SetStateMachine public void SetStateMachine(IAsyncStateMachine stateMachine) { UnityEngine.Debug.Log("BzTaskBuilder.SetStateMachine"); } } }
そして最後にこんなテストコードを試しに作ってみます。
void Start() { Debug.Log("Start"); TestTask(); } async BzTask HelloWorldTask() { Debug.Log("Hello World!!"); } async BzTask TestTask() { Debug.Log("TestTask Start"); await HelloWorldTask(); Debug.Log("TestTask End"); }
これでも一応エラーになりません(まったく待機とかしないけど)
そして実行してみます。
動いた!
現在のままだとawaitとか言っておきながら全く待機していないので今度は例えば60フレーム後に処理が進行するようにしてみたいです。
このBzTaskのIAwaiterに何か実装を渡せばその実装に合わせて動くようになっています。
IAwaiterの中身はUniTaskのものとほぼ同じです。
public interface IAwaiter : ICriticalNotifyCompletion { // BzTask.Awaiter内部で呼び出される。 bool IsCompleted { get; } void GetResult(); }
そしてこのIAwaiterを実装した60フレーム待機するクラスを作ってみます。
private interface IOnUpdate { bool OnUpdate(); } private class FrameAwater : IAwaiter, IOnUpdate { int frame = 0; int waitFrameCount = 0; Action moveNext = null; public FrameAwater(int waitFrameCount) { this.waitFrameCount = waitFrameCount; } public bool OnUpdate() { Debug.Log($"OnUpdate[frame:{frame}, waitFrameCount:{waitFrameCount}]"); if (frame == waitFrameCount) { moveNext(); return false; } frame += 1; return true; } // BzTask.Awaiter内部で呼び出される。 public bool IsCompleted { get { return false; } } public void GetResult() { } public void OnCompleted(Action continuation) { moveNext = continuation; } public void UnsafeOnCompleted(Action continuation) { moveNext = continuation; } }
そしてMonoBehaviour側にIOnUpdateをUpdate時にコールする、みたいなやつを追加します。
List<IOnUpdate> list = new List<IOnUpdate>(); List<IOnUpdate> removeList = new List<IOnUpdate>(); void Update() { removeList.Clear(); foreach (var update in list) { if (!update.OnUpdate()) { removeList.Add(update); } } foreach (var item in removeList) { list.Remove(item); } }
そして試しに60フレーム待機するように以下のように書き換えてみます。
void Start() { Debug.Log("Start"); TestTask(); } BzTask WaitForFrame(int frame) { Debug.Log("WaitForFrame"); var frameAwaiter = new FrameAwater(frame); list.Add(frameAwaiter); return new BzTask(frameAwaiter); } async BzTask TestTask() { Debug.Log("TestTask Start"); await WaitForFrame(60); Debug.Log("TestTask End"); }
意図した通りに動くのであれば、
60フレーム経過したらUnsafeOnCompleted()で受け取ったデリゲートを実行し、それによってタスクは終了するはずです。
しっかり60フレーム待機してくれています!
長くなりましたがたまにこうやって他の人の作ったプラグインのソースコードを除くのは楽しいですね。
ここまで読んでくださってありがとうございました!