# UniTaskを使おう! **※こちらの記事は2020年12月に執筆したものです。現在とは仕様が違う可能性があります。予めご了承ください。** # はじめに Unityで**async/await**やってますか?やってない? そんなあなたに**UniTask**! # 目次 - [誰向けの記事なんだこれは](#誰向けの記事なんだこれは) - [UniTask とは](#UniTask-とは) - [UniTaskを使う条件](#UniTaskを使う条件) - [非同期処理ってなに? async/await って?](#非同期処理ってなに?-async/await-って?) - [Unityで使える非同期処理](#Unityで使える非同期) - [Taskとの違い](#Taskとの違い) - [UniTaskが提供する機能](#UniTaskが提供する機能) - [UniTask , Unitask\<T>型](#UniTask-,-Unitask\<T>型) - [UniTaskCompletionSource](#UniTaskCompletionSource) - [UniTaskVoid型](#UniTaskVoid型) - [UniTaskの静的メソッドたち](#UniTaskの静的メソッドたち) - [いろいろなAwaiter](#いろいろなAwaiter) - [コルーチン、UniRxとの相互変換](#コルーチン,-UniRxとの相互変換) - [UniTaskTracker](#UniTaskTracker) - [キャンセル](#キャンセル) - [別の非同期との使い分け](#別の非同期との使い分け) - [結局UniTaskを使うと何がいいの?](#結局UniTaskを使うと何がいいの?) - [まとめ](#まとめ) - [おまけ(DOTweenをawaitできるようにしよう!)](#おまけ(DOTweenをawaitできるようにしよう!)) - [あとがき](#あとがき) # 誰向けの記事なんだこれは 主にUnityやC#を触って**結構慣れてきた人向け**です。 async/awaitの説明は軽くしかしないので少し理解してる人が読むといいかと思います。 ところどころ難しポイントはあるのでそういうとこは読み飛ばしちゃってください。 全然わっかんねぇって人は、将来UniTaskを勉強したくなったらこの記事を見返してみてください。 ネット上にUniTaskの情報は結構乗ってますが、この記事でUniTaskという存在そのものを知るきっかけになってくれたら良いかなと思います。 リファレンス的な記事なので良かったら何度も読み返してみてください。 # UniTask とは **『Unityでasync/awaitを最高のパフォーマンスで実現するライブラリ』** ([公式](http://tech.cygames.co.jp/archives/3256/)より引用) UniTaskは2019年12月1日現在、まだAssetStoreに公開されていないので[github](https://github.com/Cysharp/UniTask)から入手します。 昔はUniRxの一部だったので名前空間が今でもUniRx.Asyncという歴史的経緯があります。 # UniTaskを使う条件 UniTaskはC#7の機能に依存しています。 ではUnityのどのバージョンから使えるんだ?ってことで対応表を作ってみました。 | Unityのバージョン | C#のバージョン | |:------------:|:------------:| |2017.4.x |6.0 | |2018.2.x |6.0 | |2018.3.x |7.3 | |2018.4.x |7.3 | |2019.x |7.3 | 対応表を見ると、C#7以上に対応しているのは、Unity2018.3以上です。 Incremental Compilerを導入すれば2018.2からでも使えますが、よっぽどの理由がない限り使わなくていいです。まあ今は2018.2以下を使っている人はあまりいないと思うので問題ないですかね。 # 非同期処理ってなに? async/await って? ## 非同期処理とは - 関数の**結果を待たず**に次の処理へ進む処理のこと - 処理を**別のタイミング**で実行すること なに言ってんだ? とりあえず次のコードを見てみましょう ```C# Debug.Log("はじまり~"); StartCoroutine(TimerDebug("こるーちん!",3)); //3秒後に出力 Debug.Log("おわり~"); IEnumerator TimerDebug(object obj,float time){ yield return new WaitForSeconds(time); Debug.Log(obj); } ``` よくあるコルーチンのコードですね。では出力結果はどうなるでしょうか? ![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260914/b012047f-e32c-eda3-fca1-e57f0bc2e781.png) あれ?プログラムの順番は「はじまり~」→「こるーちん!」→「おわり~」なのに、順番が変わりました。 この例では ```C# StartCoroutine(TimerDebug("こるーちん!",3)); ``` この関数の**結果を待たず**に次の行の処理が行われているのです。要するに、通常通り逐次実行されるものと、コルーチンを実行するもの、**二手に分かれて**います。 **これが非同期処理です。** な~んだもうみんなやってたんだ。 ## async/await 読んで字のごとく、非同期を待つ。です。 さっきはコルーチンを待たずに2手に分岐して処理を行いました。 async/awaitでは、**非同期メソッドがその結果を得るまで待つ**ことができます。 難しいですが、あとでちゃんと解説します。 ```C# private async Task HogeAsync(){ //メソッド宣言時にasync修飾子をつける Debug.Log("まつ!"); await Task.Delay(1000); //1000ミリ秒待つ } await HogeAsync(); //使うときはawaitをつける ``` # Unityで使える非同期 ## コルーチン 先ほどの例でだしたやつです。IEnumeratorを返り値とし、yieldなんちゃらを使うやつです。 大体はStartCoroutineで起動します。その場合GemeObjectと結びついていて扱いやすいですが、返り値を返すことができないというデメリットも抱えています。 try-catchブロック内では、yieldを使いことができないため、例外処理も困難です。 一応これらの問題は引数にデリゲートを渡すことで解消できますが冗長になってしまいがちです。 ## Invokeメソッド ~~これつかってる人いるの…?~~ ```C# void HogeMethod(){ Debug.Log("ほげほげ"); } Invoke(nameof(HogeMethod),3); //3秒後に出力 ``` コルーチンのStartCoroutine同様、メソッドを遅延起動させるメソッドです。手軽っちゃ手軽。 返り値を返せないし引数を渡すこともできません。 ## Task C#標準の機能です。 Unityでも使えます。 ## UniRx 非同期処理向けライブラリです。イベント処理のイメージが強い印象です。 ## UniTask そして今回紹介するのがこのUniTask UniTask + async/await で何でもできます。 ```C# var result = await HogeUniTaskAsync(); //ちゃんとメインスレッドに戻ってくる Debug.Log(result); ``` # UniTaskでできること - C#のTaskをそのままUniTaskで書ける - Unityイベントをawaitできる - 何をawaitしてるかが見れる(Editor拡張) こんな感じですかね。基本はC#のTaskが優秀で、それとほぼほぼ同じく使えるのが強いポイントかと。 # Taskとの違い じゃあUniTaskじゃなくてTaskで良くない?ってことで違いをまとめてみました || Task | UniTask | |:-----------|:------------:|:------------:| | 機能 | Unityで使うには<br>不要な機能が多い | Unity用に最適化 | | サイズ | Unityで使うには<br>無駄が多い | コンパクト | | 最低C#<br>バージョン | 5.0 | 7.0 | | await中のタスク | 見れない | 見れる | こんな感じでしょうか。 C#のTaskにはUnityで使うにはいらない機能が多いようです。 UniTaskはその不要な機能をカットしたコンパクトなものになっています。 また、グラフでは省きましたが、依存しているものの違いもあります。 UniTaskはUnityのPlayerLoopを使っています。そのためUnityとの親和性が高いです。 # UniTaskが提供する機能 ## UniTask , Unitask\<T>型 TaskとTask\<T>をUnity向けに最適化したものです。 基本的に**TaskをUniTaskに書き換えるだけ**でUniTaskを使うことができます。 ```C# // Task private async Task HogeAsync(){ Debug.Log("まつ!"); await Task.Delay(1000); } // UniTask private async UniTask HogeAsync(){ await Task.Delay(1000); //Taskは自動でUniTaskに変換されて返される Debug.Log("まつ!"); } ``` 返り値をUniTaskに変えただけです。すごい! 呼び出すときはこんな感じ ```C# private async void Start(){ Debug.Log("はじめ!"); await HogeAsync(); Debug.Log("おわり!"); } ``` ![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260914/29230714-1436-ff80-afef-f6a542816c9f.png) ちゃんと1秒待ってからメインに戻ってきてくれました。 これが非同期を待つって感じです。便利ですね。 ジェネリックでも同じです。 ```C# private async UniTask<int> SumAsync(int num1,int num2){ return await UniTask.Run(() => num1 + num2); } ``` ## UniTaskCompletionSource CompletionSourceからの生成も同様にTaskをUniTaskに書き換えるだけで使えます。 ```C# var taskCompletionSource = new TaskCompletionSource<int>(); // Task var uniTaskCompletionSource = new UniTaskCompletionSource(); // UniTask ``` ## UniTaskVoid型 async/awaitには fire and forget という概念があります。 投げっぱなしってことですね。 async void 関数がそれにあたります。 ですがこれだと通常のTaskで動いてしまうのでUniTaskには**UniTaskVoid**構造体が用意されています。 ```C# async void HogeTaskAsync(){ //これだとTaskで動いちゃう Debug.Log("ほげ"); } async UniTaskVoid HogeUniTaskAsync(){ //UniTaskで動かす場合はこう Debug.Log("ほげ"); } ``` ## UniTaskの静的メソッドたち UniTaskが提供する静的メソッドの中から一部を紹介します。 ### **UniTask.Delay** Task.Delayとほぼ同じ。 引数で指定した秒数分待機できます。 これは、後に紹介するものも同様に、時間やフレームで待機するメソッドは、引数にUnityの**どのタイミングで計測するか**(デフォルトはUpdate())を指定できます。便利。 ここがTaskと違うところですね。 ```C# // FixedUpdateのタイミングで10秒待つ await UniTask.Delay(1000, delayTiming : PlayerLoopTiming.FixedUpdate); ``` ### **UniTask.DelayFrame** UniTask.Delayのフレーム待機版です。 指定したフレーム数待機できます。 ```C# // FixedUpdateのタイミングで10フレーム待つ await UniTask.DelayFrame(10,PlayerLoopTiming.FixedUpdate); ``` ### **UniTask.Yield** 指定したタイミングで1フレーム待機できます。 コルーチンの yield return null に近いです。 ```C# // Updateのタイミングで1フレーム待機する await UniTask.Yield(); // 次のFixedUpdateのタイミングまで待機する await UniTask.Yield(PlayerLoopTiming.FixedUpdate); ``` ここで注意?というか良いところと言いますか。 ```C# // 次のFixedUpdateのタイミングまで待機する await UniTask.Yield(PlayerLoopTiming.FixedUpdate); ``` これを実行した次の行からFixeUpdateのタイミングで処理が走るという特性があります。 ```C# // 次のFixedUpdateのタイミングまで待機する await UniTask.Yield(PlayerLoopTiming.FixedUpdate); // ここからFixedUpdateのタイミングで実行される Debug.Log("ほげ") Debug.Log("ふが") ``` Updateのタイミングに戻したいときは ```C# // 1フレーム待ってからUpdate()のタイミングに戻る await UniTask.Yield(); //この行以降はUpdateのタイミング Debug.Log("ぴよ") ``` これでUpdateのタイミングに戻ります。 **** ### ※余談 ```C# await UniTask.SwitchToThreadPool(); ``` このメソッドにより、実行コンテキストをスレッドプールに変更できます。 それを戻すのにもUniTask.Yieldを使うこともできます。 しかしUniTask.SwitchToMainThreadというメソッドもあります。 読んで字のごとくメインスレッドにコンテキストを変更するメソッドです。 では何が違うのでしょうか?次のコードを見てください。 ```C# // 現在のコンテキストに関わらず1フレーム待ってからメインスレッドに変更する await UniTask.Yield(); // 現在メインスレッドにいるなら何もせず通過する await UniTask.SwitchToMainThread(); ``` この二つのメソッド、こんな感じで若干の違いがあります。注意しましょう~ **** ### **UniTask.Run** Task.Runと同じです。 同期処理をまとめて別スレッドで実行します。 ```C# async UniTask<string> HogeUniTaskAsync(string str1, string str2){ return await UniTask.Run( () => str1 + str2 ); } ``` しかし、別スレッド上では実行できないメソッドというものが存在します。注意しましょう。 ```C# async UniTask HogeUniTaskAsync(GameObject obj){ return await UniTask.Run( () => Instantiate(obj)); //Instantiateはメインスレッドでしか使えない } ``` ### **UniTask.WhenAll** 指定した**全てのUniTaskが完了するまで待機**できます。 UniTaskの型引数が異なる型でもOKです。 ```C# var task1 = UniTask.DelayFrame(10); // int型 var task2 = UniTask.Run(() => "完了!"); // string型 //結果をタプルで受け取れる var (re1, re2) = await UniTask.WhenAll(task1, task2); Debug.Log(re1); // 10 Debug.Log(re2); // 完了! ``` ### **UniTask.WhenAny** 指定した**UniTaskのどれかが1つが完了するまで待機**できます。 ```C# var task1 = UniTask.DelayFrame(10); var task2 = UniTask.Run(() => "完了!"); var task3 = subject.ToUniTask(); await UniTask.WhenAny(task1, task2, task3); Debug.Log("どれかが完了!"); ``` そして、どのUniTaskが完了したのかを、インデックスで取得することができます。 ![5.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260914/3e6f25dc-e7a4-4e91-fca4-96c428c70917.png) winArgumentIndexが完了したUniTaskのインデックスです。 さらに、型引数が全て一致しているなら、どのUniTaskが完了したのかをオブジェクトで直接取得できます。 ```C# foreach(var player in players){ var task = UniTask.WaitUntil(() => player.transform.position.y > 30) //ゴール .ContinueWith(() => player.Name); playersTasks.Add(task); } var (_,p) = await UniTask.WhenAny(playersTasks.ToArray()); Debug.Log(p); // 一番最初にゴールしたプレイヤーの名前 ``` ### **UniTask.WaitUntil** 指定した条件がtrueになるまで待機できます。 ```C# await UniTask.WaitUntil(() => isStart); // ゲームが始まるとtrueになるフラグ Debug.Log("ゲーム開始!"); ``` ### **UniTask.WaitWhile** 指定した条件がfalseになるまで待機できます。 ```C# await UniTask.WaitWhile(() => hp > 0); // hpが0未満になると完了する Debug.Log("負け……"); ``` ### **WaitUntilValueChanged** UniRxのObserveEveryValueChangedと同じ ```C# await UniTask.WaitUntilValueChanged(this.transform, x => x.position.y); Debug.Log("縦に動いた!"); ``` ### **ToCoroutine** UniTaskをコルーチンに変換するメソッドです。 インスタンスメソッドにも同じものがあります。 書き心地的にこっちはあまり使わない印象です。まとめて複数のUniTaskをコルーチンに変換する場合はこっちを使うといいかもしれません。 ```C# IEnumerator HogeCoroutine(){ yield return UniTask.ToCoroutine(() => HogeUniTaskAsync()); } ``` ## いろいろなAwaiter Awaiterとは何かを説明します。 まず復習。 さっきからさんざん使ってきた非同期メソッド。 **async**という修飾子を付けることでメソッドは非同期メソッドになります。 ```C# UniTask HogeMethod(){ //これは同期メソッド //..... async UniTask HogeAsyncMethod(){ //これは非同期メソッド //.... ``` asyncをつけた非同期メソッドはこのように待機できます ```C# await HogeAsyncHethod(); ``` じゃあawaitって何を元に待機してるの? その答えが**Awaiter**です。 あるオブジェクトにGetAwaiterメソッドが実装されていて、そこからAwaiterが取得できればコンパイラはawaitできると判断します。 ちなみに**拡張メソッドでも可**です。すなわち、既存のクラスを自分で拡張してawaitできるようにできるってわけです。 [おまけ(DOTweenをawaitできるようにしよう!)](#おまけ(DOTweenをawaitできるようにしよう!)) で解説しています。 プログラム中のawaitの実装に飛ぼうとすると、どのようにawaitされているかを見ることができます。 ![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260914/ead4b136-52bb-64e5-26ae-d52fb1a06314.png) ![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260914/583f78db-d3d5-1110-93a0-058f4e7c89a4.png) UniTaskには、この機能を利用してUnity機能の様々なAwaiterが用意されています。 ### **コルーチン** UniTaskを入れていると、コルーチンもawaitできます。 UniTaskは使いたいけどあのコルーチンメソッド書き直すの嫌だな~って思ったら、そのままawaitをつければいいので便利です。 ```C# await TimerDebug("1回目", 1); //1秒待ってからログ await TimerDebug("2回目", 3); //3秒待ってからログ await TimerDebug("3回目", 2); //2秒待ってからログ IEnumerator TimerDebug(object obj,float time){ yield return new WaitForSeconds(time); Debug.Log(obj); } ``` 上記のようにStartCoroutineを使わなくてもコルーチンが起動できます。 下記のコードとの違いがわかりますか? ```C# StartCoroutine(TimerDebug("1回目", 1)); StartCoroutine(TimerDebug("2回目", 3)); StartCoroutine(TimerDebug("3回目", 2)); ``` ### **uGUIイベント** ```C# // ボタンクリックを待機する await button.OnClickAsync(); // スライダーの値変更を待機する await slider.OnValueChangedAsync(); // インプットの編集終了を待機する await inputField.OnEndEditAsync(); // トグルの変更を待機する await toggle.OnValueChangedAsync(); ``` ### **AsyncOperation** ```C# await SceneManager.LoadSceneAsync("HogeScene"); //Sceneの非同期読み込みを待機する await Resources.LoadAsync<AudioClip>("SE/jump"); await SceneManager.LoadSceneAsync("HogeScene") //Progressから進行状況が取れる .ConfigureAwait(Progress.Create<float>(n => { Debug.Log($"現在{n * 100}%"); })); ``` ### **Unityイベント** ```C# // Awakeを待機 await this.AwakeAsync(); // Startを待機 await this.StartAsync(); // OnDestroyを待機 await this.OnDestroyAsync(); // OnCollisionEnterを待機 await this.GetAsyncCollisionTrigger().OnCollisionEnterAsync(); // OnEnableを待機 await this.GetAsyncEnableDisableTrigger().OnEnableAsync(); ``` **** ## コルーチン, UniRxとの相互変換 ### **UniTask → Coroutine** ToCoroutineメソッド ```C# IEnumerator TimerDebug(object obj,float time){ // UniTaskをコルーチンに変換して待機 yield return UniTask.Delay(TimeSpan.FromMilliseconds(time)).ToCoroutine(); Debug.Log(obj); } ``` ### **Coroutine → UniTask** ```C# // コルーチンをUniTaskに変換して待機(この場合はToUniTaskは省略できる) await TimerDebug("1回目", 1).ToUniTask(); ``` ### **UniRx → UniTask** ```C# var subject = Observable.Timer(TimeSpan.FromSeconds(10)); // 10秒待つストリーム // IObservableをUniTaskに変換 var task = subject.ToUniTask(); ``` 無限に続くIObserbavleからの変換に注意しましょう。一生完了しないUniTaskを作ってしまいます。 ### **UniTask → UniRx** ```C# var task = UniTask.Delay(1000); // UniTaskをIObservableに変換 var subject = task.ToObservable(); ``` ### **※余談** **IObservableもawait**することができます。 これはUniTaskの機能ではないのですが紹介しておきます。 ```C# var timerSubject = Observable.Timer(TimeSpan.FromSeconds(1)); await timerSubject; Debug.Log("時間経過した!"); ``` しかし、これだとTaskでawaitしてしまうので、UniTaskでawaitする場合はToUniTaskメソッドを使ってawaitすると良いと思います。 ```C# await Observable.Timer(TimeSpan.FromSeconds(1)).ToUniTask(); Debug.Log("時間経過した!"); ``` **** ## UniTaskTracker これはUnityエディター上でawaitを可視化できる機能です。 どこのUniTaskがawaitしてるのかや、状態も確認等ができます。 リークしてないか確認するのに便利ですね。 ![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260914/43b24abc-922d-51c5-c80e-e4f25e00c68b.png) # キャンセル 非同期処理は**キャンセル**のことを考えなければいけません。 なにかの理由でいらなくなったときにそれを捨てることをしないといけないのです。 それがまぁちょっとめんどくさいですが、大事なことなので紹介します。 Taskでは**CancellationToken**を使ってキャンセルを通知します。 UniTaskでも同じものを使います。UniTaskだからと言ってそういうのも全部勝手にやってくれる!ってわけではありません。 作り方 ```C# CancellationTokenSource source = new CancellationTokenSource(); CancellationToken token = source.Token; ``` はい。 **CancellationTokenSource**から**CancellationToken**を取得します。 ちょっとめんどくさいですね。 そしてCancellationTokenSourceの**Cancelメソッド**によってキャンセルを通知することができます。 ```C# source.Cancel(); // キャンセル状態にする ``` ```C# if(token.IsCancellationRequested){ //キャンセルされているとtrueになる Debug.Log("キャンセルされてる!"); } // キャンセルされていたらOperationCanceledExceptionを投げるというのを1行で書ける token.ThrowIfCancellationRequested(); ``` UniTaskのメソッドには大体CancellationTokenを指定できます。 なので、めんどくさがらずやってください。 ```C# // オーバーロードでCancellationTokenを指定できる await UniTask.Delay(1000, cancellationToken:token); await button.OnClickAsync(token); ``` ```C# TimeCountAsync(token).Forget(); async UniTaskVoid TimeCountAsync(CancellationToken token){ var count = 0; while(true){ await UniTask.DelayFrame(60,cancellationToken:token); Debug.Log(++count + "秒"); } } ``` ## UniTask.Run UniTask.RunはCancellationTokenを指定できないのでラムダ式外のCancellationTokenをキャプチャする必要があります。 ```C# async UniTask SaveAsync(CancellationToken token){ await UniTask.Run(() => { // キャンセル確認 token.ThrowIfCancellationRequested(); // 重い同期処理。 HeavyMethod(); // 終わった後もキャンセル確認 token.ThrowIfCancellationRequested(); }); } await SaveAsync("Resources\data.json", token); ``` ## GetCancellationTokenOnDestroy MonoBehaviourを使う場合は**GetCancellationTokenOnDestroy**を使うといいかと思います。 ```C# var token = this.GetCancellationTokenOnDestroy(); ``` CancellationTokenをGetCancellationTokenOnDestroy()により生成することで、**GameObjectが削除されると自動でキャンセル状態**にしてくれます。 **** OperationCanceledExceptionを投げられたUniTaskはキャンセル状態になります。 エラーログには出ませんが、それを良いことに自分で投げるのはやめましょう。 CancellationTokenはメソッドにどんどん受け渡ししていくものです。 必ずCancelメソッド経由で伝播されたキャンセル要求を見るようにしましょう。 ```C# // ダメな例 async UniTask<float> SaveAsync(string path,CancellationToken token){ return await UniTask.Run(() => { // ファイルが存在しないなら例外を吐く if(!File.Exists(path)) throw new OperationCanceledException(); ``` # 別の非同期との使い分け ## async/await よっぽどの理由がない限りTaskではなくUniTaskを使う ## UniTask VS Observable イベントや**結果が複数**になるときにはObservableを使う 結果が1つのときはUniTaskを使う Observableで書いてて、**コールバック地獄**になりそうなら手続き型で書けるUniTaskを使う UniRxはイベントと非同期の役割がありますが、非同期はUniTask一本でも良さそうです。 処理にもよるけど使いやすい方を使いましょう! ## UniTask VS コルーチン UniTaskが使い慣れたらコルーチンはいらなくなります。 こっちも使いやすい方を選択しましょう。 ## ~~UniTask VS Invoke~~ # 結局UniTaskを使うと何がいいの? 何度か言いますが、コルーチンやInvokeといった、他の非同期処理がいらなくなります。 StartCoroutineもInvokeもMonoBehaviourのメンバなため、MonoBehaviourでないと使えないという制約があります。 そういった制約もUniTaskならありません。 しかし、完全な上位互換とはいかず、結構キャンセルが面倒という問題もあります。 コルーチンはゲームオブジェクトがDestroyしたら勝手に消えてくれましたが、UniTaskはそうはいきません。 ですが、使いこなせば想像以上のパフォーマンスを見せてくれると思います。 めちゃくちゃ便利なのでとりあえず使ってみましょう! # まとめ UniTaskは**TaskをUnity用に最適化**、**使いやすく**なったつよつよライブラリ! **キャンセル**しっかりやる! UniTaskとasync/awaitでなんでもできるようになる! # おまけ(DOTweenをawaitできるようにしよう!) おまけとして[いろいろなAwaiter](#いろいろなAwaiter)で紹介したことを踏まえて、DOTweenをawaitできるようにしようと思います。 DOTweenとは、名前の通りtween系ライブラリです。結構使いやすいです。 [AssetStore](https://assetstore.unity.com/packages/tools/animation/dotween-hotween-v2-27676) から無料でダウンロードできます。 **そもそもAwaitableってなにをもってしたらAwatableなの?** Interfaceも実装しなくていいの? ↓ その通り ↓ ということは型で判断してるのではない? ↓ そういうこと ↓ じゃあどうやるの? ということで説明します。 ## Awaiter と Awaitable ### Awaitableになる条件 - **Awaiterを返すGetAwaiterという名前のメソッドを持つ** ```C# // Awaitableオブジェクトの例 public class HogeAwaitable{ public HogeAwaiter GetAwaiter(){ return new HogeAwaiter(this); } } ``` これはインスタンスメソッドでも拡張メソッドでも大丈夫です。 ### Awaiterになる条件 - **IsCompletedという名前のプロパティを持つ** - **INotifyCompletionインターフェイスを実装する** - **GetResultという名前のメソッドを持つ** ```C# // Awaiterの例 public struct HogeAwaiter : INotifyCompletion { public void OnCompleted(Action continuation){} // INotifyCompletionで実装される public bool IsCompleted{get;} public void GetResult(){} } ``` - **IsCompletedプロパティ** タスクが完了していればtrueを返す - **OnCompletedメソッド** IsCompletedがfalseなら引数のデリゲートを継続登録する - **GetResultメソッド** - 非同期処理の結果を返す。戻り値の型は何でも良く、ジェネリックやvoidで対応する。 AwaiterがINotifyCompletionを実装する以外、「○○という名前の~~を持つ」という条件になってます。このように、型ではなく名前で処理を振り分けることをダックタイピングと言います。 ## 実際にDOTweenを拡張してみる ### **準備** **汎用的にするためにIAwaiterインターフェースを定義する。** ```C# public interface IAwaiter : ICriticalNotifyCompletion{ bool IsCompleted{get;} void GetResult(); } ``` ※補足 今回はGetResultの返り値はvoidですが、返り血が欲しい場合は以下のようにしましょう ```C# public interface IAwaiter<out T> : IAwaiter{ new T GetResult(); } ``` ちなみに両方ともUniTaskの実装とほぼ一緒です。 **** **DOTweenAsyncExtentionクラスを定義する** ```C# public static class DOTweenAsyncExtention{} ``` ### **実装** DOTweenAsyncExtentionクラスにメンバを追加します。 **TweenAwaiter構造体** ```C# struct TweenAwaiter : IAwaiter{ Tween tween; public TweenAwaiter(Tween tween) => this.tween = tween; public void OnCompleted(Action continuation){ UnsafeOnCompleted(continuation); } // ICriticalNotifyCompletionにより実装されたメソッド // OnCompletedと同じような意味合い // awaitが展開されるとコールバックが登録される public void UnsafeOnCompleted(Action continuation){ tween.OnKill(() => { continuation(); }); } // tweenが終わったらtrue public bool IsCompleted => tween.IsComplete(); public void GetResult(){} } ``` **** **GetAwaiterメソッド** ```C# // Awaiterを返すGetAwaiter public static IAwaiter GetAwaiter(this Tween self){ return new TweenAwaiter(self); } ``` **** デモということでキャンセル対応はしてないですが許してください…… コードと実行結果はこんな感じです。 ```C# private async void Start(){ await transform.DOMove(new Vector3(-3, 3, 0), 1); // 動く await transform.DOScale(new Vector3(0.5f, 2, 1), 2); // 大きさ変える await transform.DORotate(new Vector3(180, 90, 0), 3); // 回る } ``` ![DoTweenAsyncDemo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260914/963e3393-0661-a8b4-cb83-3fba12a90c9d.gif) # あとがき 記事を読んでくれてありがとうございました。 UniTaskの記事を書いてみましたが、どうでしたか? 興味を持ってくれたらとても嬉しいです。 そもそもUniTaskっていう名前も知らない、名前は知ってるけどどういうものか知らない、async/awaitって怖い。みたいな人たちに刺さるような内容になったと思います。 最初から何を言ってるのか何もわからない!っていう人は、後々UniTaskに興味を持ったりしたときに再び読んでじわじわと取得してくれたら幸いです。 紹介しきれなかった機能もありますので、これを機に調べてみてはいかがでしょうか。 私自身まだ習熟していなく、わからないこともいっぱいあります。ということで勉強がてら記事を書いてみました。 Twitter等での質問も遠慮なくお願いします。 間違っていることがあれば優しく教えてくれるとありがたいです。 ## 謝辞 この記事を作成するにあたり、さまざまな記事を参考にさせていただきました。感謝します。 ~~参考って言うかパクr~~ UniTask入門(https://speakerdeck.com/torisoup/unitaskru-men) UniTask - Unityでasync/awaitを最高のパフォーマンスで実現するライブラリ(http://tech.cygames.co.jp/archives/3256/) neue cc - UniTask - Unity + async/awaitの完全でハイパフォーマンスな統合(http://neue.cc/2018/07/12_567.html) Observableで非同期処理(https://www.slideshare.net/torisoup/observable) 非同期メソッドの内部実装(https://ufcpp.net/study/csharp/sp5_awaitable.html)