ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Unity编辑器扩展实战:5分钟构建自定义Excel数据导入工具

Unity编辑器扩展实战:5分钟构建自定义Excel数据导入工具 1. 项目概述为什么我们需要一个自定义的Excel工具窗口在Unity项目开发中尤其是涉及大量配置数据驱动的游戏如RPG、策略、模拟经营等策划同学最常用的工具就是Excel。数值平衡、关卡配置、道具属性、对话文本……这些海量数据最初都躺在Excel表格里。传统的流程是策划改表 - 导出为CSV/JSON - 程序写解析代码 - 导入Unity。这个过程繁琐、容易出错且每次微调都需要重复操作沟通成本极高。一个集成在Unity编辑器内的自定义Excel工具窗口就能彻底改变这个局面。它允许策划或开发者直接在Unity中打开、预览甚至编辑Excel数据并实时将结构化数据映射到游戏内的ScriptableObject或预制体上。这不仅仅是“读取Excel”而是打造一个连接数据生产端Excel与游戏运行时Unity的高效数据管道。想象一下策划调整了一个角色的攻击力数值你只需要在Unity里点一下“导入”按钮游戏内所有引用该角色的地方立即生效无需重启游戏甚至无需重新打包。这种效率提升对于追求快速迭代的项目来说是质的飞跃。我经历过太多因为数据配置错误导致的Bug排查到深夜的情况。一个字段名对不上、一个数据类型转换出错都可能导致游戏逻辑崩溃。自己动手做一个定制化的工具窗口不仅能固化数据导入规范还能加入数据校验、版本对比等高级功能把问题扼杀在摇篮里。今天我就带你用大约5分钟的核心骨架时间搭建一个属于你自己的、功能可无限扩展的Excel工具窗口。我们将从零开始涵盖窗口创建、Excel解析、UI绘制到数据应用的完整闭环并提供可直接复用的完整代码。2. 核心思路与架构设计在动手写代码之前我们先厘清核心思路。我们的目标不是做一个通用的、大而全的Excel编辑器那是Office的事而是做一个高度定制化、服务于特定游戏数据格式的导入工具。因此架构设计上要遵循“轻量、聚焦、可扩展”的原则。2.1 技术选型与依赖分析首先我们需要选择在Unity中读取Excel文件的方式。Unity本身并不直接支持.xlsx或.xls文件。主流方案有以下几种EPPlus、NPOI等.NET库功能强大能完整读写Excel文件但需要处理DLL兼容性在部分Unity版本或平台上可能遇到问题。第三方Unity Asset Store插件如ExcelDataReader、SimpleJSON封装较好开箱即用但可能需要付费且灵活性受插件限制。将Excel另存为CSV后读取这是最轻量、兼容性最好的方案。策划依然用Excel维护数据最终通过“另存为CSV”提供数据源。Unity内置的StreamReader可以轻松读取CSV。对于这个“5分钟实战”的目标我强烈推荐方案3CSV路径。理由如下零依赖无需引入任何第三方DLL或插件项目最干净。简单可靠CSV是纯文本格式解析逻辑简单不易出错。流程可控可以要求策划将最终版Excel“另存为CSV”到指定项目目录这本身就是一个简单的发布和版本管理步骤。性能好文本解析速度快。当然它的缺点是失去了Excel中原生的公式、多工作表、单元格格式等高级特性。但对于游戏配置数据导入这个核心场景我们99%的时间只需要纯数据。所以我们的架构将基于CSV解析来构建。2.2 工具窗口的基本结构一个标准的Unity编辑器工具窗口继承自EditorWindow。其核心生命周期和UI构建依赖于OnGUI()方法。我们的窗口结构规划如下顶部工具栏包含“选择CSV文件”、“导入”、“验证数据”、“保存为Asset”等按钮。数据预览区域以表格或列表形式展示从CSV解析出来的原始数据。数据映射与配置区域让用户定义CSV的每一列对应到游戏数据类如ItemData的哪个字段并选择数据类型int, float, string等。生成结果预览区域展示即将生成的ScriptableObject数组或Prefab列表。日志/状态输出区域显示导入过程中的操作日志、错误或警告信息。整个数据流是选择CSV - 解析为原始字符串矩阵 - 配置映射关系 - 转换为强类型对象列表 - 序列化为Unity资产。2.3 数据流向设计清晰的数据流向是工具稳定的关键[策划的Excel] --(另存为)-- [项目内的CSV文件] | v [工具窗口] --(读取/解析)-- [ListListstring原始数据] | v [用户配置映射] --(转换)-- [ListT 强类型数据列表] (T如: ItemData) | v [序列化] -- [ScriptableObject数组或Prefab] -- [被游戏逻辑使用]这个流程中最关键的转换步骤发生在“用户配置映射”到“强类型数据列表”之间。我们需要一个灵活且类型安全的反射或代码生成机制来处理它。3. 实战第一步创建自定义编辑器窗口骨架让我们打开Unity创建一个新的C#脚本开始搭建窗口的骨架。这是最快能见到效果的一步。3.1 创建基础窗口类在项目的Editor文件夹下如果没有就创建一个这是Unity的约定Editor文件夹下的代码不会被打进游戏包体新建一个C#脚本命名为ExcelDataImporterWindow.cs。using UnityEngine; using UnityEditor; using System.Collections.Generic; // 注意暂时不需要引入Excel相关库 public class ExcelDataImporterWindow : EditorWindow { // 单例模式方便从菜单栏打开 private static ExcelDataImporterWindow window; // 添加一个菜单项 [MenuItem(Tools/Excel数据导入工具)] public static void ShowWindow() { window GetWindowExcelDataImporterWindow(); window.titleContent new GUIContent(Excel导入工具); window.Show(); } // 窗口的核心UI绘制方法 private void OnGUI() { GUILayout.Label(Excel/CSV 数据导入工具, EditorStyles.boldLabel); EditorGUILayout.Space(); // 1. 文件选择区域 DrawFileSelectionArea(); // 2. 数据预览区域 (当有数据时显示) if (rawData ! null rawData.Count 0) { DrawDataPreviewArea(); } // 3. 映射配置区域 (当有数据且未配置时显示) if (rawData ! null rawData.Count 1) // 假设第一行是表头 { DrawMappingConfigurationArea(); } // 4. 操作按钮区域 DrawActionButtonsArea(); } // 临时变量用于存储选中的文件路径和解析的原始数据 private string csvFilePath ; private ListListstring rawData null; // 二维列表存储行和列 private void DrawFileSelectionArea() { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField(CSV文件路径:, csvFilePath, EditorStyles.wordWrappedLabel); if (GUILayout.Button(浏览..., GUILayout.Width(60))) { // 弹出系统文件选择框 string path EditorUtility.OpenFilePanel(选择CSV文件, Application.dataPath, csv); if (!string.IsNullOrEmpty(path)) { csvFilePath path; // 选择新文件后清空旧数据 rawData null; // 可以在这里自动触发一次解析为了简单我们先不自动解析 } } EditorGUILayout.EndHorizontal(); } private void DrawDataPreviewArea() { /* 稍后实现 */ } private void DrawMappingConfigurationArea() { /* 稍后实现 */ } private void DrawActionButtonsArea() { /* 稍后实现 */ } }保存脚本后回到Unity编辑器顶部菜单栏就会出现Tools - Excel数据导入工具。点击它一个空的工具窗口就弹出来了虽然现在除了一个标题和一个浏览按钮什么都做不了但骨架已经搭好。这里有个实操心得EditorWindow的OnGUI方法每帧都会调用类似于游戏对象的Update所以不要在OnGUI里执行耗时的操作如解析大文件否则会卡住编辑器界面。文件解析这类操作最好在按钮点击的事件响应里做。3.2 解析CSV数据接下来我们实现DrawActionButtonsArea中的一个“解析”按钮并填充DrawDataPreviewArea来展示数据。CSV解析的核心在于处理引号和逗号。这里我们实现一个简单但够用的解析器。private void DrawActionButtonsArea() { EditorGUILayout.BeginHorizontal(); if (GUILayout.Button(解析CSV)) { if (string.IsNullOrEmpty(csvFilePath)) { EditorUtility.DisplayDialog(错误, 请先选择CSV文件, 确定); return; } ParseCSVFile(csvFilePath); } // 后续可以在这里添加“生成数据”、“保存”等按钮 EditorGUILayout.EndHorizontal(); } private void ParseCSVFile(string filePath) { if (!System.IO.File.Exists(filePath)) { Debug.LogError($文件不存在: {filePath}); return; } rawData new ListListstring(); try { // 使用StreamReader读取文件 using (var reader new System.IO.StreamReader(filePath)) { while (!reader.EndOfStream) { string line reader.ReadLine(); if (string.IsNullOrWhiteSpace(line)) continue; Liststring row ParseCSVLine(line); rawData.Add(row); } } Debug.Log($CSV解析成功共{rawData.Count}行数据。); // 强制重绘窗口以显示预览区域 Repaint(); } catch (System.Exception e) { Debug.LogError($解析CSV文件时出错: {e.Message}); rawData null; } } // 一个简单的CSV行解析器处理带引号的字段 private Liststring ParseCSVLine(string line) { Liststring result new Liststring(); bool inQuotes false; System.Text.StringBuilder currentField new System.Text.StringBuilder(); for (int i 0; i line.Length; i) { char c line[i]; if (c \) // 遇到引号 { // 判断是否是转义的引号即两个连续的引号 if (inQuotes i 1 line.Length line[i 1] \) { currentField.Append(\); i; // 跳过下一个引号 } else { inQuotes !inQuotes; // 切换引号状态 } } else if (c , !inQuotes) // 遇到逗号且不在引号内表示一个字段结束 { result.Add(currentField.ToString()); currentField.Clear(); } else { currentField.Append(c); } } // 添加最后一个字段 result.Add(currentField.ToString()); return result; }现在我们实现DrawDataPreviewArea用一个ScrollView来展示原始数据这对于检查CSV格式是否正确非常有用。private Vector2 dataPreviewScrollPos; private void DrawDataPreviewArea() { EditorGUILayout.Space(); GUILayout.Label(原始数据预览:, EditorStyles.boldLabel); dataPreviewScrollPos EditorGUILayout.BeginScrollView(dataPreviewScrollPos, GUILayout.Height(200)); if (rawData ! null) { for (int i 0; i rawData.Count; i) { EditorGUILayout.BeginHorizontal(); GUILayout.Label($行{i}:, GUILayout.Width(40)); for (int j 0; j rawData[i].Count; j) { // 显示每个单元格的内容第一行表头可以高亮显示 var style (i 0) ? EditorStyles.boldLabel : EditorStyles.label; EditorGUILayout.LabelField(rawData[i][j], style, GUILayout.MinWidth(80)); } EditorGUILayout.EndHorizontal(); } } EditorGUILayout.EndScrollView(); }至此你已经拥有了一个能选择CSV文件并展示其内容的基础工具窗口。选择你的测试CSV文件点击“解析CSV”数据就会以行列形式展示出来。注意事项这个简易解析器假设CSV是用逗号分隔的。如果你的数据里包含逗号必须用双引号将整个字段括起来这是标准CSV格式。例如Hello, World, 123。4. 核心难点动态数据映射与类型转换这是整个工具最核心、也最具挑战性的部分。我们需要让用户指定CSV的每一列对应表头映射到哪个C#类的哪个属性并且要进行正确的类型转换string - int/float/bool等。4.1 定义数据模型与映射配置首先我们定义一个简单的数据模型例如一个ItemData类它最终可以生成ScriptableObject。// 这是一个运行时可能用到的数据类放在非Editor文件夹下 [System.Serializable] public class ItemData { public int id; public string name; public string description; public float price; public bool isConsumable; }然后在编辑器工具中我们需要一个类来保存“映射配置”。这个配置需要知道CSV的列索引、目标字段名、目标字段类型。// 在ExcelDataImporterWindow类内部或单独定义 [System.Serializable] public class ColumnMapping { public string csvHeaderName; // CSV表头名称 public string targetFieldName; // 目标类中的字段名 public System.Type fieldType; // 字段类型如int, string, float // 注意System.Type无法直接序列化我们需要一个变通方法比如存类型名称字符串 public string fieldTypeName; } // 在ExcelDataImporterWindow中 private ListColumnMapping columnMappings new ListColumnMapping(); private System.Type selectedTargetType typeof(ItemData); // 默认目标类型4.2 构建映射配置界面我们需要一个界面让用户为CSV的每一列通常是第一行创建映射。DrawMappingConfigurationArea方法将变得复杂一些。private Vector2 mappingScrollPos; private void DrawMappingConfigurationArea() { EditorGUILayout.Space(); GUILayout.Label(数据映射配置:, EditorStyles.boldLabel); // 1. 选择目标数据类型可以通过反射获取所有符合条件的类 EditorGUILayout.BeginHorizontal(); GUILayout.Label(目标数据类:, GUILayout.Width(80)); // 这里简化处理手动输入类名。更高级的做法是下拉菜单选择程序集中所有[Serializable]的类。 string typeName EditorGUILayout.TextField(selectedTargetType.FullName); // 尝试根据输入更新类型 if (GUILayout.Button(刷新类型)) { System.Type newType System.Type.GetType(typeName); if (newType ! null) selectedTargetType newType; } EditorGUILayout.EndHorizontal(); // 2. 自动生成映射建议基于表头名称和目标类字段名的模糊匹配 if (GUILayout.Button(智能匹配映射)) { AutoGenerateMappings(); } // 3. 显示和编辑每一列的映射 mappingScrollPos EditorGUILayout.BeginScrollView(mappingScrollPos, GUILayout.Height(250)); if (rawData.Count 0) { Liststring headers rawData[0]; // 第一行是表头 for (int i 0; i headers.Count; i) { EditorGUILayout.BeginHorizontal(); // 显示CSV列名 EditorGUILayout.LabelField($[{i}] {headers[i]}, GUILayout.Width(120)); // 查找或创建该列的映射配置 ColumnMapping mapping columnMappings.Find(m m.csvHeaderName headers[i]); bool isNewMapping mapping null; if (isNewMapping) { mapping new ColumnMapping { csvHeaderName headers[i] }; } // 下拉菜单选择目标字段 mapping.targetFieldName EditorGUILayout.TextField(mapping.targetFieldName, GUILayout.Width(100)); // 下拉菜单选择字段类型 string[] typeOptions new string[] { string, int, float, bool }; int currentTypeIndex System.Array.IndexOf(typeOptions, mapping.fieldTypeName ?? string); int newTypeIndex EditorGUILayout.Popup(currentTypeIndex, typeOptions, GUILayout.Width(80)); mapping.fieldTypeName typeOptions[newTypeIndex]; if (isNewMapping !string.IsNullOrEmpty(mapping.targetFieldName)) { columnMappings.Add(mapping); } // 可以添加一个“删除映射”按钮 if (GUILayout.Button(X, GUILayout.Width(20))) { columnMappings.Remove(mapping); } EditorGUILayout.EndHorizontal(); } } EditorGUILayout.EndScrollView(); } private void AutoGenerateMappings() { if (rawData.Count 0 || selectedTargetType null) return; columnMappings.Clear(); var fields selectedTargetType.GetFields(); // 获取公共字段 Liststring headers rawData[0]; foreach (var header in headers) { // 简单的大小写不敏感匹配 var matchedField System.Array.Find(fields, f f.Name.ToLower() header.ToLower().Replace( , )); if (matchedField ! null) { columnMappings.Add(new ColumnMapping { csvHeaderName header, targetFieldName matchedField.Name, fieldTypeName matchedField.FieldType.Name.ToLower() // 如 int32 - int }); } } Debug.Log($智能匹配生成了{columnMappings.Count}条映射。); }这个界面允许用户为每一列手动指定目标字段和类型并提供了一个“智能匹配”按钮来根据名称自动匹配。实操心得在实际项目中字段名和CSV表头名往往不完全一致比如CSV用“ID”代码用“id”所以智能匹配逻辑可以做得更复杂比如支持同义词映射表“ItemID” - “id”, “Price” - “price”这能极大提升策划的体验。4.3 执行数据转换配置好映射后我们需要一个“生成数据”按钮来执行最终的转换。这是类型安全和错误处理的关键环节。private Listobject convertedDataList new Listobject(); // 存储转换后的对象 private void DrawActionButtonsArea() { EditorGUILayout.BeginHorizontal(); // ... 之前的解析按钮 ... if (GUILayout.Button(生成数据)) { if (rawData null || rawData.Count 2) { EditorUtility.DisplayDialog(错误, 请先解析包含表头和数据行的CSV文件, 确定); return; } if (columnMappings.Count 0) { EditorUtility.DisplayDialog(警告, 未配置任何数据映射将无法生成数据。, 确定); return; } ConvertData(); } if (GUILayout.Button(保存为ScriptableObject)) { SaveAsScriptableObject(); } EditorGUILayout.EndHorizontal(); } private void ConvertData() { convertedDataList.Clear(); Liststring headers rawData[0]; // 从第二行开始是数据行 for (int rowIndex 1; rowIndex rawData.Count; rowIndex) { var row rawData[rowIndex]; // 动态创建目标类型的实例 object dataItem System.Activator.CreateInstance(selectedTargetType); bool rowHasError false; for (int colIndex 0; colIndex Mathf.Min(row.Count, headers.Count); colIndex) { string header headers[colIndex]; string cellValue row[colIndex]; // 找到该列对应的映射配置 ColumnMapping mapping columnMappings.Find(m m.csvHeaderName header); if (mapping null || string.IsNullOrEmpty(mapping.targetFieldName)) { continue; // 该列未映射跳过 } // 通过反射找到目标字段 var fieldInfo selectedTargetType.GetField(mapping.targetFieldName); if (fieldInfo null) { Debug.LogWarning($第{rowIndex1}行列{header}目标类中未找到字段 {mapping.targetFieldName}); continue; } // 类型转换并赋值 try { object convertedValue ConvertStringToType(cellValue, mapping.fieldTypeName, fieldInfo.FieldType); fieldInfo.SetValue(dataItem, convertedValue); } catch (System.Exception e) { Debug.LogError($第{rowIndex1}行列{header}值:{cellValue}转换失败: {e.Message}); rowHasError true; } } if (!rowHasError) { convertedDataList.Add(dataItem); } } Debug.Log($数据转换完成成功{convertedDataList.Count}条失败{rawData.Count - 1 - convertedDataList.Count}条。); // 转换完成后可以刷新一个结果预览区域 } // 核心转换函数 private object ConvertStringToType(string str, string typeName, System.Type targetType) { if (string.IsNullOrEmpty(str)) str ; switch (typeName.ToLower()) { case string: return str; case int: if (int.TryParse(str, out int intVal)) return intVal; throw new System.FormatException($无法将 {str} 转换为整数。); case float: if (float.TryParse(str, out float floatVal)) return floatVal; throw new System.FormatException($无法将 {str} 转换为浮点数。); case bool: // 支持1/0, true/false, 是/否等多种布尔表示 str str.ToLower().Trim(); if (str 1 || str true || str 是 || str yes || str y) return true; if (str 0 || str false || str 否 || str no || str n) return false; throw new System.FormatException($无法将 {str} 转换为布尔值。); default: // 尝试其他基础类型或枚举 if (targetType.IsEnum) { try { return System.Enum.Parse(targetType, str); } catch { throw new System.FormatException($无法将 {str} 转换为枚举 {targetType.Name}。); } } // 更复杂的类型如Vector3可以在这里扩展 throw new System.NotSupportedException($不支持的类型转换: {typeName}); } }这段代码是工具的心脏。它遍历每一行数据根据映射配置通过反射将字符串值转换为目标类型并赋值给新创建的对象实例。注意事项反射在编辑器工具中使用是可以接受的但如果转换的数据量极大数万行可能需要考虑性能优化比如为常用类型int, float, string写死转换方法或者使用System.ComponentModel.TypeDescriptor或FastMember这类库。对于常规的策划配置表几百上千行这个性能完全足够。5. 数据落地生成Unity资产转换后的数据还在内存里我们需要将其持久化为Unity可以使用的资产最常用的就是ScriptableObject。5.1 创建或更新ScriptableObject假设我们为ItemData准备一个容器ItemDatabase它继承自ScriptableObject并包含一个ItemData列表。// 运行时脚本 public class ItemDatabase : ScriptableObject { public ListItemData items new ListItemData(); }在工具窗口中我们实现SaveAsScriptableObject方法。private void SaveAsScriptableObject() { if (convertedDataList.Count 0) { EditorUtility.DisplayDialog(错误, 没有已转换的数据可供保存。请先点击‘生成数据’。, 确定); return; } // 让用户选择保存路径和文件名 string defaultName System.IO.Path.GetFileNameWithoutExtension(csvFilePath) _Database; string path EditorUtility.SaveFilePanelInProject(保存数据资产, defaultName, asset, 请选择保存ScriptableObject的路径); if (string.IsNullOrEmpty(path)) return; // 创建或加载现有的数据库 ItemDatabase database AssetDatabase.LoadAssetAtPathItemDatabase(path); bool isNew database null; if (isNew) { database ScriptableObject.CreateInstanceItemDatabase(); } else { // 如果是覆盖清空旧数据 database.items.Clear(); } // 将转换后的数据添加到数据库 foreach (var obj in convertedDataList) { // 注意convertedDataList里存储的是object需要转换为ItemData // 因为我们目前只支持一种类型所以直接转换。更通用的做法需要类型判断。 if (obj is ItemData item) { database.items.Add(item); } } // 保存资产 if (isNew) { AssetDatabase.CreateAsset(database, path); } else { EditorUtility.SetDirty(database); // 标记为已修改 } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($数据已保存至: {path}); EditorUtility.DisplayDialog(成功, $数据资产已保存\n路径: {path}, 确定); }现在整个流程就打通了选择CSV - 解析 - 配置映射 - 转换 - 保存为Asset。你可以在Project窗口中双击生成的.asset文件在Inspector里查看导入的数据。5.2 扩展支持多种数据模型和导出方式一个强大的工具不应该只支持一种数据类型。我们可以通过反射枚举所有符合条件的类比如带有[System.Serializable]特性的类让用户在工具窗口的下拉菜单中选择。导出方式也可以多样化单个ScriptableObject列表如上所示。多个独立的ScriptableObject资产每一行数据生成一个.asset文件适用于角色、物品等独立配置。生成Prefab将数据附加到GameObject上然后保存为Prefab。这对于需要关联模型、音效等复杂游戏对象的数据非常有用。直接生成C#代码或JSON为热更新或服务器通信准备数据。实现这些扩展核心在于将selectedTargetType和生成逻辑抽象出来根据用户的选择调用不同的“生成器”(Generator)类。这会让工具代码量增加但架构会清晰得多。6. 打磨体验错误处理、批量操作与效率技巧一个专业的工具稳定性和用户体验至关重要。下面分享几个提升工具可靠性和效率的实战技巧。6.1 健壮的错误处理与数据验证在ConvertStringToType函数中我们进行了基本的类型转换错误处理。但数据验证可以更前置、更丰富表头校验解析CSV后立即检查表头是否包含空值或重复值并给出明确警告。空行/空列处理在解析或转换时跳过完全为空的行或列避免生成无效数据。数据范围校验在映射配置中可以为数值类型字段添加范围约束如int字段要求值在1-100之间。在转换过程中进行校验将超出范围的行记录为错误。唯一性校验对于ID类字段检查转换后的数据列表中是否有重复值。引用校验如果某字段的值需要引用另一个配置表如itemId需要存在于ItemTable中可以在这里进行关联性检查。可以在工具窗口中增加一个“验证数据”按钮专门执行这些校验并生成一份详细的报告用不同颜色如红色错误、黄色警告在日志区域显示。6.2 支持批量导入与自动化策划可能有几十张配置表。我们可以扩展工具支持批量处理监视文件夹指定一个Resources/Config文件夹工具自动监视该文件夹下的所有CSV文件变化通过FileSystemWatcher或定时检查一旦有文件更新自动触发导入流程。批处理模式创建一个Editor脚本通过命令行或菜单项一次性导入所有配置表这在打包前进行资源构建时非常有用。预设映射配置将columnMappings序列化为JSON或ScriptableObject保存下来。对于同一张表结构下次打开工具时可以直接加载预设无需重新配置。6.3 编辑器性能优化当数据量很大时在OnGUI中绘制所有数据预览会非常卡顿。虚拟列表只绘制可视区域内的行这是大型列表控件的标准优化方案。Unity的EditorGUILayout没有原生支持但可以基于GUI系统手动计算位置实现或者使用第三方编辑器插件如Odin Inspector的表格功能。分页显示在预览区域实现简单的上一页/下一页按钮。异步操作将文件解析、数据转换等耗时操作放在后台线程通过Task.Run或ThreadPool然后在主线程更新UI。注意Unity的API如AssetDatabase必须在主线程调用需要妥善处理线程间通信。进度条对于批量操作使用EditorUtility.DisplayProgressBar显示进度提升用户体验。7. 完整代码整合与使用指南由于篇幅限制无法将超过500行的完整代码全部贴出但以上部分已经构成了工具的所有核心模块。我将提供一个整合后的简化版入口代码框架并说明如何组织你的项目。项目文件结构建议Assets/ ├── Editor/ │ └── ExcelDataImporterWindow.cs (主窗口) │ └── CSVUtility.cs (CSV解析工具类) │ └── DataMapper.cs (数据映射与转换核心类) │ └── ScriptableObjectGenerator.cs (资产生成器) ├── Scripts/ │ └── DataModels/ (数据模型) │ └── ItemData.cs │ └── SkillData.cs │ └── ItemDatabase.cs │ └── Runtime/ (游戏运行时逻辑) └── Resources/ └── Config/ (存放CSV文件) └── ItemTable.csv └── SkillTable.csv简化版ExcelDataImporterWindow.cs整合框架using UnityEngine; using UnityEditor; using System.Collections.Generic; public class ExcelDataImporterWindow : EditorWindow { #region 变量与属性 private string csvFilePath ; private ListListstring rawData; private ListColumnMapping columnMappings new ListColumnMapping(); private System.Type selectedTargetType typeof(ItemData); // 需引用你的数据模型 private Listobject convertedDataList new Listobject(); private Vector2 previewScroll, mappingScroll; #endregion [MenuItem(Tools/Excel数据导入工具)] public static void ShowWindow() { /* 同上 */ } private void OnGUI() { DrawHeader(); DrawFileSelection(); if (rawData ! null rawData.Count 0) DrawDataPreview(); if (rawData ! null rawData.Count 1) DrawMappingConfig(); DrawActionButtons(); } // 将之前章节的各个Draw方法实现放在这里 private void DrawHeader() { /* ... */ } private void DrawFileSelection() { /* ... */ } private void DrawDataPreview() { /* ... */ } private void DrawMappingConfig() { /* ... */ } private void DrawActionButtons() { /* ... */ } // 核心功能方法 private void ParseCSVFile(string path) { /* 调用CSVUtility.Parse */ } private void AutoGenerateMappings() { /* ... */ } private void ConvertData() { /* 调用DataMapper.Convert */ } private void SaveAsScriptableObject() { /* 调用ScriptableObjectGenerator.CreateAsset */ } // 辅助类定义 [System.Serializable] private class ColumnMapping { /* ... */ } }使用步骤将上述代码模块分别创建到Editor文件夹下的对应文件中。在Scripts/DataModels/下定义你的数据类如ItemData和数据库类如ItemDatabase。将策划提供的Excel文件另存为CSV格式放入Resources/Config/或任何你喜欢的目录。在Unity中打开Tools - Excel数据导入工具。点击“浏览”选择你的CSV文件点击“解析CSV”。在“数据映射配置”区域检查并调整每一列对应的字段名和类型。可以点击“智能匹配映射”快速匹配。点击“生成数据”在Console窗口查看转换日志。点击“保存为ScriptableObject”选择保存路径生成.asset文件。在游戏脚本中通过Resources.LoadItemDatabase(路径)加载并使用这些数据。这个工具窗口从零到一搭建完成它不仅是一个简单的数据导入器更是一个可扩展的编辑器扩展框架。你可以基于它轻松定制出符合自己项目需求的、各种强大的数据管理工具将策划、美术和程序的工作流无缝衔接起来真正实现“5分钟搞定”数据配置的承诺。
返回列表