Runtime Sets

Runtime Setsはv1.1.0から利用可能です。


目的

このガイドでは、シングルトンマネージャーなしでオブジェクトコレクションを追跡するRuntime Setsの使い方を説明します。登録パターン、アクティブオブジェクトのクエリ方法、代替手段よりRuntime Setsを選ぶタイミングを学びます。


Runtime Setsとは?

Runtime Setsはランタイムで作成・破棄されるオブジェクトを追跡するScriptableObjectベースのコレクションです。オブジェクトは有効化時に登録し、無効化時に登録解除します。

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

// どこからでもアクティブオブジェクトをクエリ
int count = activeEnemies.Count;
foreach (var enemy in activeEnemies.Items) { ... }

これによりEnemyManager.Instanceのようなシングルトンマネージャーが不要になります。

ライフサイクル

オブジェクトの登録/解除は以下のように行われます。

sequenceDiagram
    participant E as Enemy
    participant RS as RuntimeSet
    participant M as Manager

    rect rgb(200, 230, 200)
    Note over E,M: 登録フロー
    E->>E: OnEnable()
    E->>RS: Add(gameObject)
    RS->>RS: items.Add()
    RS->>M: OnItemsChanged
    RS->>M: OnCountChanged(count)
    Note over M: UI更新
    end

    rect rgb(230, 200, 200)
    Note over E,M: 解除フロー
    E->>E: OnDisable()
    E->>RS: Remove(gameObject)
    RS->>RS: items.Remove()
    RS->>M: OnItemsChanged
    RS->>M: OnCountChanged(count)
    Note over M: UI更新
    end

Runtime Setsを使うタイミング

Runtime Setsを使う場合

  • 動的にスポーンされるオブジェクトを追跡する必要がある(敵、ピックアップ、発射物)
  • シングルトンマネージャーを避けたい
  • 複数のシステムが同じコレクションをクエリする必要がある
  • FindObjectsOfTypeなしで「全アクティブX」を確認する必要がある

C# Listを使う場合

  • コレクションが1つのGameObjectだけに属する
  • 毎フレームアクセスする(パフォーマンス重視)
  • システム間の可視性が不要
  • イベント通知が不要

判断ガイド

シナリオ 使用するもの
レベル内の全敵を追跡 Runtime Set
敵の個人的なウェイポイントリスト C# List
ミニマップ用の全ピックアップを追跡 Runtime Set
プレイヤーのインベントリアイテム C# List

利用可能なタイプ

タイプ 追跡対象 ユースケース
GameObject Runtime Set GameObject 最も一般的、スポーンされたエンティティ
Transform Runtime Set Transform 位置ベースのクエリ、ウェイポイント

基本的な使い方

ステップ1: Runtime Setアセットを作成

Projectウィンドウで右クリックし、以下のメニューパスを選択します。

Create > Reactive SO > Runtime Sets > GameObject Runtime Set

ActiveEnemiesSpawnedPickupsのような説明的な名前を付けます。

ステップ2: Event Channelを作成(オプション)

コレクション変更時の通知が必要な場合は、Event Channelを作成します。

Create > Reactive SO > Channels > Void Event

Runtime SetのOn Items Changedフィールドに割り当てます。

ステップ3: オブジェクトが自身を登録

OnEnable/OnDisableパターンを使用します。

using Tang3cko.ReactiveSO;
using UnityEngine;

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

    private void OnEnable()
    {
        activeEnemies?.Add(gameObject);
    }

    private void OnDisable()
    {
        activeEnemies?.Remove(gameObject);
    }
}

ステップ4: コレクションをクエリ

どこからでもアクティブオブジェクトにアクセスできます。

public class WaveManager : MonoBehaviour
{
    [SerializeField] private GameObjectRuntimeSetSO activeEnemies;
    [SerializeField] private VoidEventChannelSO onWaveComplete;

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

APIリファレンス

プロパティ

プロパティ 説明
Count int セット内のアイテム数
Items IReadOnlyList<T> 全アイテムへの読み取り専用アクセス

メソッド

メソッド 説明
Add(T item) アイテムを追加(重複防止)
Remove(T item) アイテムを削除
Contains(T item) アイテムが存在するか確認
Clear() 全アイテムを削除
DestroyItems() 全GameObjectを破棄してクリア(GameObjectセットのみ)

一般的なパターン

パターン1: ウェーブベースのスポーン

敵を追跡してウェーブ完了を検出します。

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

    public void SpawnWave(int count)
    {
        for (int i = 0; i < count; i++)
        {
            // 敵はOnEnableで自身を登録
            Instantiate(enemyPrefab, GetSpawnPosition(), Quaternion.identity);
        }
    }

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

パターン2: 最も近いオブジェクトを検索

FindObjectsOfTypeなしでクエリできます。

public GameObject GetNearestEnemy(Vector3 position)
{
    GameObject nearest = null;
    float minDistance = float.MaxValue;

    foreach (var enemy in activeEnemies.Items)
    {
        if (enemy == null) continue;

        float distance = Vector3.Distance(position, enemy.transform.position);
        if (distance < minDistance)
        {
            minDistance = distance;
            nearest = enemy;
        }
    }

    return nearest;
}

パターン3: ピックアップコレクション

収集可能アイテムを追跡します。

public class Pickup : MonoBehaviour
{
    [SerializeField] private GameObjectRuntimeSetSO activePickups;

    private void OnEnable()
    {
        activePickups?.Add(gameObject);
    }

    private void OnDisable()
    {
        activePickups?.Remove(gameObject);
    }

    public void Collect()
    {
        // ポイント付与、サウンド再生など
        Destroy(gameObject);  // OnDisableでセットから削除
    }
}

パターン4: レベルクリーンアップ

スポーンされた全オブジェクトを一度に破棄できます。

public class LevelManager : MonoBehaviour
{
    [SerializeField] private GameObjectRuntimeSetSO spawnedEnemies;
    [SerializeField] private GameObjectRuntimeSetSO spawnedPickups;

    public void EndLevel()
    {
        // 追跡中の全オブジェクトを破棄
        spawnedEnemies.DestroyItems();
        spawnedPickups.DestroyItems();
    }
}

変更を購読

アイテムの追加・削除時に反応できます。

public class EnemyCounter : MonoBehaviour
{
    [SerializeField] private GameObjectRuntimeSetSO activeEnemies;
    [SerializeField] private VoidEventChannelSO onEnemiesChanged;
    [SerializeField] private Text countText;

    private void OnEnable()
    {
        onEnemiesChanged.OnEventRaised += UpdateDisplay;
        UpdateDisplay();
    }

    private void OnDisable()
    {
        onEnemiesChanged.OnEventRaised -= UpdateDisplay;
    }

    private void UpdateDisplay()
    {
        countText.text = $"Enemies: {activeEnemies.Count}";
    }
}

Inspectorの機能

Play Mode中にRuntime Setアセットを選択すると以下が表示されます。

  • ライブアイテムリスト - 現在登録されている全オブジェクト
  • クリックでピング - アイテムをクリックしてHierarchyでハイライト
  • カウント表示 - 現在のアイテム数

Play Mode終了時にアイテムは自動的にクリアされます。


ベストプラクティス

常にOnEnable/OnDisableを使用

これにより自動的な登録とクリーンアップが保証されます。

// ✅ 良い例:バランスの取れた登録
private void OnEnable()
{
    runtimeSet?.Add(gameObject);
}

private void OnDisable()
{
    runtimeSet?.Remove(gameObject);
}
// ❌ 悪い例:登録のみで解除なし
private void Start()
{
    runtimeSet?.Add(gameObject);
}

イテレーション時にnullチェック

アイテムはチェック間に破棄される可能性があります。

foreach (var enemy in activeEnemies.Items)
{
    if (enemy == null) continue;  // 重要!
    // enemyを使用
}

シーン遷移時にクリア

シーン変更前にClear()またはDestroyItems()を呼び出しましょう。

public void LoadNextLevel()
{
    activeEnemies.Clear();  // 古い参照を防止
    SceneManager.LoadScene("NextLevel");
}

説明的な名前を使用

// ✅ 良い例:何を追跡しているか明確
ActiveEnemies
SpawnedPickups
RegisteredWaypoints

// ❌ 悪い例:曖昧
Enemies
Items
Set1

代替手段との比較

機能 Runtime Sets シングルトンマネージャー MonoBehaviour内のList
Inspectorの可視性 あり なし 限定的
イベント通知 自動 手動 手動
テスト容易性
シーン間永続化 あり あり なし
結合度

カスタムRuntime Setの作成

カスタム型にはRuntimeSetSO<T>を継承します。

[CreateAssetMenu(
    fileName = "EnemyRuntimeSet",
    menuName = "Reactive SO/Runtime Sets/Enemy Runtime Set"
)]
public class EnemyRuntimeSetSO : RuntimeSetSO<Enemy>
{
    // 必要に応じてカスタムメソッドを追加
    public Enemy GetStrongestEnemy()
    {
        return Items
            .Where(e => e != null)
            .OrderByDescending(e => e.Health)
            .FirstOrDefault();
    }
}

コンポーネントで使用します。

public class Enemy : MonoBehaviour
{
    [SerializeField] private EnemyRuntimeSetSO activeEnemies;
    public int Health { get; private set; } = 100;

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

トラブルシューティング

アイテムがInspectorに表示されない

Runtime Setの内容はPlay Mode中のみ表示されます。

オブジェクトが登録されない

OnEnableAdd()を呼び出していること、Runtime SetがInspectorで割り当てられていることを確認。

シーン変更後のnull参照

シーン変更前にClear()を呼び出して古い参照を削除。

UIでカウントが更新されない

毎フレームポーリングではなくOn Items Changed Event Channelを購読。


参照


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