アーキテクチャパターン


目的

このガイドは、各状況で適切なReactive SOツールを選択するのに役立ちます。Event Channels、Variables、Runtime Sets、Reactive Entity Setsをいつ使用するか、また標準のC#フィールドがより良い選択肢となるケースを学びます。


4つのツール

Reactive SOは4つの補完的なツールを提供します。

ツール 目的 最適な用途
Event Channels グローバル通知 Fire-and-forgetメッセージ、疎結合通信
Variables 共有状態 グローバル値、クロスシーン永続性
Runtime Sets オブジェクトコレクション アクティブオブジェクト追跡、シングルトン置き換え
Reactive Entity Sets エンティティごとの状態 IDベースルックアップ、エンティティ状態管理

Instance vs Globalルール

Reactive SOを使用する際の最も重要な判断です。

インスタンスデータ(オブジェクトごとに固有)→ C#フィールドを使用 グローバルデータ(オブジェクト間で共有)→ ScriptableObjectを使用

なぜ重要か

ScriptableObjectは共有リソースです。同じアセットを参照するすべてのスクリプトが同じデータを共有します。

// 悪い例:すべてのスポーンされた敵が同じ体力を共有!
public class Enemy : MonoBehaviour
{
    [SerializeField] private IntVariableSO health;  // 共有される!

    void TakeDamage(int damage)
    {
        health.Value -= damage;  // すべての敵に影響
    }
}
// 良い例:各敵が独立した体力を持つ
public class Enemy : MonoBehaviour
{
    private int health = 100;  // インスタンス固有

    void TakeDamage(int damage)
    {
        health -= damage;  // この敵のみに影響
    }
}

決定ツリー

以下のフローチャートで、状況に応じた適切なツールを選択できます。

flowchart LR
    A["データの種類は?"] --> B{インスタンス固有?}
    B -->|YES| C["C#フィールド<br>各オブジェクト独立"]
    B -->|NO| D{共有方法は?}
    D -->|通知のみ| E["Event Channels<br>疎結合通信"]
    D -->|状態共有| F{複数オブジェクト?}
    D -->|オブジェクト追跡| I["Runtime Sets<br>動的コレクション"]
    F -->|同じ値を共有| G["Variables<br>グローバル状態"]
    F -->|エンティティごと| H["Reactive Entity Sets<br>ID管理"]

各ツールの使用タイミング

Event Channels

システムを疎結合化するグローバル通知に使用します。

// 最適:ゲーム全体の通知
[SerializeField] VoidEventChannelSO onPlayerDied;
[SerializeField] VoidEventChannelSO onLevelCompleted;
[SerializeField] IntEventChannelSO onScoreChanged;

// 最適:システム間通信
[SerializeField] GameObjectEventChannelSO onEnemySpawned;
[SerializeField] StringEventChannelSO onAudioRequest;

インスタンス固有のイベントには使用しないでください。

// 悪い例:すべての敵が同じ死亡イベントに反応
public class Enemy : MonoBehaviour
{
    [SerializeField] private VoidEventChannelSO onDeath;

    private void OnEnable() => onDeath.OnEventRaised += Die;

    public void TakeDamage(int damage)
    {
        if (health <= 0)
            onDeath.RaiseEvent();  // すべての敵が死亡!
    }
}

Variables

複数のシステムが読み取るグローバル状態に使用します。

// 最適:グローバルゲーム状態
[SerializeField] IntVariableSO playerScore;
[SerializeField] BoolVariableSO isPaused;
[SerializeField] FloatVariableSO masterVolume;

// 最適:デザイナー設定可能な値
[SerializeField] FloatVariableSO enemySpawnRate;
[SerializeField] IntVariableSO maxEnemies;

インスタンス状態には使用しないでください。

// 悪い例:個々の敵の体力
[SerializeField] IntVariableSO health;  // すべての敵が共有!

// 良い例:代わりにC#フィールドを使用
private int health = 100;

Runtime Sets

シングルトンなしでアクティブオブジェクトを追跡します。

// 最適:動的オブジェクト管理
[SerializeField] GameObjectRuntimeSetSO enemies;
[SerializeField] GameObjectRuntimeSetSO pickups;
[SerializeField] TransformRuntimeSetSO waypoints;

// オブジェクトが自己登録
private void OnEnable() => enemies?.Add(gameObject);
private void OnDisable() => enemies?.Remove(gameObject);

Reactive Entity Sets

IDベースルックアップを持つエンティティごとの状態に使用します。

// 最適:エンティティごとのデータストレージ
[SerializeField] EnemyEntitySetSO entitySet;

// 各エンティティが独立した状態を持つ
entitySet.Register(this, new EnemyState { Health = 100 });
entitySet.UpdateData(this, state => {
    state.Health -= damage;
    return state;
});

共通パターン

パターン1: 動的な敵スポーン

インスタンスフィールドとグローバルイベントを組み合わせます。

public class Enemy : MonoBehaviour
{
    // インスタンス固有の状態
    [SerializeField] private int maxHealth = 100;
    private int currentHealth;

    // クロスシステム通知用のグローバルイベント
    [SerializeField] private IntEventChannelSO onAnyEnemyDamaged;
    [SerializeField] private VoidEventChannelSO onAnyEnemyDied;

    private void Start() => currentHealth = maxHealth;

    public void TakeDamage(int damage)
    {
        currentHealth -= damage;
        onAnyEnemyDamaged?.RaiseEvent(currentHealth);

        if (currentHealth <= 0)
        {
            onAnyEnemyDied?.RaiseEvent();
            Destroy(gameObject);
        }
    }
}

パターン2: グローバルスコアシステム

真に共有される状態にはVariablesを使用します。

public class ScoreManager : MonoBehaviour
{
    [SerializeField] private IntVariableSO score;

    private void OnEnable() => score.ResetToInitial();

    public void AddPoints(int points)
    {
        score.Value += points;  // 自動的にOnValueChangedを発火
    }
}

public class ScoreDisplay : MonoBehaviour
{
    [SerializeField] private IntVariableSO score;
    [SerializeField] private Text scoreText;

    private void Update()
    {
        scoreText.text = $"Score: {score.Value}";
    }
}

パターン3: Runtime Setsでの敵管理

シングルトンマネージャーを置き換えます。

public class EnemySpawner : MonoBehaviour
{
    [SerializeField] private GameObject enemyPrefab;
    [SerializeField] private GameObjectRuntimeSetSO activeEnemies;
    [SerializeField] private VoidEventChannelSO onWaveComplete;

    private void Update()
    {
        // FindObjectsOfType不要
        if (activeEnemies.Count == 0)
            onWaveComplete?.RaiseEvent();
    }
}

public class Enemy : MonoBehaviour
{
    [SerializeField] private GameObjectRuntimeSetSO activeEnemies;

    private void OnEnable() => activeEnemies?.Add(gameObject);
    private void OnDisable() => activeEnemies?.Remove(gameObject);
}

パターン4: ハイブリッドアプローチ

複数のパターンを組み合わせます。

public class Player : MonoBehaviour
{
    // インスタンス状態
    private int currentAmmo = 30;

    // グローバル設定(デザイナー調整可能)
    [SerializeField] private IntVariableSO maxAmmo;
    [SerializeField] private FloatVariableSO reloadTime;

    // グローバルイベント
    [SerializeField] private IntEventChannelSO onAmmoChanged;

    public void Shoot()
    {
        if (currentAmmo > 0)
        {
            currentAmmo--;
            onAmmoChanged?.RaiseEvent(currentAmmo);
        }
    }

    public void Reload()
    {
        currentAmmo = maxAmmo.Value;  // グローバル設定から読み取り
    }
}

よくある間違い

間違い1: インスタンスイベントにEvent Channelsを使用

// 悪い例:すべての敵が同じ死亡イベントを購読
onDeath.OnEventRaised += Die;  // 1体が死ぬとすべて死亡!

// 良い例:インスタンスロジックには直接メソッド呼び出しを使用
private void Die()
{
    onAnyEnemyDied?.RaiseEvent();  // グローバルに通知(オプション)
    Destroy(gameObject);           // この敵のみ
}

間違い2: インスタンス状態にVariablesを使用

// 悪い例:共有体力
[SerializeField] IntVariableSO health;

// 良い例:インスタンス体力
private int health = 100;

間違い3: ScriptableObjectの過剰使用

// 悪い例:すべてがScriptableObjectである必要はない
[SerializeField] private FloatVariableSO moveSpeed;
[SerializeField] private FloatVariableSO jumpHeight;
[SerializeField] private FloatVariableSO gravity;

// 良い例:インスタンス固有の設定にはC#フィールドを使用
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float jumpHeight = 2f;

決定ガイド

状態管理

シナリオ 使用 理由
プレイヤースコア(単一値) VariableSO 真にグローバル、共有
ゲーム設定(音量など) VariableSO デザイナー調整可能、永続
敵の体力(複数インスタンス) C#フィールド 各自が独立した状態が必要
弾の速度(複数インスタンス) C#フィールド インスタンス固有、パフォーマンス
現在のレベル VariableSO グローバル、クロスシーン
アクティブな敵リスト RuntimeSetSO 動的コレクション追跡
エンティティごとの状態(体力、ステータス) ReactiveEntitySetSO IDベースルックアップ、エンティティごとのイベント

イベント通信

シナリオ 使用 理由
プレイヤー死亡通知 EventChannelSO グローバル、複数リスナー
敵がダメージを受けた(どの敵でも) EventChannelSO UI、オーディオにグローバル通知
個々の敵の死亡ロジック C#メソッド インスタンス固有
ボタンクリック → UI応答 EventChannelSO UIとゲームロジックを分離
衝突処理 C#メソッド インスタンス固有の物理

コレクション管理

シナリオ 使用 理由
すべてのアクティブな敵を追跡 RuntimeSetSO シングルトンマネージャーを置き換え
スポーンされた弾を管理 RuntimeSetSO 自動クリーンアップ付き動的
近くのオブジェクトをクエリ Physics.OverlapSphere パフォーマンス、空間的
マネージャー内の静的List RuntimeSetSO より良いテスト性

まとめ

ツール 使用タイミング 避けるタイミング
Event Channels グローバル通知、疎結合メッセージング インスタンス固有イベント
Variables グローバル状態、デザイナー設定 インスタンス状態
Runtime Sets オブジェクト追跡、シングルトン置き換え エンティティごとのデータストレージ
Reactive Entity Sets エンティティ状態、IDベースルックアップ 単純なオブジェクト追跡
C#フィールド インスタンス状態、パフォーマンス重視 システム間で共有される状態

参照


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