如何处理运行时需修改数据的Scriptable Object物品
我有一个Item类用于存储ItemSO脚本对象:
public class Item : MonoBehaviour { [SerializeField] private ItemSO inventoryItem; }
ItemSO是抽象类,存储所有物品共有的通用信息:
public abstract class ItemSO : ScriptableObject { public string ItemName; public GameObject prefab; }
每种特定物品类型都继承自ItemSO,比如燃料类:
public class FuelItemSO : ItemSO, IDestroyableItem { [Tooltip("Burn Time of the fuel in minutes")] public float BurnTimeIncreasePerKg; public float TemperatureIncreasePerKg; }
此前用Scriptable Object做数据容器一切正常,但现在需要添加可修改变量的物品类型,比如容器类:
public class ContainerItemSO : ItemSO, IDestroyableItem { public float maxAmount; public float CurrentAmount; }
我无法让Item类根据ItemSO的类型获取和修改可变数据(比如给容器加水)。曾考虑让ContainerItemSO继承IHaveChangableData接口:
public interface IHaveChangableData { ContainerData GetData(); }
并通过ContainerData存储变量,但不知道如何存储ItemSO的特定数据,不确定这种方式是否过于复杂。同时想了解:如果有多个可修改数据的物品,是否应该从Scriptable Object切换为普通类?
1. 核心问题:ScriptableObject的特性
ScriptableObject是共享数据容器,修改它的字段会影响所有引用它的实例。像CurrentAmount这种每个物品实例独有的可变数据,绝对不能放在ScriptableObject里——这会导致所有容器物品的当前水量同步变化,完全不符合需求。
2. 正确方案:模板与实例数据分离
把数据拆成两类:
- 模板数据(不可变):放在ScriptableObject中,比如物品名称、最大容量
maxAmount、预制体等,同类型物品共享。 - 实例数据(可变):每个物品实例单独存储,比如
CurrentAmount,放在Item类或专门的实例数据类中。
具体实现步骤
步骤1:修改ContainerItemSO,只保留模板数据
public class ContainerItemSO : ItemSO, IDestroyableItem { public float maxAmount; // 最大容量是模板数据,不可变 // 移除CurrentAmount,它属于实例可变数据 }
步骤2:在Item类中存储实例可变数据
给Item类添加字段存储可变数据,用字典适配多种类型:
public class Item : MonoBehaviour { [SerializeField] private ItemSO inventoryItem; private Dictionary<Type, object> _instanceData = new Dictionary<Type, object>(); private void Awake() { // 初始化对应类型的实例数据 if (inventoryItem is ContainerItemSO) { _instanceData.Add(typeof(ContainerData), new ContainerData()); } // 其他可变数据类型同理,比如可升级物品的UpgradeData } // 提供获取可变数据的通用方法 public T GetInstanceData<T>() where T : class { _instanceData.TryGetValue(typeof(T), out var data); return data as T; } } // 容器实例数据类 public class ContainerData { public float CurrentAmount; // 初始值可在Awake时设置 }
步骤3:用接口统一处理可变数据(可选,更优雅)
如果有多种可变数据类型,用接口定义行为,避免大量类型判断:
// 通用可变数据接口 public interface IInstanceDataHolder { object CreateInstanceData(); } // 让ContainerItemSO实现接口 public class ContainerItemSO : ItemSO, IDestroyableItem, IInstanceDataHolder { public float maxAmount; public object CreateInstanceData() { return new ContainerData(); } } // 修改Item类自动初始化数据 public class Item : MonoBehaviour { [SerializeField] private ItemSO inventoryItem; private Dictionary<Type, object> _instanceData = new Dictionary<Type, object>(); private void Awake() { if (inventoryItem is IInstanceDataHolder dataHolder) { var data = dataHolder.CreateInstanceData(); _instanceData.Add(data.GetType(), data); } } public T GetInstanceData<T>() where T : class { _instanceData.TryGetValue(typeof(T), out var data); return data as T; } }
步骤4:使用示例(给容器加水)
public void AddWaterToContainer(Item containerItem) { var containerData = containerItem.GetInstanceData<ContainerData>(); var containerSO = containerItem.GetComponent<Item>().inventoryItem as ContainerItemSO; if (containerData != null && containerSO != null) { // 加水不超过最大容量 containerData.CurrentAmount = Mathf.Min(containerData.CurrentAmount + 10, containerSO.maxAmount); } }
3. 是否需要切换为普通类?
不需要完全抛弃ScriptableObject,它在存储共享模板数据时效率极高(不用重复创建相同配置)。只需要把实例独有的可变数据从ScriptableObject中剥离,放到普通类即可。
如果所有物品数据都是每个实例独有的、没有共享模板的需求,才考虑用普通类;但大部分游戏物品都有共享配置(比如所有水罐的最大容量一致),所以混合使用是最优解:
- ScriptableObject存模板(不变)
- 普通类存实例数据(可变)
内容的提问来源于stack exchange,提问作者Vav Avagyan

