はじめに
何番煎じなのかわかりませんが、過去にいろいろ解説した記事へのリンクをまとめる意味も込めて解説します。
UniRxとは
おすすめ資料
- UniRx入門シリーズ 目次
- UniRx オペレータ一覧
- UniRx オペレータ逆引き
- ReactiveCommand/AsyncReactiveCommandについて
- ObserverパターンからはじめるUniRx
- Observable の非同期処理への活用
概要
UniRxとは、Reactive Extensions
をUnity向けの実装したC#ライブラリです。
かなり昔のバージョンのUnityでも扱うことができます。
導入することで、Unityにおいて次のような処理の実装が簡単になります。
-
非同期処理
- 何らかの処理の完了を待ち受けて次の処理を行うような処理の管理
- エラーハンドリングやリトライ処理の簡略化
- 実行結果のハンドリングやキャッシュ
- 実行スレッドの柔軟な切り替え
// data.txtをスレッドプール上で読み込み、メインスレッド上で表示する
Observable
.Start(() => File.ReadAllText("data.txt"))
.ObserveOnMainThread()
.Subscribe(x => Debug.Log(x));
-
イベント処理
- 処理のトリガーと実際にハンドリングする場所を分離して記述できる
- イベントメッセージの加工や合成
- 実行スレッドの柔軟な切り替え
void Start()
{
// Jumpボタンが押されたらログに出す
// 1度発動したら1秒間は何もしない
this.UpdateAsObservable()
.Where(_ => Input.GetButtonDown("Jump"))
.ThrottleFirst(TimeSpan.FromSeconds(1000))
.Subscribe(_ => Debug.Log("Jump!!"));
}
UniRxは「非同期処理」と「イベント処理」の2つを扱うことができるライブラリです。
特に「時間」の扱いに長けており、Unityにおける「フレームをまたいだ処理」などの実装が簡単になります。
なお、UniRx
が提供するこれら便利な機能は正式には「Observable
」と呼ばれています。
Observable
Observable
とはUniRx
が提供する、「メッセージを通知のための機構およびそのオブジェクト」の指します。
型としてはIObservable<T>
で表現されます。
// 実行結果は IObservable<T> でハンドリングする
private IObservable<string> ReadFileObservable(string path)
{
return Observable
.Start(() => File.ReadAllText(path))
.ObserveOnMainThread();
}
Observable
の仕組み、およびScheduler
やOperator
の概念について詳しく知りたい方は次の資料を御覧ください。
Operator一覧
UniRx
でもっとも便利な機能がこのOperator
です。
Operator
はObservable
に対するさまざまな処理を提供してくれます。
そのためこのOperator
を組み合わせるだけで、だいたいの処理の実装が完了してしまいます。
UniTaskとは
おすすめ資料
- Deep Dive async/await in Unity with UniTask(UniRx.Async)
- さては非同期だなオメー!async/await完全に理解しよう
- async/await のしくみ
- 実践! ライブコーディングで覚えるasync/await
- UniTask入門
- 【Unity開発者向け】「SynchronizationContext」と「Taskのawait」
- UniTask機能紹介
- UniRxとUniTask 相互変換の変わったパターン紹介
概要
UniTaskとは、C#の標準TaskおよびTaskSchedulerをUnity向けに最適化して実装したC#ライブラリです。
UniTaskは標準Taskと比べてパフォーマンスが出るため、非同期処理を扱う場合は是非導入してほしいライブラリです。ただし導入時はUnity 2018.3以降を推奨。
UniTaskを導入することで、次のようなメリットがあります。
-
Unity向けに最適化されたTaskおよびTaskSchedulerが利用できる
-
ValueTask
のUnity向け実装 - UnityのPlayerLoopを用いたSchedule管理
- SynchronizationContextに依存しない
-
async void Start()
{
var path = "data.txt";
// UniTaskのawaitは基本的にメインスレッドに戻る
var result = await LoadFileAsync(path);
Debug.Log(result);
}
private UniTask<string> LoadFileAsync(string path)
{
return UniTask.Run(() => File.ReadAllText(path));
}
-
さまざまなオブジェクトへの
Awaiter
実装の提供- コルーチンを
async/await
に置き換える可能に -
yield return
の代わりにawait
で非同期処理の待機ができるようになる
- コルーチンを
private void Start()
{
JumpAsync(this.GetCancellationTokenOnDestroy()).Forget();
}
/// <summary>
/// ボタンが押されたらジャンプする
/// </summary>
private async UniTaskVoid JumpAsync(CancellationToken token)
{
var rigidBody = GetComponent<Rigidbody>();
while (!token.IsCancellationRequested)
{
// ボタンが押されるのをUpdateタイミングで待つ
await UniTask.WaitUntil(() => Input.GetButtonDown("Jump"),
PlayerLoopTiming.Update,
cancellationToken: token);
// 押されたらFixedUpdateのタイミングでジャンプする
await UniTask.Yield(PlayerLoopTiming.FixedUpdate);
rigidBody.AddForce(Vector3.up, ForceMode.Impulse);
// 1秒待って繰り返す
await UniTask.Delay(1000, cancellationToken: token);
}
}
UniTaskは「非同期処理」に特化したライブラリです。
導入することでほぼすべてのコルーチンをasync/await
に置換することができます。
標準Taskとの比較
UniTask
はC# 7.0
で追加された、ValueTask
ライクに作られています。
そのためasync/await
時、処理が同期で完了する場合においてはヒープアロケーションが発生しないという特徴があります。また、UniTask
は実行コンテキストの管理にSynchronizationContext
ではなくUnity Player Loop
を用いるように作られています。
そのため、標準のTask
/ValueTask
と比較してUniTaskの方がよりUnityではパフォーマンスを出すことができるようになっています。
Task | UniTask | |
---|---|---|
機能 | Unityでは不要な機能が多い | Unityで活用できる機能のみ実装 |
オブジェクトサイズ | 大きい | 小さい |
実行コンテキストの管理 | TaskScheduler & SynchronizationContext | UnityのPlayerLoop |
必要なC# version | C# 5.0以上 | C# 7.0以上 |
TaskTracker | 無 | UnityEditor上で確認可能 |
メモリアロケーション | 常にヒープを確保する | 同期処理で完了する場合はヒープを確保しない |
UniRxとUniTask、それぞれの使い分け
「UniTask
とUniRx
の非同期処理の使い分けの基準は何か」という疑問を持つ人もいるでしょう。
それぞれの使い分けについて説明します。
UniTaskを使うべき場合
非同期処理の結果通知が「1回」で済む場合
結果通知が1回である、つまりは単発で完了する普通の非同期処理の場合です。
こちらはUniTask
(とasync/await
)を使うべきです。
理由として次が挙げられます。
- 同期処理と見た目がほぼ変わらないコードを書ける
- 単発で済む非同期処理に対して、
Observable
自体がオーバースペック気味
async/await
を使うと非同期処理をほぼ同期処理と変わらない見た目で記述することができます。
そのため同期処理で書いていた部分をあとから非同期化する、またはその逆といった対応も比較的楽にすみます。
Observable
の場合、処理が1回で済む非同期処理に対してはかなりオーバースペックになってしまいます。まず「メッセージの発行元は何か」「メッセージは何回発行されるのか」を常に意識しなくてはいけません。
さらにはHotやColdといった性質も考えなくてはならず、UniTask
ほど気軽に扱えません。
また、Observable
を用いた非同期処理は同期処理とまったく異なる記述法になってしまいます。
そのため、あとから処理内容を同期処理へ直そうと思ったときに、Observable
を使っている場所ほぼすべてを新しく書き直す必要が出てきてしまいます。
処理を手続き的に記述したい場合
Observable
では非同期処理をOperator
を用いて処理を宣言的に記述することができました。
これは処理の内容がOperator
の表現範囲で済む限りにおいては便利ではあります。
ですが実際の開発においてはそう簡単に行きません。Operator
ではどうしても表現しきれない処理が出てきた場合、これを手続き的に書き下す必要があります。
また、Observable
は「処理の条件分岐ができない」という欠点があります。
メッセージ内容に応じてその場で実行する処理をごっそり切り替えるといったことができません。
(処理内容ごとに新しいObservable
を定義しなくてはいけないため無駄が多い)
UniTask
(とasync/await
)であれば、処理をほぼ同期処理と変わらずに手続き的に記述することができます。
そのため、要求どおりの仕様を満たした処理をObservable
とOperator
で記述するよりも、よりわかりやすい簡単に実装することができます。
UniRx(Observable)を使うべき場合
非同期処理の結果が複数個になる場合
ここでいう複数個の結果とは、UniTask<IEnumerable<T>>
で済むような場合の話ではありません。非同期処理そのものが継続的に実行され、結果が断続的に発行されるようなシチュエーションを想定しています。
(たとえばディレクトリの中身をまとめて読み込んで、読み込みが先に終わったファイルから処理を次々に行うなど)
このような非同期処理はUniTask
では表現が不可能なため、IObservable<T>
を使うことになります。
補足 IAsyncEnumerable<T>
とIObservable<T>
の違い
C# 8.0
でIAsyncEnumerable<T>
というものが追加されました。名前のとおり「非同期ストリーム」です。IObservable<T>
と役割が被ってそうですが、そこは明確に異なります。
-
IObservable<T>
: 非同期処理が常に動いておりその結果がPUSHで通知される -
IAsyncEnumerable<T>
: 結果をPULLしたときに1つ非同期処理が走る
非同期処理を裏で動かしてその結果がPUSH通知されるのを待つならIObservable<T>
を使う。
await foreach
と組み合わせて、逐次処理をしていくならIAsyncEnumerable<T>
を使う。
といった使い分けが今後は必要になってくるでしょう。
(まだUnityはC# 8.0
に対応していないので、もう少しあとの話ですが)
イベント処理を行う場合
C#
のevent
構文や、UnityEvent
の代替としてUniRx
は利用することができます。
またUpdate()
やFixedUpdate()
をObservable
に変換したり、uGUI
と組み合わせて使うと表現力が広がりかなり便利に使うことができます。
(そもそも「イベント処理」=「値が複数回発行される非同期処理」とほぼ同義なので、同じことを2回説明しているだけですが)
Unityで動くC#バージョンが低い場合
古のUnityでは対応しているC#バージョンが低く、async/await
すら使えない場合があります。
特に長期運用しているプロジェクトですと未だにUnity 5系を使っているなんてこともあるでしょう。
この場合はUniTask
を用いることはできないので、UniRxが非同期処理における唯一の選択肢となります。
コルーチンとObservable
を併用するなどするとよいでしょう。
相互変換
なお、Observable
とUniTask
はそれぞれ相互変換が可能です。
そのためどちらか片方にこだわる必要はなく、場面に応じて変換して使い分けるとよいでしょう。
UniTask -> UniRx
キャンセルを考えない場合
UniTask.ToObservable()
で変換できます。
この場合、Scheduler
は自動的にMainThreadScheduler
指定となります。
using UniRx;
using UniRx.Async;
using UnityEngine;
using UnityEngine.Networking;
public class UniTaskToUniRx : MonoBehaviour
{
void Start()
{
// UniTask<string>
var uniTask = LoadTextAsync("https://github.com/");
// UniTask<string> -> IObservable<string>
// UniTaskのキャンセルを考えないならこれだけでOK
uniTask.ToObservable().Subscribe(Debug.Log);
}
/// <summary>
/// Textを指定のパスから読み込む
/// </summary>
private async UniTask<string> LoadTextAsync(string path)
{
var uwr = UnityWebRequest.Get(path);
await uwr.SendWebRequest();
return uwr.downloadHandler.text;
}
}
キャンセルを考える場合
一発で変換するメソッドはありません。
そのため次のような方法でがんばるしかないです。
UniRx -> UniTask
IObservable -> UniTask
ToUniTask()
を使うことでUniTask
に変換できます。
ただしデフォルトではOnCompleted
メッセージが発行されるまで待ち受けてしまいます。
次に発行される1メッセージだけを待ち受けたい場合は、useFirstValue
オプションをつけましょう。
private async UniTaskVoid CheckEnemyAsync(CancellationToken cancellationToken)
{
// 新たに生成されたEnemyが通知されるObservableがあったとして
IObservable<Enemy> enemyObservable = _enemySpawner.OnEnemySpawned;
while (!cancellationToken.IsCancellationRequested)
{
// ToUniTask(useFirstValue: true) でUniTask化してawaitできる
var enemy = await enemyObservable
.ToUniTask(cancellationToken, useFirstValue: true);
Debug.Log(enemy.Name);
}
}
IReadonlyReactivePropertyのawait
正確にいえばUniTask
変換ではないですが、どうせasync/await
とセットで使うので解説します。
IReadonlyReactiveProperty<T>
はawait
することで、次のメッセージ発行を待ち受けることができます。
using System.Threading;
using UniRx;
using UniRx.Async;
using UniRx.Async.Triggers;
using UnityEngine;
public class ReactivePropertyAwait : MonoBehaviour
{
// なにかのステート
private enum GameState
{
Ready,
Battle,
Result
}
// ステート管理するReactiveProperty
private ReactiveProperty<GameState> _currentGameState
= new ReactiveProperty<GameState>();
private void Start()
{
StateChangedAsync(this.GetCancellationTokenOnDestroy()).Forget();
}
/// <summary>
/// ステート遷移するたびに処理を走らせる
/// </summary>
private async UniTaskVoid StateChangedAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
// ステート遷移を待つ
var next = await _currentGameState;
// 遷移先に合わせて処理をする
switch (next)
{
case GameState.Ready:
// Do something...
break;
case GameState.Battle:
// Do something...
break;
case GameState.Result:
// Do something...
break;
}
}
}
}
その他、変わった変換の例
その他の変わった変換パターンについては別記事にまとめました。
まとめ
UniRx
とUniTask
はUnityでC#
を触るならぜひとも導入してほしいライブラリです。
入れて絶対に損はしない、むしろ無いと困るくらいには便利なものなので使い方を覚えて活用できるようになるとよいでしょう。
また、現在UniRxとUniTaskの書籍の執筆を行っています。全体の執筆が概ね終わり、現在はレビューフェーズに入っています。発売されたらぜひよろしくおねがいします。