Unity Term Book
脚本与生命周期

ScriptableObject

ScriptableObject 是 Unity 独立的数据容器,不挂在 GameObject 上,保存为 Asset,可被多个对象共享引用而不复制数据。适合游戏配置表、武器数值、对话与事件频道,帮助程序与策划把数据与场景实例解耦,减少 Prefab 膨胀,并提升长期可维护性与运行时热调与协作效率。

想象一下...

ScriptableObject 就像一份餐厅菜单。菜单(SO)保存菜品信息——名称、价格、描述。每张桌子(GameObject)都引用同一份菜单;没人私藏复印件。在主菜单上改价格 → 每张桌子立刻看到。菜单不关心谁在读——它只存数据

概念详解

ScriptableObject 继承 UnityEngine.Object(不是 MonoBehaviour)。用 [CreateAssetMenu] 创建 → 会出现在 Assets → Create 下。文件以 .asset 形式存在 Assets 中。

为何不用 static 或 [SerializeField]?Static 不会序列化, 也不方便在 Inspector 中编辑。MonoBehaviour 上的 [SerializeField] 会按实例复制数据 → 更占内存、难同步。SO:一份源头,多处引用。

用途:道具/敌人数值(WeaponData、EnemyStats)、 游戏配置(AudioSettings、InputConfig)、事件通道(松耦合)、状态容器 (GameStateSO)。SO 不跑 Update/Awake,但会收到 OnEnable/OnDisable。

Editor 与 Runtime:编辑模式下的修改会持久化。Play Mode 中的修改在 Editor 里也会持久化(与 MonoBehaviour 字段不同)。在玩家构建中,游戏重启时 SO 值会从 asset 重置。

SO 与 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

仅一份 — 改 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; → 把 asset 拖进 Inspector。

交互模拟器

选择一个 ScriptableObject asset,编辑字段,观察所有消费者立刻更新。

SO Assets:

Inspector (ScriptableObject)

使用此 SO 的对象

📌 Shared Reference

上方对象都引用同一资源。改一次 → 全部更新。

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 = 不绑定场景的数据容器
  • 多对象引用同一 SO —— 不复制数据
  • Play Mode 中修改 SO 在 Editor 里会持久化
  • SO Event Channel = 无需场景引用的解耦 pub/sub

⚠️ 常见错误

  • ❌ 在正式构建的运行时修改共享 SO

    共享 SO —— 改动会影响到使用它的每一个对象

    ✅ 需要按实例数据时克隆:Instantiate(soAsset)

  • ❌ 忘记写 [CreateAssetMenu]

    无法从 Editor 菜单创建 .asset

    ✅ 加上该特性,或用 ScriptableObject.CreateInstance<T>()