1. 项目概述:为什么Unity开发者需要掌握Excel读写?
在游戏开发,尤其是Unity项目中,我们经常需要处理大量的配置数据。从角色的属性表、物品的掉落列表,到关卡的配置信息、本地化的文本映射,这些数据如果硬编码在脚本里,不仅难以维护,策划和运营同学也无法直接参与修改。Excel表格,凭借其直观的网格界面和强大的数据处理能力,自然成为了游戏行业最通用的数据配置工具之一。
然而,Unity本身并不直接支持读写Excel文件(.xlsx, .xls)。直接操作这些二进制文件格式复杂,且涉及商业授权问题。因此,我们需要借助第三方库来充当Unity与Excel之间的“翻译官”。这个需求催生了多种技术方案,从早期的COM组件调用到如今主流的纯C#解析库。掌握一套稳定、高效的Excel读写方案,意味着你能将游戏的数据层与逻辑层清晰分离,实现“策划改表,游戏热更”的敏捷开发流程,这对于提升团队协作效率和项目迭代速度至关重要。
本文将为你拆解在Unity中实现Excel文件完整读写操作的几种主流方案,深入剖析其背后的原理、适用场景、具体实现步骤以及我踩过的无数个坑。无论你是想实现一个简单的配置表加载器,还是构建一个复杂的数据编辑和导出工具链,这里都有你需要的干货。
2. 核心方案选型:几种主流技术路径的深度对比
面对Unity读写Excel的需求,新手最容易犯的错误就是抓起第一个搜到的方案就用,结果在项目后期遇到编码、性能、跨平台或授权等各种问题。我们先来系统性地看看有哪几条路可以走,以及为什么我最终会推荐其中一两种。
2.1 方案一:利用 .NET 的 COM 组件(Microsoft.Office.Interop.Excel)
这是最“原始”也最强大的方法。它通过调用安装在系统上的Microsoft Excel应用程序的COM接口来操作Excel文件。
工作原理:你的C#脚本通过Microsoft.Office.Interop.Excel这个程序集,与本地安装的Excel进程进行进程间通信(IPC)。你代码中的每一个操作,比如打开工作簿、读取单元格,实际上都是在向Excel.exe发送指令,由它来执行并返回结果。
优点:
- 功能完整:由于直接调用Excel本身,所有Excel的高级功能(公式计算、图表、样式、VBA宏等)都能支持。
- 无需关心文件格式:无论是.xls还是.xlsx,Excel自己处理,兼容性最好。
致命缺点:
- 依赖本地Excel安装:目标机器上必须安装有相应版本的Microsoft Excel。这对于要分发给玩家的游戏客户端是完全不可接受的。
- 仅限Windows:COM是Windows特有的技术,这意味着你的Unity编辑器或游戏无法在Mac或Linux上运行相关代码。
- 性能开销大:启动整个Excel进程开销巨大,读写大量数据时速度慢。
- 线程与稳定性问题:COM调用对线程模型有要求,处理不当容易导致Excel进程无法正常关闭,造成“僵尸进程”和内存泄漏。
实操心得:这个方案仅适合在Unity Editor环境下开发辅助工具,比如策划用的数据表编辑插件、批量导出资源的工具等。绝对不要将它用于运行时(Runtime)的游戏逻辑中。我曾在一个工具开发中用过,必须记得在代码中显式释放每一个COM对象,调用
Marshal.FinalReleaseComObject(),并确保excelApp.Quit()被执行,否则编辑器会卡死。
2.2 方案二:使用轻量级纯C#解析库(如 EPPlus, NPOI)
这是目前Unity社区在运行时(Runtime)和编辑器(Editor)两种环境下最主流、最推荐的方案。它们不依赖Office软件,直接解析Excel文件的Open XML格式(.xlsx)或二进制格式(.xls)。
工作原理:以EPPlus为例,它是一个开源库,完全用C#实现了对OOXML(Office Open XML)格式的解析。它直接将.xlsx文件(本质上是一个ZIP压缩包,里面包含XML文件)解压并在内存中构建文档对象模型(DOM),允许你以编程方式访问和修改其中的元素。
优点:
- 零依赖,跨平台:纯C#实现,只要Unity的Mono或IL2CPP支持,就能在Windows、Mac、Linux、iOS、Android等所有Unity支持的平台上运行。
- 性能较好:直接在内存中操作数据,避免了进程间通信的巨大开销。
- 功能足够:支持单元格读写、公式(部分计算)、基础样式(字体、颜色、边框)、合并单元格等游戏配置表所需的绝大多数功能。
- 开源免费:通常采用MIT或Apache等宽松许可证,可放心用于商业项目。
缺点:
- 不支持Excel的所有高级特性(如复杂的图表、数据透视表、VBA)。
- 对于极端复杂的.xlsx文件,解析可能会稍慢或遇到兼容性问题(但游戏配置表通常很简单)。
NPOI是另一个强大的选择,它源自Java的POI项目,功能比EPPlus更全面,同时支持.xls和.xlsx格式,但库的体积相对更大,API设计也更“Java风格”一些。
核心建议:对于99%的Unity项目数据配置需求,在运行时请毫不犹豫地选择EPPlus或NPOI。EPPlus的API更简洁,对于.xlsx文件的支持非常好,是我的首选。NPOI则在需要兼容旧版.xls格式时是唯一选择。
2.3 方案三:转换为中间格式(如 CSV, JSON, ScriptableObject)
这不是直接读写Excel,而是一种更架构化的思路:将Excel作为数据源,在编辑阶段或构建阶段将其转换为Unity更易读取的格式。
工作流程:
- 策划在Excel中编辑数据。
- 通过一个编辑器工具(使用EPPlus/NPOI或COM方案),将Excel表格导出为CSV、JSON或直接生成C#类实例并保存为ScriptableObject资产。
- 游戏运行时,直接读取轻量级的CSV/JSON文件,或引用ScriptableObject。
优点:
- 运行时性能极佳:CSV/JSON解析速度飞快,ScriptableObject更是Unity原生资源,直接反序列化到内存。
- 安全性好:剥离了Excel的复杂结构,只保留纯数据,避免了解析库可能存在的漏洞。
- 内存占用小:去除了所有样式、公式等元信息。
- 便于版本管理:CSV/JSON是纯文本,可以用Git等工具进行diff和merge,方便查看数据变更。
缺点:
- 增加了“导出”这个构建步骤,需要编写额外的编辑器扩展工具。
- 失去了在运行时动态修改Excel文件的能力(但这通常也不是运行时需求)。
架构层面的思考:对于大型项目,我强烈推荐采用“Excel编辑 -> 导出为JSON/ScriptableObject -> 运行时加载”的流水线。这实现了数据与逻辑的彻底解耦。策划可以自由使用Excel的所有功能进行数据设计和验证,而游戏运行时则享受高效、安全的数据加载体验。我们团队的项目就采用这种方式,通过Jenkins构建时自动触发导出脚本,保证了数据与代码的同步。
3. 实战:使用EPPlus在Unity中实现Excel读写
理论分析完毕,我们进入实战环节。我将以EPPlus为例,展示如何在Unity中集成并使用它来完成最常见的读写任务。为什么选EPPlus?因为它API友好,文档齐全,社区活跃,且对.xlsx格式支持完美。
3.1 环境准备与库的导入
EPPlus是一个.NET Standard 2.0库,与Unity的.NET兼容性良好。导入Unity有以下几种方式:
方法一:通过Unity的Package Manager (UPM) 从Git URL添加(推荐)这是最干净的方法,便于版本管理。
- 打开Unity,进入
Window -> Package Manager。 - 点击左上角的
+号,选择Add package from git URL...。 - 输入EPPlus的官方Git仓库地址:
https://github.com/EPPlusSoftware/EPPlus.git - 等待Unity下载和编译。你可以指定版本,例如
https://github.com/EPPlusSoftware/EPPlus.git#v7.0.0。
方法二:下载DLL文件
- 从NuGet(https://www.nuget.org/packages/EPPlus/)或GitHub Releases下载
EPPlus.dll。 - 在Unity项目的
Assets文件夹下,创建一个Plugins文件夹(如果还没有)。 - 将
EPPlus.dll复制到Plugins中。对于跨平台,你可能需要将DLL放在Plugins/x86_64等平台特定目录下。
方法三:使用.NET的dotnet add package命令如果你的项目已切换为使用PackageReference的NuGet方式(Unity 2019.3+的部分新项目模板支持),可以在项目根目录的.csproj文件所在位置执行命令。
注意事项:导入后,如果遇到命名空间错误,请检查Unity的API Compatibility Level(在Player Settings中)。确保它设置为.NET Standard 2.0或.NET Framework(而不是较旧的
.NET 2.0 Subset),因为EPPlus依赖一些较新的API。
3.2 核心API解析与基础读写操作
成功导入后,就可以在C#脚本中使用using OfficeOpenXml;了。EPPlus的核心对象模型非常直观:
ExcelPackage: 代表一个Excel文件(.xlsx包)。ExcelWorkbook: 代表一个工作簿,包含多个工作表。ExcelWorksheet: 代表一个工作表,是我们操作的主要对象。ExcelRange: 代表一个或一组单元格,用于读写数据。
场景一:创建一个新的Excel文件并写入数据假设我们要生成一个物品配置表。
using OfficeOpenXml; using System.IO; public class ExcelWriterExample { public void CreateItemTable() { // 设置EPPlus的许可证上下文(社区版免费,但需要设置,否则写入时会抛异常) ExcelPackage.LicenseContext = LicenseContext.NonCommercial; // 非商业用途 // 如果是商业项目,需要购买商业许可证,并设置为 LicenseContext.Commercial // 创建一个新的Excel包 using (var package = new ExcelPackage()) { // 添加一个工作表,命名为“Items” ExcelWorksheet worksheet = package.Workbook.Worksheets.Add("Items"); // 写入表头 worksheet.Cells[1, 1].Value = "ID"; worksheet.Cells[1, 2].Value = "Name"; worksheet.Cells[1, 3].Value = "Attack"; worksheet.Cells[1, 4].Value = "Price"; // 可以设置表头样式 using (var headerRange = worksheet.Cells[1, 1, 1, 4]) { headerRange.Style.Font.Bold = true; headerRange.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid; headerRange.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.LightGray); } // 写入数据行 worksheet.Cells[2, 1].Value = 1001; worksheet.Cells[2, 2].Value = "Iron Sword"; worksheet.Cells[2, 3].Value = 15; worksheet.Cells[2, 4].Value = 50.5f; // 支持各种数据类型 worksheet.Cells[3, 1].Value = 1002; worksheet.Cells[3, 2].Value = "Health Potion"; worksheet.Cells[3, 3].Value = 0; // 非武器,攻击力为0 worksheet.Cells[3, 4].Value = 20; // 自动调整列宽以适应内容(这是一个很有用的功能) worksheet.Cells[worksheet.Dimension.Address].AutoFitColumns(); // 确定保存路径。在Editor下,我们可以保存到项目路径 string filePath = Path.Combine(Application.dataPath, "..", "ExportedConfigs", "Items.xlsx"); // 确保目录存在 Directory.CreateDirectory(Path.GetDirectoryName(filePath)); // 将Excel包保存为文件 File.WriteAllBytes(filePath, package.GetAsByteArray()); Debug.Log($"Excel文件已保存至:{filePath}"); } // using语句结束时会自动释放package占用的资源 } }场景二:读取一个已有的Excel文件现在我们来读取刚才生成的物品表。
public class ExcelReaderExample { public List<ItemData> LoadItemTable(string filePath) { List<ItemData> itemList = new List<ItemData>(); // 检查文件是否存在 if (!File.Exists(filePath)) { Debug.LogError($"文件不存在:{filePath}"); return itemList; } // 设置许可证上下文 ExcelPackage.LicenseContext = LicenseContext.NonCommercial; // 通过文件信息加载Excel包 FileInfo fileInfo = new FileInfo(filePath); using (var package = new ExcelPackage(fileInfo)) { // 获取名为“Items”的工作表,如果不存在则获取第一个 ExcelWorksheet worksheet = package.Workbook.Worksheets["Items"] ?? package.Workbook.Worksheets[0]; if (worksheet == null) { Debug.LogError("工作表中未找到‘Items’工作表,且没有其他工作表。"); return itemList; } // 获取工作表的数据范围 int rowCount = worksheet.Dimension.Rows; // 总行数 int colCount = worksheet.Dimension.Columns; // 总列数 // 通常第一行是表头,我们从第二行开始读取数据 for (int row = 2; row <= rowCount; row++) { // 判断整行是否为空,避免读取无意义的空行 bool isEmptyRow = true; for (int col = 1; col <= colCount; col++) { if (worksheet.Cells[row, col].Value != null) { isEmptyRow = false; break; } } if (isEmptyRow) continue; // 读取单元格数据 ItemData item = new ItemData(); // 使用Value属性获取原始对象,再根据预期类型转换 item.ID = Convert.ToInt32(worksheet.Cells[row, 1].Value); item.Name = worksheet.Cells[row, 2].Value?.ToString() ?? ""; // 处理空值 item.Attack = Convert.ToInt32(worksheet.Cells[row, 3].Value); // 注意:Excel中数字可能以double形式存储,读取时需小心 object priceValue = worksheet.Cells[row, 4].Value; if (priceValue != null) { // 使用更安全的方式转换,避免因类型问题导致异常 if (priceValue is double) item.Price = (float)(double)priceValue; else if (priceValue is int) item.Price = (int)priceValue; else float.TryParse(priceValue.ToString(), out item.Price); } itemList.Add(item); } } Debug.Log($"成功从 {filePath} 加载了 {itemList.Count} 个物品。"); return itemList; } } // 对应的数据类 [System.Serializable] public class ItemData { public int ID; public string Name; public int Attack; public float Price; }3.3 高级功能与性能优化技巧
基础的读写只是开始,在实际项目中,我们还需要处理更复杂的情况。
1. 处理合并单元格合并单元格在配置表中很常见,比如一个大类标题。EPPlus提供了Merge和Unmerge属性。
// 合并A1到D1的单元格 worksheet.Cells["A1:D1"].Merge = true; worksheet.Cells["A1"].Value = "武器列表"; // 读取合并单元格的值时,只需读取合并区域的左上角单元格 var mergedCellValue = worksheet.Cells["A1"].Value; // 即使读取B1,也会得到A1的值2. 应用单元格样式让导出的表格更美观。
using (var range = worksheet.Cells["A2:D2"]) { range.Style.Font.Bold = true; range.Style.Font.Color.SetColor(System.Drawing.Color.Red); range.Style.Border.Top.Style = ExcelBorderStyle.Thin; range.Style.Border.Bottom.Style = ExcelBorderStyle.Thin; range.Style.Fill.PatternType = ExcelFillStyle.Solid; range.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.FromArgb(255, 255, 200)); // 浅黄色背景 }3. 使用公式EPPlus支持写入和读取公式,但注意:在非Windows环境下,EPPlus可能无法计算公式的结果,它只能保存公式字符串。
// 写入公式,计算总价 = 数量 * 单价 worksheet.Cells["E2"].Formula = "C2*D2"; // 如果你需要计算结果,并且环境支持,可以调用Calculate() // worksheet.Calculate();4. 性能优化:批量读写与流式API当处理成千上万行数据时,逐单元格读写会非常慢。EPPlus提供了LoadFromCollection和LoadFromArrays等批量方法。
// 假设有一个ItemData的列表 List<ItemData> hugeItemList = GetHugeList(); using (var package = new ExcelPackage()) { var worksheet = package.Workbook.Worksheets.Add("Items"); // 将列表直接加载到工作表中,从A1开始 worksheet.Cells["A1"].LoadFromCollection(hugeItemList, true); // 参数true表示打印表头(使用类的属性名) }对于超大型数据,可以考虑使用EPPlus的ExcelRangeBase.Value属性直接设置一个二维数组,这是最快的写入方式之一。
5. 内存管理与异常处理ExcelPackage实现了IDisposable接口,务必使用using语句或在finally块中确保其被释放,尤其是在编辑器工具中频繁操作时,避免内存泄漏。 读取时,对单元格值的类型转换要做好防御性编程,使用TryParse或as关键字,避免因策划在表格中输入了非预期内容(如字符串“N/A”)导致程序崩溃。
4. 构建数据管道:从Excel到游戏可用的资源
如前所述,在运行时直接解析Excel并非最佳实践。更专业的做法是建立一条数据流水线。下面我将展示一个完整的编辑器工具示例,它监听Excel文件的变化,自动将其转换为Unity原生的ScriptableObject资源。
4.1 创建自定义编辑器导入工具
我们创建一个ExcelImporter编辑器窗口,并利用FileSystemWatcher或Unity的AssetPostprocessor来触发自动转换。
#if UNITY_EDITOR using UnityEditor; using UnityEngine; using OfficeOpenXml; using System.IO; using System.Collections.Generic; public class ExcelToScriptableObjectImporter : AssetPostprocessor { // 当Assets目录下的任何文件发生变化时,Unity会调用此方法 static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { foreach (string assetPath in importedAssets) { // 检查是否是.xlsx文件,并且位于我们监控的特定目录下,例如 "Assets/Configs/Excel/" if (assetPath.EndsWith(".xlsx") && assetPath.Contains("Assets/Configs/Excel/")) { ConvertExcelToScriptableObject(assetPath); } } } static void ConvertExcelToScriptableObject(string excelFilePath) { Debug.Log($"开始转换Excel文件: {excelFilePath}"); ExcelPackage.LicenseContext = LicenseContext.NonCommercial; FileInfo excelFile = new FileInfo(excelFilePath); using (var package = new ExcelPackage(excelFile)) { // 假设每个Excel文件对应一种类型的配置,工作表名就是类型名 foreach (var worksheet in package.Workbook.Worksheets) { string configType = worksheet.Name; string outputAssetPath = excelFilePath.Replace(".xlsx", $"_{configType}.asset").Replace("Excel/", "ScriptableObjects/"); // 根据工作表名,决定生成哪种类型的ScriptableObject switch (configType) { case "Items": CreateItemConfig(worksheet, outputAssetPath); break; case "Monsters": CreateMonsterConfig(worksheet, outputAssetPath); break; // ... 添加其他类型 default: Debug.LogWarning($"未识别的配置表类型: {configType},跳过。"); break; } } } // 刷新Asset数据库,让Unity识别新生成的.asset文件 AssetDatabase.Refresh(); Debug.Log($"Excel文件转换完成: {excelFilePath}"); } static void CreateItemConfig(ExcelWorksheet worksheet, string outputPath) { // 读取数据逻辑,与之前的ExcelReaderExample类似 List<ItemData> items = new List<ItemData>(); int rowCount = worksheet.Dimension.Rows; int colCount = worksheet.Dimension.Columns; for (int row = 2; row <= rowCount; row++) { // ... 解析每一行数据,创建ItemData对象 ... ItemData item = new ItemData(); item.ID = GetCellValue<int>(worksheet, row, 1); item.Name = GetCellValue<string>(worksheet, row, 2); // ... 填充其他字段 items.Add(item); } // 创建ScriptableObject实例并保存 ItemConfig asset = ScriptableObject.CreateInstance<ItemConfig>(); asset.Items = items; // 确保输出目录存在 string directory = Path.GetDirectoryName(outputPath); if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); AssetDatabase.CreateAsset(asset, outputPath); EditorUtility.SetDirty(asset); AssetDatabase.SaveAssets(); } // 一个安全的泛型方法用于获取单元格值 static T GetCellValue<T>(ExcelWorksheet ws, int row, int col) { object val = ws.Cells[row, col].Value; if (val == null) return default(T); try { return (T)Convert.ChangeType(val, typeof(T)); } catch { Debug.LogWarning($"在[{row},{col}]转换类型失败,期望{typeof(T)},实际是{val.GetType()}。"); return default(T); } } } // 对应的ScriptableObject类 [CreateAssetMenu(fileName = "ItemConfig", menuName = "Game Configs/Item Config")] public class ItemConfig : ScriptableObject { public List<ItemData> Items; } #endif4.2 运行时加载与使用
转换完成后,运行时加载就变得极其简单和高效:
public class GameManager : MonoBehaviour { public ItemConfig itemConfig; // 可以在Inspector中直接拖拽赋值 void Start() { // 或者通过Resources.Load动态加载(如果放在Resources文件夹下) // itemConfig = Resources.Load<ItemConfig>("Configs/ScriptableObjects/Items_Items"); if (itemConfig != null) { foreach (var item in itemConfig.Items) { Debug.Log($"物品: {item.Name}, 攻击力: {item.Attack}"); // 在这里初始化游戏中的物品系统 } } } }这种方式的优势非常明显:
- 加载速度快:ScriptableObject是Unity序列化资源,加载速度远快于解析Excel。
- 内存友好:数据直接存在于内存中,无需额外的解析库开销。
- 类型安全:在导入阶段就完成了类型检查和转换,运行时无需担心数据格式错误。
- 支持热重载:在Editor模式下,修改ScriptableObject并保存后,游戏运行时可以立即看到变化,非常适合调试。
5. 常见问题、疑难杂症与排查实录
即使方案选对了,在集成和使用的过程中,你也一定会遇到各种“坑”。下面是我总结的一些典型问题及其解决方案。
5.1 平台兼容性与构建错误
问题:在Editor中运行正常,但打包(尤其是IL2CPP)到移动端(iOS/Android)时,出现FileNotFoundException(找不到EPPlus相关dll)或TypeLoadException等链接错误。
原因与解决方案:
- DLL导入设置不正确:确保EPPlus的DLL文件(如果以DLL方式导入)的平台兼容性设置正确。在Unity中选中DLL,在Inspector面板中,确保“Any Platform”被取消,并为你需要的平台(如iOS, Android, Standalone)勾选上。对于不支持的平台(如WebGL),务必取消勾选。
- IL2CPP代码裁剪:IL2CPP在构建时会进行代码裁剪,移除它认为“未使用”的代码。EPPlus使用了大量的反射和动态代码生成,很容易被错误裁剪。解决方案是在项目的
Assets目录下创建一个名为link.xml的文件,告诉IL2CPP不要裁剪指定的程序集或类型。<!-- Assets/link.xml --> <linker> <assembly fullname="EPPlus" preserve="all"/> <!-- 如果EPPlus依赖了其他程序集,也需要添加 --> <assembly fullname="System.Drawing.Common" preserve="all"/> </linker> - 使用UPM包:通过Git URL导入的UPM包,Unity会负责为不同平台编译源码,通常能更好地处理跨平台问题,比直接导入预编译的DLL更可靠。
5.2 性能瓶颈与内存泄漏
问题:读取一个几兆的Excel文件感觉卡顿,或者频繁操作后编辑器内存持续增长。
优化策略:
- 针对性读取:如果表格很大,但只需要其中一小部分数据,不要使用
worksheet.Dimension读取整个范围。使用worksheet.Cells["A1:B10"]精确指定范围。 - 禁用计算链:如果你的Excel文件包含大量公式,在加载时设置
ExcelPackage.Workbook.CalcMode = ExcelCalcMode.Manual;可以避免EPPlus尝试计算它们,提升加载速度。 - 及时释放资源:反复强调,
ExcelPackage一定要放在using块中。对于在编辑器工具中长时间运行的程序,考虑定期手动触发垃圾回收(System.GC.Collect()),但这只是权宜之计,根本还是确保对象被正确释放。 - 使用流式读取(对于只读场景):EPPlus也提供了较低级别的流式API(
ExcelReader),它不需要将整个文档加载到DOM中,对于超大文件的只读操作可以节省大量内存,但API也更复杂。
5.3 数据格式与类型转换的坑
问题:策划在Excel里输入了“123abc”,你的代码用Convert.ToInt32读取,直接崩溃。
防御性编程实践:
- 永远不要假设单元格里是你期望的类型。使用
TryParse系列方法。if (int.TryParse(cellValue?.ToString(), out int result)) { // 使用result } else { // 记录错误或使用默认值 Debug.LogError($"在单元格[{row},{col}]无法将值‘{cellValue}’解析为整数。"); } - 对于可能为空的单元格,使用空值合并运算符(
??)和条件成员访问(?.)来安全处理。string name = worksheet.Cells[row, col].Value?.ToString() ?? "DefaultName"; - 统一处理日期:Excel中的日期是浮点数。EPPlus提供了
GetValue<DateTime>()方法,但最好先判断单元格的Style.Numberformat.Format是否包含日期格式字符,或者使用DateTime.FromOADate方法进行转换。
5.4 中文与编码问题
问题:读取的Excel文件中,中文字符显示为乱码。
解决方案:
- 确保文件本身编码正确:Excel文件内部使用Unicode(UTF-8),通常不会有问题。乱码可能发生在你将其另存为CSV或其他文本格式时。保存CSV时,选择“UTF-8 BOM”编码可以最大程度保证兼容性。
- 在Unity中正确设置文本资产导入:如果你最终是读取导出的CSV/JSON文本文件,在Unity中选中该文件,在Inspector中将“Encoding”从默认的“Auto”改为“UTF-8”。
- EPPlus读取本身:EPPlus处理内部字符串通常是正确的。如果遇到乱码,检查是否在某个环节(如将单元格值写入到Unity的UI Text)出现了编码转换错误。
5.5 在Unity WebGL平台上的特殊限制
问题:WebGL平台无法直接访问本地文件系统。
根本解决方案:对于WebGL项目,绝对不能在运行时尝试读取用户本地路径(如Application.dataPath)下的Excel文件。数据必须通过以下方式提供:
- 预转换:必须采用“Excel -> 导出为JSON/二进制 -> 随游戏资源发布”的流程。将数据文件放在
StreamingAssets或作为AssetBundle的一部分。 - 服务器加载:从网络服务器下载数据文件(JSON/二进制),使用
UnityWebRequest进行加载。 - 用户上传:如果确实需要用户上传Excel文件,可以使用HTML5的File API通过
<input type="file">元素让用户选择文件,然后通过JS桥接将文件内容(ArrayBuffer)传递到Unity中,再由C#端的EPPlus进行解析。这个过程相对复杂,且文件大小受浏览器限制。
最后,关于版本控制,切记不要将生成的Excel文件(.xlsx)或大型的ScriptableObject资产直接以二进制形式纳入Git,这会导致仓库体积暴增且无法diff。应该将Excel文件作为“源文件”,而将导出的JSON或生成ScriptableObject的编辑器脚本纳入版本控制,在构建时重新生成数据资源。