UniRx ではじめるリアクティブな Unity
Update ポーリングやイベントの氾濫を、時間軸のストリームとして宣言的に書く UniRx 入門。Observable の基本・主要オペレータ・購読の後始末まで。
「入力を監視して反応する」処理を、Unity では Update のポーリングやコールバックの寄せ集めで書きがちです。UniRx は、それらを 「時間軸に流れるストリーム」 として宣言的に扱えるようにするライブラリです。この記事では、なぜ UniRx なのか・Observable の基本・主要オペレータ・購読の後始末までを押さえます。
⚠️ UniRx は実質メンテ終了で、Cysharp による後継 R3 が新規プロジェクトの推奨。ただし考え方(Rx)は共通なので、まず UniRx で概念を掴むのは有効。本記事の内容は R3 にもほぼそのまま通じる。
なぜ UniRx なのか
Update に処理を書き足していくと、状態フラグと if が増えて追いにくくなります。UniRx は「いつ・何が起きたか」を**イベントの流れ(ストリーム)**として捉え、それに変換・フィルタを重ねて宣言的に書きます。
// 素朴な Update ポーリング
void Update()
{
if (Input.GetMouseButtonDown(0)) Fire();
}
// UniRx: クリックというストリームを購読する
Observable.EveryUpdate()
.Where(_ => Input.GetMouseButtonDown(0))
.Subscribe(_ => Fire())
.AddTo(this);
🧭 UniRx は .NET の Rx(System.Reactive)を Unity 向けに移植したもの。
IObservable<T>/IObserver<T>は元々 BCL の型で、LINQ 風のオペレータもほぼ同じ。C# で Rx を触ったことがあれば知識がそのまま効く。
Observable と Subscribe
中心にあるのは IObservable<T>(値が流れてくる源)と Subscribe(流れを受け取る)です。Subscribe は IDisposable を返し、これを捨てると購読が止まります。
IObservable<long> stream = Observable.Interval(TimeSpan.FromSeconds(1)); // 1秒ごとに発行
IDisposable subscription = stream.Subscribe(
x => Debug.Log($"OnNext: {x}"), // 値が来るたび
ex => Debug.LogError(ex), // エラー時
() => Debug.Log("OnCompleted")); // 完了時
イベントやコールバックも Observable に変換できます。uGUI のボタンなどは専用の拡張が用意されています。
button.OnClickAsObservable()
.Subscribe(_ => Debug.Log("clicked"))
.AddTo(this);
主要オペレータ
ストリームに変換・フィルタを重ねるのが Rx の肝です。よく使うものだけ挙げます。
| オペレータ | 役割 |
|---|---|
Where | 条件に合う値だけ通す |
Select | 値を別の形に変換する |
Throttle | 発行が落ち着くまで待って最後の1つを流す(検索入力など) |
ThrottleFirst | 最初の1つを流し一定時間は無視する(連打防止など) |
DistinctUntilChanged | 前回と同じ値なら流さない |
Merge / CombineLatest | 複数ストリームを合流・組み合わせる |
// 入力が 0.5 秒落ち着いてから検索する
inputField.OnValueChangedAsObservable()
.Throttle(TimeSpan.FromSeconds(0.5))
.DistinctUntilChanged()
.Subscribe(text => Search(text))
.AddTo(this);
購読の後始末(AddTo)
購読しっぱなしはリークになります。破棄されたオブジェクトを参照し続けたり、二重購読で処理が多重に走ったりします。AddTo で MonoBehaviour のライフタイムに結びつけ、破棄時に自動で購読解除します。
Observable.EveryUpdate()
.Subscribe(_ => Tick())
.AddTo(this); // この GameObject/Component 破棄で購読解除
🧭 UniTask で
GetCancellationTokenOnDestroy()のトークンを渡すのと同じ発想。非同期・購読は「オブジェクトの寿命に必ず紐づける」のが Unity での鉄則。詳しくは UniTask 入門 を参照。
ReactiveProperty で状態を持つ
値の変化そのものをストリームにするのが ReactiveProperty<T> です。.Value で読み書きし、変化すると購読側に流れます。MVP/MVVM で View と状態を繋ぐのに向きます。
public class Player : MonoBehaviour
{
public ReactiveProperty<int> Hp = new ReactiveProperty<int>(100);
void Start()
{
// HP が変わるたびに UI を更新(初期値も届く)
Hp.Subscribe(hp => hpText.text = $"HP: {hp}").AddTo(this);
}
public void Damage(int n) => Hp.Value -= n; // 代入するだけで購読側に伝わる
}
まとめ
- UniRx はイベントや入力を 時間軸のストリームとして宣言的に扱うライブラリ。
Updateポーリングとフラグ地獄を減らせる。 - 中心は
IObservable<T>とSubscribe。Subscribeが返すIDisposableが購読の寿命。 Where/Select/Throttleなどのオペレータを重ねて、変換・フィルタを宣言的に書く。AddToで購読をオブジェクトの寿命に紐づけるのが必須(リーク防止)。ReactiveProperty<T>は「値の変化」自体をストリーム化し、状態と View を繋ぐのに向く。- 新規なら後継の R3 も検討する(概念は本記事のまま通用する)。
次にやること: CombineLatest で複数の入力条件をまとめたり、UniRx と UniTask を ToObservable() / ToUniTask() で相互変換して、非同期とストリームを行き来する書き方に進むと応用が利く。