基于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);
                }
                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);