如何基于Scriptable Objects构建通用动态变量系统
我完全懂你的痛点——为每种数值类型重复写几乎一样的Variable类和对应的Setter脚本,简直是重复劳动的噩梦!好在C#的泛型和Unity的编辑器扩展可以帮我们解决这个问题,不用再为每个类型复制粘贴代码。
第一步:构建泛型Variable基类
首先,我们需要一个泛型的基类,它能处理所有数值类型的序列化和值同步,同时兼容Unity的Scriptable Object系统。因为Unity不能直接序列化泛型字段,我们需要用一个object类型作为中间存储,在序列化回调里完成类型转换:
public abstract class Variable<T> : ScriptableObject, ISerializationCallbackReceiver { [SerializeField] private T initialValue; [NonSerialized] public T value; public void OnAfterDeserialize() { value = initialValue; } public void OnBeforeSerialize() { // 可选:如果需要在编辑模式下保存运行时修改的值,取消下面注释 // initialValue = value; } // 统一的赋值方法,方便外部调用 public void SetValue(T newValue) { value = newValue; } // 运算符重载,让变量可以直接当作对应类型使用(比如 float current = myFloatVar;) public static implicit operator T(Variable<T> variable) { return variable.value; } }
这个基类已经封装了核心逻辑:初始值序列化、运行时值同步,还通过运算符重载简化了变量的使用方式,后续所有类型的Variable都基于这个基类扩展。
第二步:快速创建具体类型的Variable
Unity目前不支持直接创建泛型Scriptable Object的资产,我们有两种简单的解决方式:
选项1:极简子类实现(几行代码搞定)
每个类型只需要写一行继承代码,核心逻辑全部复用基类:
[CreateAssetMenu(menuName = "Variables/Float")] public class FloatVariable : Variable<float> { } [CreateAssetMenu(menuName = "Variables/Double")] public class DoubleVariable : Variable<double> { } [CreateAssetMenu(menuName = "Variables/Int")] public class IntVariable : Variable<int> { }
虽然要写这些子类,但完全没有重复逻辑,后续修改只需要维护泛型基类即可。
选项2:编辑器脚本自动生成(彻底自动化)
如果你连子类都不想写,可以用编辑器脚本实现一键创建不同类型的Variable资产:
using UnityEngine; using UnityEditor; public static class VariableCreator { [MenuItem("Assets/Create/Variable/Float")] public static void CreateFloatVariable() => CreateVariable<float>("Float Variable"); [MenuItem("Assets/Create/Variable/Double")] public static void CreateDoubleVariable() => CreateVariable<double>("Double Variable"); [MenuItem("Assets/Create/Variable/Int")] public static void CreateIntVariable() => CreateVariable<int>("Int Variable"); private static void CreateVariable<T>(string name) { var variable = ScriptableObject.CreateInstance<Variable<T>>(); string path = $"Assets/New{name}.asset"; AssetDatabase.CreateAsset(variable, path); AssetDatabase.SaveAssets(); EditorUtility.FocusProjectWindow(); Selection.activeObject = variable; } }
这样你在Unity的Assets菜单里就能直接创建各种类型的Variable资产,完全不用手写子类!
第三步:通用ImageFillSetter脚本
现在解决Setter脚本的重复问题,我们用泛型+反射实现一个支持所有数值类型的通用版本:
using UnityEngine; using UnityEngine.UI; using System.Reflection; public class GenericImageFillSetter : MonoBehaviour { [SerializeField] private ScriptableObject variable; [SerializeField] private Image targetImage; private PropertyInfo _valueProperty; private void Awake() { // 反射获取Variable<T>的value属性 _valueProperty = variable.GetType().GetProperty("value", BindingFlags.Public | BindingFlags.Instance); } private void Update() { if (_valueProperty == null) return; var value = _valueProperty.GetValue(variable); // 利用IConvertible接口自动转换为float(所有C#数值类型都支持) if (value is IConvertible convertible) { targetImage.fillAmount = convertible.ToSingle(null); } } }
这个脚本不需要为每种类型单独编写,只要是能转换为float的数值类型,都能自动适配,完美解决重复实现的问题。
额外提示:Inspector显示优化
默认情况下,泛型基类的initialValue字段在Inspector里会正常显示,因为我们给它加了[SerializeField]标记,你创建的FloatVariable、IntVariable等资产,在Inspector里能直接编辑初始值,和之前的单独类体验完全一致。
内容的提问来源于stack exchange,提问作者João Santana

