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处理由现代Excel(2007及以上版本)生成的.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项目中引入DLL
Unity管理外部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通常更安全。
- 在Unity项目的
使用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 | 铁剑 | Weapon | 15 | 120.5 |
| 1002 | 治疗药水 | Consumable | 0 | 25.0 |
我们的目标是将其读取到一个List<ItemConfig>中。
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) { // 假设列顺序固定:A=ID, B=Name, C=Type, D=AttackPower, E=Price 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 name="fileStream">Excel文件流</param> /// <param name="worksheetName">工作表名,默认为第一个</param> /// <param name="hasHeader">是否包含标题行</param> /// <returns></returns> public static List<ItemConfig> ReadItemsFromExcel(Stream fileStream, string worksheetName = null, bool hasHeader = true) { List<ItemConfig> itemList = new List<ItemConfig>(); // 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 = GetCellValue<int>(worksheet.Cells[row, 1]); item.Name = GetCellValue<string>(worksheet.Cells[row, 2]); item.Type = GetCellValue<string>(worksheet.Cells[row, 3]); item.AttackPower = GetCellValue<int>(worksheet.Cells[row, 4]); item.Price = GetCellValue<float>(worksheet.Cells[row, 5]); // 方法二:通过列名读取(更灵活,但需额外映射逻辑) // 例如,可以先读取第一行标题,建立“列名->列索引”的字典 itemList.Add(item); } } // using结束,package.Dispose()会自动调用,释放资源 return itemList; } /// <summary> /// 泛型方法,安全地获取单元格值并进行类型转换 /// </summary> private static T GetCellValue<T>(ExcelRange cell) { object cellValue = cell.Value; if (cellValue == null) { return default(T); // 返回类型的默认值(如int为0,string为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 List<ItemConfig> 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(适用于小文件或WebGL)Unity可以将二进制文件(如Excel)导入为TextAsset,但需要将导入类型设置为Binary。这种方式将文件内容直接嵌入到资源中。
- 将
Items.xlsx文件拖入Unity项目的Assets/Resources文件夹(或任何Resources子文件夹)。 - 选中该文件,在Inspector面板中,将
Texture Type设置为Default,并将Import Type下拉框选择为**Binary**(重要!)。 - 读取代码:
public void LoadFromTextAsset() { TextAsset excelTextAsset = Resources.Load<TextAsset>("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 使用列名映射而非列索引
上面的例子通过固定的列索引(1,2,3...)来读取,这非常脆弱。一旦策划在中间插入一列,所有代码的索引就都错位了。更健壮的方式是通过第一行的标题名来动态确定列索引。
public static List<Dictionary<string, object>> ReadExcelWithColumnMapping(Stream fileStream, string worksheetName = null) { var result = new List<Dictionary<string, 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. 读取第一行,建立列名到索引的映射 Dictionary<string, int> columnMap = new Dictionary<string, 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 Dictionary<string, 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可能得到的是公式字符串(如=A1+B1),而.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。
- DLL未正确导入:
- 解决方案:
- 确认
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 Framework(Unity 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方案。
- 升级Epplus版本:尝试使用最新的Epplus 5.x或6.x版本,它们对
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。
- 解决方案:
- 使用前面示例中的
GetCellValue<T>安全转换方法,它处理了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配置文件。
- 使用FileShare.ReadWrite:在打开
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() { GetWindow<ExcelToScriptableObjectEditor>("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 List<ItemConfig> items = new List<ItemConfig>(); 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 = GetCellValue<int>(worksheet.Cells[row, 1]); item.Name = GetCellValue<string>(worksheet.Cells[row, 2]); item.Type = GetCellValue<string>(worksheet.Cells[row, 3]); item.AttackPower = GetCellValue<int>(worksheet.Cells[row, 4]); item.Price = GetCellValue<float>(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.LoadAssetAtPath<ItemConfigDatabase>(outputAssetPath); if (database == null) { database = ScriptableObject.CreateInstance<ItemConfigDatabase>(); 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 GetCellValue<T>(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或二进制文件,游戏运行时享受极快的加载速度和零外部依赖。这套流程经过多个项目验证,能有效平衡开发效率与运行性能。