Unity Term Book
スクリプティングとライフサイクル

ScriptableObject

ScriptableObject は GameObject に付けない独立したデータ容器です。.asset として保存され、多くのオブジェクトが同じデータをコピーせず共有参照できます。CreateAssetMenu、共有ステータス設計、Event Channel による疎結合パターンの基礎も学べます。

想像してみてください...

ScriptableObject はレストランのメニューのようです。メニュー(SO)には料理情報——名前、値段、説明——が載ります。どのテーブル(GameObject)も同じメニューを参照し、各自がコピーを持ちません。マスターメニューの値段を更新すれば、全テーブルが即座に見ます。メニューは誰が読んでいるか知ら——データを保存するだけです。

概念の詳細

ScriptableObjectUnityEngine.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 を編集 → 全員更新

ハンズオン手順

1

ScriptableObject クラスを作成

[CreateAssetMenu(menuName="Game/ItemData")] を付け、Assets → Create に表示させます。

2

Project に Asset を作成

右クリック → Assets → Create → Game → ItemData。データ名で命名(例:Sword_Data.asset)。

3

Inspector でフィールドを埋める

.asset を選択 → Inspector にフィールド表示 → ステータス、スプライト、サウンド… を入力。

4

MonoBehaviour から参照

[SerializeField] private ItemData _data; → Inspector に Asset をドラッグ。

インタラクティブシミュレーター

ScriptableObject Asset を選びフィールドを編集すると、すべての消費者に即座に反映されます。

SO Assets:

Inspector (ScriptableObject)

この SO を使うオブジェクト

📌 Shared Reference

上のオブジェクトは同じアセットを参照。1 回編集 → 全員更新。

Selected: GoblinData.asset|フィールドを変えてリアルタイム更新を見る

コード例

基本

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>() を使う