一般在游戲開發中策划都會把數據配置在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就可以取得數據了.
