Unity ScriptableObject 架构
在 Unity 6.3 LTS 中,使用 ScriptableObject 资产来存储共享数据并解耦系统—— 配置、事件通道和注册表以项目资产的形式存在,而不是硬编码到场景或单例中。目标为 Unity 6.3 LTS (6000.3)。
何时使用
- 当你需要可由设计师编辑的配置(武器属性、关卡数据)、在不相关的系统之间共享一个值、通过事件通道将发送方与监听方解耦,或者构建一个活动对象的运行时注册表时使用——而无需
static/单例管理器。 - 当项目中有由
: ScriptableObject类支持的*.asset数据文件时使用。
何时不使用: 每个 GameObject 不同的实例运行时状态(那应放在 MonoBehaviour 上)——ScriptableObject 资产会被所有引用它的对象共享。将玩家进度保存到磁盘 → save-systems。永远不需要成为资产的普通 DTO 可以只是 [System.Serializable] 类。
核心工作流
- 定义类,派生自
ScriptableObject,并用[CreateAssetMenu]标记,以便设计师可以从 Assets 菜单创建实例。 - 创建一个或多个
.asset实例,在 Project 窗口中;每个实例都是被[SerializeField]字段引用的共享、具名数据。 - 引用而不是复制。 MonoBehaviour 持有对资产的引用;它们都看到相同数据,因此更改资产会更改每个消费者。
- 对于解耦,将 _信号_ 和 _共享变量_ 建模为 ScriptableObject:一个 HUD 读取而玩家写入的 "FloatVariable";一个玩家触发而许多系统监听的“事件通道”。双方都不引用对方。
- 在
OnEnable中重置运行时修改,如果资产在播放期间被修改,因为在 Editor 运行时对资产所做的编辑会持久化在资产上(这是“我玩过一次后值变了”的常见来源)。 - 验证:在 Play 模式下检查资产值,并确认消费者会响应。
模式
1. 配置/数据资产
using UnityEngine;
[CreateAssetMenu(fileName = "WeaponData", menuName = "Game/Weapon Data", order = 0)]
public class WeaponData : ScriptableObject
{
public string displayName = "Pistol";
public int damage = 10;
public float fireRate = 0.25f;
public GameObject projectilePrefab;
}
public class Weapon : MonoBehaviour
{
[SerializeField] private WeaponData data; // assign the shared asset in the Inspector
private void Fire() => Debug.Log($"{data.displayName} for {data.damage}");
}
2. 共享运行时变量(将生产者与消费者解耦)
[CreateAssetMenu(menuName = "Game/Float Variable")]
public class FloatVariable : ScriptableObject
{
[SerializeField] private float initialValue;
[System.NonSerialized] public float runtimeValue; // not saved to the asset
private void OnEnable() => runtimeValue = initialValue; // reset each play session
}
// Player writes playerHealth.runtimeValue; the HUD reads it — neither references the other.
3. 在运行时创建实例(不是磁盘上的资产)
// For transient SO data you build in code (e.g. a generated config).
var temp = ScriptableObject.CreateInstance<WeaponData>();
temp.damage = 25;
// ...use temp... Destroy(temp); // clean up runtime-created instances
陷阱
- 在 Editor 运行时编辑 SO 会持久化 — 你在 Play 期间更改的值在你停止后仍会留在资产上。将可变运行时状态保留在
[NonSerialized]字段中,并在OnEnable中重置,否则它会给你造成意外。(在 _构建版本_ 中,资产编辑不会跨启动持久化。) - 禁用 Domain Reload 会跳过你的
OnEnable重置 — 当启用 Enter Play Mode Options 并关闭 Reload Domain(Unity 6.3 LTS 的快速迭代设置)时,已加载的 SO 在你按 Play 时 _不会_ 重新创建,因此OnEnable不会触发,runtimeValue会保留上一次会话的值。请改用ISerializationCallbackReceiver或场景加载钩子显式重置,而不是仅依赖OnEnable。 - 期望每个对象拥有独立状态 — 每个引用都指向 _同一个_ 资产。如果两个敌人需要不同的当前 HP,请将 HP 存储在 MonoBehaviour 上,而不是共享的 SO 上。
- 没有帧生命周期 — ScriptableObject 有
OnEnable/OnDisable/OnDestroy,但没有Update。不要期待每帧回调。 - 将 SO 用作存档文件 — 它们是作者资产,不是运行时持久化;请使用
save-systems写入进度。 - 泄漏
CreateInstance对象 — 运行时创建的实例不会像普通 C# 对象那样被垃圾回收;完成时请Destroy它们。
参考
- 有关 事件通道 模式(一个
GameEventSO + 监听器、类型安全的载荷)以及 运行时集合/注册表(共享的活动敌人列表),请阅读references/event-channels.md。 - 主要文档:Unity Manual “ScriptableObject”(
/Manual/class-ScriptableObject.html)以及ScriptReference/ScriptableObject、ScriptReference/CreateAssetMenuAttribute。
相关技能
unity-csharp-scripting— 使用这些资产的 MonoBehaviour。save-systems— 将状态持久化到磁盘(SO _不是_ 用于此目的)。card-game/rpg/survival-crafting— 依赖 SO 驱动数据的类型。