一般在游戏开发中策划都会把数据配置在excel中.所以我们需要从excel中导出数据,并且把数据保存在本地.
有很多种方式可以把数据导出成我们想要的格式,比如说导出为json,cs,或者xml.不过我更喜欢直接把数据序列化为二进制文件.然后在游戏中加载的时候直接反序列化就可以了.这样引用数据的时候非常方便.
首先说下流程.
1. 从excel中读取数据 2. 根据数据类型.动态生成每个表的C#类 3. 动态编译C#类.然后输出为一个动态库 4. 实例化C#类,并且把数据填入到实例化的对象中 5. 序列化数据,保存在Unity中的Resources目录中 6. 在Unity中引用之前输出的动态库,在游戏运行时加载数据.并且进行反序列化
先来看看配置表的格式:
在上面的配置表中,前三行为我预留的位置,第四行和第五行分别表示这个字段在类中的类型和成员变量名 格式定好了,那我们就按照格式把数据从excel中读取出来就行了.
string[] files = Directory.GetFiles(excelPath, "*.xlsx"); List<string> codeList = new List<string>(); Dictionary<string, List<ConfigData[]>> dataDict = new Dictionary<string, List<ConfigData[]>>(); for (int i = 0; i < files.Length; ++i) {//打开excel string file = files[i]; FileStream stream = File.Open(file, FileMode.Open, FileAccess.Read); IExcelDataReader excelReader = ExcelReaderFactory.CreateOpenXmlReader(stream); if (!excelReader.IsValid) { Console.WriteLine("invalid excel " + file); continue; }
这里是开始在读取excel表.我引用了一个第三方库来读取excel.各位可以用其它方法来读取.只要能读取到每一行的数据,那都是一样的.
class ConfigData { public string Type; public string Name; public string Data; }
首先我定义了一个类,每一个字段都会实例化一个这个类.并且把类型和名称以及数据保存在这个类中.
string[] types = null; string[] names = null; List<ConfigData[]> dataList = new List<ConfigData[]>(); int index = 1;
//开始读取 while (excelReader.Read()) {//这里读取的是每一行的数据 string[] datas = new string[excelReader.FieldCount]; for (int j = 0; j < excelReader.FieldCount; ++j) { datas[j] = excelReader.GetString(j); }
//空行不处理 if (datas.Length == 0 || string.IsNullOrEmpty(datas[0])) { ++index; continue; }
//第三行表示类型 if (index == PROPERTY_TYPE_LINE) types = datas;//第四行表示变量名 else if (index == PROPERTY_NAME_LINE) names = datas;//后面的表示数据 else if (index > PROPERTY_NAME_LINE) {//把读取的数据和数据类型,名称保存起来,后面用来动态生成类 List<ConfigData> configDataList = new List<ConfigData>(); for (int j = 0; j < datas.Length; ++j) { ConfigData data = new ConfigData(); data.Type = types[j]; data.Name = names[j]; data.Data = datas[j]; if (string.IsNullOrEmpty(data.Type) || string.IsNullOrEmpty(data.Data)) continue; configDataList.Add(data); } dataList.Add(configDataList.ToArray()); } ++index; }
//类名 string className = excelReader.Name;//根据刚才的数据来生成C#脚本 ScriptGenerator generator = new ScriptGenerator(className, names, types);//所有生成的类最终保存在这个链表中 codeList.Add(generator.Generate()); if (dataDict.ContainsKey(className)) Console.WriteLine("相同的表名 " + className); else dataDict.Add(className, dataList);
//脚本生成器 class ScriptGenerator { public string[] Fileds; public string[] Types; public string ClassName;
public ScriptGenerator(string className, string[] fileds, string[] types) { ClassName = className; Fileds = fileds; Types = types; }
//开始生成脚本 public string Generate() { if (Types == null || Fileds == null || ClassName == null) return null; return CreateCode(ClassName, Types, Fileds); }
//创建代码。 private string CreateCode(string tableName, string[] types, string[] fields) {//生成类 StringBuilder classSource = new StringBuilder(); classSource.Append("/*Auto create\n"); classSource.Append("Don't Edit it*/\n"); classSource.Append("\n"); classSource.Append("using System;\n"); classSource.Append("using System.Reflection;\n"); classSource.Append("using System.Collections.Generic;\n"); classSource.Append("[Serializable]\n"); classSource.Append("public class " + tableName + "\n"); classSource.Append("{\n");//设置成员 for (int i = 0; i < fields.Length; ++i) { classSource.Append(PropertyString(types[i], fields[i])); } classSource.Append("}\n");
//生成Container classSource.Append("\n"); classSource.Append("[Serializable]\n"); classSource.Append("public class " + tableName + "Container\n"); classSource.Append("{\n"); classSource.Append("\tpublic " + "Dictionary<int, " + tableName + ">" + " Dict" + " = new Dictionary<int, " + tableName + ">();\n"); classSource.Append("}\n"); return classSource.ToString(); }
private string PropertyString(string type, string propertyName) { if (string.IsNullOrEmpty(type) || string.IsNullOrEmpty(propertyName)) return null;
if (type == SupportType.LIST_INT) type = "List<int>"; else if (type == SupportType.LIST_FLOAT) type = "List<float>"; else if (type == SupportType.LIST_STRING) type = "List<string>"; StringBuilder sbProperty = new StringBuilder(); sbProperty.Append("\tpublic " + type + " " + propertyName + ";\n"); return sbProperty.ToString(); } }
这个类用于生成配置表类代码.
//编译代码,序列化数据 Assembly assembly = CompileCode(codeList.ToArray(), null); string path = _rootPath + _dataPath; if (Directory.Exists(path)) Directory.Delete(path, true); Directory.CreateDirectory(path); foreach (KeyValuePair<string, List<ConfigData[]>> each in dataDict) { object container = assembly.CreateInstance(each.Key + "Container"); Type temp = assembly.GetType(each.Key); Serialize(container, temp, each.Value, path); }
得到了生成的类代码过后.我们需要动态编译这些代码.并且填充数据.
//编译代码 private static Assembly CompileCode(string[] scripts, string[] dllNames) { string path = _rootPath + _dllPath; if (!Directory.Exists(path)) Directory.CreateDirectory(path);//编译参数 CSharpCodeProvider codeProvider = new CSharpCodeProvider(); CompilerParameters objCompilerParameters = new CompilerParameters(); objCompilerParameters.ReferencedAssemblies.AddRange(new string[] { "System.dll" }); objCompilerParameters.OutputAssembly = path + "Config.dll"; objCompilerParameters.GenerateExecutable = false; objCompilerParameters.GenerateInMemory = true;
//开始编译脚本 CompilerResults cr = codeProvider.CompileAssemblyFromSource(objCompilerParameters, scripts); if (cr.Errors.HasErrors) { Console.WriteLine("编译错误:"); foreach (CompilerError err in cr.Errors) Console.WriteLine(err.ErrorText); return null; } return cr.CompiledAssembly; }
//序列化对象 private static void Serialize(object container, Type temp, List<ConfigData[]> dataList, string path) {//设置数据 foreach (ConfigData[] datas in dataList) { object t = temp.Assembly.CreateInstance(temp.FullName); foreach (ConfigData data in datas) { FieldInfo info = temp.GetField(data.Name); info.SetValue(t, ParseValue(data.Type, data.Data)); }
object id = temp.GetField("id").GetValue(t); FieldInfo dictInfo = container.GetType().GetField("Dict"); object dict = dictInfo.GetValue(container);
bool isExist = (bool)dict.GetType().GetMethod("ContainsKey").Invoke(dict, new Object[] {id}); if (isExist) { Console.WriteLine("repetitive key " + id + " in " + container.GetType().Name); Console.Read(); return; } dict.GetType().GetMethod("Add").Invoke(dict, new Object[] { id, t }); }
IFormatter f = new BinaryFormatter(); Stream s = new FileStream(path + temp.Name + ".bytes", FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write); f.Serialize(s, container); s.Close(); }
CreateDataManager(assembly);
最后这里还创建了一个DataManager用于管理之前导出的数据.这也是Unity中获取数据的接口
//创建数据管理器脚本 private static void CreateDataManager(Assembly assembly) { IEnumerable types = assembly.GetTypes().Where(t => { return t.Name.Contains("Container"); });
StringBuilder source = new StringBuilder(); source.Append("/*Auto create\n"); source.Append("Don't Edit it*/\n"); source.Append("\n");
source.Append("using System;\n"); source.Append("using UnityEngine;\n"); source.Append("using System.Runtime.Serialization;\n"); source.Append("using System.Runtime.Serialization.Formatters.Binary;\n"); source.Append("using System.IO;\n\n"); source.Append("[Serializable]\n"); source.Append("public class DataManager : SingletonTemplate<DataManager>\n"); source.Append("{\n");
//定义变量 foreach (Type t in types) { source.Append("\tpublic " + t.Name + " " + t.Name.Remove(0, 2) + ";\n"); } source.Append("\n");
//定义方法 foreach (Type t in types) { string typeName = t.Name.Remove(t.Name.IndexOf("Container")); string funcName = t.Name.Remove(0, 2); funcName = funcName.Substring(0, 1).ToUpper() + funcName.Substring(1); funcName = funcName.Remove(funcName.IndexOf("Container")); source.Append("\tpublic " + typeName + " Get" + funcName + "(int id)\n"); source.Append("\t{\n"); source.Append("\t\t" + typeName + " t = null;\n"); source.Append("\t\t" + t.Name.Remove(0, 2) + ".Dict.TryGetValue(id, out t);\n"); source.Append("\t\tif (t == null) Debug.LogError(" + '"' + "can't find the id " + '"' + " + id " + "+ " + '"' + " in " + t.Name + '"' + ");\n"); source.Append("\t\treturn t;\n"); source.Append("\t}\n"); }
加载所有配置表 source.Append("\tpublic void LoadAll()\n"); source.Append("\t{\n"); foreach (Type t in types) { string typeName = t.Name.Remove(t.Name.IndexOf("Container")); source.Append("\t\t" + t.Name.Remove(0, 2) + " = Load(" + '"' + typeName + '"' + ") as " + t.Name + ";\n"); } source.Append("\t}\n\n");
//反序列化 source.Append("\tprivate System.Object Load(string name)\n"); source.Append("\t{\n"); source.Append("\t\tIFormatter f = new BinaryFormatter();\n"); source.Append("\t\tTextAsset text = Resources.Load<TextAsset>(" + '"' + "ConfigBin/" + '"' + " + name);\n"); source.Append("\t\tStream s = new MemoryStream(text.bytes);\n"); source.Append("\t\tSystem.Object obj = f.Deserialize(s);\n"); source.Append("\t\ts.Close();\n"); source.Append("\t\treturn obj;\n"); source.Append("\t}\n");
source.Append("}\n");//保存脚本 string path = _rootPath + _scriptPath; if (!Directory.Exists(path)) Directory.CreateDirectory(path); StreamWriter sw = new StreamWriter(path + "DataManager.cs"); sw.WriteLine(source.ToString()); sw.Close(); }
经过动态编译过后.会输出一个库,这个库里面有所有配置表的类型.我们只需要在Unity中引用这个库就行了. 实际测试,在PC端,Android,IOS都是可以使用的.
生成的类就是这样的.
DataManager也是自动生成的.在游戏进入时调用一下LoadAll函数加载数据.后面直接调用对应函数, 根据id就可以取得数据了.
转载于:https://www.cnblogs.com/Damon-3707/p/11477231.html