ScriptableObject
ScriptableObject は GameObject に付けない独立したデータ容器です。.asset として保存され、多くのオブジェクトが同じデータをコピーせず共有参照できます。CreateAssetMenu、共有ステータス設計、Event Channel による疎結合パターンの基礎も学べます。
想像してみてください...
ScriptableObject はレストランのメニューのようです。メニュー(SO)には料理情報——名前、値段、説明——が載ります。どのテーブル(GameObject)も同じメニューを参照し、各自がコピーを持ちません。マスターメニューの値段を更新すれば、全テーブルが即座に見ます。メニューは誰が読んでいるか知ら——データを保存するだけです。
概念の詳細
ScriptableObject は
UnityEngine.Object(MonoBehaviour ではない)を継承します。
[CreateAssetMenu] で作ると Assets → Create に現れます。ファイルは Assets 配下の
.asset として保存されます。
なぜ static や [SerializeField] ではない? static は
シリアライズされず Inspector にも不向き。MonoBehaviour 上の [SerializeField] は
インスタンスごとにデータをコピー → メモリ増、同期も困難。SO:1 ソース、多数の参照。
用途: Item/Enemy ステータス(WeaponData、EnemyStats)、 ゲーム設定(AudioSettings、InputConfig)、イベントチャネル(疎結合)、状態コンテナ (GameStateSO)。SO は Update/Awake を走らせませんが OnEnable/OnDisable は受けます。
Editor vs Runtime: Edit Mode の編集は永続します。 Play Mode の編集も Editor では永続(MonoBehaviour フィールドと違い)。プレイヤービルドでは ゲーム再起動時に Asset から値がリセットされます。
SO vs MonoBehaviour データ
❌ [SerializeField] on MonoBehaviour
Enemy_01 (MonoBehaviour)
hp: 100, speed: 3
← 個別コピー
Enemy_02 (MonoBehaviour)
hp: 100, speed: 3
← 個別コピー
Enemy_03 (MonoBehaviour)
hp: 100, speed: 3
← 個別コピー
3 コピー × N バイト = メモリ浪費
✅ ScriptableObject
GoblinData.asset (SO)
hp: 100, speed: 3
Enemy_01
→ ref SO
Enemy_02
→ ref SO
Enemy_03
→ ref SO
1 つだけ — SO を編集 → 全員更新
ハンズオン手順
ScriptableObject クラスを作成
[CreateAssetMenu(menuName="Game/ItemData")] を付け、Assets → Create に表示させます。
Project に Asset を作成
右クリック → Assets → Create → Game → ItemData。データ名で命名(例:Sword_Data.asset)。
Inspector でフィールドを埋める
.asset を選択 → Inspector にフィールド表示 → ステータス、スプライト、サウンド… を入力。
MonoBehaviour から参照
[SerializeField] private ItemData _data; → Inspector に Asset をドラッグ。
インタラクティブシミュレーター
ScriptableObject Asset を選びフィールドを編集すると、すべての消費者に即座に反映されます。
Inspector (ScriptableObject)
この SO を使うオブジェクト
📌 Shared Reference
上のオブジェクトは同じアセットを参照。1 回編集 → 全員更新。
コード例
基本WeaponData ScriptableObject を作り、MonoBehaviour から使う。
using UnityEngine;
// Menu: Assets → Create → Game/Data → Weapon Data
[CreateAssetMenu(menuName = "Game/Data/Weapon Data", fileName = "New Weapon")]
public class WeaponData : ScriptableObject
{
public string weaponName;
public int damage;
public float attackSpeed;
public Sprite icon;
public AudioClip swingSound;
}
public class PlayerWeapon : MonoBehaviour
{
// Drag the .asset from Project onto this field
[SerializeField] private WeaponData currentWeapon;
public void Attack()
{
Debug.Log($"Attacking with {currentWeapon.weaponName}: {currentWeapon.damage} dmg");
AudioSource.PlayClipAtPoint(currentWeapon.swingSound, transform.position);
}
public void EquipWeapon(WeaponData newWeapon)
{
currentWeapon = newWeapon; // instant swap — only the reference changes
}
}コード例
上級SO を Event Channel に——発信者とリスナーを疎結合にする。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
// Event Channel — Project asset, independent of scene hierarchy
[CreateAssetMenu(menuName = "Events/Void Event")]
public class VoidEventSO : ScriptableObject
{
private List<UnityAction> _listeners = new();
public void Raise()
{
for (int i = _listeners.Count - 1; i >= 0; i--)
_listeners[i]?.Invoke();
}
public void Subscribe(UnityAction listener) => _listeners.Add(listener);
public void Unsubscribe(UnityAction listener) => _listeners.Remove(listener);
}
// Broadcaster: does not know who is listening
public class PlayerDeath : MonoBehaviour
{
[SerializeField] private VoidEventSO onPlayerDied;
public void Die() => onPlayerDied.Raise();
}
// Listener: does not know who raised the event
public class GameOverUI : MonoBehaviour
{
[SerializeField] private VoidEventSO onPlayerDied;
void OnEnable() => onPlayerDied.Subscribe(ShowGameOver);
void OnDisable() => onPlayerDied.Unsubscribe(ShowGameOver);
void ShowGameOver() => gameObject.SetActive(true);
}📌 要点
- ▸SO = シーンに縛られないデータ容器
- ▸多数のオブジェクトが 1 つの SO を参照——データコピーなし
- ▸Play Mode での SO 編集は Editor では永続する
- ▸SO Event Channel = シーン参照なしの疎結合 pub/sub
⚠️ よくあるミス
❌ 本番ビルドでランタイムに SO を書き換える
共有 SO——変更がそれを使う全オブジェクトに及ぶ
✅ インスタンス固有データが必要なら Instantiate(soAsset) でクローン
❌ [CreateAssetMenu] を忘れる
Editor メニューから .asset を作れない
✅ 属性を付けるか、ScriptableObject.CreateInstance<T>() を使う