Unity读写Excel,生成SO,以及自定义操作窗口

文章介绍了如何在Unity中读取Excel文件数据,将其转化为ScriptableObject资源,以便在游戏中直接使用。作者提供了一个工具,包括读取Excel核心代码、格式转换方法以及自定义编辑器窗口,允许用户导入和导出Excel数据。此外,还讨论了可能的性能优化和潜在问题。
摘要由CSDN通过智能技术生成

因为花了好几个星期做的功能,不可能做到面面俱到,所以重点说明很难理解的部分,最后贴上项目地址,可自行查看细节

读取Excel文件的数据

个人感觉不需要去理解这部分代码,拿来主义就好,因为这种功能不太需要额外扩展
先下载所需要的Dll文件
解压并放到项目中,最好和下面文件同目录

引入命名空间

每次看教程,找不到命名空间就很无奈。。。

using Excel;
using System.Data;
using System.IO;

基础的数据结构

	public class ExcelTool
    {
        public int col = 0;
        public int row = 0;
        //读取的表格数据集合
        DataRowCollection collect;
        //表格数据转到二维数组中,感觉这写的有点捞,谜之操作
        string[][] data = null;
        public string[][] Data
        {
            get
            {
                if (data == null)
                    Swap();
                return data;
            }
        }
        //构造函数
        public ExcelTool(string path,int sheetIndex = 0)
        {
            ReadExcel(path, sheetIndex);
        }
	}

读取Excel的核心代码

//表格路径,表格索引(一个excel文件中,存在好几张表)
 		void ReadExcel(string path, int sheetIndex)
        {
        //打开文件流 ,路径是从磁盘的根目录开始的,到具体的excel文件如:C:\Users\Ai\Desktop\text.xlsx
            FileStream stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
            //读取excel文件
            IExcelDataReader excleRead = ExcelReaderFactory.CreateOpenXmlReader(stream);
            //获取excel文件的读取结果
            DataSet result = excleRead.AsDataSet();
            //
            collect = result.Tables[sheetIndex].Rows;
            col = result.Tables[sheetIndex].Columns.Count;
            row = result.Tables[sheetIndex].Rows.Count;
            //因为excel设置内容格式会导致多读行,列的内容,这里是为了去除多读的内容
            while (row > 0)
            {
                if (collect[row - 1][0].ToString() == "")
                {
                    row--;
                    continue;
                }
                break;
            }
            while (col > 0)
            {
                if (collect[0][col - 1].ToString() == "")
                {
                    col--;
                    continue;
                }
                break;
            }
        }

格式转换

没啥好说的,基本操作

 		private void Swap()
        {
            data = new string[row][];
            for (int i = 0; i < row; i++)
            {
                string[] str = new string[col];
                for (int j = 0; j < col; j++)
                {
                    str[j] = collect[i][j].ToString();
                }
                data[i] = str;
            }
        }

实际运用

//返回表格的第sheetIndex(索引从0开始)张表格所有数据
ExcelTool excelData = new ExcelTool(excelPath,sheetIndex);

生成ScriptableObject文件

因为表格的数据格式是非常明确地,所有可以定义一个继承ScriptableObject类的基类

封装ScriptableObject基类

	/// <summary>
	/// 为了窗口可视化,增加一个类ExcelToolRowData
	/// </summary>
	public class ExcelToolBaseSO : ScriptableObject
	{
		//默认表格的第一行数据为key
		public string[] keys;
		public List<ExcelToolRowData> data = new List<ExcelToolRowData>();
		public void AddNode(string[] values)
		{
			data.Add(new ExcelToolRowData(values));
		}
	}
	//加上此属性,可以在inspect面板上显示类里面的数据
	//该类可以存储表格中,某一行的所有数据
	[System.Serializable]
	public class ExcelToolRowData
	{
		public string[] rowData;
	}

读取Excel文件是,生成ScriptableObject文件,并写入相关数据

		//表格路径
		//ScriptableObject资源生成的路径
		//表格的第几张表
		//忽略的行数 ,因为第一行是key,第二行可能是备注信息等等
		public void ReadExcelToSO<T>(string excelPath, string assetPath,int sheetIndex, int ignoreRowNum = 2) where T : ExcelToolBaseSO
		{
			//路径处理,当相对路径是,修改为绝对路径
			if (excelPath.StartsWith("Assets"))
			{
				excelPath = Application.dataPath + excelPath.Substring(6);
			}
			//读取表格,并获取表格中所有数据
			ExcelTool excelData = new ExcelTool(excelPath,sheetIndex);
			//读取ScriptableObject资源文件
			var excelAsset = AssetDatabase.LoadAssetAtPath<T>(assetPath);
			//如果没有资源文件,就创建资源文件
			if (excelAsset == null)
			{
				excelAsset = ScriptableObject.CreateInstance<T>();
				AssetDatabase.CreateAsset(excelAsset, assetPath);
				AssetDatabase.SaveAssets();
				AssetDatabase.Refresh();
			}
			//资源文件清空并赋值
			excelAsset.data.Clear();
			excelAsset.keys=excelData.Data[0];
			excelAsset.valueTypes = excelData.Data[1];
			foreach (var rowData in excelData.Data)
			{
				if (ignoreRowNum > 0)
				{
					ignoreRowNum--;
					continue;
				}
				excelAsset.AddNode(rowData);
			}
		}

读取Excel功能总结

  • excel文件数据是不可直接使用的,读取生成ScriptableObject资源文件后,可以直接在程序中使用。
    而这个过程有两个必须的参数,excel文件路径,以及后续生成的ScriptableObject文件的路径
  • 并且这个过程都是需要在编辑模式下进行的,制定一个自定义窗口,填写两个文件路径,就可以根据excel资源路径,获取ScriptableObject资源文件。

写入Excel功能

其实我不太希望在Unity编辑ScriptableObject文件,进而影响程序的,但是考虑到,直接修改效率会高一点,属于是可以不用,但是功能必须给到。

唯一坑的地方就是写入的时候数组下标从1开始(简直了)

		/// <summary>
		/// 使用SO的内容,并覆写Excel文件
		/// LookAt ExcelPackage 里面的数组从1开始:坑啊
		/// </summary>
		/// <typeparam name="T">SO的文件类型</typeparam>
		/// <param name="excelPath">excel资源路径</param>
		/// <param name="assetPath">输出到SO的路径</param>
		/// <param name="ignoreRowNum">忽略的首行数,默认为2(第一行Keys,第二行中文备注)</param>
		public void WriteSOToExcel<T>(string excelPath, string assetPath, int ignoreRowNum = 2) where T : ExcelToolBaseSO
		{
			if (excelPath.StartsWith("Assets"))
			{
				excelPath = Application.dataPath + excelPath.Substring(6);
			}
			FileInfo excelFile = new FileInfo(excelPath);
			ExcelPackage package = new ExcelPackage(excelFile);
			ExcelWorksheet worksheet = package.Workbook.Worksheets[1];
			T excelAsset = AssetDatabase.LoadAssetAtPath<T>(assetPath);
			for (int i = 1; i <= excelAsset.data.Count; i++)
			{
				string[] rowData = excelAsset.data[i-1].rowData;
				for (int j = 1; j <= rowData.Length; j++)
				{
					worksheet.SetValue(i + ignoreRowNum, j, rowData[j-1]);
				}	
			}
			//修改某一行的数据
			//worksheet.Cells[4, 3].Value = "女";
			//worksheet.SetValue(4, 3, "女");
			//保存excel
			package.Save();
		}

Unity自定义窗口

这部分就没什么技巧,都是体力活,可能相关自定义窗口语法不清楚,我在代码中给到注释,以及给出我参考他人的案例
自定义语法参考

最后的结果

我个人还是很满意写的这个工具窗口,希望阅读的你也喜欢,要是你能用上我的工具,那就是这篇文章存在的意义了。


自定义窗口总结

  • 虽然有好几个类都是实现自定义窗口,一套写下来,感觉GUILayout自带的布局功能不错
 public class ExcelToolExtension : EditorWindow
    {

        string excelPath = @"";
        string outputPath = @"";
		string notePath = @"Assets/FrameWork/ExcelTool/ExcelToolLoadNoteAsset.asset";
		bool firstState = true;
		int toolGirdId = 0;
		Vector2 scrollPos = Vector2.zero;
		ExcelToolLoadNoteSO note;
		public ExcelToolLoadNoteSO Note
		{
			get
			{
				if (note == null)
				{
					note = AssetDatabase.LoadAssetAtPath<ExcelToolLoadNoteSO>(notePath);
				}
				return note;
			}
		}
        
		[MenuItem("Framework/Window/ExcalTool")]
		public static void Init()
        {
			ExcelToolExtension exWindow = GetWindow<ExcelToolExtension>();
			exWindow.Show();
		}

        public void OnGUI()
        {
            EditorGUILayout.LabelField("Excel数据与Asset资源处理");
            toolGirdId = GUILayout.Toolbar(toolGirdId, new[] { "重新导入Excel", "导入新Excel","移除Excel的Asset文件","Asset文件写入Excel" },GUILayout.Height(50));
			if (firstState)
			{
				outputPath = Note.outputPath;
			}
            switch (toolGirdId)
            {
                case 0:
					if (GUILayout.Button("重新导入所有Excel!!!",GUILayout.Height(50)))
					{
						LoadAllExcel();
					};
					GUILayout.BeginHorizontal();
					GUILayout.Space(10);
					scrollPos = GUILayout.BeginScrollView(scrollPos);
					ShowAllLoadedExcel(LoadExcel,"重新导入");
					GUILayout.EndScrollView();
                    GUILayout.EndHorizontal();

					break;
                case 1:
					GUILayout.Space(5);
					EditorGUILayout.LabelField("Excel资源路径案例:	" + @"Assets/FrameWork/ExcelTool/ExcelSample.xlsx");
					EditorGUILayout.LabelField("Asset输出目录案例:	" + @"Assets/FrameWork/ExcelTool");
					GUILayout.Label("输出路径与Excel同目录:	生成相应的Asset文件,文件在Excel同目录下,名称为Excel文件名+表名");
					GUILayout.Box("", GUILayout.Height(5), GUILayout.ExpandWidth(true));
					excelPath = EditorGUILayout.TextField("Excel资源路径", excelPath);
					outputPath = EditorGUILayout.TextField("Asset输出目录", outputPath);
					GUILayout.Space(10);
					GUILayout.BeginHorizontal();
					if (GUILayout.Button("输出路径与表格同目录",GUILayout.Height(50)))
					{
						outputPath = GetDefaultPathByExcelPath(excelPath);
					}
					if (GUILayout.Button("导入Excel数据", GUILayout.Height(50)))
                    {
						LoadExcel(excelPath, outputPath);
					}
					GUILayout.EndHorizontal();
					break;
				case 2:
					if (GUILayout.Button("移除所有Excel的Asset文件,并删除导入记录!!!", GUILayout.Height(50)))
					{
						RemoveAllExcelSO();
					};
					GUILayout.BeginHorizontal();
					GUILayout.Space(10);
					scrollPos = GUILayout.BeginScrollView(scrollPos);
					ShowAllLoadedExcel(RemoveExcelSO,"移除Asset文件");
					GUILayout.EndScrollView();
					GUILayout.EndHorizontal();
					break;
				case 3:
					if (GUILayout.Button("所有Asset文件写入Excel!!!", GUILayout.Height(50)))
					{
						WriteAllExcel();
					};
					GUILayout.BeginHorizontal();
					GUILayout.Space(10);
					scrollPos = GUILayout.BeginScrollView(scrollPos);
					ShowAllLoadedExcel(WriteExcel, "写入Excel文件");
					GUILayout.EndScrollView();
					GUILayout.EndHorizontal();
					break;
				default:
                    break;
            }
			firstState = false;
		}

        public void LoadExcel(string excelPath,string outputPath)
        {
			if(string.IsNullOrEmpty(excelPath) || string.IsNullOrEmpty(outputPath))
			{
				Debug.Log("Excel路径和资源输出路径不能为空");
				return;
			}
			List<string> sheetNames = AssetUtil.Ins.GetExcelTableNames(excelPath);
			string excelName = AssetUtil.Ins.GetFileNameByPath(excelPath);
			List<string> paths = new List<string>();
			for (int i = 0;i < sheetNames.Count; i++)
			{
				string path = outputPath+"/" + excelName + sheetNames[i] + ".asset";
				paths.Add(path);
				AssetUtil.Ins.ReadExcelToSO<ExcelToolBaseSO>(excelPath, path,i);
			}
			Note.AddNode(excelPath,outputPath,paths);
			Debug.Log("导入" + excelPath + "表格");
		}

		public void LoadAllExcel()
		{
			foreach (var item in Note.Notes)
			{
				LoadExcel(item.excelPath, outputPath);
			}
			Debug.Log("重新导入所有表格");
		}

		public void RemoveExcelSO(string excelPath)
		{
			Note.Remove(excelPath);
			Debug.Log("移除"+excelPath+"的Asset文件");
		}
		public void RemoveAllExcelSO()
		{
			foreach (var item in Note.Notes)
			{
				RemoveExcelSO(item.excelPath);
			}
			Note.Notes.Clear();
			Debug.Log("移除所有Excel的Asset文件");
		}

		public void WriteExcel(string excelPath, string outputPath)
		{
			AssetUtil.Ins.WriteSOToExcel<ExcelToolBaseSO>(excelPath, outputPath);
			Debug.Log("覆写" + excelPath + "文件");
		}

		public void WriteAllExcel()
		{
			foreach (var item in Note.Notes)
			{
				WriteExcel(item.excelPath, item.outputPath);
			}
			Debug.Log("所有Asset文件覆写Excel!!!");
		}

		public void ShowAllLoadedExcel(Action<string,string> action,string btnText)
		{
			for (int i = 0; i < Note.Notes.Count; i++)
			{
				string excelPath = Note.Notes[i].excelPath;
				string outputPath = Note.Notes[i].outputPath;
				List<string> paths = Note.Notes[i].paths;
				GUILayout.BeginHorizontal(GUILayout.MinHeight(50));
				GUILayout.BeginVertical(GUILayout.Width(500));
				GUILayout.Space(5);
				EditorGUILayout.LabelField("Excel表格资源路径:	" + excelPath);
				for (int j = 0; j < paths.Count; j++)
				{
					GUILayout.Space(5);
					EditorGUILayout.LabelField("Asset路径:	" + paths[j]);
				}
				GUILayout.EndVertical();
				GUILayout.FlexibleSpace();
				if (GUILayout.Button(btnText, GUILayout.Width(100), GUILayout.ExpandHeight(true)))
				{
					action(excelPath, outputPath);
				};
				GUILayout.Space(15);

				GUILayout.EndHorizontal();
				GUILayout.Box("", GUILayout.Height(5), GUILayout.ExpandWidth(true));
			}
		}

		public void ShowAllLoadedExcel(Action<string> action, string btnText)
		{
			for (int i = 0; i < Note.Notes.Count; i++)
			{
				string excelPath = Note.Notes[i].excelPath;
				string outputPath = Note.Notes[i].outputPath;
				List<string> paths = Note.Notes[i].paths;
				GUILayout.BeginHorizontal(GUILayout.MinHeight(50));
				GUILayout.BeginVertical(GUILayout.Width(500));
				GUILayout.Space(5);
				EditorGUILayout.LabelField("Excel表格资源路径:	" + excelPath);
				for (int j = 0; j < paths.Count; j++)
				{
					GUILayout.Space(5);
					EditorGUILayout.LabelField("Asset路径:	" + paths[j]);
				}
				GUILayout.EndVertical();
				GUILayout.FlexibleSpace();
				if (GUILayout.Button(btnText, GUILayout.Width(100),GUILayout.ExpandHeight(true)))
				{
					action(excelPath);
				};
				GUILayout.Space(15);

				GUILayout.EndHorizontal();
				GUILayout.Box("", GUILayout.Height(5), GUILayout.ExpandWidth(true));
			}
		}

		public string GetDefaultPathByExcelPath(string excelPath)
		{
			if (string.IsNullOrEmpty(excelPath))
			{
				Debug.Log("Excel路径不能为空");
				return null;
			}

			string path = AssetUtil.Ins.GetDirectoryPathByPath(excelPath);
			Note.outputPath = path;
			return path;
		}
	}

用到的几个自定义函数

		/// <summary>
		/// 获取Excel所有表的名称
		/// </summary>
		/// <param name="path">Excel的文件路径</param>
		/// <returns></returns>
		public List<string> GetExcelTableNames(string path)
		{

			if (path.StartsWith("Assets"))
			{
				path = Application.dataPath + path.Substring(6);
			}
			FileStream stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
			IExcelDataReader excleRead = ExcelReaderFactory.CreateOpenXmlReader(stream);
			DataSet result = excleRead.AsDataSet();
			List<string> names = new List<string>();
			for (int i = 0; i < result.Tables.Count; i++)
			{
				names.Add(result.Tables[i].TableName);
			}
			return names;
		}
		
		public string GetFileNameByPath(string path)
		{
			string[] subPath = path.Split(".xlsx");
			string[] subStr = subPath[0].Split("/");
			return subStr[subStr.Length - 1];
		}

		public string GetDirectoryPathByPath(string path)
		{
			string[] subPath = path.Split("/");
			string directoryPath = "";
			for (int i = 0; i < subPath.Length-1; i++)
			{
				directoryPath += subPath[i];
				directoryPath += "/";
			}
			return directoryPath.Substring(0,directoryPath.Length - 1);
		}

项目地址

我自己写的一些工具,系统,里面库里面的ExcelTool下包含此文章的所有内容,希望可以被建议或者帮组到读者。

最后的总结

  • 工具本身没什么问题
  • 最后得到的ScriptableObject资源文件的成员都是string类型,会不利于后续的其他类型的数据访问,频繁的装箱,插箱会影响性能。
  • 设想通过表格的第二行存储当前列的数据格式,然后动态生成相应的类(这个功能尝试很久无果),后续的设计也就没法进行
  • 希望有志人士可以精进一下
  • 如果你真的看到这里,点个赞,让我知道,我也曾被欣赏。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值