1. 项目概述:为什么Unity开发者需要NPOI?
如果你是一个Unity开发者,无论是做游戏还是企业级应用,迟早会遇到一个绕不开的需求:处理Excel文件。可能是读取策划同学配好的数值表,可能是导出玩家的行为数据报表,也可能是生成一份复杂的配置清单。Unity引擎本身并没有内置对Excel文件(.xls, .xlsx)的原生支持,你总不能指望用TextAsset去读二进制文件,或者让用户手动把Excel另存为CSV吧?这时候,一个成熟、稳定、免费的.NET Excel处理库就显得至关重要,而NPOI正是这个领域里经久不衰的“瑞士军刀”。
NPOI是.NET平台下操作Microsoft Office文档(尤其是Excel)的开源组件。它的名字来源于“Non-POI”,而POI是Apache基金会的Java项目“Poor Obfuscation Implementation”的缩写,意为处理OLE2文档的库。简单说,NPOI就是POI的.NET移植版本。它允许你在不安装Microsoft Office的环境下,读写Excel、Word等文件,这对于服务器端处理或客户端应用(比如Unity打包的游戏)来说是刚需。最新版的NPOI 2.5.2带来了更好的性能、更完善的API以及对新Excel格式(如.xlsx)更稳定的支持。
在Unity项目里集成NPOI,意味着你可以:
- 动态读取配置:将游戏平衡性参数、道具属性、关卡数据等放在Excel里,游戏运行时加载,策划修改表格后无需重新打包游戏。
- 数据导出与分析:记录玩家日志、消费行为、关卡通过率等,定期生成Excel报表,方便运营和数据分析。
- 工具链自动化:在编辑器扩展中,用Excel表格作为数据源,自动生成脚本、预制体或资源文件,提升开发效率。
我见过不少团队初期用JSON或ScriptableObject,但随着数据量增大和策划协作需求,最终都转向了Excel+NPOI的方案。接下来,我就结合NPOI 2.5.2,详细拆解在Unity中集成和使用它的完整流程、核心技巧以及那些官方文档不会告诉你的“坑”。
2. 核心思路与方案选型:为什么是NPOI 2.5.2?
面对Excel处理,Unity开发者通常有几个选择:付费的第三方插件(如ExcelDataReader的封装)、自己用System.Data.OleDb(依赖系统Office组件,不可靠)、或者使用开源库。NPOI在其中脱颖而出,原因在于它的纯粹性和可控性。
2.1 NPOI的核心优势
- 零依赖,纯托管代码:NPOI的DLL是纯.NET程序集,不依赖Microsoft Office或任何其他原生组件。这意味着你把它放进Unity的Plugins文件夹,无论是在Windows、Mac还是将来可能的其他IL2CPP支持平台,它都能运行(需注意平台兼容性,后文详述)。这对于需要跨平台分发的Unity应用来说是决定性的优势。
- 功能全面且强大:它不仅能读写单元格数据,还能操作单元格样式(字体、颜色、边框)、公式、合并单元格、图表(有限支持)、图片插入等。对于游戏配置表这种需求,它完全胜任,甚至能处理一些复杂的报表生成。
- 性能与内存友好:NPOI提供了流式API(如
SXSSFWorkbook用于写大型.xlsx文件),可以处理海量数据而不会导致内存溢出。对于读取,也有事件模型(如XSSFSheetXMLHandler)来逐行处理,避免一次性加载整个文件。 - 活跃的社区与长期维护:作为Apache项目,NPOI有稳定的维护者和社区,2.5.2版本修复了大量旧版的Bug,并提升了稳定性。
2.2 版本选择:2.5.2的考量
选择2.5.2最新版,而非更老的1.x或2.0版本,主要基于以下几点:
- 更好的 .NET Standard 2.0 支持:2.5.2版本加强了对.NET Standard 2.0的兼容性,这对于Unity(尤其是较新版本,其运行时更接近.NET Standard)来说,意味着更少的兼容性问题和更好的性能。
- Bug修复与稳定性:每个小版本更新都包含大量的问题修复。例如,早期版本在处理某些特定格式的.xlsx文件时可能崩溃,或公式计算有误,2.5.2版本在这些方面更加可靠。
- API的完善:新版本的API设计有时更合理,废弃了一些过时的方法,引入了更易用的新接口。
注意:在Unity中使用时,务必确认你的Unity版本对应的.NET运行时版本。Unity 2021 LTS及更新版本通常支持.NET Standard 2.1或.NET Framework 4.x,与NPOI 2.5.2兼容性较好。对于较老的Unity版本(如2018.4),可能需要测试或选择稍旧的NPOI版本。
2.3 备选方案简析
- EPPlus:另一个优秀的开源库,但它的开源协议(Polyform Noncommercial License)对于商业项目可能存在风险,需要仔细评估。功能上它与NPOI各有千秋,但在Unity社区,NPOI的历史更久,资源更多。
- ClosedXML:基于EPPlus和Open XML SDK的封装,API非常友好,但同样存在协议问题,且对Unity的IL2CPP支持情况不如NPOI经过大量项目验证。
- Unity Asset Store插件:付费插件通常提供了更友好的Unity编辑器集成和可视化工具,但锁定了供应商,且可能基于旧版NPOI或EPPlus封装。如果你需要快速原型开发且预算充足,可以考虑;但对于需要深度定制和长期维护的项目,直接使用NPOI是更根本的解决方案。
综合来看,对于大多数追求控制力、零成本和长期稳定性的Unity团队,NPOI 2.5.2是目前最稳妥的选择。
3. 环境准备与NPOI 2.5.2集成实战
理论说再多,不如动手搭一遍。这里我将详细演示如何将NPOI 2.5.2集成到一个Unity项目中,并解决可能遇到的典型问题。
3.1 获取NPOI 2.5.2 DLL
官方推荐通过NuGet安装,但Unity项目通常不直接使用NuGet。最可靠的方式是从GitHub发布页或官方网站下载编译好的二进制包。
- 访问发布页面:前往NPOI在GitHub的仓库(搜索“NPOI”),找到Releases页面,下载
npoi.2.5.2.zip或类似名称的压缩包。 - 提取关键DLL:解压后,你会在
lib或bin文件夹下看到多个子文件夹(如netstandard2.0,net40,net45)。对于Unity,我们通常选择netstandard2.0文件夹下的DLL,因为它兼容性最广。核心DLL包括:NPOI.dllNPOI.OOXML.dll(用于处理.xlsx格式)NPOI.OpenXml4Net.dll(OpenXML底层支持)NPOI.OpenXmlFormats.dll(OpenXML格式定义)ICSharpCode.SharpZipLib.dll(依赖项,用于压缩)
- 导入Unity项目:
- 在你的Unity项目Assets目录下,创建
Plugins文件夹(如果不存在)。 - 将上述所有DLL文件复制到
Assets/Plugins中。对于需要区分平台的场景,你可以进一步在Plugins下创建x86,x86_64等文件夹,但NPOI是纯托管代码,通常直接放在Plugins根目录即可。 - 选中这些DLL,在Unity Inspector窗口中,确保它们的“Platform”设置正确。通常,
Any Platform都可以勾选,但如果你只为特定平台(如Windows Standalone)打包,可以取消其他平台以减少包体。
- 在你的Unity项目Assets目录下,创建
3.2 解决可能的兼容性问题
集成后,你可能会在Unity编辑器控制台看到警告或错误。最常见的是DLL冲突或版本不匹配。
场景一:与Unity内置ICSharpCode.SharpZipLib冲突Unity自身可能使用了不同版本的
ICSharpCode.SharpZipLib.dll。如果你同时导入了NPOI自带的这个DLL,就会冲突。- 解决方案:尝试不导入NPOI包里的
ICSharpCode.SharpZipLib.dll,而是使用Unity内置的版本。大多数情况下,NPOI 2.5.2与Unity内置版本是兼容的。如果运行时出现FileNotFoundException或MissingMethodException,再考虑使用NPOI自带的DLL,并可能需要通过程序集重定向(Assembly-CSharp.csproj文件)来解决。一个更稳妥的实践是:先不导入这个DLL,如果编译和运行都正常,就说明没问题;如果出错,再从NPOI包里导入,并确保其“API Compatibility Level”设置与项目其他部分一致。
- 解决方案:尝试不导入NPOI包里的
场景二:在IL2CPP编译目标下报错IL2CPP会将C#代码预编译为C++,对于使用了大量反射或动态代码生成的库,可能需要额外配置。
- 解决方案:NPOI内部确实使用了反射。你需要为IL2CPP添加链接器配置文件(
link.xml)来防止必要的代码被剪裁掉。在Assets目录下创建link.xml文件,内容如下:
这个文件告诉Unity的代码剪裁器,保留这些程序集的所有类型和方法,确保运行时不会因为剪裁而丢失功能。<linker> <assembly fullname="NPOI" preserve="all"/> <assembly fullname="NPOI.OOXML" preserve="all"/> <assembly fullname="NPOI.OpenXml4Net" preserve="all"/> <assembly fullname="NPOI.OpenXmlFormats" preserve="all"/> <assembly fullname="ICSharpCode.SharpZipLib" preserve="all"/> </linker>
- 解决方案:NPOI内部确实使用了反射。你需要为IL2CPP添加链接器配置文件(
3.3 编写一个简单的测试脚本
集成完成后,写个简单脚本验证功能是否正常。在Unity中创建一个C#脚本TestNPOI.cs:
using UnityEngine; using System.IO; using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; // 用于.xlsx using NPOI.HSSF.UserModel; // 用于.xls public class TestNPOI : MonoBehaviour { void Start() { // 1. 创建一个新的工作簿(.xlsx格式) IWorkbook workbook = new XSSFWorkbook(); ISheet sheet = workbook.CreateSheet("TestSheet"); // 2. 创建第一行,作为表头 IRow headerRow = sheet.CreateRow(0); headerRow.CreateCell(0).SetCellValue("ID"); headerRow.CreateCell(1).SetCellValue("Name"); headerRow.CreateCell(2).SetCellValue("Score"); // 3. 创建数据行 IRow dataRow = sheet.CreateRow(1); dataRow.CreateCell(0).SetCellValue(1); dataRow.CreateCell(1).SetCellValue("Player1"); dataRow.CreateCell(2).SetCellValue(95.5); // 4. 保存到项目的PersistentDataPath string filePath = Path.Combine(Application.persistentDataPath, "test_npoi.xlsx"); using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write)) { workbook.Write(fs); } Debug.Log($"Excel文件已生成: {filePath}"); // 5. (可选)读取刚写入的文件 using (FileStream fsRead = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { IWorkbook readWorkbook = new XSSFWorkbook(fsRead); ISheet readSheet = readWorkbook.GetSheetAt(0); IRow firstDataRowRead = readSheet.GetRow(1); double score = firstDataRowRead.GetCell(2).NumericCellValue; Debug.Log($"从文件读取的Score值为: {score}"); } } }将这个脚本挂载到场景中任意GameObject上,运行Unity。如果一切正常,你会在控制台看到生成文件路径和读取的数值,并且在对应的路径下找到生成的test_npoi.xlsx文件,用Excel打开可以查看内容。
4. 核心功能深度解析与最佳实践
通过了基础测试,我们深入看看NPOI在Unity项目中最常用的几个核心场景,以及如何高效、稳健地使用它。
4.1 读取游戏配置表(最常用场景)
假设我们有一个ItemConfig.xlsx文件,定义了游戏物品属性。
| ID | Name | Type | Attack | Price | Description |
|---|---|---|---|---|---|
| 1001 | 木剑 | Weapon | 10 | 50 | 一把普通的木制剑。 |
| 1002 | 小红药 | Consumable | 0 | 20 | 恢复少量生命值。 |
我们的目标是将其读入内存,转换为C#对象列表(如List<ItemConfig>)。
using System.Collections.Generic; using System.IO; using NPOI.SS.UserModel; using UnityEngine; public class ItemConfig { public int ID; public string Name; public string Type; public int Attack; public int Price; public string Description; } public class ExcelConfigLoader : MonoBehaviour { public TextAsset excelTextAsset; // 将Excel文件拖拽到这里,注意Unity不能直接识别.xlsx,需要先放入Resources或StreamingAssets,并以bytes形式读取。 void Start() { // 通常Excel文件放在StreamingAssets中,以便打包后仍可读取和更新 string filePath = Path.Combine(Application.streamingAssetsPath, "Configs/ItemConfig.xlsx"); // 或者从TextAsset读取(适用于打包进Resources的小型配置) // byte[] fileBytes = excelTextAsset.bytes; List<ItemConfig> itemList = LoadItemConfigFromExcel(filePath); foreach (var item in itemList) { Debug.Log($"Item: {item.Name}, Attack: {item.Attack}"); } } List<ItemConfig> LoadItemConfigFromExcel(string filePath) { List<ItemConfig> configs = new List<ItemConfig>(); using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { IWorkbook workbook = WorkbookFactory.Create(fs); // WorkbookFactory自动判断.xls或.xlsx ISheet sheet = workbook.GetSheetAt(0); // 第一个工作表 // 假设第一行是表头,从第二行开始是数据 for (int rowIndex = 1; rowIndex <= sheet.LastRowNum; rowIndex++) { IRow row = sheet.GetRow(rowIndex); if (row == null) continue; // 跳过空行 ItemConfig config = new ItemConfig(); // 按列索引读取,强烈建议根据表头名称动态映射,这里用硬编码示例 config.ID = (int)row.GetCell(0).NumericCellValue; config.Name = row.GetCell(1).StringCellValue; config.Type = row.GetCell(2).StringCellValue; config.Attack = (int)row.GetCell(3).NumericCellValue; config.Price = (int)row.GetCell(4).NumericCellValue; config.Description = row.GetCell(5)?.StringCellValue ?? ""; // 安全获取,防止空单元格 configs.Add(config); } } return configs; } }实操心得:
- 表头映射:生产环境千万不要用硬编码列索引(如
row.GetCell(0))。一旦策划调整了表格列顺序,代码就全错了。正确做法是:先读取第一行表头,建立一个Dictionary<string, int>,将列名映射到列索引。然后通过列名来获取单元格,例如row.GetCell(headerMap["Name"])。- 单元格类型判断:使用
CellType属性判断单元格是数字、字符串、布尔值还是公式,再调用对应的方法(NumericCellValue,StringCellValue,BooleanCellValue)。直接调用错误的方法会抛出异常。- 空值处理:使用
row.GetCell(colIndex)可能返回null,使用?.操作符或先判断null再取值。- 性能考虑:如果配置表非常大(数万行),频繁的
GetCell和类型判断会有开销。可以考虑在编辑器环境下,将Excel预处理成二进制格式(如ScriptableObject或自定义二进制文件),运行时加载处理后的文件,速度会快很多。
4.2 导出游戏数据报表
与读取相反,我们可能需要将游戏内的数据(如玩家日志、排行榜)导出为Excel。
public class DataExporter { public void ExportPlayerLogs(List<PlayerLog> logs, string filePath) { IWorkbook workbook = new XSSFWorkbook(); ISheet sheet = workbook.CreateSheet("PlayerLogs"); // 创建表头 IRow headerRow = sheet.CreateRow(0); string[] headers = { "Timestamp", "PlayerID", "Action", "Param1", "Param2" }; for (int i = 0; i < headers.Length; i++) { headerRow.CreateCell(i).SetCellValue(headers[i]); } // 填充数据 for (int i = 0; i < logs.Count; i++) { IRow row = sheet.CreateRow(i + 1); PlayerLog log = logs[i]; row.CreateCell(0).SetCellValue(log.timestamp.ToString("yyyy-MM-dd HH:mm:ss")); row.CreateCell(1).SetCellValue(log.playerId); row.CreateCell(2).SetCellValue(log.action); row.CreateCell(3).SetCellValue(log.param1); // 数字类型用SetCellValue(double),避免被当作字符串 if (double.TryParse(log.param2, out double numValue)) row.CreateCell(4).SetCellValue(numValue); else row.CreateCell(4).SetCellValue(log.param2); } // 自动调整列宽(近似) for (int i = 0; i < headers.Length; i++) { sheet.AutoSizeColumn(i); } // 保存 using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write)) { workbook.Write(fs); } Debug.Log($"数据已导出至: {filePath}"); } }注意事项:
- 大数据量导出:如果
logs有几十万条,上述代码会一次性在内存中创建所有行,可能导致内存压力。此时应使用SXSSFWorkbook(流式工作簿)。SXSSFWorkbook会保持一定数量的行在内存中,将之前的行写入临时文件,非常适合海量数据导出。- 样式设置:可以通过
ICellStyle来设置字体、背景色、对齐方式等,让报表更美观。但注意,创建太多样式对象会影响性能,应尽量复用。- 文件路径:在移动平台(iOS/Android)上,写入权限受限。通常只能写入
Application.persistentDataPath。导出后,可能需要通过系统分享功能或服务器上传将文件发送出去。
4.3 处理公式与单元格样式
有时配置表里可能有简单的公式(如合计栏),或者你需要高亮某些数据。
// 设置单元格样式示例 ICellStyle headerStyle = workbook.CreateCellStyle(); IFont font = workbook.CreateFont(); font.FontName = "微软雅黑"; font.FontHeightInPoints = 12; font.IsBold = true; headerStyle.SetFont(font); headerStyle.FillForegroundColor = IndexedColors.LightBlue.Index; headerStyle.FillPattern = FillPattern.SolidForeground; headerStyle.Alignment = HorizontalAlignment.Center; // 应用样式到表头单元格 foreach (ICell cell in headerRow.Cells) { cell.CellStyle = headerStyle; } // 设置公式单元格 ICell formulaCell = sheet.CreateRow(sheet.LastRowNum + 1).CreateCell(5); // 假设在第6列(F列)写合计 formulaCell.SetCellFormula("SUM(E2:E" + (sheet.LastRowNum) + ")"); // 对Price列求和 // 注意:NPOI可以设置公式,但默认不计算公式结果。如果需要结果,可以调用: // IFormulaEvaluator evaluator = workbook.GetCreationHelper().CreateFormulaEvaluator(); // evaluator.EvaluateFormulaCell(formulaCell); // 但更常见的做法是,让Excel在打开时自动计算。5. 高级技巧、性能优化与疑难杂症
掌握了基本读写,我们来看看一些能提升效率和稳定性的高级技巧,以及如何应对那些棘手的坑。
5.1 使用缓存与对象池优化性能
在频繁读写Excel的场景(如工具链批量处理),反复创建Workbook、Sheet、CellStyle对象开销很大。
- 样式池:将常用的单元格样式(如默认文本样式、数字样式、标题样式)创建一次并缓存起来,后续直接赋值,避免重复创建。
public class ExcelStylePool { private IWorkbook _workbook; private Dictionary<string, ICellStyle> _styleCache = new Dictionary<string, ICellStyle>(); public ExcelStylePool(IWorkbook workbook) { _workbook = workbook; } public ICellStyle GetOrCreateStyle(string styleKey, Action<ICellStyle> styleBuilder) { if (!_styleCache.TryGetValue(styleKey, out ICellStyle style)) { style = _workbook.CreateCellStyle(); styleBuilder(style); _styleCache[styleKey] = style; } return style; } } - 数据批处理:避免在循环内频繁进行文件IO操作。一次性将所有数据准备好,再创建
Workbook并写入,或者使用SXSSFWorkbook的流式特性。
5.2 处理特殊数据类型与格式
- 日期时间:Excel内部以浮点数存储日期。NPOI的
DateUtil类提供了转换方法。DateTime date = row.GetCell(0).DateCellValue; // 直接获取 // 或者 double excelDateNum = row.GetCell(0).NumericCellValue; DateTime dateFromNum = DateUtil.GetJavaDate(excelDateNum); // 注意时区 - 富文本与换行:单元格内换行符是
\n,但在Excel中显示需要设置单元格样式WrapText = true。 - 超链接:可以通过
CreationHelper.CreateHyperlink创建超链接并赋值给单元格。
5.3 跨平台注意事项(IL2CPP, AOT)
这是Unity集成NPOI最容易出问题的地方。
- 代码剪裁(Stripping):如前所述,必须使用
link.xml文件来保留NPOI相关程序集。 - AOT编译限制:NPOI内部大量使用反射和动态代码生成(如
System.Linq.Expressions),这在iOS等AOT平台上可能受限。虽然NPOI 2.x对此有改善,但复杂操作仍可能触发ExecutionEngineException。- 应对策略:尽可能在编辑器阶段完成所有Excel处理,将结果序列化为运行时友好的格式(如JSON、二进制)。如果必须在运行时处理,则严格测试所有功能路径,并考虑使用预编译的委托或接口来规避反射。
- 文件系统路径:
Application.streamingAssetsPath在Android平台是压缩包内的路径,不能直接用FileStream读取。需要使用UnityWebRequest或WWW类先加载到内存,再交给NPOI处理。IEnumerator LoadExcelFromStreamingAssets(string filePathInStreamingAssets) { string path = Path.Combine(Application.streamingAssetsPath, filePathInStreamingAssets); byte[] fileData; if (path.Contains("://") || path.Contains(":///")) // Android或WebGL { UnityWebRequest www = UnityWebRequest.Get(path); yield return www.SendWebRequest(); fileData = www.downloadHandler.data; } else // 其他平台 { fileData = File.ReadAllBytes(path); } // 使用MemoryStream处理byte[] using (MemoryStream ms = new MemoryStream(fileData)) { IWorkbook workbook = WorkbookFactory.Create(ms); // ... 处理workbook } }
5.4 常见错误与排查
- “Could not load file or assembly ‘ICSharpCode.SharpZipLib’...”:DLL冲突或缺失。按3.2节方案处理。
- “Index was out of range...”:读取单元格时索引越界。检查表格是否为空,或行/列索引是否正确。使用
sheet.LastRowNum和row.LastCellNum时要注意,它们返回的是最后有内容的行/列索引(从0开始),但中间可能有空行导致GetRow返回null。 - 生成的Excel文件用WPS打开正常,用Microsoft Excel打开报错:可能是使用了某些不兼容的样式或属性。尽量使用最基本的样式。也可以尝试先用NPOI生成,然后用Excel程序打开并另存一次,有时能修复文件结构。
- 内存泄漏:
IWorkbook、ISheet等对象实现了IDisposable。务必使用using语句或在finally块中调用Dispose()方法,特别是在服务器端或长时间运行的工具中。对于SXSSFWorkbook,还需要调用Dispose()来清理临时文件。
6. 实战案例:构建一个简易的Unity-Excel配置表编辑器扩展
为了将以上知识融会贯通,我们设计一个简单的Unity编辑器工具,它能自动将指定Excel配置表转换为C#数据类和ScriptableObject资源,实现策划改表,程序无需手动同步。
6.1 工具设计目标
- 在Unity Editor菜单中提供一个入口。
- 选择Excel文件后,自动解析其表头。
- 根据表头生成对应的C#数据类文件(如
ItemConfig.cs)。 - 读取Excel数据,生成或更新对应的ScriptableObject资产文件(如
ItemConfig.asset)。 - 在Inspector中提供一个按钮,一键同步所有配置表。
6.2 关键实现步骤
- 创建基础数据容器:首先定义一个基类
BaseExcelConfig,所有配置表SO都继承它。 - 解析Excel并生成C#类:使用
System.CodeDom或模板字符串,根据Excel第一行的列名和数据类型(可简单推断)动态生成.cs文件内容,并写入项目Scripts目录。 - 生成ScriptableObject:利用反射,根据生成的类名,创建SO实例,并通过反射将Excel每一行数据填充到SO的列表属性中。
- 提供编辑器界面:创建一个
EditorWindow,列出所有配置表,提供“导入”、“生成代码”、“生成资源”等按钮。
由于代码较长,这里给出核心片段——根据表头生成C#类的思路:
// 伪代码,展示逻辑 public static void GenerateConfigClass(string excelPath, string className) { using (var fs = new FileStream(excelPath, FileMode.Open, FileAccess.Read)) { IWorkbook workbook = WorkbookFactory.Create(fs); ISheet sheet = workbook.GetSheetAt(0); IRow headerRow = sheet.GetRow(0); StringBuilder sb = new StringBuilder(); sb.AppendLine("using System;"); sb.AppendLine("using UnityEngine;"); sb.AppendLine(); sb.AppendLine("[Serializable]"); sb.AppendLine($"public class {className} : BaseExcelConfigRow"); sb.AppendLine("{"); for (int i = 0; i < headerRow.LastCellNum; i++) { string fieldName = headerRow.GetCell(i).StringCellValue; // 简单推断类型,实际项目需要更复杂的逻辑(如读取几行样本数据) string fieldType = InferFieldType(sheet, i); string safeFieldName = MakeValidIdentifier(fieldName); sb.AppendLine($" public {fieldType} {safeFieldName};"); } sb.AppendLine("}"); // 将sb.ToString()写入到 Assets/Scripts/Generated/ 目录下的.cs文件 File.WriteAllText($"{outputPath}/{className}.cs", sb.ToString()); AssetDatabase.Refresh(); // 刷新Unity,让新脚本被编译 } } private string InferFieldType(ISheet sheet, int colIndex) { // 简单推断:如果前几行都是数字,认为是int或double;如果是“true/false”,认为是bool;否则是string。 // 这是一个简化示例,真实工具需要更健壮的类型推断。 for (int rowIdx = 1; rowIdx <= Math.Min(5, sheet.LastRowNum); rowIdx++) { ICell cell = sheet.GetRow(rowIdx)?.GetCell(colIndex); if (cell != null && cell.CellType == CellType.Numeric) { double val = cell.NumericCellValue; if (Math.Abs(val % 1) < double.Epsilon) return "int"; else return "float"; } } return "string"; }6.3 工具使用流程与价值策划将ItemConfig.xlsx放入项目的Excels文件夹。程序员点击菜单“Tools/Excel Config/Generate All”,工具自动完成:生成ItemConfig.cs类、创建ItemConfigSO.asset资源、并将数据填充进去。游戏运行时只需加载这个.asset文件即可。这样彻底将数据配置与逻辑代码解耦,极大提升了协作效率。
这个案例涵盖了NPOI读取、类型推断、文件操作、Unity编辑器扩展和反射,是一个综合性的实战应用。虽然实现完整的工具需要更多细节处理(如错误处理、类型映射配置、数组支持等),但它清晰地展示了NPOI在Unity工作流中的强大潜力。