UniTaskとは
UniTask
とはUnity向けに最適化されたTask
実装を提供するライブラリです。
もとはUniRxに組み込まれていましたが途中で分離し、独立した1つのライブラリとして提供されるようになりました。
もっと詳しい資料
導入
C# 7
系の機能に依存しているため、UniTaskを利用する場合はUnity 2018.3以降を推奨します。
導入はGitHubからパッケージをダウンロードし、Unityに取り込むだけでOKです。
UniTaskの機能紹介
Taskより軽量な「UniTask」
UniTaskでは、非同期処理をUniTaskというクラスで扱えるようにしています。
これはTask
をUnityに最適化する形で実装された非同期処理機構です。
(中身はValueTask
に近いです)
Taskの実装を見てもらうとわかると思うのですが、Taskは結構ヘビーな存在です。
しかもしれっとSynchronizationContext
に依存してたり、それの有無によって挙動が変わったりと結構作りがヤベーやつでした。
それをよりUnityに最適化する形で再実装したものがUniTaskであり、Taskより軽量になっています。
またSynchronizationContext
への依存もありません。
asyncとUniTask
このUniTask
ですが、なんとasyncの戻り値にすることができます。当然、awaitもできます。
つまり、通常のasync/awaitの返り値をTask<T>
からUniTask<T>
に書き換えるだけで利用することができるようになります。
using System.Threading.Tasks;
using UniRx.Async; //必要
using UnityEngine;
public class Sample1 : MonoBehaviour
{
async void Start()
{
// こっちは普通のTaskのawait
var result = await Work();
Debug.Log(result);
// こっちはUniTaskのawait
var result2 = await Work2();
// ちゃんとメインスレッドに戻ってくる
Debug.Log(result2);
// awaitせずに、非同期処理を投げっぱなしで実行する場合は
// Forget()をつけるとコンパイラのawaitしろ警告を抑圧できる
Work2().Forget();
}
async Task<string> Work()
{
// 適当にマルチスレッド処理
return await Task.Run(() => "Hello!");
}
// 返り値をUniTaskに書き換えただけ
async UniTask<string> Work2()
{
return await Task.Run(() => "Hello!");
}
}
UniTaskの生成
asyncメソッドを使う以外にも、UniTask
を生成する方法があります。
Func<UniTask>
からつくる
UniTask
のコンストラクタはFunc<UniTask>
を取るようになっています。
これを用いることで、非同期遅延初期化(AsyncLazy)的なことができるようになります。
using UniRx.Async;
using UnityEngine;
namespace Assets
{
class ResourceLoadAsyncSample : MonoBehaviour
{
/// <summary>
/// 初回アクセス時のみ非同期的に初期化する
/// 2度目以降、処理が完了しているならキャッシュを利用する
/// </summary>
public UniTask<Texture> PlayerTexture { get; private set; }
void Awake()
{
PlayerTexture = new UniTask<Texture>(() => LoadAsSprite());
}
async UniTask<Texture> LoadAsSprite()
{
var resource = await Resources.LoadAsync<Texture>("Player");
return (resource as Texture);
}
}
}
UniTaskCompletionSource
からつくる
UniTaskCompletionSource
を使うことで、任意のタイミングで結果を発行するUniTask
を生成できます。
TaskCompletionSource<T>
と大体同じです。。Rx的にいうと、Observable.Create<T>()
みたいなもんです。
public UniTask<string> WrapMyAsync()
{
var utcs = new UniTaskCompletionSource<string>();
// 何かしらの非同期処理
HogeAsyncAction(result =>
{
// 成功
utcs.TrySetResult(result);
}, ex =>
{
// 失敗
utcs.TrySetException(ex);
});
return utcs.Task; //UniTask<string>を返す
}
/// <summary>
/// 結果をActionで返す非同期処理
/// </summary>
private void HogeAsyncAction(Action<string> onSuccess, Action<Exception> onError)
{
// Do something.
}
いろんなものをawaitできる
UniRx.Asyncの機能の1つとして、Unityの非同期処理をだいたいawaitすることができるようになっています。
AsyncOperationのawait
ResourceRequest
などの、AsyncOperation
を継承したものをawaitすることができます。
using UniRx.Async; //必要
using UnityEngine;
public class Sample2 : MonoBehaviour
{
async void Start()
{
var texture = await Resources.LoadAsync<Texture>("Player");
gameObject.GetComponent<Renderer>().material.mainTexture = texture as Texture;
}
}
using UniRx.Async; //必要
using UnityEngine;
using UnityEngine.Networking;
public class Sample3 : MonoBehaviour
{
async void Start()
{
var uri =
"https://2.bp.blogspot.com/-tcLjNKJqOIQ/WkXHUuSC4qI/AAAAAAABJX0/ArQTS8DS9SEOJI4Mb5tvZg4GXuoED8iIQCLcBGAs/s800/otaku_winter.png";
//テクスチャをダウンロード
var texture = await DownloadTexture(uri);
// テクスチャセット
gameObject.GetComponent<Renderer>().material.mainTexture = texture;
}
// テクスチャをダウンロードする
async UniTask<Texture> DownloadTexture(string uri)
{
// 適当に画像のURL
var r = UnityWebRequestTexture.GetTexture(uri);
await r.SendWebRequest(); // UnityWebRequestをawaitできる
return DownloadHandlerTexture.GetContent(r);
}
}
また、WWW
にも使えます。
using UniRx.Async; //必要
using UnityEngine;
public class Sample4 : MonoBehaviour
{
async void Start()
{
var uri =
"https://2.bp.blogspot.com/-tcLjNKJqOIQ/WkXHUuSC4qI/AAAAAAABJX0/ArQTS8DS9SEOJI4Mb5tvZg4GXuoED8iIQCLcBGAs/s800/otaku_winter.png";
//テクスチャをダウンロード
var texture = await DownloadTexture(uri);
// テクスチャセット
gameObject.GetComponent<Renderer>().material.mainTexture = texture;
}
// テクスチャをダウンロードする
async UniTask<Texture> DownloadTexture(string uri)
{
// 適当に画像のURL
var www = new WWW(uri);
await www; // WWWをawaitできる
return www.texture;
}
}
AsyncOperationのConfigureAwait
AsyncOperation
をawaitするときにConfigureAwait
を呼び出すことで、非同期処理の進捗(Progress)を取得することができます。
using System;
using UniRx.Async; //必要
using UnityEngine;
using UnityEngine.Networking;
public class Sample5 : MonoBehaviour
{
async void Start()
{
var request = UnityWebRequest.Get("https://unity3d.com");
//ConfigureAwaitで途中経過取得
var result = await request.SendWebRequest().ConfigureAwait(Progress.Create<float>(x => Debug.Log(x)));
Debug.Log(result.downloadHandler.text);
}
}
コルーチンのawait
yield return null
のみを使ったコルーチンであれば、直接awaitできます。
詳しくはこちら 【Unity】UniRx.Asyncでコルーチンをawaitするときの注意点
using System.Collections;
using UniRx.Async; //必要
using UnityEngine;
public class Sample6 : MonoBehaviour
{
async void Start()
{
// コルーチン(IEnumerator)をawaitできる
await MoveCoroutine();
Debug.Log("Done!");
}
//数秒間前に進むコルーチン
IEnumerator MoveCoroutine()
{
var start = Time.time;
while (Time.time - start < 2)
{
transform.position += Vector3.forward * Time.deltaTime;
yield return null;
}
}
}
PlayerLoopに同期する
UniRx.AsyncはUnityのPlayerLoop
を内部で利用しており、処理を任意のタイミングから実行させることができるようになっています。
UniTask.Yield
に、指定のPlayerLoop
タイムを指定することでタイミングの切り替えが可能です。
コルーチンのWaitForFixedUpdate
などに相当します。
using UniRx.Async; //必要
using UnityEngine;
public class Sample7 : MonoBehaviour
{
async void Start()
{
Debug.Log(Time.time);
// 次のFixedUpdateのタイミングを待つ
await UniTask.Yield(PlayerLoopTiming.FixedUpdate);
//↓↓↓これ以降はFixedUpdateと同期して実行される↓↓↓
Debug.Log(Time.time);
// 次のPostLateUpdateを待つ
await UniTask.Yield(PlayerLoopTiming.PostLateUpdate);
//↓↓↓これ以降はPostLateUpdate(WaitForEndOfFrame)と同期して実行される↓↓↓
Debug.Log(Time.time);
}
}
指定フレーム数待つ
UniTask.DelayFrame
を使うと任意のフレーム数待機できます。
using UniRx.Async; //必要
using UnityEngine;
public class Sample8 : MonoBehaviour
{
async void Start()
{
Debug.Log(Time.frameCount);
// 5フレーム待機
await UniTask.DelayFrame(5);
Debug.Log(Time.frameCount);
// FixedUpdateを5回分待機
await UniTask.DelayFrame(5, PlayerLoopTiming.FixedUpdate);
// ↓これ以降はFixedUpdateのタイミングに切り替わる点に注意↓
Debug.Log("End");
}
}
Unityコールバックイベントをawaitする
UniRx.Async.Triggersをusingすることで、各種GameObjectのコールバックイベントをUniTask
として取得することができるようになります。
使い方は、GameObject
またはComponent
に対してGetAsync*Trigger()
を呼ぶことで、各種UniTask
を返すAsyncTriggerComponent
が取得できます。このコンポーネントから対象のイベントをUniTask
として取得することができるようになっているため、それawaitすることで、イベント発生を待ち受けることができるようになります。
なお、GetAsync*Trigger()
は内部でGetComponent
を実行するため、取得したAsyncTriggerComponent
はちゃんとキャッシュするようにしておくと良いでしょう。
using UniRx.Async;
using UniRx.Async.Triggers;
using UnityEngine;
public class AwaitAsyncTrigger : MonoBehaviour
{
private AsyncCollisionTrigger asyncCollisionTrigger;
void Start()
{
// AsyncTrigger(extends MonoBehaviour)を取得する
asyncCollisionTrigger = this.GetAsyncCollisionTrigger();
DoAsync().Forget();
}
async UniTask DoAsync()
{
Debug.Log("Start");
// OnCollisionEnterが発生するまで待機する
var target = await asyncCollisionTrigger.OnCollisionEnterAsync();
Debug.Log("Hit! " + target.gameObject.name);
// OnCollisionExitが発生するまで待つ
await asyncCollisionTrigger.OnCollisionExitAsync();
Debug.Log("Bye!");
}
}
UnityEventをawaitする
UnityEvent
は、UnityEvent.GetAsyncEventHandler()
を実行するとAsyncEventHandler
というものを取得できます。
このAsyncEventHandler.OnInvokeAsync()
を呼び出すことで、UnityEvent
のawaitができます。
(一度しかawaitしないなら、UnityEvent.OnInvokeAsync()
を直接awaitしても大丈夫です。)
using UniRx.Async;
using UnityEngine;
using UnityEngine.UI;
public class AwaitUnityEvent : MonoBehaviour
{
[SerializeField] private Button button;
void Start()
{
DoAsync().Forget();
}
async UniTask DoAsync()
{
// UnityEventをGetAsyncEventHandlerに変換
var asyncEventHandler = button.onClick.GetAsyncEventHandler();
// ボタンが押されるまで待つ
await asyncEventHandler.OnInvokeAsync();
Debug.Log("1");
// ボタンが押されるまで待つ
await asyncEventHandler.OnInvokeAsync();
Debug.Log("2");
}
}
条件を与えてawaitする
UniTask.Wait*
シリーズを使うことで、条件を指定してawaitができます。
using UniRx.Async;
using UnityEngine;
public class Sample11 : MonoBehaviour
{
async void Start()
{
// 条件を満たしたら待機終了
await UniTask.WaitUntil(() => transform.position.y < 0);
// 指定オブジェクトの状態が変化するのを待つ
await UniTask.WaitUntilValueChanged(transform, x => x.position.x); //x方向に移動した
// 指定オブジェクトの状態が変化するのを待つ & 対象がDestroyされたら待機終了
await UniTask.WaitUntilValueChangedWithIsDestroyed(transform, x => x.position.x);
// 条件を満たさなくなったら待機終了
await UniTask.WaitWhile(() => transform.position.z < 0);
}
}
ReactivePropertyをawaitする
ReactiveProperty
もawaitできるようになっています。
この場合は「値が変化するまで待機」という挙動になります。
なお、ReactiveProperty
は直接awaitした方が軽量かつ安全に機能します。
AsObservable
したり、末尾にオペレータをつなぐとObservable
のawaitに切り替わってしまい、直接awaitするのと比べるとパフォーマンスが落ちてしまいます。
using UniRx;
using UniRx.Async;
using UnityEngine;
class AwaitReactiveProperty : MonoBehaviour
{
[SerializeField] BoolReactiveProperty isDead = new BoolReactiveProperty(false);
void Start()
{
DoAsync().Forget();
}
async UniTask DoAsync()
{
// 死ぬまで待機
await isDead;
Debug.Log("Dead!");
}
public void Kill()
{
isDead.Value = true;
}
}
Observableをawaitする
厳密に言うとUniRx.Asyncではなく、UniRx側の機能です。
Observable
をawaitで待機できます。
この場合はObseravble
が内部でAsyncSubject
に変換されます。
そのためOnCompleted
メッセージが発行されるまでずっとawaitし続けてしまうので注意が必要です。
using UniRx;
using UnityEngine;
public class Sample12 : MonoBehaviour
{
async void Start()
{
var result = await Observable.Return(1);
Debug.Log(result);
}
}
スレッドを切り替える
UniTask.SwitchToThreadPool
またはUniTask.SwitchToTaskPool
をawaitすることで、以降の処理を別スレッドに切り替えることができます。(ThreadPool
とTaskPool
とありますが、ThreadPool
だけ使っていれば問題ないです。Unityの場合はTaskPool
は利用するメリットがありません。)
using System.Threading;
using UniRx.Async;
using UnityEngine;
public class SampleX : MonoBehaviour
{
async void Start()
{
Debug.Log(Thread.CurrentThread.ManagedThreadId);
// TaskPoolに切り替え
await UniTask.SwitchToTaskPool();
// ↓これ以降はTaskPoolで実行される↓
Debug.Log(Thread.CurrentThread.ManagedThreadId);
// メインスレッド(Updateタイミング)に戻す
await UniTask.Yield();
// ↓これ以降はメインスレッドで実行される↓
Debug.Log(Thread.CurrentThread.ManagedThreadId);
}
}
ObservableとUniTaskの相互変換
Observable.ToUniTask()
、UniTask.ToObservable()
でお互いに相互変換できます。
相互変換できるので、非同期処理はとりあえずasync/await
で書いておいて、状況に応じてObservable
に変換すればOKになります。
using UniRx;
using UniRx.Async;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
public class Sample13 : MonoBehaviour
{
[SerializeField] private Button button;
[SerializeField] private Text text;
void Start()
{
// ボタンがクリックされたらHTTPで通信する
// 通信中はボタンを無効化する
button.BindToOnClick(_ =>
{
return GetHtmlAsync().ToObservable()
.ForEachAsync(x => { text.text = x.Substring(0, 100); });
});
}
// HTTPでHTMLをダウンロードする
async UniTask<string> GetHtmlAsync()
{
var uri = "https://unity3d.com/jp";
var r = UnityWebRequest.Get(uri);
var result = await r.SendWebRequest();
return result.downloadHandler.text;
}
}
AsyncReactiveCommandと非同期処理をくっつけた例です。
タイムアウト処理
UniTask
にはタイムアウトの機能がついています。
タイムアウトするとTimeoutException
がthrowされます。
using System;
using UniRx.Async;
using UnityEngine;
using UnityEngine.Networking;
public class Sample14 : MonoBehaviour
{
async void Start()
{
var request = UnityWebRequest.Get("https://unity3d.com");
// 明示的にUniTaskに変換し、Timeoutをくっつける
var result = await request.SendWebRequest().ToUniTask().Timeout(TimeSpan.FromSeconds(3));
Debug.Log(result.downloadHandler.text);
}
}
並列実行&待機
UniTask.WhenAll
を使うと、UniTask
の終了を同時に複数待ち受けることができます。
using UniRx.Async;
using UnityEngine;
using UnityEngine.Networking;
public class Sample15 : MonoBehaviour
{
async void Start()
{
var task1 = GetHtmlAsync("https://unity3d.com/jp");
var task2 = GetHtmlAsync("https://github.com");
var task3 = GetHtmlAsync("https://www.yahoo.co.jp");
// まとめて結果をとれる(便利!)
var (unity, github, yahoo) = await UniTask.WhenAll(task1, task2, task3);
}
// HTTPでHTMLをダウンロードする
async UniTask<string> GetHtmlAsync(string uri)
{
var r = UnityWebRequest.Get(uri);
var result = await r.SendWebRequest();
return result.downloadHandler.text;
}
}
キャンセル処理
UniTask
の実行やawaitをキャンセルしたい場合はCancellationToken
を利用する必要があります。
ぶっちゃけここだけはもとTaskのasync/awaitのころからシンドイ部分ではあります。
キャンセルのやり方は次みたいな感じに。
-
CancellationTokenSource
を用意する -
CancellationTokenSource.Token
でCancellationToken
が取得できる - 取得した
CancellationToken
を実行するUniTask
に渡していく(シンドイ) - そのとき、定義したasyncメソッド内で逐一キャンセル時の処理を書く(シンドイ)
- キャンセルしたくなったタイミングで、
CancellationTokenSource.Cancel()
を実行する
何度も書きますが、正直めっちゃシンドイです。どうにかならないのかなコレ…。
using System.Threading;
using UniRx.Async;
using UniRx.Async.Triggers;
using UnityEngine;
public class AwaitAsyncTrigger : MonoBehaviour
{
private AsyncCollisionTrigger asyncCollisionTrigger;
private CancellationTokenSource cancellationTokenSource;
void Start()
{
// CancellationTokenSourceを生成
cancellationTokenSource = new CancellationTokenSource();
// AsyncTrigger(extends MonoBehaviour)を取得する
asyncCollisionTrigger = this.GetAsyncCollisionTrigger();
// 引数にCancellationTokenを渡す
DoAsync(cancellationTokenSource.Token).Forget();
}
void OnDestroy()
{
// GameObject破棄時にキャンセル実行
cancellationTokenSource?.Cancel();
}
async UniTask DoAsync(CancellationToken token)
{
// OnCollisionEnterが発生するまで待機する
var (isCanceled, target) = await asyncCollisionTrigger.OnCollisionEnterAsync()
.WithCancellationWithoutException(token); //tokenでキャンセルできるようにする
if (isCanceled) return; //Finish
Debug.Log("Hit! " + target.gameObject.name);
// OnCollisionExitが発生するまで待つ
(isCanceled, target) = await asyncCollisionTrigger.OnCollisionExitAsync()
.WithCancellationWithoutException(token); //tokenでキャンセルできるようにする
if (isCanceled) return; //Finish
Debug.Log("Bye! " + target.gameObject.name);
}
}
キャンセル用の拡張メソッド
さすがにこのままだとツライので、こういう拡張メソッドを生やしておくとちょっとは便利になる…のかな?
using System.Threading;
using UniRx.Async;
using UniRx.Async.Triggers;
using UnityEngine;
public static class CancellationTokenSourceExtension
{
public static void CancelWith(this CancellationTokenSource cts, Component component)
{
cts.CancelWith(component.gameObject);
}
public static void CancelWith(this CancellationTokenSource cts, GameObject gameObject)
{
gameObject.OnDestroyAsync().ContinueWith(() => cts.Cancel()).Forget();
}
}
using System.Threading;
using UniRx.Async;
using UniRx.Async.Triggers;
using UnityEngine;
public class AwaitAsyncTrigger : MonoBehaviour
{
private CancellationTokenSource cancellationTokenSource;
void Start()
{
// CancellationTokenSourceを生成
cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.CancelWith(this); //このGameObjectが破棄されたらCancel
// 以下略
}
UniTaskを使うと何が変わるか
コルーチンをほぼasync/awaitで代用できるようになります。
コルーチンと同等の処理をasync/awaitで記述できるようになる上、コルーチンと違ってUniTask<T>
を使えば結果を外に返すことができるようになります。
また、StartCoroutine()
はGameObject
に依存していたため、MonoBehaviour
が無いと実行できないとか、Disableになったらコルーチンごと止まるとかいろいろありましたが、async/await
+ UniTask
ならそういう制約はなくなります。
というわけで、「コルーチンの上位互換」として使える…、かと思いきや途中のキャンセルが結構面倒くさいので一長一短だったりします。 コルーチンの場合はGameObject
をDestroy
したら勝手に止まりましたけど、async/awaitの場合は止まりません。
(await対象のオブジェクトがGCで回収されれば勝手にawait処理自体が止まって消え去るので、そこさえGCされてれば最悪大事には至らないかな…?)
Observable
との使い分けですが、非同期処理は基本すべてasync/await+UniTaskで書くことになるかと。
そしてObservable
は基本イベント処理(非同期的に複数の値を発行する)への用途がメインになるかと。
とりあえずは思考停止でこの方針で書いてみて、必要に応じて相互変換すればよいかと。
まとめ
すべての機能をまとめきれているわけではないですが、とりあえず目についた部分は書いてみました。
Unityにおける非同期処理がすごいイケてる感じになりそうです!
あ、あと宣伝なのですが、Unity 非同期完全に理解した勉強会を開催します。
大反響で会場のキャパを遥かに超える参加登録が来ており、ちょっとどうするか検討中です。