Unity游戏开发:从零构建Excel转Json导表工具V1.0 1. 项目概述为什么我们需要一个自己的导表工具在Unity游戏开发尤其是中大型项目里策划同学最常用的工具是什么十有八九是Excel。从角色属性、技能配置、任务对话到道具商店海量的游戏数据都躺在那一张张表格里。作为程序我们的任务就是把这些数据“喂”给游戏引擎让它能跑起来。最经典、最通用的数据交换格式就是Json它轻量、易读、跨平台是Unity里C#脚本反序列化的好伙伴。那么问题来了策划改了一个数值程序怎么更新到游戏里手动复制粘贴效率低下且极易出错。让策划直接改Json文件不现实对非技术人员来说门槛太高。所以一个能自动、准确、高效地将Excel表格转换成Json格式的“导表工具”就成了连接策划与程序、数据与逻辑的关键桥梁。市面上的确有一些现成的工具或插件比如在Unity社区里常被提及的Excel2Json。但用别人的工具总会遇到一些“水土不服”字段命名规则不匹配、需要特定的Excel格式比如必须把第一行作为字段名第二行作为数据类型、对复杂数据结构如数组嵌套、字典支持不佳或者生成Json的格式不符合项目已有的数据解析库要求。更头疼的是当工具出现Bug或者需要定制功能时你只能干等着作者更新或者硬着头皮去读别人的源码。因此自己动手开发一个“Unity导表工具V1.0”远不止是重复造轮子。它是一个高度定制化的解决方案核心目标是将策划的Excel工作流与程序的Json数据需求无缝对接实现“表格即配置导出即可用”的高效协作模式。这个V1.0版本意味着我们从零开始搭建一个稳定、可靠、符合自身项目规范的基础框架后续所有复杂功能如多表关联、数据校验、二进制导出等都将基于此扩展。2. 工具核心设计与思路拆解2.1 需求分析与技术选型在动手写代码之前我们先明确工具要解决的核心痛点输入支持通用的.xlsx格式兼容.xls可作为备选。处理能解析表格结构识别表头字段名、数据类型读取数据内容。输出生成标准、格式化的Json文件。集成最好能与Unity编辑器集成提供一键导出按钮。可维护性代码结构清晰便于后续增加如数据校验、生成C#数据类、多表合并等功能。基于这些需求技术栈的选择就清晰了.NET环境Unity基于.NET或.NET Core/.NET Standard我们自然使用C#进行开发。Excel读取库这是关键。我们放弃直接使用微软昂贵的Office Interop而是选择成熟的开源库。常见的有EPPlus对于.xlsx格式支持非常好性能优异无需安装Office。这是我们的首选。NPOI一个老牌的.NET库同时支持.xls和.xlsx兼容性极强。选择理由考虑到现在基本都使用.xlsx格式且EPPlus的API相对更现代、易用性能也更好我们V1.0版本选择EPPlus作为核心依赖。可以通过Unity的Package Manager添加EPPlus需确认有合适的Unity兼容版本或者直接导入其DLL。Json序列化库.NET自带的System.Text.Json.NET Core 3.0或第三方Newtonsoft.JsonJson.NET。Unity 2021 LTS及以上版本对System.Text.Json的支持已经比较完善它性能更高且无需额外依赖。如果项目较老或已广泛使用Json.NET则选择后者。这里我们以较新的System.Text.Json为例。Unity编辑器扩展使用UnityEditor命名空间下的API创建自定义的编辑器窗口EditorWindow或为特定组件添加按钮Editor实现可视化操作。2.2 整体架构设计工具的整体工作流可以抽象为以下几个模块配置模块定义Excel与Json的映射规则。例如约定Excel的第一行为字段名英文第二行为数据类型如int,string,int[],Dictionarystring, float等。读取解析模块使用EPPlus打开Excel文件遍历工作表Sheet和单元格根据配置规则将表格数据读取到内存中的数据结构里通常是ListDictionarystring, object或自定义的类对象列表。数据处理与转换模块对读取到的原始数据进行清洗、类型转换将字符串100转为整数100、复杂结构解析如将1,2,3字符串转为int[] {1,2,3}。序列化输出模块将内存中的数据结构使用Json序列化库转换成Json字符串并写入到指定的.json文件。编辑器界面模块提供一个简单的Unity编辑器窗口让用户可以选择Excel文件、设置输出路径、执行导出操作并显示日志反馈。这个架构保证了各模块职责单一未来若要支持导出为二进制、XML或直接生成ScriptableObject只需替换或扩展“序列化输出模块”即可。3. 核心细节解析与实操要点3.1 Excel表格的规范约定一个健壮的工具始于清晰的约定。为了避免解析时的歧义我们需要和策划同学共同制定一份简单的Excel填写规范表头行第1行字段名行使用英文或拼音命名作为Json中的key。例如id,name,attackPower。第2行数据类型行定义每个字段的数据类型。这是实现自动类型转换的关键。我们约定一些简单的标记基础类型int,float,string,bool。数组在基础类型后加[]如int[]。数据单元格内填写用逗号或分号分隔的值如10,20,30。V1.0可暂缓字典/对象可以用Dictstring,int或自定义格式初期可以用一个复杂的string类型后期专门解析。数据行从第3行开始每一行代表一条配置记录。工作表Sheet一个Sheet通常导出一份独立的Json文件。可以用Sheet名来命名输出的Json文件。特殊值处理约定空单元格、“#N/A”等错误值的处理方式比如转为null或默认值。注意这个规范是工具与策划之间的“合同”。工具代码将严格依赖此规范进行解析。在工具开发初期务必和策划充分沟通并确认。3.2 使用EPPlus读取Excel的要点EPPlus的核心对象是ExcelPackage和ExcelWorksheet。以下是一些关键操作和避坑点using OfficeOpenXml; // EPPlus的命名空间 public ListDictionarystring, object ParseExcel(string filePath) { ListDictionarystring, object dataList new ListDictionarystring, object(); // 设置EPPlus的LicenseContext社区版通常为非商业用途 ExcelPackage.LicenseContext LicenseContext.NonCommercial; FileInfo fileInfo new FileInfo(filePath); using (ExcelPackage package new ExcelPackage(fileInfo)) { // 获取第一个工作表也可以遍历所有 ExcelWorksheet worksheet package.Workbook.Worksheets[0]; // 1. 读取表头第1、2行 int colCount worksheet.Dimension.End.Column; // 获取最大列数 int rowCount worksheet.Dimension.End.Row; // 获取最大行数 Dictionaryint, string fieldNameMap new Dictionaryint, string(); Dictionaryint, string fieldTypeMap new Dictionaryint, string(); for (int col 1; col colCount; col) { fieldNameMap[col] worksheet.Cells[1, col].Text?.Trim(); fieldTypeMap[col] worksheet.Cells[2, col].Text?.Trim(); // 如果字段名为空则认为该列无效后续跳过 } // 2. 从第3行开始读取数据 for (int row 3; row rowCount; row) { Dictionarystring, object rowData new Dictionarystring, object(); bool isEmptyRow true; // 标记是否为空行 for (int col 1; col colCount; col) { string fieldName fieldNameMap[col]; string fieldType fieldTypeMap[col]; if (string.IsNullOrEmpty(fieldName) || string.IsNullOrEmpty(fieldType)) { continue; // 跳过未定义类型的列 } var cell worksheet.Cells[row, col]; string cellText cell.Text; // 判断是否为空行所有有效列都为空 if (!string.IsNullOrEmpty(cellText)) isEmptyRow false; // 关键根据fieldType将cellText转换为对应的C#对象 object value ConvertCellValue(cellText, fieldType, cell); rowData[fieldName] value; } // 跳过全空的行 if (!isEmptyRow) { dataList.Add(rowData); } } } return dataList; }实操心得worksheet.Dimension可能为null如果打开一个完全空的工作表。在使用前务必进行判空。cell.Text属性获取的是单元格显示的文本。对于公式单元格它返回的是公式计算后的结果。如果你需要原始公式请使用cell.Formula。性能方面如果表格非常大逐单元格读取cell.Text可能会比较慢。EPPlus提供了worksheet.Cells.Value来获取整个范围的值数组但对于包含合并单元格等复杂情况直接使用Value可能需要更复杂的处理。V1.0从简单可靠出发采用逐单元格读取。记得使用using语句包裹ExcelPackage确保文件句柄被正确释放。3.3 类型转换器ConvertCellValue的实现这是工具的核心逻辑之一负责将字符串的单元格内容按照我们约定的类型标记转换成C#中的强类型对象。private object ConvertCellValue(string cellText, string fieldType, ExcelRange cell) { if (string.IsNullOrEmpty(cellText)) { // 返回对应类型的默认值 return GetDefaultValue(fieldType); } try { switch (fieldType.ToLower()) { case int: case int32: if (int.TryParse(cellText, out int intVal)) return intVal; break; case float: case single: if (float.TryParse(cellText, out float floatVal)) return floatVal; break; case bool: // 支持1/0, true/false, 是/否等多种布尔表示 cellText cellText.Trim().ToLower(); if (cellText 1 || cellText true || cellText 是 || cellText yes) return true; if (cellText 0 || cellText false || cellText 否 || cellText no) return false; break; case string: return cellText; case int[]: // 假设单元格内容为 10,20,30 if (string.IsNullOrWhiteSpace(cellText)) return new int[0]; string[] parts cellText.Split(new char[] { ,, ;, }, StringSplitOptions.RemoveEmptyEntries); int[] array new int[parts.Length]; for (int i 0; i parts.Length; i) { if (int.TryParse(parts[i], out int elem)) array[i] elem; } return array; case string[]: // 同理 if (string.IsNullOrWhiteSpace(cellText)) return new string[0]; return cellText.Split(new char[] { ,, ; }, StringSplitOptions.RemoveEmptyEntries); // 可以继续扩展 long, double, Vector2, Vector3 等复杂类型 default: // 如果是不认识的类型先按字符串处理并输出警告日志 Debug.LogWarning($未知字段类型: {fieldType} 单元格值 {cellText} 将按字符串处理。); return cellText; } } catch (Exception e) { Debug.LogError($转换单元格值失败。位置{cell.Address} 类型{fieldType} 原始值{cellText}。错误{e.Message}); return GetDefaultValue(fieldType); } // 如果解析失败如TryParse返回false返回默认值并记录警告 Debug.LogWarning($无法将 {cellText} 转换为 {fieldType} 使用默认值。位置{cell.Address}); return GetDefaultValue(fieldType); } private object GetDefaultValue(string fieldType) { switch (fieldType.ToLower()) { case int: case int32: case int[]: return 0; case float: case single: return 0f; case bool: return false; case string: case string[]: return string.Empty; default: return null; } }提示这里的类型转换逻辑可以根据项目需求无限扩展。例如支持“Vector3(1,2,3)”这样的字符串转换为Unity的Vector3结构体或者支持枚举类型的转换如字段类型写“ClassType”单元格写“Warrior”工具自动映射到对应的枚举值。4. 实操过程与核心环节实现4.1 构建Unity编辑器界面为了让策划和测试同学也能方便使用我们创建一个简单的编辑器窗口。创建编辑器脚本在项目的Editor文件夹下如果没有就创建一个新建一个C#脚本例如ExcelToJsonToolWindow.cs。继承EditorWindow让这个类继承自UnityEditor.EditorWindow。添加菜单项使用[MenuItem(“Tools/Excel转Json工具”)]属性在Unity顶部菜单栏创建一个入口。绘制GUI在OnGUI方法中使用GUILayout或EditorGUILayout来绘制文件选择字段、按钮和日志输出区域。using UnityEditor; using UnityEngine; using System.IO; using System.Collections.Generic; public class ExcelToJsonToolWindow : EditorWindow { private string excelFilePath ; private string outputFolderPath Assets/GameData/Json/; private string logText ; private Vector2 scrollPos; [MenuItem(Tools/Excel转Json工具 V1.0)] public static void ShowWindow() { GetWindowExcelToJsonToolWindow(导表工具); } private void OnGUI() { GUILayout.Label(Excel转Json配置, EditorStyles.boldLabel); EditorGUILayout.Space(); // 1. Excel文件选择 EditorGUILayout.BeginHorizontal(); excelFilePath EditorGUILayout.TextField(Excel文件路径, excelFilePath); if (GUILayout.Button(浏览..., GUILayout.Width(60))) { string path EditorUtility.OpenFilePanel(选择Excel文件, , xlsx,xls); if (!string.IsNullOrEmpty(path)) { excelFilePath path; } } EditorGUILayout.EndHorizontal(); // 2. 输出文件夹选择 EditorGUILayout.BeginHorizontal(); outputFolderPath EditorGUILayout.TextField(Json输出文件夹, outputFolderPath); if (GUILayout.Button(浏览..., GUILayout.Width(60))) { string path EditorUtility.OpenFolderPanel(选择输出文件夹, , ); if (!string.IsNullOrEmpty(path)) { // 将绝对路径转换为相对于项目的路径 if (path.StartsWith(Application.dataPath)) { outputFolderPath Assets path.Substring(Application.dataPath.Length); } else { outputFolderPath path; } } } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); // 3. 导出按钮 if (GUILayout.Button(开始导出, GUILayout.Height(30))) { if (File.Exists(excelFilePath)) { logText $开始处理: {excelFilePath}\n; try { ExportExcelToJson(excelFilePath, outputFolderPath); logText 导出成功\n; AssetDatabase.Refresh(); // 刷新Unity资源数据库让新生成的Json文件显示出来 } catch (System.Exception e) { logText $导出失败: {e.Message}\n{e.StackTrace}; } } else { logText 错误Excel文件不存在\n; } } EditorGUILayout.Space(); GUILayout.Label(操作日志, EditorStyles.boldLabel); // 4. 日志显示区域 scrollPos EditorGUILayout.BeginScrollView(scrollPos, GUILayout.Height(200)); EditorGUILayout.TextArea(logText, GUILayout.ExpandHeight(true)); EditorGUILayout.EndScrollView(); } // 实际的导出逻辑调用前面写的ParseExcel方法 private void ExportExcelToJson(string excelPath, string outputDir) { // 1. 解析Excel var data ParseExcel(excelPath); // 这里调用上一节的解析方法 if (data null || data.Count 0) { logText 警告未解析到有效数据。\n; return; } // 2. 准备Json序列化选项美化输出 var jsonOptions new System.Text.Json.JsonSerializerOptions { WriteIndented true, // 缩进方便阅读 Encoder System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping // 避免中文等被转义 }; // 3. 序列化并写入文件 string jsonString System.Text.Json.JsonSerializer.Serialize(data, jsonOptions); string fileName Path.GetFileNameWithoutExtension(excelPath) .json; string outputPath Path.Combine(outputDir, fileName); // 确保输出目录存在 if (!Directory.Exists(outputDir)) { Directory.CreateDirectory(outputDir); } File.WriteAllText(outputPath, jsonString); logText $已生成: {outputPath}\n; } }这个窗口提供了最基本的功能选择文件、选择输出目录、执行导出、查看日志。界面虽然简陋但完全可用。4.2 生成Json文件与Unity资源集成导出生成的Json文件我们通常希望它被Unity作为TextAsset来管理。最简单的方法就是把它放在项目的Assets目录下的某个文件夹里如Assets/Resources/或Assets/StreamingAssets/或任何自定义的Assets/GameData/目录。输出路径处理如上例所示我们将输出文件夹设置为Assets下的一个路径。这样当导出完成后调用AssetDatabase.Refresh()Unity编辑器会自动识别新文件并将其作为TextAsset导入。在Unity中读取游戏运行时可以使用Resources.LoadTextAsset(“path/without/extension”)或System.IO.File.ReadAllText(Application.streamingAssetsPath “/fileName.json”)来读取Json文件内容然后用JsonUtility.FromJson或System.Text.Json.JsonSerializer.Deserialize反序列化成C#对象。// 假设我们有一个对应的数据类 [System.Serializable] public class ItemConfig { public int id; public string name; public int attackPower; } // 在游戏初始化时加载 public void LoadConfig() { TextAsset jsonFile Resources.LoadTextAsset(GameData/Json/ItemConfig); if (jsonFile ! null) { ListItemConfig itemList System.Text.Json.JsonSerializer.DeserializeListItemConfig(jsonFile.text); // 现在itemList里就是所有道具配置了 foreach (var item in itemList) { Debug.Log($道具: {item.id}, {item.name}, 攻击力: {item.attackPower}); } } }一个重要的进阶思考直接反序列化成ListDictionarystring, object虽然灵活但在运行时访问数据时是弱类型的row[“attackPower”]返回的是object需要强制转换。更专业的做法是导表工具同时生成对应的C#数据类文件。这属于V2.0的功能范畴但思路可以提前规划解析Excel表头时根据字段名和类型用字符串拼接或模板引擎如StringBuilder动态生成一个.cs文件里面定义了public int id {get; set;}这样的属性。这样运行时反序列化直接得到强类型对象列表效率和安全性都更高。5. 常见问题与排查技巧实录在实际开发和使用的过程中你肯定会遇到各种各样的问题。下面是我踩过的一些坑和对应的解决办法。5.1 问题排查清单问题现象可能原因排查步骤与解决方案打开工具窗口时报错提示找不到EPPlus等DLL。1. DLL未正确导入Unity。2. DLL与当前Unity的.NET版本不兼容。1. 确认EPPlus.dll及其依赖项如果有放在了项目的Assets/Plugins文件夹下。对于Unity 2020可能需要放在Assets/Plugins/的子文件夹中并设置正确的平台兼容性Inspector面板中。2. 尝试寻找为.NET Standard 2.0或2.1编译的EPPlus版本这与Unity的兼容性最好。导出时提示“文件正在被另一个进程使用”。1. Excel文件在Excel软件中处于打开状态。2. 上一次导出异常文件句柄未释放。1.关闭Excel软件。这是最常见的原因。2. 检查代码确保ExcelPackage对象在using语句中或者手动调用了Dispose()。生成的Json文件内容为空[]或者缺少数据。1. Excel文件路径错误实际打开的是空文件或错误文件。2. 表头行第1、2行定义不规范导致解析逻辑跳过所有列。3. 数据行全部被判定为空行例如所有单元格都是公式但显示为空。1. 在ParseExcel方法开始时打印出打开的文件名和Sheet名。2. 在读取表头后打印出fieldNameMap和fieldTypeMap检查是否成功读取。3. 在判断空行的逻辑处加调试日志查看cell.Text的实际值。对于公式单元格考虑使用cell.Value或cell.GetValueT()。数值字段在Json中变成了字符串如“attackPower”: “100”。类型转换失败最终按string类型处理了。1. 检查Excel中该单元格的格式是否是“文本”格式如果是即使输入100cell.Text返回的也是“100”字符串。可以尝试用cell.GetValueint()直接获取值。2. 在ConvertCellValue方法中为int.TryParse失败的情况添加更详细的日志打印出具体的单元格地址和原始字符串。数组字段如int[]解析错误报格式异常。1. 单元格内分隔符与代码中Split使用的字符不一致。2. 单元格中存在空格等不可见字符。1. 统一约定分隔符比如只用逗号,。在Split前先Trim()每个部分。2. 在代码中增加清洗逻辑cellText cellText.Replace(“”, “,”).Replace(“”, “;”).Trim();处理中文标点。导出后Unity编辑器不显示新的Json文件。1. 输出路径不在Assets目录下。2. 没有调用AssetDatabase.Refresh()。1. 确保outputFolderPath是以“Assets/...”开头的相对路径。2. 在文件写入操作File.WriteAllText后立即调用AssetDatabase.Refresh()。反序列化Json时C#类字段值与Json键名大小写不匹配。System.Text.Json默认区分大小写。在序列化/反序列化选项中设置PropertyNameCaseInsensitive true。或者确保C#类属性名与Json键名完全一致包括大小写。5.2 性能优化与稳定性心得关于大表格如果一张表有上万行逐单元格读取可能会有点慢。可以考虑的优化方向是使用EPPlus的worksheet.Cells.Value获取整个区域的对象数组object[,]进行批量处理但这需要对表头和数据区域有非常精确的定位。对于V1.0如果性能不是瓶颈建议先保持可读性更高的逐单元格逻辑。内存释放ExcelPackage和ExcelWorksheet对象占用内存可能较大特别是在处理多个大文件时。务必确保它们在using块中或者在使用完毕后显式调用Dispose()。错误处理与日志工具是给非程序员使用的友好的错误提示至关重要。不要只抛出异常要用Debug.LogError或logText将错误信息清晰地展示在界面上并尽可能指出是哪个Sheet、哪一行、哪一列出了问题。版本控制生成的Json文件建议纳入版本控制如Git。但要注意如果Json文件很大且频繁更改可能会影响仓库大小。一种做法是不将生成的Json入库而是将Excel源文件入库并在CI/CD流程中自动执行导表工具生成Json。对于小团队直接管理Json更简单。5.3 后续扩展方向V2.0展望当V1.0稳定运行后你可以考虑为它添加更多强大功能多Sheet与复杂关系支持一个Excel文件中多个Sheet并能定义Sheet之间的关联关系如主键-外键。数据校验在导出前检查数据的有效性如ID是否唯一、数值范围是否合理、引用是否存在等并生成错误报告。生成C#数据类自动生成与表格对应的、强类型的C#类文件彻底告别字典查找和类型转换。支持多种输出格式除了Json还可以支持导出为Unity的ScriptableObject、二进制文件用于热更、或Lua表等。批处理与自动化支持拖拽整个文件夹进行批量导出或者通过命令行调用方便集成到自动化构建流程中。开发自己的导表工具是一个典型的“磨刀不误砍柴工”的过程。初期投入的时间会在项目漫长的开发周期中通过无数次高效、准确的数据配置迭代加倍回报回来。这个V1.0版本就是你打造专属高效开发流水线的坚实第一步。