Unity:如何在ScriptableObject中使用泛型并实现高度泛化?
问题1:为什么无法直接使用List<PlayerPrefsDefault<int>>并在Inspector暴露?
这本质上是Unity内置序列化系统的限制:Unity的Inspector无法直接处理自定义泛型类的实例,哪怕你指定了具体的类型参数(比如int)。Unity的序列化逻辑需要明确的、非泛型的类型定义才能生成对应的Inspector绘制界面——它没办法自动为PlayerPrefsDefault<int>这种泛型特化类型创建编辑器控件。
如果你想尝试接近这种写法,有两种可行方案:
- 保留当前子类方案(最简便):就像你现在做的那样,为每种需要的类型创建非泛型子类(比如
PlayerPrefsDefaultInt继承PlayerPrefsDefault<int>),这是Unity官方推荐的兼容做法,不需要额外编写编辑器代码。 - 自定义PropertyDrawer(进阶):如果你不想创建大量子类,可以为
PlayerPrefsDefault<T>编写自定义的PropertyDrawer,手动处理序列化和Inspector绘制逻辑。不过这种方式需要熟悉Unity的SerializedPropertyAPI,实现成本较高。
问题2:如何实现支持任意类型的PlayerPrefsDefault类?
你想要的“任意类型存储+Inspector适配”需求,核心难点在于Unity无法直接序列化object类型。这里有两种成熟的解决方案:
方案A:利用SerializeReference实现多态序列化(Unity 2019.3+)
这是最优雅的方式,通过多态基类+SerializeReference属性,让Unity自动处理不同类型的序列化和Inspector显示:
// 基类定义 [Serializable] public abstract class PlayerPrefsValueBase { public abstract object GetValue(); public abstract void SetValue(object value); public abstract System.Type GetValueType(); } // 每种类型的具体实现 [Serializable] public class PlayerPrefsIntValue : PlayerPrefsValueBase { public int value; public override object GetValue() => value; public override void SetValue(object val) => value = (int)val; public override System.Type GetValueType() => typeof(int); } [Serializable] public class PlayerPrefsFloatValue : PlayerPrefsValueBase { public float value; public override object GetValue() => value; public override void SetValue(object val) => value = (float)val; public override System.Type GetValueType() => typeof(float); } // 同理可以扩展bool、string、Vector3等类型 // 最终的PlayerPrefsDefault类 [Serializable] public class PlayerPrefsDefault { public string key; [SerializeReference] public PlayerPrefsValueBase value; }
在Inspector中,value字段会显示一个下拉菜单,让你选择具体的类型(比如PlayerPrefsIntValue),然后就能直接编辑对应类型的值。你可以通过value.GetValueType()获取类型,GetValue()获取实际值,完美适配你的需求。
方案B:手动序列化+自定义Editor(兼容旧版本)
如果你的项目用的是Unity 2019.3之前的版本,可以通过ISerializationCallbackReceiver手动处理不同类型的序列化,再配合自定义Editor实现Inspector适配:
[Serializable] public class PlayerPrefsDefault : ISerializationCallbackReceiver { public System.Type type; public string key; // 隐藏的序列化字段,对应不同类型 [HideInInspector] public int intValue; [HideInInspector] public float floatValue; [HideInInspector] public bool boolValue; [HideInInspector] public string stringValue; [HideInInspector] public Vector3 vector3Value; // 运行时使用的object值 [NonSerialized] public object value; public void OnBeforeSerialize() { if (value == null || type == null) return; // 根据类型把value同步到对应的序列化字段 switch (System.Type.GetTypeCode(type)) { case System.TypeCode.Int32: intValue = (int)value; break; case System.TypeCode.Single: floatValue = (float)value; break; case System.TypeCode.Boolean: boolValue = (bool)value; break; case System.TypeCode.String: stringValue = (string)value; break; default: if (type == typeof(UnityEngine.Vector3)) { vector3Value = (UnityEngine.Vector3)value; } break; } } public void OnAfterDeserialize() { if (type == null) return; // 根据类型把序列化字段的值恢复到value switch (System.Type.GetTypeCode(type)) { case System.TypeCode.Int32: value = intValue; break; case System.TypeCode.Single: value = floatValue; break; case System.TypeCode.Boolean: value = boolValue; break; case System.TypeCode.String: value = stringValue; break; default: if (type == typeof(UnityEngine.Vector3)) { value = vector3Value; } break; } } }
然后你需要编写自定义Editor类,在Inspector中先让用户选择type,再根据类型绘制对应的字段(比如选int就显示intValue),并实时同步到value变量。这种方式不需要创建太多子类,但需要手动维护每种类型的序列化逻辑。
如何让ScriptableObject尽可能泛化?
结合上面的方案,你可以把ScriptableObject改成单一列表存储所有类型的默认值,彻底摆脱“为每种类型创建单独列表”的限制:
[CreateAssetMenu(fileName = "PlayerPrefsDefaults", menuName = "Base/PlayerPrefsDefaults", order = 1)] public class PlayerPrefsDefaults : ScriptableObject { // 一个列表就能存储所有类型的PlayerPrefs默认值 public List<PlayerPrefsDefault> playerPrefsDefaults = new List<PlayerPrefsDefault>(); }
不管用SerializeReference的多态方案还是手动序列化方案,这个ScriptableObject都能灵活存储任意类型的默认值,完全满足泛化需求。
内容的提问来源于stack exchange,提问作者Benjamin Zach

