【Unity】UniTask 【1】#91
前回の成果
UniRxのHotとColdについて理解した。
今回やること
UniTaskについて学びます。
UniTaskとは
Unityでasync/awaitを最高のパフォーマンスで実現するライブラリ
UniTask – Unityでasync/awaitを最高のパフォーマンスで実現するライブラリ | Cygames Engineers' Blog:より引用
asyncとawaitは既存のC#にTaskが存在するのですが、Taskよりパフォーマンスを良くしたものになります。
asyncとawaitとは
asyncとawaitは非同期処理を行うために使用するものになります。
これらを使うことで、同期処理を書くような感覚で非同期処理を書くことができます。
非同期処理
非同期処理とは、ある処理を実行している間に他のタスクで別の処理を実行するものになります。
逆に、ある処理を実行している間は他のタスクの処理を中断するものを同期処理と呼びます。
同期処理と非同期処理のイメージ
この非同期処理は、コルーチンでも実装することが出来ます。
以下のようなソースコードで非同期処理を確認することができます。
コルーチン
using System.Collections; using System.Collections.Generic; using UnityEngine; public class AsynchronousSimple : MonoBehaviour { void Start() { LogCoroutine(); } private void LogCoroutine() { Debug.Log("コルーチン開始"); StartCoroutine(CoroutineTimer(1)); Debug.Log("コルーチン終了"); } private IEnumerator CoroutineTimer(int time) { yield return new WaitForSeconds(time); Debug.Log("指定時間経過"); } }
結果
プログラムの順番的には、
- コルーチン開始
- 指定時間経過
- コルーチン終了
で記載しているのに、実際は
- コルーチン開始
- コルーチン終了
- 指定時間経過
となっています。
これは、コルーチンが非同期処理で行われているからになります。
具体例
非同期処理について理解できたので、asyncとawaitの具体例をみてみます。
基本的な構文
async 戻り値 メソッド名(引数) {
// 完了するまで待つ
await 処理;
}
asyncとawaitの具体例
private async Task TaskTimer() { Debug.Log("タスク開始"); // 1000ミリ秒待つ await Task.Delay(1000); Debug.Log("タスク終了"); }
結果
タスク開始から1000ミリ秒後にタスク終了のログが出ています。
Task
Taskとは、言葉のとおり仕事という意味です。
以下のソースコードを見ていただけるとわかりやすいです。
1000ミリ秒待機する「タスク」の完了を待つ「タスク」
private async Task TaskTimer() { Debug.Log("タスク開始"); // 1000ミリ秒待つ「タスク」の完了を待つ await Task.Delay(1000); Debug.Log("タスク終了"); } // という1つの「タスク」
async、await、taskを簡単にまとめると以下の表のようになります。
\ | 意味 |
---|---|
async | メソッドを非同期にする |
await | 指定したTaskの完了を待ち、結果を取り出す |
Task | 仕事、処理 |
詳しくはこちらのサイト様がわかりやすいです。
UniTaskとTask
UniTaskは、Taskをそのまま置き換えることで使用できます。
UniTask
private async UniTask UniTaskTimer() { Debug.Log("UniTask開始"); // 1000ミリ秒待つ await UniTask.Delay(1000); Debug.Log("UniTask終了"); }
比較してみるとわかりやすいです。
// Task private async Task TaskTimer() { Debug.Log("タスク開始"); // 1000ミリ秒待つ await Task.Delay(1000); Debug.Log("タスク終了"); } // UniTask private async UniTask UniTaskTimer() { Debug.Log("UniTask開始"); // 1000ミリ秒待つ await UniTask.Delay(1000); Debug.Log("UniTask終了"); }
結果
結果もTaskの時と同じです。
ですが、UniTaskとTaskでは以下の違いがあります。
\ | Task | UniTask |
---|---|---|
機能 | Unityで不要なものが多い | Unity用に最適化 |
オブジェクトのサイズ | 大きい | 小さい |
C#の最低バージョン | 5.0 | 7.0 |
await中のタスク | 見れない | 見れる |
基本的にUniTaskがTaskの上位互換となっています。
UniTaskが提供している機能
では実際にUniTaskにはどのような機能があるか見ていきます。
UniTask、UniTask<T>
TaskをUnity用に最適化したものになります。
基本的には、先程も紹介したとおりTaskをUniTaskへ置き換えるだけで使用することができます。
UniTask型の制作方法について、いくつか挙げていきます。
async、awaitの戻り値
asyncとawaitの戻り値をUniTaskに置き換えることで制作できます。
戻り値の置き換え
/// <summary> /// async,awaitの戻り値でUniTask生成 /// </summary> /// <returns></returns> private async UniTask AsyncCreate() { await UniTask.Delay(1000); Debug.Log("タスク完了"); }
結果
UniTaskCompletionSource
TaskにはTaskCompletionSourceというTaskの結果を取得できるものがあります。
これと同じような機能がUniTaskにも用意されているので、それを使用することでもUniTaskを制作できます。
UniTaskの結果を受け取る
// UniTaskのステータス private enum STATUS { SUCCESS, ERROR, CANCEL, } [SerializeField] private STATUS _status; /// <summary> /// UniTaskCompletionSourceでUniTask生成 /// </summary> /// <returns></returns> private void CompletionSourceCreate() { UniTaskCompletionSource<int> utc = new UniTaskCompletionSource<int>(); UniTask<int> task = utc.Task; switch (_status) { case STATUS.SUCCESS: // 結果を設定して完了状態にする utc.TrySetResult(123); break; case STATUS.ERROR: // 失敗状態にする utc.TrySetException(new Exception("error message")); break; case STATUS.CANCEL: // キャンセル状態にする utc.TrySetCanceled(); break; } Debug.Log("Task Status : " + task.Status); }
_status = SUCCESS
_status = ERROR
_status = CANCEL
IObservableから変換
UniRxのIObservableから変換することも出来ます。
逆に、UniTaskをIObservableに変換することも出来ます。
コメントにも記載してあるのですが、Observableが完了しないものをUniTaskに変換しないように注意してください。
IObservableから変換する例
/// <summary> /// IObservableをUniTaskに変換 /// </summary> private void IObservableCreate() { IObservable<int> observable = Observable.Return(0); // IObservableからUniTaskへ変換 UniTask<int> uniTask = observable.ToUniTask(); // UniTaslからIObservableへ変換 IObservable<int> newObservable = uniTask.ToObservable(); // Observableが終わらないものは変換しない UniTask<long> badUniTask = Observable.EveryUpdate().ToUniTask(); }
静的なメソッド群
次にUniTaskが提供しているstaticなメソッドを紹介します。
全ては紹介しきれないので、いくつか挙げます。
UniTask.Delay
引数で指定した秒数、待つことができます。
また、Unityのどのタイミングで計測するかを指定することができます。
デフォルトはUpdate()のタイミングとなっています。
UniTask.Delayの例
/// <summary> /// 指定秒数待つ /// </summary> /// <returns></returns> private async UniTask ExcuteDelay() { // 1000ミリ秒待つ await UniTask.Delay(1000); Debug.Log("1000ミリ秒経過"); // 1秒待つ await UniTask.Delay(TimeSpan.FromSeconds(1)); Debug.Log("1秒経過"); // FixedUpdateタイミングで1000ミリ秒待つ、デフォルトはUpdateタイミング await UniTask.Delay(1000, delayTiming: PlayerLoopTiming.FixedUpdate); Debug.Log("FixedUpdateで1000ミリ秒経過"); }
UniTask.DelayFrame
引数で指定したフレーム数待つことができます。
また、Unityのどのタイミングで計測するかを指定することができます。
デフォルトはUpdate()のタイミングとなっています。
UniTask.DelayFrameの例
/// <summary> /// 指定フレーム待つ /// </summary> /// <returns></returns> private async UniTask ExcuteDelayFrame() { await UniTask.DelayFrame(60); Debug.Log("60フレーム経過"); await UniTask.DelayFrame(60, delayTiming: PlayerLoopTiming.FixedUpdate); Debug.Log("FixedUpdateで60フレーム経過"); }
UniTask.Yield
指定したタイミングで1フレーム待つことができます。
イメージとしては、コルーチンのyeild return nullに近いです。
注意点として、1フレーム待ったあとの処理は指定したタイミングで実行されます。
ですので、Updateタイミングに戻したい場合は引数無しか、Updateを指定する必要があります。
/// <summary> /// 指定タイミングで1フレーム待機 /// </summary> /// <returns></returns> private async UniTask ExcuteYield() { // Updateで1フレーム待機 await UniTask.Yield(); // FixedUpdateで1フレーム待機 await UniTask.Yield(PlayerLoopTiming.FixedUpdate); Debug.Log("このLogはFixedUpdateのタイミングで実行される"); await UniTask.Yield(); Debug.Log("このLogはUpdateのタイミングで実行される"); }
UniTask.SwitchToThreadPool / UniTask.SwitchToMainThread
UniTask.SwitchToThreadPoolは以降の処理をスレッドプールで行うようにできます。
UniTask.SwitchToMainThreadはメインスレッドで行うようにできます。
先ほど紹介したYield()でもメインスレッドに切り替えることができますが、必ず1フレーム待ちます。
SwitchToThreadPool / SwitchToMainThreadの例
/// <summary> /// スレッドを切り替える /// </summary> /// <returns></returns> private async UniTask ExcuteThreadPool() { // スレッドプールに切り替える await UniTask.SwitchToThreadPool(); Debug.Log("スレッドプール上での処理"); // メインスレッドに切り替える await UniTask.SwitchToMainThread(); Debug.Log("メインスレッド上での処理"); await UniTask.SwitchToThreadPool(); // Yieldでメインスレッドに切り替えることもできるが、1フレーム待ってしまう await UniTask.Yield(); }
UniTask.Run
引数で指定したデリゲートをスレッドプール上で実行することができます。
実行後は、メインスレッドへ戻ります。
Instantiateのようなスレッドプール上では実行ができないメソッドもあるので、気をつけてください。
UniTask.Runの例
/// <summary> /// 引数のデリゲートを実行 /// </summary> /// <returns></returns> private async UniTask ExcuteRun() { int hoge = 1; int fuga = 1; // スレッドプール上で実行、処理後はメインスレッドへ await UniTask.Run(() => { int hogeFuga = hoge + fuga; Debug.Log(hogeFuga); }); // Instantiateのようなスレッドプールでは実行できないものもある await UniTask.Run(() => { GameObject obj = Instantiate(_prefab, this.transform); }); }
UniTask.WhenAll
指定した全てのUniTaskが完了するのを待つことができます。
また、タプルを使用することで結果を受け取ることもできます。
UniTask.WhenAllの例
/// <summary> /// 指定した全てのUniTaskが完了するまで待機 /// </summary> /// <returns></returns> private async UniTask ExcuteWhenAll() { UniTask<int> task1 = UniTask.Run(() => 100); UniTask<string> task2 = UniTask.Run(() => "Finish"); // 指定した全てのUniTaskの完了を待機 // 結果をタプルで受け取ることもできる var (t1, t2) = await UniTask.WhenAll(task1, task2); Debug.Log("t1 : " + t1); Debug.Log("t2 : " + t2); }
結果
UniTask.Runでの結果をUniTask毎に確認することができます。
UniTask.WhenAny
指定したUniTaskのどれか1つでも完了するまで待機します。
指定したUniTaskの方が全て同じの場合、どれが完了したのかを直接取得することもできます。
UniTask.WhenAnyの例
/// <summary> /// 指定したどれかのUniTaskが完了するまで待機 /// </summary> /// <returns></returns> private async UniTask ExcuteWhenAny() { UniTask task1 = UniTask.Delay(3000); UniTask<string> task2 = UniTask.Run(() => "Finish"); // 指定したどれかのUnitaskの完了を待機 await UniTask.WhenAny(task1, task2); Debug.Log("どちらかのUnitaskが完了"); List<UniTask<string>> listTask = new List<UniTask<string>>(); listTask.Add(UniTask.Run(() => "No.1 Task")); listTask.Add(UniTask.Run(() => "No.2 Task")); listTask.Add(UniTask.Run(() => "No.3 Task")); // タスクの方が全て同じの場合、完了したタスクを直接取得することができる var (_, result) = await UniTask.WhenAny(listTask.ToArray()); Debug.Log("終了したタスク : " + result); }
結果
「どちらかのUnitaskが完了」でDelayを待たずに、タスクが完了したログが出ています。
また、「終了したタスク : No.1 Task」でどのタスクが完了してWhenAnyの待機が終了したかが確認できているのがわかります。
UniTask.WaitUntil / UniTask.WaitWhile
UniTask.WaitUntilは指定した条件がtrueになるまで待機します。
UniTask.WaitWhileは指定した条件がfalseになるまで待機します。
UniTask.WaitUntil / UniTask.WaitWhileの例
[SerializeField] private bool _isWaitFlag; /// <summary> /// 指定した条件がtrue/falseになるまで待機 /// </summary> /// <returns></returns> private async UniTask ExcuteWaitFlag() { _isWaitFlag = false; // 指定した条件がtrueになるまで待機 await UniTask.WaitUntil(() => _isWaitFlag); Debug.Log("WaitUntil終了"); // 指定した条件がfalseになるまで待機 await UniTask.WaitWhile(() => _isWaitFlag); Debug.Log("WaitWhile終了"); }
結果
指定したフラグがtrue、falseになるまで待機しています。
UniTask.WaitUntilValueChanged
指定した値が変化するまで待機します。
UniRxのObserveEveryValueChangedと同じです。
UniTask.WaitUntilValueChangedの例
/// <summary> /// 指定した値が変化するまで待機 /// </summary> /// <returns></returns> private async UniTask ExcuteWaitUntilValueChanged() { // 自身の座標が変化するまで待機 await UniTask.WaitUntilValueChanged(transform, x => x.position); Debug.Log("移動した"); }
今回は以上となります。
ここまでご視聴ありがとうございます。