1、最后得到的ScriptableObject资源文件的成员都是string类型,会不利于后续的其他类型的数据访问,频繁的装箱,插箱会影响性能。
2、设想通过表格的第二行存储当前列的数据格式,然后动态生成相应的类(这个功能尝试很久无果),后续的设计也就没法进行
上面是之前设计遗留的问题。虽然看的人不多,那就我来精进一下吧

具体实现步骤

先看实现后的自定义窗口

unity导excel表工具_游戏引擎


为了可以把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();
		}

上面代码可能看上去很奇怪,结合生成的文本内容看,就清晰明了了。

文本生成案例,这是一个学生配置表的案例

表格信息

unity导excel表工具_unity导excel表工具_02

文本生成的内容

[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类的实现,导入记录的存储实现等等
如果存疑,请先浏览我的源码
项目地址 温馨提示:通过类名查找内容,因为我经常整理项目结构。