Unity集成EPPlus实现Excel数据读取:完整流程与避坑指南

Unity集成EPPlus实现Excel数据读取:完整流程与避坑指南 1. 项目概述为什么Unity开发者需要Epplus在Unity项目开发中尤其是涉及游戏配置、数值平衡、关卡设计或本地化文本管理时我们常常需要处理大量的结构化数据。这些数据如果直接硬编码在脚本里不仅难以维护策划或运营同学每次调整一个数值都需要程序员重新编译打包协作效率极低。因此将数据存储在外部文件如Excel、CSV、JSON中运行时动态读取成为了一个标准且高效的工作流。在众多选择中Excel因其强大的表格编辑、公式计算和可视化能力成为策划和运营人员最青睐的数据编辑工具。然而Unity原生并不直接支持读取.xlsx或.xls格式的Excel文件。这时我们就需要借助第三方库来充当“翻译官”而Epplus正是.NET生态中一个非常成熟、高效且免费对于非商业用途或特定许可的Excel读写库。简单来说这个项目的目标就是在Unity一个基于.NET/Mono环境的游戏引擎中集成并使用Epplus库实现从Excel文件到游戏内数据结构如List、Dictionary或自定义类的顺畅转换并解决在这个过程中几乎必然会遇到的各种“坑”。这不仅仅是调用几个API那么简单它涉及到Unity特殊的运行时环境、平台差异、依赖管理以及性能优化等一系列问题。接下来我将结合自己多次在项目中的实战经验为你拆解完整流程和所有关键细节。2. 核心思路与方案选型为什么是Epplus面对“Unity读取Excel”这个需求市面上常见的方案有好几种我们需要做一个清晰的对比才能理解为什么Epplus是综合最优选。2.1 主流方案横向对比方案原理/工具优点缺点适用场景CSV文件使用StreamReader或TextAsset读取纯文本按逗号分割。轻量无需额外依赖跨平台兼容性极佳读取速度快。无法处理多工作表、单元格格式、公式数据中包含逗号时需转义易出错编辑体验不如Excel友好。数据结构极其简单策划接受纯文本编辑或对包体大小极其敏感的项目。JSON/XML将Excel另存为或导出为JSON/XML格式Unity使用JsonUtility或XmlSerializer解析。结构清晰标准序列化方案与Unity结合好可读性较强。需要额外的导出步骤无法直接编辑Excel源文件数据量巨大时JSON文件可能臃肿。数据驱动UI网络数据传输或工具链完善有自动导出流程的项目。Unity官方 Asset如ScriptableObject配合自定义编辑器工具手动填写。完全Unity原生性能好类型安全便于版本管理。创建和编辑大量数据非常繁琐对非技术人员极不友好难以做复杂的批量操作。小型项目或仅程序员维护的少量核心配置数据。第三方 .NET Excel库 (Epplus, NPOI)在Unity中引入DLL直接调用API读写.xlsx。直接操作源文件策划可独立工作功能强大支持公式、样式、多工作表等一次集成长期受益。需要管理依赖在部分平台如WebGL、部分移动端可能有兼容性问题需处理可能的许可问题。中大型商业项目策划与程序分工明确需要复杂数据编辑和管理的场景。2.2 为什么最终选择Epplus在第三方库中Epplus和NPOI是最著名的两个。NPOI是Apache项目功能全面但相对庞大API设计略显陈旧。Epplus则以其优雅流畅的LINQ风格API、卓越的性能尤其在读写.xlsx时以及对Open XML标准的纯粹支持而备受青睐。对于Unity项目Epplus的DLL体积相对可控且其面向对象的操作方式将工作表、行、单元格视为对象更符合C#程序员的思维习惯。更重要的是Epplus处理由现代Excel2007及以上版本生成的.xlsx文件效率非常高因为.xlsx本质上是一个ZIP压缩包内含一系列XML文件。Epplus直接基于Open XML SDK构建省去了大量中间转换开销。因此对于追求开发效率、需要与策划紧密协作的Unity团队Epplus通常是首选。3. 环境准备与核心依赖管理在开始写代码之前正确的环境搭建是成功的一半。这一步的疏忽会导致后面各种诡异的错误。3.1 获取Epplus库文件Epplus并非Unity官方包我们需要手动获取其编译好的动态链接库DLL。官方途径推荐访问Epplus在 GitHub 的发布页面下载最新的稳定版如EPPlus.xxx.zip。解压后在bin文件夹中找到.NET Framework 4.5或.NET Standard 2.0版本的EPPlus.dll。对于Unity 2018及以上版本默认使用.NET 4.x等效或.NET Standard 2.1.NET Standard 2.0版本的兼容性最好。NuGet包需转换如果你熟悉NuGet可以通过Install-Package EPPlus安装。安装后在项目的packages目录下找到DLL。但直接使用NuGet包可能需要处理更多依赖。注意务必确认你下载的Epplus版本是免费许可的EPPlus 5.x及以后版本对于非商业用途或在符合Polyform Noncommercial License 1.0.0许可下的商业用途是免费的。商业项目务必仔细阅读其许可协议。3.2 在Unity项目中引入DLLUnity管理外部DLL通常有两种方式推荐第一种直接放入Plugins文件夹在Unity项目的Assets目录下创建Plugins文件夹如果不存在。将下载好的EPPlus.dll文件复制到Assets/Plugins中。关键步骤选中这个DLL文件在Unity Inspector面板中确保其**“Platforms”设置正确。通常你需要取消勾选“Any Platform”然后根据你的目标平台单独勾选。例如如果只用于Windows/Mac/Linux的PC端或编辑器下就只勾选“Editor”和“Standalone”**。务必不要勾选“WSAPlayer”UWP、“WebGL”、“iOS”、“Android”等除非你已确认该平台兼容并经过测试。这是因为Epplus依赖完整的.NET API在部分受限平台可能无法运行。同时检查**“API Compatibility Level”**。确保其与你项目的设置匹配项目设置 - Player - Other Settings - Configuration - Api Compatibility Level。.NET Standard 2.0或.NET 4.x通常更安全。使用Assembly Definition (asmdef)引用如果你的项目结构复杂使用了多个程序集定义文件来模块化管理代码你可以在需要用到Epplus的程序集的.asmdef文件中在“Assembly Definition References”里添加对EPPlus程序集的引用。前提是DLL已按方式1放入项目并被Unity识别。3.3 处理可能的额外依赖Epplus 5 版本对.NET Standard 2.0的支持已经很好了通常不需要额外DLL。但如果你遇到关于System.Drawing或System.Text.Encoding的缺失错误可能需要引入Microsoft.Windows.Compatibility包同样以DLL形式放入Plugins或者确保你的Unity版本足够新如2021 LTS或更新它们内置了更完整的.NET支持。4. 基础读取流程与代码实战环境准备好后我们来编写最核心的读取代码。我将以一个典型的游戏道具配置表为例展示从Excel到C#对象的完整过程。假设我们有一个Items.xlsx文件其中Sheet1工作表结构如下ID整数Name字符串Type字符串AttackPower整数Price浮点数1001铁剑Weapon15120.51002治疗药水Consumable025.0我们的目标是将其读取到一个ListItemConfig中。4.1 定义数据模型C#类首先定义与表格列对应的数据类。这是面向对象操作的基础。// ItemConfig.cs [System.Serializable] // 可选方便在Inspector中查看 public class ItemConfig { public int ID; public string Name; public string Type; public int AttackPower; public float Price; // 可以添加一个方法用于从Excel行初始化对象 public void FillFromExcelRow(ExcelRange row) { // 假设列顺序固定AID, BName, CType, DAttackPower, EPrice this.ID Convert.ToInt32(row[1, 1].Value); // 第一行第一列是标题数据从第二行开始 this.Name row[1, 2].Value?.ToString(); this.Type row[1, 3].Value?.ToString(); this.AttackPower Convert.ToInt32(row[1, 4].Value); this.Price Convert.ToSingle(row[1, 5].Value); } }4.2 核心读取方法与步骤解析接下来创建一个ExcelReader工具类封装读取逻辑。// ExcelReader.cs using System.Collections.Generic; using System.IO; using OfficeOpenXml; // Epplus的命名空间 using UnityEngine; public static class ExcelReader { // 设置Epplus的许可上下文对于免费版非商业用途需设置 static ExcelReader() { ExcelPackage.LicenseContext LicenseContext.NonCommercial; // 如果是非商业用途 // 商业用途请参考官网获取商业许可并设置LicenseContext.Commercial } /// summary /// 从Stream读取Excel并转换为ItemConfig列表 /// /summary /// param namefileStreamExcel文件流/param /// param nameworksheetName工作表名默认为第一个/param /// param namehasHeader是否包含标题行/param /// returns/returns public static ListItemConfig ReadItemsFromExcel(Stream fileStream, string worksheetName null, bool hasHeader true) { ListItemConfig itemList new ListItemConfig(); // 1. 使用using语句确保资源释放 using (ExcelPackage package new ExcelPackage(fileStream)) { // 2. 获取工作表 ExcelWorksheet worksheet; if (string.IsNullOrEmpty(worksheetName)) { worksheet package.Workbook.Worksheets[0]; // 第一个工作表 } else { worksheet package.Workbook.Worksheets[worksheetName]; } if (worksheet null) { Debug.LogError($未找到工作表: {worksheetName}); return itemList; } // 3. 确定数据起始行 int startRow hasHeader ? 2 : 1; // 有标题则从第2行开始读数据 int rowCount worksheet.Dimension.Rows; // 总行数 int colCount worksheet.Dimension.Columns; // 总列数 // 4. 遍历每一行数据 for (int row startRow; row rowCount; row) { // 检查整行是否为空可选优化 if (IsRowEmpty(worksheet, row, colCount)) { continue; } // 5. 创建数据对象并填充 ItemConfig item new ItemConfig(); // 方法一通过列索引直接读取效率高但需知列顺序 item.ID GetCellValueint(worksheet.Cells[row, 1]); item.Name GetCellValuestring(worksheet.Cells[row, 2]); item.Type GetCellValuestring(worksheet.Cells[row, 3]); item.AttackPower GetCellValueint(worksheet.Cells[row, 4]); item.Price GetCellValuefloat(worksheet.Cells[row, 5]); // 方法二通过列名读取更灵活但需额外映射逻辑 // 例如可以先读取第一行标题建立“列名-列索引”的字典 itemList.Add(item); } } // using结束package.Dispose()会自动调用释放资源 return itemList; } /// summary /// 泛型方法安全地获取单元格值并进行类型转换 /// /summary private static T GetCellValueT(ExcelRange cell) { object cellValue cell.Value; if (cellValue null) { return default(T); // 返回类型的默认值如int为0string为null } try { return (T)Convert.ChangeType(cellValue, typeof(T)); } catch (InvalidCastException) { Debug.LogWarning($单元格[{cell.Address}]类型转换失败。值{cellValue}目标类型{typeof(T).Name}); return default(T); } } /// summary /// 判断一行是否为空 /// /summary private static bool IsRowEmpty(ExcelWorksheet ws, int row, int colCount) { for (int col 1; col colCount; col) { if (ws.Cells[row, col].Value ! null !string.IsNullOrWhiteSpace(ws.Cells[row, col].Value.ToString())) { return false; } } return true; } }4.3 在Unity中调用读取方法最后我们需要一个MonoBehaviour脚本来驱动读取过程并处理文件路径问题。Unity中获取Excel文件流有多种方式这里介绍两种最常用的。方式一文件放在StreamingAssets文件夹推荐用于开发阶段和PC端StreamingAssets文件夹下的内容在打包后会原封不动地包含在发布包中且在各平台有统一的访问方式。// ItemManager.cs using System.Collections.Generic; using System.IO; using UnityEngine; public class ItemManager : MonoBehaviour { public ListItemConfig allItems; void Start() { LoadItemConfigs(); } void LoadItemConfigs() { // 构建文件路径 string filePath Path.Combine(Application.streamingAssetsPath, Configs, Items.xlsx); // 检查文件是否存在 if (!File.Exists(filePath)) { Debug.LogError($配置文件不存在: {filePath}); return; } // 创建文件流并读取 FileStream fileStream null; try { fileStream new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); allItems ExcelReader.ReadItemsFromExcel(fileStream, Sheet1, true); Debug.Log($成功加载 {allItems.Count} 个道具配置。); // 测试输出 foreach (var item in allItems) { Debug.Log($ID:{item.ID}, Name:{item.Name}, Attack:{item.AttackPower}); } } catch (System.Exception e) { Debug.LogError($读取Excel文件失败: {e.Message}\n{e.StackTrace}); } finally { fileStream?.Close(); // 确保流被关闭 } } }方式二使用TextAsset配合MemoryStream适用于小文件或WebGLUnity可以将二进制文件如Excel导入为TextAsset但需要将导入类型设置为Binary。这种方式将文件内容直接嵌入到资源中。将Items.xlsx文件拖入Unity项目的Assets/Resources文件夹或任何Resources子文件夹。选中该文件在Inspector面板中将Texture Type设置为Default并将Import Type下拉框选择为**Binary**重要。读取代码public void LoadFromTextAsset() { TextAsset excelTextAsset Resources.LoadTextAsset(Items); // 无需后缀名 if (excelTextAsset null) { Debug.LogError(未找到Items TextAsset资源。); return; } using (MemoryStream stream new MemoryStream(excelTextAsset.bytes)) { allItems ExcelReader.ReadItemsFromExcel(stream, Sheet1, true); } // ... 后续处理 }实操心得StreamingAssets方式更灵活支持热更新通过替换文件但需要处理不同平台的路径差异Application.streamingAssetsPath已解决。Resources方式打包后不可修改但加载简单。对于配置表我强烈推荐使用StreamingAssets便于策划独立更新Excel文件而不需要程序员重新打包。5. 进阶技巧与性能优化基础读取跑通后我们来看看如何做得更专业、更高效。5.1 使用列名映射而非列索引上面的例子通过固定的列索引123...来读取这非常脆弱。一旦策划在中间插入一列所有代码的索引就都错位了。更健壮的方式是通过第一行的标题名来动态确定列索引。public static ListDictionarystring, object ReadExcelWithColumnMapping(Stream fileStream, string worksheetName null) { var result new ListDictionarystring, object(); using (var package new ExcelPackage(fileStream)) { var worksheet string.IsNullOrEmpty(worksheetName) ? package.Workbook.Worksheets[0] : package.Workbook.Worksheets[worksheetName]; int rowCount worksheet.Dimension.Rows; int colCount worksheet.Dimension.Columns; // 1. 读取第一行建立列名到索引的映射 Dictionarystring, int columnMap new Dictionarystring, int(); for (int col 1; col colCount; col) { string header worksheet.Cells[1, col].Value?.ToString()?.Trim(); if (!string.IsNullOrEmpty(header)) { columnMap[header] col; } } // 2. 从第二行开始遍历数据 for (int row 2; row rowCount; row) { var rowData new Dictionarystring, object(); bool rowHasData false; foreach (var kvp in columnMap) { string columnName kvp.Key; int columnIndex kvp.Value; object cellValue worksheet.Cells[row, columnIndex].Value; if (cellValue ! null) { rowHasData true; } rowData[columnName] cellValue; } if (rowHasData) { result.Add(rowData); } } } return result; }这样无论Excel列顺序如何变化只要标题名不变代码就能正确读取数据。你可以进一步将这个Dictionary转换为强类型的对象。5.2 处理复杂数据类型与公式Epplus可以读取单元格的原始值.Value或计算后的值.Formula/.CalculatedValue。对于包含公式的单元格直接读.Value可能得到的是公式字符串如A1B1而.CalculatedValue会得到计算结果。ExcelRange cell worksheet.Cells[A1]; if (!string.IsNullOrEmpty(cell.Formula)) // 判断是否有公式 { Console.WriteLine($公式: {cell.Formula}); Console.WriteLine($计算结果: {cell.Value}); // 注意在Epplus中如果文件已保存.Value通常是计算后的结果。 // 若要强制计算可能需要使用 package.Workbook.Calculate(); }对于日期、时间等特殊格式Epplus的.Value返回的可能是DateTime对象或表示日期的双精度浮点数OLE Automation Date。需要做好类型判断和转换。5.3 性能优化缓存与惰性加载如果配置表很大或者需要在游戏运行时频繁读取每次从磁盘加载并解析整个Excel文件是不可接受的。标准的优化策略是预解析与序列化在构建Build时或资源导入时用一个编辑器工具Editor Script将Excel文件解析并序列化成Unity更擅长快速加载的二进制格式如ScriptableObject资产或紧凑的JSON/二进制文件。运行时直接加载这个优化后的文件。缓存机制在内存中维护一个全局的配置数据管理器如ConfigManager游戏启动时一次性加载所有必要配置到字典或列表中后续通过ID直接查找避免重复IO操作。分块读取对于超大型表格如果确实需要动态读取可以只加载特定的工作表或单元格区域而不是整个文件。6. 常见错误、异常与解决方案实录这是最有价值的部分记录了我在多个项目中踩过的坑和解决方案。6.1 编译错误“The type or namespace name OfficeOpenXml could not be found”问题描述在Unity中编写using OfficeOpenXml;时出现红色波浪线编译失败。原因分析DLL未正确导入EPPlus.dll没有放在Assets/Plugins文件夹下或者放错了位置。平台设置错误DLL的Inspector面板中目标平台没有包含当前构建平台如正在为Standalone构建但DLL只勾选了Editor。API兼容性不匹配项目设置的Api Compatibility Level如.NET Standard 2.0与Epplus DLL编译所用的框架版本不兼容。Epplus for .NET Standard 2.0的DLL需要项目至少设置为.NET Standard 2.0。解决方案确认EPPlus.dll在Assets/Plugins中。选中DLL在Inspector中检查“Select platforms for plugin”部分。对于主要在编辑器下和PC端使用勾选“Editor”和“Standalone”就足够了。如果你需要在其他平台使用必须经过充分测试并且可能需要处理额外的依赖。前往Edit - Project Settings - Player - Other Settings - Configuration将Api Compatibility Level改为.NET Standard 2.0或.NET FrameworkUnity 2022 可能是.NET 8等选择兼容的版本。6.2 运行时错误“Could not load file or assembly System.Drawing... or one of its dependencies”问题描述在运行时尤其是在某些独立平台或编辑器播放模式下初始化ExcelPackage时抛出FileNotFoundException或DllNotFoundException提及System.Drawing。原因分析Epplus的某些功能如图片处理依赖于System.Drawing而Unity的.NET运行时环境可能不包含完整的System.Drawing库尤其是在非Windows平台或特定构建目标下。解决方案升级Epplus版本尝试使用最新的Epplus 5.x或6.x版本它们对.NET Standard 2.0的支持更好减少了对System.Drawing的硬依赖。添加兼容包通过NuGet获取System.Drawing.Common的DLL并将其放入Assets/Plugins。但要注意平台兼容性。规避使用相关功能确保你的代码没有调用Epplus中涉及图片、复杂样式等可能触发System.Drawing的功能。如果只是读写数据通常不会触发此错误。使用替代库如果问题在目标平台如WebGL、iOS上无法解决考虑换用纯.NET Standard 2.0实现的库或者回退到CSV/JSON方案。6.3 运行时错误“LicenseContext is not set...”问题描述在创建ExcelPackage实例时抛出关于许可证的异常。原因分析Epplus 5.0 版本引入了更严格的许可证检查。如果未设置许可上下文在调用某些功能时会抛出异常。解决方案在程序初始化时如静态构造函数或Awake方法中设置许可上下文。根据你的用途选择// 在调用任何Epplus功能之前设置例如在静态构造函数中 ExcelPackage.LicenseContext LicenseContext.NonCommercial; // 非商业用途 // 或者如果你购买了商业许可证 // ExcelPackage.LicenseContext LicenseContext.Commercial;6.4 读取到的数据为null或类型转换失败问题描述单元格明明有值但cell.Value返回null或者转换int、float时失败。原因分析空白单元格或公式返回空单元格看起来有内容但可能是空格或公式返回了空值。数据类型不匹配Excel单元格可能是文本格式存储的数字或者日期直接强制转换会失败。合并单元格只读取了合并区域左上角的单元格其他位置值为null。解决方案使用前面示例中的GetCellValueT安全转换方法它处理了null和转换异常。在读取前进行类型判断和清洗object val cell.Value; if (val is double) { /* 处理数字 */ } else if (val is string) { /* 处理字符串可能需要Trim() */ } else if (val is DateTime) { /* 处理日期 */ } else if (val is bool) { /* 处理布尔值 */ }对于可能为空的数值使用int.TryParse或float.TryParse。处理合并单元格时使用worksheet.MergedCells属性判断单元格是否属于合并区域并获取其Start单元格的值。6.5 文件被锁定或访问被拒绝问题描述当Excel文件在编辑器中被打开例如策划正在用Microsoft Excel编辑该文件Unity尝试读取时会抛出IOException提示文件正在被另一个进程使用。原因分析Windows上Excel在打开文件时会施加一个读写锁防止其他进程修改。解决方案使用FileShare.ReadWrite在打开FileStream时指定共享模式如示例中的FileShare.ReadWrite。这允许其他进程如Excel以写入方式打开你仍可以读取。但这并非总是有效取决于Excel的打开方式。复制文件再读取最可靠的方法是先将要读取的Excel文件复制到一个临时路径然后读取这个副本。string sourcePath Path.Combine(Application.streamingAssetsPath, Items.xlsx); string tempPath Path.Combine(Application.temporaryCachePath, Items_Temp.xlsx); File.Copy(sourcePath, tempPath, true); // 覆盖已存在的副本 using (var stream new FileStream(tempPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { // 读取stream } // 可选读取后删除临时文件 File.Delete(tempPath);流程规范与团队约定在打包或测试前关闭所有正在编辑的Excel配置文件。6.6 在移动平台iOS/Android或WebGL上无法运行问题描述在编辑器下工作正常但打包到移动端或WebGL后功能失效或报错。原因分析这些平台属于“受限环境”.NET运行时功能不完整或者文件系统访问方式不同。Epplus依赖的一些API如System.IO.Compression的特定方式、System.Drawing可能不可用。解决方案彻底避免在运行时使用这是最根本的解决方案。将Excel读取逻辑限制在编辑器扩展Editor Scripts中使用。策划编辑Excel程序员通过一个编辑器工具按钮点击后触发读取Excel并生成Unity原生格式如ScriptableObject、JSON或二进制文件的配置文件。运行时只加载这些优化后的配置文件。如果必须在运行时使用充分测试在目标设备上进行详尽的测试。使用Application.streamingAssetsPath确保文件放在这个路径下并使用UnityWebRequest或File.ReadAllBytes取决于平台来读取字节流再交给Epplus。简化Excel文件避免使用复杂公式、图表、图片、宏等高级功能。准备备选方案准备好CSV或JSON的备用数据源当Epplus失败时自动降级使用。7. 一个完整的编辑器工具示例为了将上述所有知识融会贯通这里提供一个简单的编辑器工具脚本它允许你在Unity Editor中一键将指定的Excel配置表转换为ScriptableObject资产从而彻底避免运行时依赖Epplus。// ExcelToScriptableObjectEditor.cs #if UNITY_EDITOR using UnityEditor; using UnityEngine; using System.IO; using OfficeOpenXml; using System.Collections.Generic; public class ExcelToScriptableObjectEditor : EditorWindow { private string excelFilePath Assets/StreamingAssets/Configs/Items.xlsx; private string outputAssetPath Assets/Resources/Configs/ItemConfig.asset; [MenuItem(Tools/Excel/Convert Items Config)] static void Init() { GetWindowExcelToScriptableObjectEditor(Excel Converter).Show(); } void OnGUI() { GUILayout.Label(Excel to ScriptableObject Converter, EditorStyles.boldLabel); excelFilePath EditorGUILayout.TextField(Excel File Path:, excelFilePath); outputAssetPath EditorGUILayout.TextField(Output Asset Path:, outputAssetPath); if (GUILayout.Button(Convert)) { ConvertExcelToSO(); } } void ConvertExcelToSO() { // 1. 设置许可 ExcelPackage.LicenseContext LicenseContext.NonCommercial; // 2. 检查文件 if (!File.Exists(excelFilePath)) { Debug.LogError($Excel file not found at: {excelFilePath}); return; } // 3. 读取Excel ListItemConfig items new ListItemConfig(); FileStream fileStream null; try { fileStream new FileStream(excelFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using (ExcelPackage package new ExcelPackage(fileStream)) { var worksheet package.Workbook.Worksheets[0]; int rowCount worksheet.Dimension.Rows; for (int row 2; row rowCount; row) // 假设第一行是标题 { if (worksheet.Cells[row, 1].Value null) break; // 遇到空ID行则停止 ItemConfig item new ItemConfig(); item.ID GetCellValueint(worksheet.Cells[row, 1]); item.Name GetCellValuestring(worksheet.Cells[row, 2]); item.Type GetCellValuestring(worksheet.Cells[row, 3]); item.AttackPower GetCellValueint(worksheet.Cells[row, 4]); item.Price GetCellValuefloat(worksheet.Cells[row, 5]); items.Add(item); } } Debug.Log($Read {items.Count} items from Excel.); } catch (System.Exception e) { Debug.LogError($Failed to read Excel: {e.Message}); return; } finally { fileStream?.Close(); } // 4. 创建或更新ScriptableObject ItemConfigDatabase database AssetDatabase.LoadAssetAtPathItemConfigDatabase(outputAssetPath); if (database null) { database ScriptableObject.CreateInstanceItemConfigDatabase(); AssetDatabase.CreateAsset(database, outputAssetPath); } database.items items.ToArray(); // 假设ItemConfigDatabase有一个ItemConfig[]数组 EditorUtility.SetDirty(database); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($Successfully converted and saved to {outputAssetPath}); } // 安全获取值的辅助方法同上文 private T GetCellValueT(ExcelRange cell) { /* 实现略 */ } } // 对应的ScriptableObject数据容器 [CreateAssetMenu(fileName ItemConfigDatabase, menuName Config/Item Database)] public class ItemConfigDatabase : ScriptableObject { public ItemConfig[] items; } #endif这个工具将运行时依赖转移到了编辑时是生产环境的最佳实践。策划更新Excel后程序员或策划自己点一下按钮就能生成游戏运行时直接可用的高效资产文件。最后关于Unity与Excel的交互核心在于理解需求边界。如果只是简单的数据存储CSV或许更轻快如果需要复杂的离线编辑和计算Epplus是强大的桥梁而为了最终发布的性能和稳定性将Excel数据“烘焙”成Unity原生格式永远是值得投入的优化步骤。在实际项目中我通常会结合使用策划用Excel维护数据通过编辑器工具自动转换为ScriptableObject或二进制文件游戏运行时享受极快的加载速度和零外部依赖。这套流程经过多个项目验证能有效平衡开发效率与运行性能。