基于DataTable, Json的额外序列化数据
最近很多功能都涉及到用户设置相关的东西, 比如一个沙盘, 希望在无操作后3秒钟就自动进行相机自动旋转的操作, 代码很简单:
public class XXX : MonoBehaviour { public float cameraRotateSpeed = 2f; public float waitTime = 3f; private float m_startRunningTime = 0.0f; void Awake(){ ResetStartRunningTime(); } private void ResetStartRunningTime() { m_startRunningTime = Time.realtimeSinceStartup + waitTime; } private void Update() { if(Time.realtimeSinceStartup > m_startRunningTime) { var rotateAngle = cameraRotateSpeed * Time.deltaTime; // ... } } }
可是用户如果想自己能设置这个功能的变量时, 我们可以怎么做呢?
1. 搞个快捷键? 各种功能都有用户需求的话, 快捷键没那么多. 几十个快捷键没人能记住.
2. 做个网页后台进行设置, 然后运行时从远程获取变量? 太麻烦, 而且依赖后台, 而且还是异步的.
3. 在本地写个json文件配置表从里面读取? 对开发不友好, 维护麻烦.
4. 做个UI面板运行时打开进行设置? 也是过于麻烦. 并且也需要存储数据.
并且对于一个已经开发到一定程度的工程来说, 额外添加的这个需求不能在开发层面要求过多, 且不能影响原有功能的设计.
结果还是从本地文件读取结果最靠谱, 假设上面的代码是单例或者使用时唯一, 那么就可以简单的做个容器来存放基本数据就行了:
Dictionary<string, Common.DataTable>
string 就是成员变量名
Common.DataTable 就是变量
基本上是通过反射来获取和设置变量的了, 不过在写接口的时候, 希望能有硬连接而不是软连接:
public class Person { public string Name { get; set; } } void Test(){ var name = nameof(Person.Name); }
这样即使代码修改了也会报错, 不过工程用的还是C#4的语法, 只能通过表达式数的方式来得到, 因为有Linq扩展可以写成lambda的方式 :
using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Linq.Expressions; using System.Reflection; namespace RuntimeData { using InternalModule.Common; internal class CompilerSerializedData { public string filePath { get; private set; } private bool inited = false; private Dictionary<string, Common.DataTable> m_datas = null; public CompilerSerializedData(string loadPath) { filePath = loadPath; } #region Main Funcs public Common.DataTable GetValue<T, TMem>(T host, Expression<System.Func<T, TMem>> key) where TMem : System.IConvertible { CheckInited(); Common.DataTable data = 0; var memberExpression = key.Body as MemberExpression; if(memberExpression != null && memberExpression.Member != null) { var memberInfo = memberExpression.Member; if(m_datas.TryGetValue(memberInfo.Name, out data) == false) { data.SetData(ReflectionHelper.GetValueFromMemberInfo<TMem>(host, memberInfo)); m_datas[memberInfo.Name] = data; } } return data; } public bool SetValue<T, TMem>(T host, Expression<System.Func<T, TMem>> key, TMem value) where TMem : System.IConvertible { CheckInited(); var memberExpression = key.Body as MemberExpression; if(memberExpression != null && memberExpression.Member != null) { var memberInfo = memberExpression.Member; Common.DataTable data = 0; data.SetData(value); m_datas[memberInfo.Name] = data; ReflectionHelper.SetValue(memberInfo, host, value); return true; } return false; } public void SaveToFile() { Common.JsonHelper.SaveJsonToFile(m_datas, filePath); } #endregion #region Main Funcs -- Static Support public Common.DataTable GetValue<TValue>(string name, System.Func<TValue> defaultValueFunc) where TValue : System.IConvertible { CheckInited(); Common.DataTable data = 0; if(m_datas.TryGetValue(name, out data) == false) { data.SetData(defaultValueFunc.Invoke()); m_datas[name] = data; } return data; } public bool TryGetValue(string name, out Common.DataTable dataTable) { CheckInited(); if(m_datas.TryGetValue(name, out dataTable)) { return true; } return false; } public void SetValue<TValue>(MemberInfo memberInfo, TValue value) where TValue : System.IConvertible { CheckInited(); Common.DataTable data = 0; if(m_datas.TryGetValue(memberInfo.Name, out data) == false) { data.SetData(value); m_datas[memberInfo.Name] = data; } } public bool SetValue<T, TMem>(Expression<System.Func<TMem>> key, TMem value) where TMem : System.IConvertible { CheckInited(); var memberExpression = key.Body as MemberExpression; if(memberExpression != null && memberExpression.Member != null) { var memberInfo = memberExpression.Member; Common.DataTable data = 0; data.SetData(value); m_datas[memberInfo.Name] = data; ReflectionHelper.SetValue(memberInfo, default(T), value); return true; } return false; } #endregion #region Help Funcs private void CheckInited() { if(false == inited) { inited = true; m_datas = Common.JsonHelper.LoadFromFile<Dictionary<string, Common.DataTable>>(filePath); if(m_datas == null) { m_datas = new Dictionary<string, Common.DataTable>(); } } } #endregion } public static class CompilerSerializedDatas { private static readonly Dictionary<System.Type, CompilerSerializedData> dictionary = new Dictionary<System.Type, CompilerSerializedData>(); public static Data.Common.PreCacheData<string> SavePathFolder = new Data.Common.PreCacheData<string>(() => { return Application.streamingAssetsPath + "/CompilerSerializedDatas"; }); #region Main Funcs public static Common.DataTable GetCompilerSerializedValue<T, TMem>(this T instance, Expression<System.Func<T, TMem>> key) where TMem : System.IConvertible { var data = RequireCompilerSerializedData(typeof(T)); Common.DataTable dataTable = data.GetValue(instance, key); return dataTable; } public static bool SetCompilerSerializedValue<T, TMem>(this T instance, Expression<System.Func<T, TMem>> key, TMem value) where TMem : System.IConvertible { var data = RequireCompilerSerializedData(typeof(T)); return data.SetValue(instance, key, value); } public static void Save() { foreach(var data in dictionary.Values) { data.SaveToFile(); } } #endregion #region Main Funcs -- Static Support private static Dictionary<string, object> CompiledFunc = new Dictionary<string, object>(); private static CompilerSerializedData RequireCompilerSerializedData(System.Type type) { CompilerSerializedData data = null; if(dictionary.TryGetValue(type, out data)) { return data; } RequireFolder(SavePathFolder.data); data = new CompilerSerializedData(string.Concat(SavePathFolder.data, "/", type.Name, ".json")); dictionary[type] = data; return data; } public static Common.DataTable GetCompilerSerializedValue<TValue>(Expression<System.Func<TValue>> expFunc) where TValue : System.IConvertible { var memberExpression = expFunc.Body as MemberExpression; if(memberExpression != null && memberExpression.Member != null) { var memberInfo = memberExpression.Member; var type = memberInfo.DeclaringType; var data = RequireCompilerSerializedData(type); Common.DataTable retVal = 0; if(data.TryGetValue(memberInfo.Name, out retVal)) { return retVal; } else { var value = CallExpression(expFunc); data.SetValue(memberInfo, value); retVal.SetData(value);
data.SaveToFile(); } return retVal; } return 0; } public static bool SetCompilerSerializedValue<T, TValue>(Expression<System.Func<TValue>> expFunc, TValue value) where TValue : System.IConvertible { var data = RequireCompilerSerializedData(typeof(T)); return data.SetValue<T, TValue>(expFunc, value); } private static bool MemberIsStatic(MemberInfo memberInfo) { var fieldInfo = memberInfo as FieldInfo; if(fieldInfo != null) { return (fieldInfo.IsStatic); } else { var propertyInfo = memberInfo as PropertyInfo; if(propertyInfo != null) { return (propertyInfo.GetAccessors(true)[0].IsStatic); } } return false; } private static System.Func<TValue> ExpressionToCall<TValue>(Expression<System.Func<TValue>> expFunc) where TValue : System.IConvertible { var key = expFunc.ToString(); var call = CompiledFunc.TryGetNullableValue(key) as System.Func<TValue>; if(call == null) { call = expFunc.Compile(); CompiledFunc[key] = call; } return call; } private static TValue CallExpression<TValue>(Expression<System.Func<TValue>> expFunc) where TValue : System.IConvertible { var call = ExpressionToCall(expFunc); return call.Invoke(); } #endregion #region Help Funcs private static bool RequireFolder(string folderPath) { if(System.IO.Directory.Exists(folderPath) == false) { var info = System.IO.Directory.CreateDirectory(folderPath); return info.Exists; } return true; } #endregion } }
通过代码注入添加了序列化方法, 最初的代码修改成:
public class XXX : MonoBehaviour { public float cameraRotateSpeed = 2f; public float waitTime = 3f; private float m_startRunningTime = 0.0f; void Awake(){ ResetStartRunningTime(); } private void ResetStartRunningTime() { m_startRunningTime = Time.realtimeSinceStartup + (float)this.GetCompilerSerializedValue(self => self.waitTime);; } private void Update() { if(Time.realtimeSinceStartup > m_startRunningTime) { var rotateAngle = (float)this.GetCompilerSerializedValue(self => self.cameraRotateSpeed) * Time.deltaTime; // ... } } }
这样在第一次运行之后, 就可以自动生成json文件了, 在启动时会自动读取文件设置, 既然是用户配置了, 那就不需要进行Set操作了, 不过以防万一也提供了Set操作:
Tools.AutoCameraRoundController.instance.SetCompilerSerializedValue(self => self.waitTime, 100.0f);
-----------------------------------------------------------------------
看到一个Unity的代码, 他们也是使用了表达式树, 在后处理扩展里面 :
using System; using System.Linq.Expressions; using UnityEngine.Rendering.PostProcessing; namespace UnityEditor.Rendering.PostProcessing { public class PostProcessEffectEditor<T> : PostProcessEffectBaseEditor where T : PostProcessEffectSettings { protected SerializedProperty FindProperty<TValue>(Expression<Func<T, TValue>> expr) { return serializedObject.FindProperty(RuntimeUtilities.GetFieldPath(expr)); } protected SerializedParameterOverride FindParameterOverride<TValue>(Expression<Func<T, TValue>> expr) { var property = serializedObject.FindProperty(RuntimeUtilities.GetFieldPath(expr)); var attributes = RuntimeUtilities.GetMemberAttributes(expr); return new SerializedParameterOverride(property, attributes); } } }
恩, 使用表达式树有它自己的优势, 特别是这种跨版本的维护的时候.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律