自分用R3辞書
基本形
Observable<T>
.Where(...)
.Select(...)
.Subscribe(...);
生成 → 加工 → 購読、という順序で読むと理解しやすい。
R3は通常のRxと似ているが、エラーでストリームが自動終了しないなど、UniRxやSystem.Reactiveとは重要な違いがある。
索引
基本
| メソッド | 説明 |
|---|---|
Subscribe |
流れてきた値を受け取り、実際の処理を実行する。 |
AddTo |
購読をGameObjectやComponentの破棄に連動させる。 |
Where |
条件に一致する値だけを通す。 |
Select |
値を別の値に変換する。 |
DistinctUntilChanged |
前回と同じ値なら通知しない。 |
Take |
先頭から指定個数だけ受け取って終了する。 |
Skip |
先頭から指定個数を無視する。 |
TakeUntil |
別のObservableが値を発行するか完了するまで、またはCancellationTokenがキャンセルされるまで値を受け取る。 |
組み合わせる系
| メソッド | 説明 |
|---|---|
Merge |
複数のObservableを一つにまとめる。どれかから値が来たら、その値を流す。 |
CombineLatest |
複数のObservableの最新値を組み合わせる。一方が更新されるたびに、それぞれの最新値から新しい結果を作る。 |
Zip |
複数のObservableを、到着順に一つずつ組み合わせる。 |
ストリーム制御系
| メソッド | 説明 |
|---|---|
Debounce |
値が連続して来たとき、一定時間入力が途切れてから最後の値を流す。 |
ThrottleFirst |
一定期間内の最初の値だけを通す。 |
ThrottleLast |
最初の値が到着した時点から一定期間の区間を開始し、その区間内で最後に到着した値を、期間終了時に一度流す。 |
タイミング系
| メソッド | 説明 |
|---|---|
Delay / DelayFrame |
通知を遅らせる。 |
Observable.Timer |
指定時間後に一度だけ通知する。 |
Observable.Interval |
一定時間ごとに通知する。 |
Observable.EveryUpdate |
毎フレーム通知する。Update() に近い。 |
Observable.NextFrame |
次のフレームで一度通知する。 |
通知送る側
| メソッド | 説明 |
|---|---|
ReactiveProperty<T> |
値を保持しつつ、変更をObservableとして通知する。 |
Subject<T> |
任意のタイミングでイベントを発行する。 |
Unity拡張
| メソッド | 説明 |
|---|---|
OnClickAsObservable などのUnity UI変換 |
uGUIのイベントをObservableに変換する。 |
SubscribeToText / SubscribeToInteractable |
UIへの反映を簡潔に書くUnity用拡張。 |
OnCollisionEnterAsObservable などのCollision・Trigger系 |
MonoBehaviourのイベントをObservableとして扱う。 |
非同期処理系
| メソッド | 説明 |
|---|---|
SelectAwait |
各値に対して非同期処理を実行し、その結果を次へ流す。 |
FirstAsync |
最初の値を Task<T> として待つ。 |
LastAsync |
最後の値を Task<T> として待つ。 |
Subscribe
流れてきた値を受け取り、実際の処理を実行する。ほぼすべてのストリームの終点になる。
playerHp
.Subscribe(hp =>
{
Debug.Log($"HP: {hp}");
});
Subscribe の戻り値は IDisposable。これを破棄すると購読が解除される。
IDisposable subscription = playerHp.Subscribe(Debug.Log);
// 購読解除
subscription.Dispose();
R3を使ううえで最も重要なのは、Subscribe したら必ずライフサイクルを管理することだ。放置すると、そのGameObjectが不要になっても処理が残る可能性がある。
AddTo
購読をGameObjectやComponentの破棄に連動させる。
button.OnClickAsObservable()
.Subscribe(_ =>
{
Debug.Log("Clicked");
})
.AddTo(this);
このMonoBehaviourが破棄されると、購読も自動的に破棄される。
Unityでは基本的に、
.Subscribe(...)
.AddTo(this);
までを一組として考えるとよい。R3の AddTo(Component/GameObject) は、非アクティブのまま破棄されたGameObjectも考慮した専用の破棄監視を行う。購読数が非常に多い場合は、Disposable.CreateBuilder() と destroyCancellationToken を使う方式のほうが効率的だ。
var builder = Disposable.CreateBuilder();
stream1.Subscribe(...).AddTo(ref builder);
stream2.Subscribe(...).AddTo(ref builder);
stream3.Subscribe(...).AddTo(ref builder);
builder.RegisterTo(destroyCancellationToken);
普通のゲームコードなら、まずは .AddTo(this) で十分。
Where
条件に一致する値だけを通す。LINQの Where と同じ発想だ。
Observable.EveryUpdate()
.Where(_ => Input.GetKeyDown(KeyCode.Space))
.Subscribe(_ =>
{
Jump();
})
.AddTo(this);
値そのものに条件を付ける例。
hp
.Where(value => value <= 0)
.Subscribe(_ => Die())
.AddTo(this);
ただし、HPが0以下の間に何度も値が更新されれば、Die() も複数回呼ばれる。状態が切り替わった瞬間だけ欲しいなら、後述の DistinctUntilChanged と組み合わせるべきだ。
Select
値を別の値に変換する。
hp
.Select(value => value <= 0)
.Subscribe(isDead =>
{
Debug.Log($"死亡状態: {isDead}");
})
.AddTo(this);
int のHPを bool の死亡状態に変換している。
UI表示用の文字列を作ることも多い。
hp
.Select(value => $"HP: {value}")
.Subscribe(text => hpText.text = text)
.AddTo(this);
Where は値を選別し、Select は値を変換する。この違いは覚えておくこと。
DistinctUntilChanged
前回と同じ値なら通知しない。
hp
.Select(value => value <= 0)
.DistinctUntilChanged()
.Subscribe(isDead =>
{
Debug.Log($"死亡状態が変化: {isDead}");
})
.AddTo(this);
たとえばHPが 100 → 90 → 50 と変化しても、死亡状態はずっと false なので通知されない。50 → 0 で初めて true が通知される。
状態監視ではかなり頻繁に使う。
なお、ReactiveProperty<T> 自体も、通常は現在値と同じ値を代入した場合には通知しない。強制通知したい場合は .Value = value ではなく OnNext(value) を使う。
Take
先頭から指定個数だけ受け取って終了する。
button.OnClickAsObservable()
.Take(1)
.Subscribe(_ =>
{
StartGame();
})
.AddTo(this);
何度クリックされても、最初の1回だけ実行される。
stream.Take(5);
なら5回受け取った時点で完了する。
「1回だけ待つ」という用途なら、Take(1) は非常によく使う。
Skip
先頭から指定個数を無視する。
hp
.Skip(1)
.Subscribe(value =>
{
Debug.Log($"初期値以外のHP変更: {value}");
})
.AddTo(this);
ReactiveProperty<T> は購読時に現在値を通知するため、「初期値は要らず、変更だけ欲しい」という場合に Skip(1) が使われる。
ただし、安易な Skip(1) は設計意図を隠しやすい。初期値も意味のある状態なら、無視せず正しく処理したほうがいい。
TakeUntil
別のObservableが値を発行するか完了するまで、またはCancellationTokenがキャンセルされるまで値を受け取る。
Observable.EveryUpdate()
.TakeUntil(destroyCancellationToken)
.Subscribe(_ =>
{
// Update相当
});
CancellationTokenがキャンセルされると完了し、購読が解除される。
別のObservableを終了条件にすることもできる。
dragObservable
.TakeUntil(pointerUpObservable)
.Subscribe(position =>
{
UpdateDrag(position);
})
.AddTo(this);
「ボタンを離すまで」「シーンを終了するまで」「通信をキャンセルするまで」という処理に向いている。R3ではCancellationTokenを受け取る生成系・演算子が用意されており、キャンセル時には完了通知を通して購読を終了できる。
Merge
複数のObservableを一つにまとめる。どれかから値が来たら、その値を流す。
var keyboardAttack = Observable.EveryUpdate()
.Where(_ => Input.GetKeyDown(KeyCode.Space))
.Select(_ => Unit.Default);
var buttonAttack = attackButton.OnClickAsObservable();
keyboardAttack
.Merge(buttonAttack)
.Subscribe(_ =>
{
Attack();
})
.AddTo(this);
キーボードでもUIボタンでも、同じ Attack() を実行できる。
複数の入力経路を一つのゲーム操作に統合するときに便利だ。
CombineLatest
複数のObservableの最新値を組み合わせる。一方が更新されるたびに、それぞれの最新値から新しい結果を作る。
ReactiveProperty<int> currentHp = new(100);
ReactiveProperty<int> maxHp = new(100);
currentHp
.CombineLatest(maxHp, (current, max) => (float)current / max)
.Subscribe(rate =>
{
hpSlider.value = rate;
})
.AddTo(this);
currentHp または maxHp が変わるたび、最新の両方を使って割合を再計算する。
フォームの入力可否にも使える。
nameInput
.CombineLatest(passwordInput,
(name, password) =>
!string.IsNullOrEmpty(name) &&
password.Length >= 8)
.Subscribe(canLogin =>
{
loginButton.interactable = canLogin;
})
.AddTo(this);
「複数の状態から派生状態を作る」場合の中心的な演算子だ。
Zip
複数のObservableを、到着順に一つずつ組み合わせる。
streamA
.Zip(streamB, (a, b) => (a, b))
.Subscribe(pair =>
{
Debug.Log($"{pair.a}, {pair.b}");
});
CombineLatest とは挙動が違う。
CombineLatest は常に最新値を再利用する。一方、Zip は未使用の値同士を一対一で組み合わせる。ゲームの状態監視では CombineLatest のほうが登場頻度が高く、イベントの対応付けでは Zip が向いている。
Debounce
値が連続して来たとき、一定時間入力が途切れてから最後の値を流す。
searchInput.OnValueChangedAsObservable()
.Debounce(TimeSpan.FromMilliseconds(300))
.Subscribe(keyword =>
{
Search(keyword);
})
.AddTo(this);
文字を入力するたびに検索するのではなく、入力が300ミリ秒止まったところで検索する。
検索欄、設定値の保存、連続的に変化するスライダーなどで有効だ。
UniRxや一部のRxライブラリで Throttle と呼ばれていた「最後の入力を待つ処理」は、R3では主に Debounce と考えたほうが混乱しない。R3の公式APIでも時間ベース・フレームベースの Debounce が提供されている。
ThrottleFirst
一定期間内の最初の値だけを通す。
attackButton.OnClickAsObservable()
.ThrottleFirst(TimeSpan.FromSeconds(1))
.Subscribe(_ =>
{
Attack();
})
.AddTo(this);
ボタンを連打されても、1秒間に最初の1回しか通さない。
誤連打防止やクールダウンの簡易実装に使える。ただし、本格的な戦闘ロジックなら、時間制限をUIストリームだけに埋め込まず、ゲーム側の状態として管理したほうが堅牢だ。
ThrottleLast
最初の値が到着した時点から一定期間の区間を開始し、その区間内で最後に到着した値を、期間終了時に一度流す。
positionStream
.ThrottleLast(TimeSpan.FromMilliseconds(100))
.Subscribe(position =>
{
SendPosition(position);
})
.AddTo(this);
大量に発生する位置更新を間引き、各区間の最後の値を送る用途などに向いている。
フレーム単位版もある。
Observable.EveryUpdate()
.ThrottleLastFrame(10)
.Subscribe(_ =>
{
Debug.Log("10フレーム単位で実行");
})
.AddTo(this);
Delay / DelayFrame
通知を遅らせる。
damageEvent
.Delay(TimeSpan.FromSeconds(1))
.Subscribe(_ =>
{
ShowDamageResult();
})
.AddTo(this);
フレーム指定も可能。
damageEvent
.DelayFrame(3)
.Subscribe(_ =>
{
Debug.Log("3フレーム後");
})
.AddTo(this);
注意すべきなのは、Delay は「後続の通知を遅らせる」演算子であって、現在のメソッドを同期的に停止するものではないことだ。
Observable.Timer
指定時間後に一度だけ通知する。
Observable.Timer(TimeSpan.FromSeconds(2))
.Subscribe(_ =>
{
Debug.Log("2秒経過");
})
.AddTo(this);
一定間隔で繰り返すオーバーロードもある。
Observable.Timer(
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(3))
.Subscribe(_ =>
{
Debug.Log("最初は1秒後、その後は3秒ごと");
})
.AddTo(this);
Observable.Interval
一定時間ごとに通知する。
Observable.Interval(TimeSpan.FromSeconds(1))
.Subscribe(count =>
{
Debug.Log($"{count} 秒相当");
})
.AddTo(this);
Unityではデフォルトの時間処理がPlayerLoopのUpdateに連動するよう設定される。タイムスケールを無視する場合は、Unity用の TimeProvider を明示できる。
Observable.Interval(
TimeSpan.FromSeconds(1),
UnityTimeProvider.UpdateIgnoreTimeScale)
.Subscribe(_ =>
{
Debug.Log("Time.timeScaleに影響されない");
})
.AddTo(this);
Observable.EveryUpdate
毎フレーム通知する。Update() に近い。
Observable.EveryUpdate()
.Subscribe(_ =>
{
transform.Rotate(0f, 90f * Time.deltaTime, 0f);
})
.AddTo(this);
ただし、何でも EveryUpdate() に置き換えるのは賢くない。普通の Update() のほうが単純な処理までObservableにすると、コードの追跡が難しくなる。
EveryUpdate() が有効なのは、フレーム更新に Where、TakeUntil、ThrottleLastFrame などを組み合わせたい場合だ。
実行タイミングも指定できる。
Observable.EveryUpdate(UnityFrameProvider.FixedUpdate)
.Subscribe(_ =>
{
// FixedUpdate相当
})
.AddTo(this);
Unity用R3には、Update、FixedUpdate、PostLateUpdateなどに対応する UnityFrameProvider が用意されている。
Observable.NextFrame
次のフレームで一度通知する。
Observable.NextFrame()
.Subscribe(_ =>
{
Debug.Log("次のフレーム");
})
.AddTo(this);
UIレイアウトの更新を1フレーム待つ、生成直後の初期化を次フレームにずらす、といった用途で使う。
ReactiveProperty<T>
値を保持しつつ、変更をObservableとして通知する。
private readonly ReactiveProperty<int> hp = new(100);
private void Start()
{
hp.Subscribe(value =>
{
Debug.Log($"HP: {value}");
}).AddTo(this);
hp.Value -= 10;
}
Subject<T> が単なるイベント発行器なのに対し、ReactiveProperty<T> は現在値を保持する。
外部には読み取り専用で公開する設計が扱いやすい。
private readonly ReactiveProperty<int> hp = new(100);
public ReadOnlyReactiveProperty<int> Hp => hp;
変更できる場所をクラス内部に限定できるため、無秩序な代入を防げる。
派生プロパティも作れる。
public ReadOnlyReactiveProperty<bool> IsDead { get; }
public Player()
{
IsDead = hp
.Select(value => value <= 0)
.ToReadOnlyReactiveProperty();
}
private void Dispose()
{
IsDead.Dispose();
hp.Dispose();
}
R3の ReactiveProperty は値を保持し、重複値を通常は除外する。読み取り専用型へ公開する設計も公式READMEで推奨されている。
Subject<T>
任意のタイミングでイベントを発行する。
private readonly Subject<int> damaged = new();
public Observable<int> OnDamaged => damaged;
public void Damage(int amount)
{
damaged.OnNext(amount);
}
購読側。
player.OnDamaged
.Subscribe(amount =>
{
ShowDamageEffect(amount);
})
.AddTo(this);
Subject<T> はイベントとして使い、ReactiveProperty<T> は状態として使う、と区別すると設計しやすい。
また、外部には Subject<T> 自体を公開しないほうがよい。公開すると外部から OnNext() を呼べてしまう。
// 良い
public Observable<int> OnDamaged => damaged;
// 避けたい
public Subject<int> OnDamaged => damaged;
OnClickAsObservable などのUnity UI変換
uGUIのイベントをObservableに変換する。
button.OnClickAsObservable()
.Subscribe(_ => StartGame())
.AddTo(this);
よく使うものには以下がある。
button.OnClickAsObservable();
toggle.OnValueChangedAsObservable();
slider.OnValueChangedAsObservable();
inputField.OnValueChangedAsObservable();
inputField.OnEndEditAsObservable();
dropdown.OnValueChangedAsObservable();
値変更系は購読開始時に現在値を流すものがあるため、「変更時だけ」と思い込むと初期化処理が二重になることがある。公式READMEでは、uGUIの OnValueChangedAsObservable は購読開始時に最新値を発行すると説明されている。
SubscribeToText / SubscribeToInteractable
UIへの反映を簡潔に書くUnity用拡張。
hp
.Select(value => $"HP: {value}")
.SubscribeToText(hpText)
.AddTo(this);
ボタンの操作可否。
canSubmit
.SubscribeToInteractable(submitButton)
.AddTo(this);
単純なUI反映では便利だが、複雑な表示処理まで無理に SubscribeTo... に押し込む必要はない。通常の Subscribe のほうが意図が明確な場合もある。
OnCollisionEnterAsObservable などのCollision・Trigger系
MonoBehaviourのイベントをObservableとして扱う。
using R3;
using R3.Triggers;
this.OnCollisionEnterAsObservable()
.Subscribe(collision =>
{
Debug.Log($"衝突: {collision.gameObject.name}");
})
.AddTo(this);
同様に、Update系、Trigger系、Pointer系などがある。R3.Triggersの拡張メソッドは、必要なObservableTriggerコンポーネントを自動的に注入する。
SelectAwait
各値に対して非同期処理を実行し、その結果を次へ流す。
button.OnClickAsObservable()
.SelectAwait(
async (_, cancellationToken) =>
{
await LoadDataAsync(cancellationToken);
return "完了";
},
AwaitOperation.Drop)
.Subscribe(result =>
{
Debug.Log(result);
})
.AddTo(this);
AwaitOperation.Drop は、処理中に新しい値が来たら捨てる。ボタンの多重実行防止に便利だ。
主なモードは、順番に処理する Sequential、処理中の入力を捨てる Drop、前の処理をキャンセルして最新を実行する Switch、並列実行する Parallel。
たとえば検索処理なら Switch が向いている。
searchInput.OnValueChangedAsObservable()
.Debounce(TimeSpan.FromMilliseconds(300))
.SelectAwait(
async (keyword, ct) =>
{
return await SearchAsync(keyword, ct);
},
AwaitOperation.Switch)
.Subscribe(results =>
{
UpdateResultView(results);
})
.AddTo(this);
新しい検索語が来たら古い検索をキャンセルできる。R3では SelectAwait、WhereAwait、SubscribeAwait に非同期処理の実行方式が明示的に用意されている。
FirstAsync
最初の値を Task<T> として待つ。
async Task WaitForClickAsync(CancellationToken ct)
{
await button.OnClickAsObservable()
.FirstAsync(ct);
Debug.Log("クリックされた");
}
Rxチェーンをずっと購読するのではなく、「イベントを一回だけ非同期に待ちたい」場合に向いている。
int value = await hp
.Where(x => x <= 0)
.FirstAsync(cancellationToken);
R3では、単一結果を返す処理は FirstAsync や LastAsync のようなTaskベースのAPIになっている。
LastAsync
ストリームが完了した時点で、最後に流れた値を Task<T> として返す。
完了しないObservableでは結果が確定しないため、
Take、TakeUntil、CancellationTokenなどで完了条件を用意する必要がある。
実戦的な組み合わせ
典型的なUIコードはこうなる。
using R3;
using UnityEngine;
using UnityEngine.UI;
public sealed class PlayerView : MonoBehaviour
{
[SerializeField] private Button healButton;
[SerializeField] private Text hpText;
private readonly ReactiveProperty<int> hp = new(100);
private void Start()
{
hp
.Select(value => $"HP: {value}")
.SubscribeToText(hpText)
.AddTo(this);
healButton.OnClickAsObservable()
.Where(_ => hp.Value < 100)
.ThrottleFirst(TimeSpan.FromMilliseconds(300))
.Subscribe(_ =>
{
hp.Value = Mathf.Min(hp.Value + 10, 100);
})
.AddTo(this);
}
}
このコードで使っている役割は明確だ。
ReactiveProperty が状態を保持し、Select が表示文字列へ変換し、SubscribeToText がUIへ反映する。ボタンクリックは OnClickAsObservable でイベント化し、Where で回復可能な場合だけ通し、ThrottleFirst で連打を抑え、Subscribe で状態を変更する。
最初に覚える優先順位を絞るなら、Subscribe、AddTo、Where、Select、ReactiveProperty、DistinctUntilChanged、Take、CombineLatest、OnClickAsObservable、SelectAwait。このあたりで、UnityのUIとゲーム状態の大部分は組める。