R3連携

R3連携はv2.1.0から利用可能です。R3パッケージのインストールが必要です。


概要

このガイドでは、Reactive SOをR3(Unity向けReactive Extensions)と組み合わせて使用する方法を説明します。R3連携により、スロットリング、バッファリング、複数イベントストリームの合成など、高度なリアクティブパターンが利用可能になります。


前提条件

NuGet for UnityまたはOpenUPM経由で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のイベントハンドリング
  • 複雑な処理が不要
  • 依存関係を最小限にしたい

関連ドキュメント


This site uses Just the Docs, a documentation theme for Jekyll.