「ゴブリンのHPを80に調整したい。……あれ、シーンに置いた20体ぜんぶ直すの?」——敵のステータスやアイテム情報をMonoBehaviourのフィールドに直接書いていると、必ずこの壁にぶつかります。データがコピーされてバラバラになり、1回の調整が20回の修正になる。
この問題を根本から解決するのが ScriptableObject(スクリプタブルオブジェクト)です。データを「シーンから独立したアセット」として1つだけ持ち、全員がそれを参照する——調整は1回、反映は全員。この記事では、ScriptableObjectの基本概念から、データ駆動設計、イベントシステムへの応用までを解説します。
この記事でわかること
- ScriptableObject=シーンから独立した「データのアセット」
[CreateAssetMenu]でのアセット作成とマスターデータ運用- 1つのデータを全員で共有——重複・修正漏れ・メモリ無駄の解消
- よくある間違い——実行時にマスターデータを書き換えない
- 応用: GameEvent(イベントチャネル)としての活用
ScriptableObjectとは?基本概念の理解
ScriptableObjectとは、Unityにおいてデータのみを保存するために設計された特別なクラスです。MonoBehaviourが「シーン内のオブジェクトの振る舞い」を定義するのに対し、ScriptableObjectは「プロジェクト内の共有データ」を定義します。
最も重要な特徴は、ScriptableObjectのインスタンスがアセットとしてプロジェクト内に保存され、シーンとは独立して存在できるという点です。これにより、データとロジック(MonoBehaviour)を完全に分離し、複数のコンポーネント間で同じデータを効率的に共有できるようになります。

ScriptableObjectの作成手順
ScriptableObjectを作成するには、まずScriptableObjectクラスを継承したC#スクリプトを作成します。そして、Unityエディタのメニューから簡単にアセットを作成できるように、クラス定義の上に[CreateAssetMenu]属性を付与します。
// Assets/Scripts/ItemData.cs
using UnityEngine;
// Unityエディタのメニューに「Create/Game Data/Item Data」を追加
[CreateAssetMenu(fileName = "NewItemData", menuName = "Game Data/Item Data")]
public class ItemData : ScriptableObject
{
// アイテム名
public string itemName = "Default Item";
// アイテムの説明
[TextArea]
public string description = "A standard item.";
// アイテムの攻撃力
public int attackPower = 10;
// アイテムのアイコン(Sprite型)
public Sprite itemIcon;
}
このスクリプトを保存した後、UnityエディタのProjectウィンドウで右クリックし、「Create」→「Game Data」→「Item Data」を選択すると、ItemData型の新しいアセットファイル(拡張子は.asset)が作成されます。このアセットが、あなたのゲームの「マスターデータ」となります。
[CreateAssetMenu]属性のmenuNameを工夫することで、プロジェクト内のアセットを整理しやすくなります。例えば、「Game Data/」のように階層化すると便利です。
データ駆動設計の実現
ScriptableObjectの真価は、データ駆動設計(Data-Driven Design)を実現する点にあります。これは、ゲームの振る舞いをコードではなくデータによって制御する設計思想です。
1. データの共有と参照
作成したItemDataアセットを、実際にゲーム内で使用するMonoBehaviourから参照します。
// Assets/Scripts/Item.cs
using UnityEngine;
public class Item : MonoBehaviour
{
// ScriptableObjectへの参照
[SerializeField]
private ItemData data;
// アイテムが使用されたときの処理
public void Use()
{
Debug.Log($"{data.itemName} を使用しました。攻撃力は {data.attackPower} です。");
// 実際のゲームロジック(例:プレイヤーのステータス変更など)
}
// 外部からデータを取得するためのプロパティ(オプション)
public ItemData Data => data;
}
このItemコンポーネントをGameObjectにアタッチし、インスペクターから作成済みのItemDataアセットをドラッグ&ドロップで割り当てます。
MonoBehaviourのインスタンス(GameObject)は複数あっても、参照しているItemDataアセットは一つだけです。これにより、データは共有され、メモリ効率が向上します。

2. データの変更を一元管理
例えば、「剣」の攻撃力を10から15に変更したい場合、ItemDataアセットを一つ修正するだけで、そのアセットを参照しているすべてのItemコンポーネントに修正が反映されます。これが保守性の向上に直結します。
3. よくある間違い
ScriptableObjectのデータを、シーン内のGameObjectのインスペクターで直接上書きしようとすることです。これは、マスターデータそのものを変更してしまうため、意図しない挙動を引き起こす可能性があります。マスターデータは原則として不変(Immutable)として扱い、実行時の状態は別のクラスで管理するのが安全です。

応用編:イベントシステムとしての活用
ScriptableObjectは、単なる静的なデータ保存だけでなく、ゲーム内のイベントや状態を管理するための「データコンテナ」としても非常に強力です。これは、特に小~中規模の個人開発において、複雑なイベントシステムを自作する手間を省くのに役立ちます。

GameEventの作成
特定のイベント(例:「プレイヤーがダメージを受けた」「スコアが更新された」)を通知するためのScriptableObjectを作成します。
// Assets/Scripts/GameEvent.cs
using UnityEngine;
using System.Collections.Generic;
[CreateAssetMenu(fileName = "NewGameEvent", menuName = "Game Data/Game Event")]
public class GameEvent : ScriptableObject
{
// このイベントを購読しているリスナーのリスト
private readonly List<GameEventListener> listeners = new List<GameEventListener>();
// イベントを発火させるメソッド
public void Raise()
{
// リスナーを逆順に回すことで、実行中にリストから削除されても安全
for (int i = listeners.Count - 1; i >= 0; i--)
{
listeners[i].OnEventRaised();
}
}
// リスナーの登録
public void RegisterListener(GameEventListener listener)
{
if (!listeners.Contains(listener))
listeners.Add(listener);
}
// リスナーの解除
public void UnregisterListener(GameEventListener listener)
{
if (listeners.Contains(listener))
listeners.Remove(listener);
}
}
GameEventListenerの作成
このイベントを受け取るためのMonoBehaviour(リスナー)を作成します。
// Assets/Scripts/GameEventListener.cs
using UnityEngine;
using UnityEngine.Events;
public class GameEventListener : MonoBehaviour
{
// 購読するイベントアセット
public GameEvent Event;
// イベント発生時に実行するUnityEvent
public UnityEvent Response;
private void OnEnable()
{
// オブジェクトが有効になったらイベントに登録
Event.RegisterListener(this);
}
private void OnDisable()
{
// オブジェクトが無効になったらイベントから解除
Event.UnregisterListener(this);
}
// イベントが発火したときに呼ばれるメソッド
public void OnEventRaised()
{
Response.Invoke();
}
}
使い方
GameEventアセット(例:PlayerDiedEvent.asset)を作成します。- イベントを発火させたい場所(例:プレイヤーのHPが0になったとき)で、
PlayerDiedEvent.Raise()を呼び出します。 - イベントを受け取りたいGameObjectに
GameEventListenerコンポーネントをアタッチし、EventフィールドにPlayerDiedEvent.assetを割り当てます。 Responseフィールドに、イベント発生時に実行したいメソッド(例:ゲームオーバー画面の表示)をインスペクターから設定します。
この仕組みにより、イベントの発火元と処理元が直接参照し合う必要がなくなり、疎結合で柔軟なシステムを構築できます。
実践:武器データベースを組む
RPGの武器一覧、ローグライトのレリック、タワーディフェンスのタワー種別——「種類がどんどん増えるモノ」の管理は、ScriptableObjectのもっとも気持ちいい使いどころです。ここでは「剣・弓・杖」の3種から始まる武器データベースを組んでみましょう。

まず武器1種=1アセットのデータ型を作ります。
// WeaponData.cs —— 武器1種類ぶんの定義
using UnityEngine;
[CreateAssetMenu(fileName = "NewWeapon", menuName = "Game Data/Weapon")]
public class WeaponData : ScriptableObject
{
public string weaponName;
public int attackPower;
public float attackInterval = 1f; // 攻撃間隔(秒)
public Sprite icon;
}
Create > Game Data > WeaponからSword.asset・Bow.asset・Staff.assetを作り、それぞれの数値をインスペクターで入力します。次に、全武器を束ねるデータベースをこれもScriptableObjectで作ります。
// WeaponDatabase.cs —— 全武器のカタログ
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "WeaponDatabase", menuName = "Game Data/Weapon Database")]
public class WeaponDatabase : ScriptableObject
{
public List<WeaponData> weapons; // インスペクターで武器アセットを登録
// 名前から武器を引く(見つからなければnull)
public WeaponData Find(string name) =>
weapons.Find(w => w.weaponName == name);
}
使う側は、データベース1個を参照するだけです。
public class PlayerAttack : MonoBehaviour
{
[SerializeField] private WeaponDatabase database;
private WeaponData currentWeapon;
void Start()
{
// 装備の切り替えは「どのアセットを指すか」を変えるだけ
currentWeapon = database.Find("剣");
}
public void Attack()
{
Debug.Log($"{currentWeapon.weaponName}で攻撃! 威力: {currentWeapon.attackPower}");
}
}
ポイントは2つです。
- 武器の追加が「アセット作成」で完結する: 4本目の武器「斧」を足すのに、コードは1行も書きません。
Axe.assetを作ってデータベースのListに登録するだけ。バランス調整もアセットの数値をいじるだけなので、企画側・デザイナーに調整を任せることもできます。 - 「所持しているか」はセーブデータ側に持つ:
WeaponDataは不変のマスターデータです。「プレイヤーがどの武器を持っているか」は、武器名やIDのListとして JSONセーブ 側に保存し、ロード時にdatabase.Find()で実体を引き直します。この役割分担がデータ設計の背 骨になります。
おまけ:先に知っておくと良いこと
- 実行時の変更はエディタでだけ永続する: エディタ上でPlay中にScriptableObjectを書き換えると、停止後も値が残ります(ビルド版では残りません)。この挙動の違いがバグの温床になるため、実行時の状態は必ず別クラスに持ちましょう。
- イベントチャネル設計の詳細: 応用編のGameEventパターンをさらに発展させた設計(型付きイベント・デバッグのしやすさ)は ScriptableObjectイベントチャネル設計入門 で深掘りしています。
- Enum+switchからの卒業: 「アイテムの種類ごとにswitch文」が肥大化してきたら、種類ごとのScriptableObjectアセットに差し替えるサインです。新アイテムの追加が「コード修正」から「アセット作成」に変わります。
- セーブデータとの関係: ScriptableObjectはマスターデータ(不変の定義)、JSONセーブ はプレイヤーの状態(可変の進捗)と役割分担するのが定石です。「所持数」はセーブに、「アイテムの定義」はSOに。
まとめ
ScriptableObjectは、Unityにおけるデータ管理と設計の質を劇的に向上させるための鍵となるツールです。この記事で学んだ要点をまとめます。
- データとロジックの分離: ScriptableObjectはデータのみを保持し、MonoBehaviourから独立させることで、コードの再利用性と保守性を高めます。
- メモリ効率の向上: データはアセットとして一度だけメモリにロードされ、複数のコンポーネントがそれを共有するため、メモリの無駄を省くことができます。
- データ駆動設計の基礎: 武器・敵・アイテムのマスターデータをアセットで管理すれば、追加も調整もコード修正なしで行えます。
- イベントシステムへの応用:
GameEventとして活用することで、イベントの発火元とリスナーを疎結合にし、柔軟なシステム構築に役立ちます。
ScriptableObjectを積極的に活用し、よりクリーンで拡張性の高いUnityプロジェクトの実現を目指しましょう。