ScriptableObject とは何か

Unity の ScriptableObject は、MonoBehaviour とは異なり GameObject にアタッチせずにプロジェクト内のアセットとして保存できるデータクラスです。Inspector から値を編集でき、シーンをまたいで同じアセットを参照することもできるため、ゲームのパラメータやマスターデータを扱うのに向いています。
MonoBehaviour がシーンに存在するオブジェクトの振る舞いを定義するためのクラスであるのに対し、ScriptableObject は純粋にデータの入れ物として設計されています。毎フレーム処理を行うようなロジックは書かず、値を保持するのが基本的な役割です。

なぜデータ駆動型設計なのか

ゲーム制作ではバランス調整や仕様変更が頻繁に発生します。敵の HP や攻撃力、アイテムの効果などをコードの中にハードコーディングしていると、値を変更するたびにプログラマが修正とリビルドを行う必要があり、開発効率が大きく低下します。
データ駆動型設計では、こうしたパラメータをコードから切り離してアセットとして管理します。ScriptableObject を使えば、企画職やデザイナーが Inspector から直接値を調整できるようになり、プログラマとの分業がスムーズに進みます。ビルド不要で値を切り替えられるため、テストプレイ中の素早いイテレーションにも貢献します。

基本的な実装パターン

ScriptableObject 派生クラスに CreateAssetMenu 属性を付与すると、Project ウィンドウの右クリックメニューからアセットを生成できるようになります。以下は敵データを表す EnemyData クラスの例です。

using UnityEngine;

[CreateAssetMenu(fileName = "NewEnemyData", menuName = "GameData/EnemyData")]
public class EnemyData : ScriptableObject
{
    public string enemyName;
    public int maxHp;
    public int attackPower;
    public float moveSpeed;
    public Sprite icon;
    public AttackType attackType;
}

public enum AttackType
{
    Melee,
    Ranged,
    Magic,
}

このクラスを定義した状態で Project ウィンドウを右クリックし Create > GameData > EnemyData を選択すると、敵データアセットが生成されます。Inspector で値を編集して保存すれば、そのままゲームのマスターデータとして使える状態になります。

アセットを参照する側の実装

EnemyData を参照する MonoBehaviour では、SerializeField でアセットを受け取るフィールドを用意し、Inspector からドラッグアンドドロップでアサインします。

using UnityEngine;

public class EnemyController : MonoBehaviour
{
    [SerializeField] private EnemyData data;
    private int currentHp;

    private void Start()
    {
        currentHp = data.maxHp;
        Debug.Log($"{data.enemyName} が出現しました");
    }

    public void TakeDamage(int damage)
    {
        currentHp -= damage;
        if (currentHp <= 0)
        {
            Destroy(gameObject);
        }
    }
}

同じ EnemyData アセットを複数の敵 Prefab に割り当てれば、すべての個体が同じ初期パラメータを共有できます。バランス調整はアセットを編集するだけで済み、コード修正もリビルドも不要です。

応用: バリアントで共通基盤を使い回す

同じ ScriptableObject 型から複数のアセットを作れる性質を活かすと、多数のアイテムや敵を効率的に管理できます。たとえばアイテムのマスターデータをまとめて扱う場合、ItemData を一度定義するだけで、ポーション・武器・鍵などのアセットをそれぞれ Project ウィンドウ上で作成していけます。

[CreateAssetMenu(fileName = "NewItemData", menuName = "GameData/ItemData")]
public class ItemData : ScriptableObject
{
    public string itemName;
    public string description;
    public int price;
    public ItemCategory category;
    public Sprite icon;
}

さらに敵にボス固有のパラメータを持たせたい場合は、EnemyData を継承した BossEnemyData クラスを用意する方法もあります。共通フィールドは親クラスに集約しつつ、ボス専用の演出フラグや第二形態のパラメータを追加することで、データ設計の拡張性を保てます。

ScriptableObject を使うときの注意点

便利な仕組みですが、いくつか注意点があります。1 つ目は、プレイモード中に ScriptableObject の値を変更すると、その変更がプレイモード終了後もアセットに残ってしまうことです。ランタイムで動的に書き換えたい情報は、別のクラスに切り出すか、セーブデータ側で管理するようにします。
2 つ目は、実行中のビルドで ScriptableObject の値を変更しても、アプリケーションを再起動した際にはアセットに含まれた元の値にリセットされるという点です。ランタイムで変化し続ける状態を保持する用途には向いていません。
基本方針として、ScriptableObject はマスターデータや設定値のような読み取り中心のデータを扱う場所として運用し、ゲーム内で変化する状態は別のクラスで持たせるように設計するのが安全です。

Serializable 構造体との使い分け

小規模なパラメータをまとめたいだけであれば、Serializable 属性を付けた構造体や通常のクラスでも Inspector から値を編集できます。ではどのような場合に ScriptableObject を選ぶと良いのでしょうか。
Serializable の構造体は、それを保持する MonoBehaviour ごとにコピーされて存在する軽量なデータ入れ物として便利です。一方、ScriptableObject はプロジェクト内で 1 つのアセットを複数の MonoBehaviour から参照するという使い方に向いています。共通のマスターデータを複数シーン・複数 Prefab で共有したい場面では ScriptableObject が優位です。
判断基準としては、値が個別オブジェクトごとに異なるなら Serializable、プロジェクト全体で共通のデータなら ScriptableObject を選ぶと整理しやすくなります。

まとめ

ScriptableObject を活用したデータ駆動型設計は、コードと設定を分離することでプロジェクトの保守性と開発効率を大きく向上させます。CreateAssetMenu でアセット化する仕組みは非常にシンプルですが、バランス調整・分業・再利用性の観点でさまざまな場面に応用できます。
ランタイムでの状態保持には向かないといった注意点はありますが、マスターデータや設定値の管理手段として、大規模プロジェクトほど恩恵の大きい設計パターンです。新しくプロジェクトを立ち上げる際には、ScriptableObject を前提にしたデータ設計を検討してみてください。