1、最后得到的ScriptableObject资源文件的成员都是string类型,会不利于后续的其他类型的数据访问,频繁的装箱,插箱会影响性能。
2、设想通过表格的第二行存储当前列的数据格式,然后动态生成相应的类(这个功能尝试很久无果),后续的设计也就没法进行
上面是之前设计遗留的问题。虽然看的人不多,那就我来精进一下吧
具体实现步骤
先看实现后的自定义窗口
为了可以把excel表格中的数据转换成平时使用的数据类,需要明确各个字段的类型,
所以表格需要且必须满足以下要求
忽略前3行和第一列的数据,作为数据的备注、说明、诠释等等作用,也可以通过配置,修改忽略的行列数。
第一行:字段名,为了防止出现number,type这种关键字名称,需要全部小写,用下划线_间隔,同时以下划线_结尾,例system_id_
第二行:设置字段的备注,有时候字段名比较简洁,无法解释字段的含义。
第三行:设置字段的类型,因为C#数据有各种类型。
第一列:备注当前行的信息,可以看备注,一目了然。
一些特殊的规则:
第一行字段名以[K]开头,代表配置的key,可以用于检索信息
字段名vaild_:是否有效,1有效。等等
生成并编译结构体
上代码
public void GenerateStructure(string excelPath)
{
//判空
string outputPath = DefaultConfig.ExcelAllSODirectory;
if (string.IsNullOrEmpty(excelPath) || string.IsNullOrEmpty(outputPath))
{
Debug.Log("Excel路径和资源输出路径不能为空");
return;
}
//获取表格中所有的表名
List<string> sheetNames = ExcelTool.Ins.GetExcelTableNames(excelPath);
//记录表格信息,方便之后信息的添加,删除等
Note.SetNodeTableInfo(excelPath);
for (int i = 0; i < sheetNames.Count; i++)
{
//获取表格基本信息
ExcelTableInfo info = ExcelTool.Ins.GetTableInfo(excelPath, i);
//根据表格信息,生成数据类和SO类的string文本
string customData = DesignTools.Ins.CreateCustomExcelDataClass(info);
//把文本保存到info.dataSOClassPath的文件中
AssetUtil.Ins.SaveTextAsset(info.dataSOClassPath, customData);
//刷新资源
AssetDatabase.Refresh();
}
Debug.Log("成功生成" + excelPath + "表格的数据类");
}
/// <summary>
/// 表格的基本信息结构体
/// </summary>
public struct ExcelTableInfo
{
public string[] fields; //所有的字段名
public string[] fieldTypes; //所有的字段类型
public List<string> keys; //所有的key的字段
public List<string> keyTypes; //所有key的类型
public int fieldNum; //字段数量
public string dataClassName; //数据类型的类名
public string SOClassName; //SO类的类名
public string dataSOClassPath; //数据类和SO类的存放路径
public bool hasKey; //是否含有主键
public void Init()
{
this.keys = new List<string>();
this.keyTypes = new List<string>();
this.fieldNum = 0;
}
}
根据表格信息,生成数据类和SO类的string文本方法具体实现,大致看看就可以往下看了
public string CreateCustomExcelDataClass(ExcelTableInfo info)
{
StringBuilder SB = new StringBuilder();
SB.Append(@"using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using Newtonsoft.Json;
namespace Super
{
[Serializable]
public class ");
SB.Append(info.dataClassName + ":ExcelToolDataBase\n");
SB.AppendLine(@" {");
for (int i = 0; i < info.fields.Length; i++)
{
//SB.AppendLine(@" [HideInInspector]");
SB.AppendLine($" public {info.fieldTypes[i]} {info.fields[i]};");
}
SB.Append(@" public override void SetValue(string[] rowData)
{
base.SetValue(rowData);");
SB.AppendLine();
for (int i = 0; i < info.fields.Length; i++)
{
SB.AppendLine($" {info.fields[i]} = ({info.fieldTypes[i]})Convert.ChangeType(rowData[{i}], typeof({info.fieldTypes[i]}));");
}
SB.AppendLine(" }");
SB.Append(@" public override void ValueBackFill()
{
base.ValueBackFill();");
SB.AppendLine();
for (int i = 0; i < info.fields.Length; i++)
{
SB.AppendLine($" rowData[{i}] = {info.fields[i]}.ToString();");
}
SB.AppendLine(" }");
SB.AppendLine(" }");
SB.Append(@"
[Serializable]
public class ");
SB.Append(info.SOClassName + ":ExcelToolBase\n");
SB.AppendLine(@" {");
SB.AppendLine($" public List<" + info.dataClassName + "> data = new List<" + info.dataClassName + ">();");
if (info.keys.Count > 0)
{
SB.AppendLine(@" private string excelDictionaryJson = """";");
SB.Append(@" private ExcelDictionary<");
for (int i = 0; i < info.keys.Count; i++) SB.Append(info.keyTypes[i] + ", ");
SB.Append($"{ info.dataClassName }> excelDictionary;\n");
SB.Append(@" public ExcelDictionary<");
for (int i = 0; i < info.keys.Count; i++) SB.Append(info.keyTypes[i] + ", ");
SB.Append($"{info.dataClassName}> ExcelDictionary\n");
SB.Append(@" {
get
{
if (excelDictionary == null)
{
if (!String.IsNullOrEmpty(excelDictionaryJson))
{" + "\n");
SB.Append(@" excelDictionary = JsonConvert.DeserializeObject<ExcelDictionary<");
for (int i = 0; i < info.keys.Count; i++) SB.Append(info.keyTypes[i] + ", ");
SB.Append($"{info.dataClassName}>>(excelDictionaryJson);\n");
SB.Append(@" }
if (excelDictionary == null)
{" + "\n");
SB.Append(@" excelDictionary = new ExcelDictionary<");
for (int i = 0; i < info.keys.Count; i++) SB.Append(info.keyTypes[i] + ", ");
SB.Append($"{info.dataClassName}>();\n");
SB.Append(@" }
}
return excelDictionary;
}
}"+"\n");
}
SB.AppendLine(@" private static "+ info.SOClassName + " ins;");
SB.AppendLine(@" public static " + info.SOClassName + " Ins");
SB.AppendLine(@" {");
SB.AppendLine(@" get");
SB.AppendLine(@" {");
SB.AppendLine(@" if (ins == null)");
SB.AppendLine(@" {");
SB.AppendLine(@" string path = DefaultConfig.ExcelAllSODirectory + ""/" + info.SOClassName + @"Asset.asset"";");
SB.AppendLine(@" ins = AssetDatabase.LoadAssetAtPath<" + info.SOClassName + ">(path);");
SB.AppendLine(@" if (ins == null)");
SB.AppendLine(@" {");
SB.AppendLine(@" ins = ScriptableObject.CreateInstance<"+ info.SOClassName +">();");
SB.AppendLine(@" AssetDatabase.CreateAsset(ins, path);");
SB.AppendLine(@" AssetDatabase.SaveAssets();");
SB.AppendLine(@" AssetDatabase.Refresh();");
SB.AppendLine(@" }");
SB.AppendLine(@" }");
SB.AppendLine(@" return ins;");
SB.AppendLine(@" }");
SB.AppendLine(@" }");
SB.AppendLine($" public void AddNodeData(" + info.dataClassName + " nodeData)");
SB.AppendLine(@" {");
SB.AppendLine(@" data.Add(nodeData);");
if(info.keys.Count > 0)
{
SB.AppendLine(@" ExcelDictionary.Add(nodeData, keys);");
}
SB.AppendLine(@" }");
SB.AppendLine($" public void Clear()");
SB.AppendLine(@" {");
SB.AppendLine(@" data.Clear();");
if (info.keys.Count > 0)
{
SB.AppendLine(@" ExcelDictionary.Clear();");
}
SB.AppendLine(@" }");
if (info.keys.Count > 0)
{
SB.Append(@" public void SaveExcelDictionary()
{
excelDictionaryJson = JsonConvert.SerializeObject(ExcelDictionary);
}" + "\n");
}
SB.AppendLine(@" }");
SB.AppendLine(@"}");
return SB.ToString();
}
上面代码可能看上去很奇怪,结合生成的文本内容看,就清晰明了了。
文本生成案例,这是一个学生配置表的案例
表格信息
文本生成的内容
[Serializable]
public class StudentConfig:ExcelToolDataBase
{
public int vaild_;
public int grade_;
public int class_;
public int serial_no_;
public string name_;
public override void SetValue(string[] rowData)
{
base.SetValue(rowData);
vaild_ = (int)Convert.ChangeType(rowData[0], typeof(int));
grade_ = (int)Convert.ChangeType(rowData[1], typeof(int));
class_ = (int)Convert.ChangeType(rowData[2], typeof(int));
serial_no_ = (int)Convert.ChangeType(rowData[3], typeof(int));
name_ = (string)Convert.ChangeType(rowData[4], typeof(string));
}
public override void ValueBackFill()
{
base.ValueBackFill();
rowData[0] = vaild_.ToString();
rowData[1] = grade_.ToString();
rowData[2] = class_.ToString();
rowData[3] = serial_no_.ToString();
rowData[4] = name_.ToString();
}
}
[Serializable]
public class StudentConfig_SO:ExcelToolBase
{
public List<StudentConfig> data = new List<StudentConfig>();
private string excelDictionaryJson = "";
private ExcelDictionary<int, int, int, StudentConfig> excelDictionary;
public ExcelDictionary<int, int, int, StudentConfig> ExcelDictionary
{
get
{
if (excelDictionary == null)
{
if (!String.IsNullOrEmpty(excelDictionaryJson))
{
excelDictionary = JsonConvert.DeserializeObject<ExcelDictionary<int, int, int, StudentConfig>>(excelDictionaryJson);
}
if (excelDictionary == null)
{
excelDictionary = new ExcelDictionary<int, int, int, StudentConfig>();
}
}
return excelDictionary;
}
}
private static StudentConfig_SO ins;
public static StudentConfig_SO Ins
{
get
{
if (ins == null)
{
string path = DefaultConfig.ExcelAllSODirectory + "/StudentConfig_SOAsset.asset";
ins = AssetDatabase.LoadAssetAtPath<StudentConfig_SO>(path);
if (ins == null)
{
ins = ScriptableObject.CreateInstance<StudentConfig_SO>();
AssetDatabase.CreateAsset(ins, path);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
}
return ins;
}
}
public void AddNodeData(StudentConfig nodeData)
{
data.Add(nodeData);
ExcelDictionary.Add(nodeData, keys);
}
public void Clear()
{
data.Clear();
ExcelDictionary.Clear();
}
public void SaveExcelDictionary()
{
excelDictionaryJson = JsonConvert.SerializeObject(ExcelDictionary);
}
}
生成的数据类StudentConfig解析
包含所有字段,类型为第三行的类型设置
SetValue方法:设置类中所有变量的值
ValueBackFill方法:用途把字段转换成string以回写到Excel中
生成的SO类StudentConfig_SO解析
data:表格中所有的数据
excelDictionary:表格的字典结构,方便数据的查找
SaveExcelDictionary:序列化字典
excelDictionaryJson:SO是没法存储直接序列化字典,没法保存在SO中,通过上面的方法保存在json中,然后使用的时候反序列化到excelDictionary中
导入Excel表格
public void LoadExcel(string excelPath)
{
//判空
string outputPath = DefaultConfig.ExcelAllSODirectory;
if (string.IsNullOrEmpty(excelPath) || string.IsNullOrEmpty(outputPath))
{
Debug.Log("Excel路径和资源输出路径不能为空");
return;
}
List<string> sheetNames =ExcelTool.Ins.GetExcelTableNames(excelPath);
//SO实例的生成路径
List<string> SOPaths = new List<string>();
for (int i = 0;i < sheetNames.Count; i++)
{
string dataSOClassName = Note.GetNodeDataSOClassName(excelPath, i);
string SOPath = outputPath+"/" + dataSOClassName + "Asset.asset";
SOPaths.Add(SOPath);
}
//记录下SO的生成路径
Note.SetNodeSOPaths(excelPath, SOPaths);
for (int i = 0; i < sheetNames.Count; i++)
{
string SOPath = SOPaths[i];
Type TypeSO = Note.GetNodeDataSOClassType(excelPath, i);
Type TypeData = Note.GetNodeDataClassType(excelPath, i);
//根据表格,SO的生产路径,第几张表,SO的类型,数据类的类型生成SO的实例
AssetUtil.Ins.ReadExcelToSO(excelPath, SOPath, i, TypeSO, TypeData);
}
Debug.Log("导入" + excelPath + "表格");
}
读表生成SO实例的实现
/// <summary>
/// 读取Excel表格数据,并存储到SO中
/// 字段类型样式int,float,string,bool,long,double,char,ulong,ushort,short
/// </summary>
/// <typeparam name="T">返回的SO类型</typeparam>
/// <param name="excelPath">excel资源路径</param>
/// <param name="assetPath">输出到SO的路径</param>
/// <returns></returns>
public void ReadExcelToSO(string excelPath, string assetPath, int sheetIndex, Type typeSO, Type typeData)
{
//获取绝对路径,自己实现的工具方法
string excelAbsolutePath = CommonUtil.GetAbsolutePath(excelPath);
//excel表格数据对象
ExcelTable excelData = new ExcelTable(excelAbsolutePath, sheetIndex);
//获取表格的信息
ExcelTableInfo info = ExcelTool.Ins.GetTableInfo(excelAbsolutePath, sheetIndex);
//创建SO的实例(这个时候是没有赋值的实例)
var excelAsset = AssetDatabase.LoadAssetAtPath(assetPath, typeSO);
if (excelAsset == null)
{
excelAsset = ScriptableObject.CreateInstance(typeSO);
AssetDatabase.CreateAsset(excelAsset, assetPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
//因为是动态生成SO类和数据类,所以通过反射赋值
//有些是ExcelToolBase基类的字段
typeSO.GetField("fields").SetValue(excelAsset, info.fields);
typeSO.GetField("fieldTypes").SetValue(excelAsset, info.fieldTypes);
typeSO.GetField("keys").SetValue(excelAsset, info.keys.ToArray());
typeSO.GetField("keyTypes").SetValue(excelAsset, info.keyTypes.ToArray());
//调用方法清空数据
typeSO.GetMethod("Clear").Invoke(excelAsset, null);
//获取添加数据的方法
MethodInfo typeSO_AddNodeData = typeSO.GetMethod("AddNodeData");
//默认忽略的ignoreRowNum 行,然后再读取数据
int ignoreRowNum = DefaultConfig.ExcelIgnoreArea.x;
foreach (var rowData in excelData.data)
{
if (ignoreRowNum > 0)
{
ignoreRowNum--;
continue;
}
//创建数据类的实例
var obj = Activator.CreateInstance(typeData);
//数据类赋值
typeData.GetMethod("SetValue").Invoke(obj, new object[] { rowData });
//添加数据到SO实例中
typeSO_AddNodeData.Invoke(excelAsset, new object[] { obj });
}
//字典数据没法保存,如果是含有主键,就把字典数据序列化到excelDictionaryJson中
if (info.hasKey)
{
MethodInfo typeSO_SaveExcelDictionary = typeSO.GetMethod("SaveExcelDictionary");
typeSO_SaveExcelDictionary.Invoke(excelAsset, null);
}
}
访问数据实例
StudentConfig studentConfig = StudentConfig_SO.Ins.ExcelDictionary.Find(6, 1, 1);
感觉有点不好用啊,Ins.ExcelDictionary这属于固定没信息的内容。
这么完美的东西竟然还可以优化。
综上就实现了根据表格内容,生成相应的数据类并生成实例化的SO,SO中包含表格中所有的数据。
总结:
1、SO不能保存字典数据的解决:excel表格配置的数据,大多都带有key,用字典嵌套的结构,查找的效率更为高效,但是字典结构不能在SO中序列化保存,通过JsonConvert.SerializeObject序列化保存到string变量中,然后把字典数据写成属性,访问发现为空的时候,将string中的文本反序列化赋值给字典变量。
2、上面用到反射,但是这些操作都是在编辑状态发生,所以性能无所谓,况且只要不反射程序集,遍历程序集,性能消耗没有多大区别。当然,微小的差距调用的量级增加就值得关注了。
3、其实表格的数据用多叉树实现也挺好的,还不需要担心SO不能保存字典的问题。逼急了我自己搞一个hash字典,在我写多叉树基类的时候,unity_读写Excel2.0已经优化完了,所以就不太想修改结构,其实嵌套字典和多叉树查询效率都是log(n)。
4、之前还不太了解反射,搞了这个工具后,反射算是基本了解了。
5、数据的拿取应该还能优化,目前还没有方案,要是有小伙伴谏言那就太好了。
补充
这不是一个小功能,所以没法面面俱到,有些遗漏的内容,比如ExcelDictionary类的实现,导入记录的存储实现等等
如果存疑,请先浏览我的源码
项目地址 温馨提示:通过类名查找内容,因为我经常整理项目结构。