EPPlus
这个工具可以读写Excel。使用方式很简单。可以参考视频学习:【Unity教程】Excel文件的读取和写入 (使用EPPlus)
一开始我是尝试在手机上解析,我测试的只有Mono环境下才能使用,并且需要设置.Net 4.x,添加118N等dll。IL2CPP下不能使用。
后面才了解到在游戏运行时不要去动态解析Excel,因为在手机上解析不了。而且你需要把这个dll加在工程里面,会增加包的大小。最好的就是先把Excel写入文件, 运行的时候读取这个文件的方式来做。
配置表工具
所以现在的流程是策划把数据配置在excel中.所以我们需要从excel中导出数据,保存在游戏的类中。然后把这个类序列化成二进制文件,然后运行游戏一开始时反序列化这个类得到需要的数据。
这个过程非常的繁琐,所以我希望代码自动完成这一步骤。因此就需要这个配置表工具了。
流程
首先说下流程.
- 从excel中读取数据
- 根据数据类型.动态生成每个表的C#类
- 动态编译C#类.然后输出为一个动态库
- 实例化C#类,并且把数据填入到实例化的对象中
- 序列化数据,保存在Unity中的Resources目录中
- 在Unity中引用之前输出的动态库,在游戏运行时加载数据.并且进行反序列化
配置表格式
这个xlsx文件我放在项目同Assets同一级的目录的Xlsx文件夹下。
public static string ExcelFile = Application.dataPath + "/../Xlsx/config.xlsx";
在上面的配置表中,第一行表示这个数据的说明,第二行和第三行分别表示这个字段在类中的类型和成员变量名。
格式定好了,那我们就按照格式把数据从excel中读取出来就行了。
动态生成每个表的C#类
首先,我们拿到这样一个表,肯定是希望生成一个这样对应的类:
[Serializable]
public class GameConfig
{
public int Id;
public string Key;
public double Value;
public string strValue;
}
类名可以从取表名,各个属性的类型和成员变量名遍历读取表的第二行和第三行就可以得到,因此我们完全可以生成这样的字符串格式,然后得到这个类。
//脚本生成器
using System.IO;
using System.Text;
using UnityEngine;
class ScriptGenerator
{
public static string _codePath = Application.dataPath + "/Worksheets/Manager/";
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;
string arg = CreateCode(ClassName, Types, Fileds);
// EditorGenerate(arg);
return arg;
}
//创建代码
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;
StringBuilder sbProperty = new StringBuilder();
sbProperty.Append("\tpublic " + type + " " + propertyName + ";\n");
return sbProperty.ToString();
}
public void EditorGenerate(string scripts)
{
string csPath = _codePath + $"{ClassName}.cs";
string dstDir = Path.GetDirectoryName(csPath);
if (!Directory.Exists(dstDir))
{
Directory.CreateDirectory(dstDir);
}
File.WriteAllText(csPath, scripts);
}
}
要想生成这个类,只要再传参传入类名,类型数组,成员变量名数组,两个数组的长度要一样。
将表里面的数据实例化并序列化成二进制文件
我们需要这样一个类,类里面的字典就保存这表里的所有数据。然后再将这个类序列化,数据就都保存起来了。
[Serializable]
public class GameConfigContainer
{
public Dictionary<int, GameConfig> Dict = new Dictionary<int, GameConfig>();
}
这个类在上面的脚本生成器中,我已经生成了。接着就是读取表里的数据,往Dict里面添加值就行了。
要想实例化一个GameConfig类,我们可以利用反射的方式。这个GameConfig类已经生成了,我们通过反射生成一个实例,然后需要拿到一个属性的属性名,和这个属性的的值。所以我在这里定义了一个类型:
public class ConfigData
{
public string Type;
public string Name;
public string Data;
}
这个类就表示例如获取第5行,第2列的Type是string,Name是Key,Data是resetLive。然后整个一行读取出来保存就是一个数组ConfigData[],通过这些数据就可以创建一个GameConfig。然后读取下面所有的行就是一个List<ConfigData[]>,把这些数据添加到Dict里面。要准备序列化的数据就准备好了,下面看代码:
/// <summary>
/// 序列化对象
/// </summary>
/// <param name="container">容器</param>
/// <param name="temp">容器内数据的类型</param>
/// <param name="dataList">容器内数据</param>
/// <param name="path">序列化二进制保存路径</param>
private static void Serialize(object container, Type type, List<ConfigData[]> dataList, string path)
{
//设置数据
foreach (ConfigData[] datas in dataList)
{
//创建一个对象
object t = type.Assembly.CreateInstance(type.FullName);
foreach (ConfigData data in datas)
{
//得到对象的一个属性
FieldInfo info = type.GetField(data.Name);
//给对象的属性赋值
info.SetValue(t, ParseValue(data.Type, data.Data));
}
//每个对象都有Id属性 唯一
object id = type.GetField("Id").GetValue(t);
//容器得到Dict字段
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)
{
EB.Debug.LogError("repetitive key " + id + " in " + container.GetType().Name);
return;
}
dict.GetType().GetMethod("Add").Invoke(dict, new object[] {id, t});
}
IFormatter f = new BinaryFormatter();
Stream s = new FileStream(path + type.Name + ".bytes", FileMode.OpenOrCreate,
FileAccess.Write, FileShare.Write);
EB.Debug.Log("create binary file:{0}",path + type.Name + ".bytes");
f.Serialize(s, container);
s.Close();
}
这个方法是根据类型,然后返回对应的数据类型:
private static object ParseValue(string dataType, string dataData)
{
switch (dataType)
{
case "int":
return int.Parse(dataData);
case "double":
return Convert.ToDouble(dataData);
case "bool":
return Convert.ToBoolean("True");
case "string":
return dataData;
default:
Debug.LogErrorFormat("未添加的属性类型{0}", dataType);
break;
}
return dataData;
}
在游戏运行时加载数据并且进行反序列化
这一步我们也希望生成代码,就是生成一个单例类,里面有一个方法直接把我们序列化好了二进制文件反序列化成类对象。然后再通过一个方法方便的获取类对象里面的值。
序列化的方法是这样的:
private System.Object Load(string name)
{
IFormatter f = new BinaryFormatter();
TextAsset text = Resources.Load<TextAsset>("ConfigBin/" + name);
Stream s = new MemoryStream(text.bytes);
System.Object obj = f.Deserialize(s);
s.Close();
return obj;
}
我们可以通过代码自动生成:
//创建数据管理器脚本
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 = ScriptGenerator._codePath;
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
StreamWriter sw = new StreamWriter(path + "DataManager.cs");
sw.WriteLine(source.ToString());
sw.Close();
}
这里使用到了一个通用的单例类:
public class SingletonTemplate <T> where T : new()
{
public SingletonTemplate() { }
public static T Instance
{
get { return SingletonCreator.instance; }
}
class SingletonCreator
{
static SingletonCreator() { }
internal static readonly T instance = new T();
}
}
读取数据表
读取数据表我们一开始说了用的是EPPlus,这里需要设置编译环境为.Net 4.x
读取数据表的东西我放在了这里最后,前面需要的东西就是我们需要创建数据类,得到各个属性的类型和成员变量名,还有就是得到一个List<ConfigData[]>得到所有数据:
[MenuItem("Tools/GenerateExcelData")]
public static void GenerateExcelData()
{
//创建一个字典保存所有的表和表里面所有的数据
Dictionary<string, List<ConfigData[]>> dataDict = new Dictionary<string, List<ConfigData[]>>();
//将要生成的数据类代码
List<string> codeList = new List<string>();
FileInfo fileInfo = new FileInfo(ExcelFile);
using (ExcelPackage excelPackage = new ExcelPackage(fileInfo))
{
string ClassName;
//获取所有的表
ExcelWorkbook excelWorkbook = excelPackage.Workbook;
foreach (var worksheet in excelWorkbook.Worksheets)
{
ClassName = worksheet.Name;
//计算行数
int lastRow = 1;
while (worksheet.Cells[lastRow, 1].Value != null)
{
lastRow++;
}
//计算列数
int lastColumn = 1;
while (worksheet.Cells[2, lastColumn].Value != null)
{
lastColumn++;
}
EB.Debug.Log("sheet:{0},row:{1},column:{2}", ClassName,lastRow,lastColumn);
string[] Types = new string[lastColumn - 1];
string[] Fileds = new string[lastColumn - 1];
//第二行是类型
for (int i = 1; i < lastColumn; i++)
{
Types[i - 1] = worksheet.Cells[2, i].GetValue<string>();
}
//第三行是字段名
for (int i = 1; i < lastColumn; i++)
{
Fileds[i - 1] = worksheet.Cells[3, i].GetValue<string>();
}
ScriptGenerator sg = new ScriptGenerator(ClassName, Fileds, Types);
string script = sg.Generate();
List<ConfigData[]> cdsList = new List<ConfigData[]>();
//第四行开始是数据
for (int i = 4; i < lastRow; i++)
{
ConfigData[] cds = new ConfigData[lastColumn - 1];
for (int j = 1; j < lastColumn; j++)
{
ConfigData cd = new ConfigData();
cd.Type = Types[j - 1];
cd.Name = Fileds[j - 1];
cd.Data = worksheet.Cells[i, j].GetValue<string>();
cds[j - 1] = cd;
}
cdsList.Add(cds);
}
dataDict.Add(ClassName, cdsList);
codeList.Add(script);
}
if (codeList.Count > 0)
{
CompileCodeAndByte(codeList, dataDict);
}
else
{
Debug.LogError("获取不到xlsx文件!");
}
}
}
编译类得到Dll
/// <summary>
/// 编译对象,容器 并且把数据保存至容器
/// </summary>
/// <param name="codeList"></param>
/// <param name="dataDict"></param>
public static void CompileCodeAndByte(List<string> codeList, Dictionary<string, List<ConfigData[]>> dataDict)
{
Assembly assembly = CompileCode(codeList.ToArray(), null);
string path = BytePath;
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
foreach (KeyValuePair<string, List<ConfigData[]>> each in dataDict)
{
object container = assembly.CreateInstance(each.Key + "Container");
Type type = assembly.GetType(each.Key);
Serialize(container, type, each.Value, path);
}
CreateDataManager(assembly);
}
具体编译代码的方法:
/// <summary>
/// 编译代码
/// </summary>
/// <param name="scripts"></param>
/// <param name="dllNames"></param>
/// <returns></returns>
private static Assembly CompileCode(string[] scripts, string[] dllNames)
{
string path = 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;
}
至此,我们所有代码已经生成了,.在游戏进入时调用一下LoadAll函数加载数据.后面直接调用对应函数, 根据id就可以取得数据了.
问题:
1.一个表格修改每次都全部新生成的byte文件。
2.dll代码增加不了注释。