R3連携
R3連携はv2.1.0から利用可能です。R3パッケージのインストールが必要です。
概要
このガイドでは、Reactive SOをR3(Unity向けReactive Extensions)と組み合わせて使用する方法を説明します。R3連携により、スロットリング、バッファリング、複数イベントストリームの合成など、高度なリアクティブパターンが利用可能になります。
前提条件
NuGet for UnityまたはOpenUPM経由でR3パッケージをインストールしてください。
- パッケージID:
com.cysharp.r3 - GitHub: https://github.com/Cysharp/R3
R3がインストールされると、拡張メソッドが自動的に利用可能になります。追加の設定は不要です。
EventChannel拡張
AsObservable
任意のEventChannelをR3のObservableに変換できます。
型付きEventChannel
using Tang3cko.ReactiveSO;
using Tang3cko.ReactiveSO.R3;
using R3;
using UnityEngine;
public class DamageHandler : MonoBehaviour
{
[SerializeField] private IntEventChannelSO onDamageReceived;
private void Start()
{
// イベントチャンネルをObservableに変換
onDamageReceived.AsObservable()
.Subscribe(damage => Debug.Log($"{damage}ダメージを受けた"))
.AddTo(this);
}
}
VoidEventChannel
using Tang3cko.ReactiveSO;
using Tang3cko.ReactiveSO.R3;
using R3;
using UnityEngine;
public class GameOverHandler : MonoBehaviour
{
[SerializeField] private VoidEventChannelSO onPlayerDeath;
private void Start()
{
// VoidEventChannelはUnitを発行
onPlayerDeath.AsObservable()
.Subscribe(_ => ShowGameOverScreen())
.AddTo(this);
}
private void ShowGameOverScreen() { /* ... */ }
}
ReactiveEntitySet拡張
ReactiveEntitySetはエンティティライフサイクルイベント用の4つの監視メソッドを提供します。
| メソッド | 発行値 | 説明 |
|---|---|---|
ObserveAdd() | int(エンティティID) | エンティティ登録時に発行 |
ObserveRemove() | int(エンティティID) | エンティティ解除時に発行 |
ObserveDataChanged() | int(エンティティID) | エンティティデータ変更時に発行 |
ObserveSetChanged() | Unit | 任意の変更(追加・削除・データ変更)時に発行 |
基本的な使い方
using Tang3cko.ReactiveSO;
using Tang3cko.ReactiveSO.R3;
using R3;
using UnityEngine;
public class EnemyTracker : MonoBehaviour
{
[SerializeField] private EnemySet enemySet;
private void Start()
{
// 敵の追加を追跡
enemySet.ObserveAdd()
.Subscribe(id => Debug.Log($"敵 {id} がスポーンした"))
.AddTo(this);
// 敵の削除を追跡
enemySet.ObserveRemove()
.Subscribe(id => Debug.Log($"敵 {id} が倒された"))
.AddTo(this);
// データ変更を追跡
enemySet.ObserveDataChanged()
.Subscribe(id => Debug.Log($"敵 {id} のデータが更新された"))
.AddTo(this);
}
}
EventChannelを公開するコンポーネント
EventChannelのAsObservable()はR3連携の基盤です。EventChannelを公開getterで提供しているコンポーネントであれば、組み込み・ユーザー定義を問わずR3で購読できます。
組み込みコンポーネント
| コンポーネント | プロパティ | 型 |
|---|---|---|
| RuntimeSetSO | OnItemsChanged | VoidEventChannelSO |
| OnCountChanged | IntEventChannelSO | |
| VariableSO | OnValueChanged | EventChannelSO |
| ReactiveEntitySetSO | OnItemAdded | IntEventChannelSO |
| OnItemRemoved | IntEventChannelSO | |
| OnDataChanged | IntEventChannelSO | |
| OnSetChanged | VoidEventChannelSO |
// RuntimeSetSO
runtimeSet.OnItemsChanged.AsObservable()
.Subscribe(_ => RefreshUI())
.AddTo(this);
runtimeSet.OnCountChanged.AsObservable()
.Throttle(TimeSpan.FromMilliseconds(100))
.Subscribe(count => UpdateCountLabel(count))
.AddTo(this);
// VariableSO
healthVariable.OnValueChanged.AsObservable()
.Where(hp => hp < 20)
.Subscribe(_ => ShowLowHealthWarning())
.AddTo(this);
// ReactiveEntitySetSO(EventChannel経由)
entitySet.OnItemAdded.AsObservable()
.Subscribe(id => Debug.Log($"追加: {id}"))
.AddTo(this);
カスタムコンポーネント
ユーザー定義のコンポーネントでも同じパターンが使えます。
public class GameManager : MonoBehaviour
{
[SerializeField] private VoidEventChannelSO onGameStarted;
[SerializeField] private IntEventChannelSO onScoreChanged;
// EventChannelを公開getterで提供
public VoidEventChannelSO OnGameStarted => onGameStarted;
public IntEventChannelSO OnScoreChanged => onScoreChanged;
}
// 使用側
gameManager.OnGameStarted.AsObservable()
.Subscribe(_ => InitializeGame())
.AddTo(this);
gameManager.OnScoreChanged.AsObservable()
.Where(score => score >= 1000)
.Subscribe(_ => UnlockAchievement())
.AddTo(this);
ReactiveEntitySetSO専用拡張との関係
ReactiveEntitySetSOには利便性のため専用拡張も用意されています。内部的にはEventChannelのAsObservable()と同等です。
// 専用拡張(短い記法)
entitySet.ObserveAdd()
.Subscribe(id => Debug.Log($"追加: {id}"))
.AddTo(this);
// EventChannel経由(同じ動作)
entitySet.OnItemAdded.AsObservable()
.Subscribe(id => Debug.Log($"追加: {id}"))
.AddTo(this);
注意: EventChannel未設定時
EventChannelがInspectorで設定されていない場合、getterは
nullを返します。AsObservable()を呼び出すとNullReferenceExceptionが発生します。
// 安全な使用方法
if (runtimeSet.OnItemsChanged != null)
{
runtimeSet.OnItemsChanged.AsObservable()
.Subscribe(_ => RefreshUI())
.AddTo(this);
}
実践的なパターン
高頻度イベントのスロットリング
UI更新が頻繁に発火するのを防ぎます。
using Tang3cko.ReactiveSO;
using Tang3cko.ReactiveSO.R3;
using R3;
using System;
using UnityEngine;
public class HealthBar : MonoBehaviour
{
[SerializeField] private IntEventChannelSO onHealthChanged;
private void Start()
{
// 100msに1回だけUIを更新
onHealthChanged.AsObservable()
.Throttle(TimeSpan.FromMilliseconds(100))
.Subscribe(health => UpdateHealthBar(health))
.AddTo(this);
}
private void UpdateHealthBar(int health) { /* ... */ }
}
複数イベントストリームの合成
単一のサブスクリプションで複数のイベントに反応します。
using Tang3cko.ReactiveSO;
using Tang3cko.ReactiveSO.R3;
using R3;
using UnityEngine;
public class EnemyListUI : MonoBehaviour
{
[SerializeField] private EnemySet enemySet;
private void Start()
{
// 追加または削除時にリストを更新
Observable.Merge(
enemySet.ObserveAdd(),
enemySet.ObserveRemove()
)
.Subscribe(_ => RefreshEnemyList())
.AddTo(this);
}
private void RefreshEnemyList() { /* ... */ }
}
イベントのフィルタリング
特定の条件を満たすイベントのみを処理します。
using Tang3cko.ReactiveSO;
using Tang3cko.ReactiveSO.R3;
using R3;
using UnityEngine;
public class CriticalDamageEffect : MonoBehaviour
{
[SerializeField] private IntEventChannelSO onDamageReceived;
private void Start()
{
// クリティカルヒット(ダメージ > 50)のみに反応
onDamageReceived.AsObservable()
.Where(damage => damage > 50)
.Subscribe(damage => ShowCriticalHitEffect())
.AddTo(this);
}
private void ShowCriticalHitEffect() { /* ... */ }
}
イベントデータの変換
処理前にイベントデータを変換します。
using Tang3cko.ReactiveSO;
using Tang3cko.ReactiveSO.R3;
using R3;
using UnityEngine;
public class DamageLogger : MonoBehaviour
{
[SerializeField] private IntEventChannelSO onDamageReceived;
private void Start()
{
onDamageReceived.AsObservable()
.Select(damage => $"ダメージ: {damage}")
.Subscribe(message => Debug.Log(message))
.AddTo(this);
}
}
イベントのバッファリング
時間経過で複数のイベントを収集します。
using Tang3cko.ReactiveSO;
using Tang3cko.ReactiveSO.R3;
using R3;
using System;
using System.Linq;
using UnityEngine;
public class ComboCounter : MonoBehaviour
{
[SerializeField] private IntEventChannelSO onDamageDealt;
private void Start()
{
// 2秒間のダメージを合計
onDamageDealt.AsObservable()
.Buffer(TimeSpan.FromSeconds(2))
.Where(damages => damages.Count > 0)
.Subscribe(damages =>
{
int totalDamage = damages.Sum();
if (totalDamage > 100)
{
ShowComboEffect(damages.Count, totalDamage);
}
})
.AddTo(this);
}
private void ShowComboEffect(int hitCount, int totalDamage) { /* ... */ }
}
ベストプラクティス
サブスクリプションは必ず破棄する
AddToを使用して、MonoBehaviour破棄時に自動的にサブスクリプションを破棄します。
// Good: GameObjectが破棄されるとサブスクリプションも破棄される
onDamageReceived.AsObservable()
.Subscribe(damage => HandleDamage(damage))
.AddTo(this);
// Bad: 手動での破棄が必要で、忘れやすい
var subscription = onDamageReceived.AsObservable()
.Subscribe(damage => HandleDamage(damage));
// subscription.Dispose()を手動で呼ぶ必要がある
複数サブスクリプションにはCompositeDisposableを使用
多数のサブスクリプションがある場合はCompositeDisposableを使用します。
using Tang3cko.ReactiveSO;
using Tang3cko.ReactiveSO.R3;
using R3;
using UnityEngine;
public class GameUI : MonoBehaviour
{
[SerializeField] private IntEventChannelSO onHealthChanged;
[SerializeField] private IntEventChannelSO onScoreChanged;
[SerializeField] private VoidEventChannelSO onGameOver;
private CompositeDisposable disposables = new();
private void Start()
{
onHealthChanged.AsObservable()
.Subscribe(UpdateHealthUI)
.AddTo(disposables);
onScoreChanged.AsObservable()
.Subscribe(UpdateScoreUI)
.AddTo(disposables);
onGameOver.AsObservable()
.Subscribe(_ => ShowGameOverScreen())
.AddTo(disposables);
}
private void OnDestroy()
{
disposables.Dispose();
}
private void UpdateHealthUI(int health) { /* ... */ }
private void UpdateScoreUI(int score) { /* ... */ }
private void ShowGameOverScreen() { /* ... */ }
}
ホットパスでのサブスクライブを避ける
サブスクリプションにはオーバーヘッドがあります。毎フレームではなく、初期化時に一度だけサブスクライブしてください。
// Good: Startで一度だけサブスクライブ
private void Start()
{
onDamageReceived.AsObservable()
.Subscribe(HandleDamage)
.AddTo(this);
}
// Bad: 頻繁に新しいサブスクリプションを作成
private void Update()
{
// これはやめてください!
onDamageReceived.AsObservable()
.Take(1)
.Subscribe(HandleDamage);
}
R3連携を使うべき場面
R3を使う場合
- 時間ベースの操作が必要(Throttle、Delay、Buffer)
- 複数のイベントストリームを合成したい(Merge、CombineLatest)
- 複雑なフィルタリングや変換ロジックが必要
- チームがすでにリアクティブプログラミングに慣れている
標準イベントを使う場合
- シンプルな1対1のイベントハンドリング
- 複雑な処理が不要
- 依存関係を最小限にしたい