转载:https://blog.csdn.net/qq_14903317/article/details/78637440
一般在游戏开发中策划都会把数据配置在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);
- 1
最后这里还创建了一个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就可以取得数据了.