在Unity游戏开发中,策划同学经常需要调整角色属性、道具参数或关卡数据。想象一下这样的场景:每次策划修改了Excel表格里的数值,程序员都要手动复制粘贴到代码里重新打包,这种重复劳动不仅效率低下,还容易出错。我在参与一个RPG项目时就遇到过这种情况 - 因为手动同步数据导致线上版本的道具价格全部错乱,造成了不小的损失。
NPOI作为.NET平台处理Excel文件的神器,能够完美解决这个问题。它可以直接读取.xlsx文件内容,把表格数据转换为游戏可用的C#对象。更重要的是,它支持复杂的Excel操作,比如合并单元格、公式计算、数据验证等,完全能满足游戏配置的各种奇葩需求。
首先需要下载NPOI的DLL文件,这里有个坑要注意:Unity只认特定版本的.NET类库。推荐使用NPOI 2.5.5版本,亲测在Unity 2021 LTS上运行稳定。下载后把以下核心DLL放入Assets/Plugins文件夹:
注意:如果遇到"不兼容"报错,记得在Player Settings里将API Compatibility Level改为.NET 4.x。我去年就因为这个配置浪费了两天时间排查问题。
先看个最简单的创建Excel的例子:
csharp复制using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
public class ExcelDemo {
void CreateSimpleExcel() {
// 就像新建一个笔记本
IWorkbook workbook = new XSSFWorkbook();
// 添加一个工作表页
ISheet sheet = workbook.CreateSheet("角色属性");
// 第一行写表头
IRow headerRow = sheet.CreateRow(0);
headerRow.CreateCell(0).SetCellValue("角色ID");
headerRow.CreateCell(1).SetCellValue("攻击力");
// 第二行写数据
IRow dataRow = sheet.CreateRow(1);
dataRow.CreateCell(0).SetCellValue(1001);
dataRow.CreateCell(1).SetCellValue(150);
// 保存到StreamingAssets目录
string path = Path.Combine(Application.streamingAssetsPath, "RoleData.xlsx");
using(FileStream fs = new FileStream(path, FileMode.Create)) {
workbook.Write(fs);
}
}
}
这个例子虽然简单,但已经包含了最核心的三大件:Workbook(整个Excel文件)、Sheet(工作表)、Row/Cell(行和单元格)。建议新手先用这个模板练手,等熟悉了再尝试更复杂的功能。
游戏中最实用的场景是把Excel配置自动转为ScriptableObject。比如我们有个道具表:
| 道具ID | 名称 | 类型 | 价格 |
|---|---|---|---|
| 1001 | 血瓶 | 消耗品 | 50 |
| 1002 | 魔法剑 | 武器 | 200 |
对应的处理代码:
csharp复制[CreateAssetMenu]
public class ItemData : ScriptableObject {
public int itemID;
public string itemName;
public string itemType;
public int price;
}
public class ExcelToSOConverter {
public void Convert(string excelPath) {
// 加载Excel文件
using(FileStream fs = new FileStream(excelPath, FileMode.Open)) {
IWorkbook workbook = new XSSFWorkbook(fs);
ISheet sheet = workbook.GetSheetAt(0);
// 跳过表头行,从第2行开始读取
for(int i=1; i<=sheet.LastRowNum; i++) {
IRow row = sheet.GetRow(i);
// 创建ScriptableObject实例
ItemData item = ScriptableObject.CreateInstance<ItemData>();
item.itemID = (int)row.GetCell(0).NumericCellValue;
item.itemName = row.GetCell(1).StringCellValue;
item.itemType = row.GetCell(2).StringCellValue;
item.price = (int)row.GetCell(3).NumericCellValue;
// 保存为asset文件
string assetPath = $"Assets/Resources/Items/Item_{item.itemID}.asset";
AssetDatabase.CreateAsset(item, assetPath);
}
}
AssetDatabase.SaveAssets();
}
}
实际项目中经常遇到Excel数据不规范的情况,比如:
我们需要添加健壮的校验逻辑:
csharp复制try {
ICell cell = row.GetCell(0);
if(cell == null || cell.CellType == CellType.Blank) {
Debug.LogError($"第{i+1}行道具ID为空!");
continue;
}
if(cell.CellType != CellType.Numeric) {
Debug.LogError($"第{i+1}行道具ID不是数字!");
continue;
}
int id = (int)cell.NumericCellValue;
if(id <= 0) {
Debug.LogError($"第{i+1}行道具ID必须大于0");
continue;
}
} catch(Exception e) {
Debug.LogError($"解析第{i+1}行数据出错:{e.Message}");
}
建议为每种数据类型编写专门的校验方法,比如验证道具类型是否在枚举范围内、价格是否为正整数等。我在最近的项目中就因为漏了价格校验,导致有玩家刷出了-100金币的BUG。
当处理大型Excel文件(比如超过1万行的道具表)时,需要注意内存问题。NPOI默认会把整个文件加载到内存,这时可以采用分页读取策略:
csharp复制// 每次只处理500行
const int BATCH_SIZE = 500;
for(int startRow=1; startRow<=sheet.LastRowNum; startRow+=BATCH_SIZE) {
int endRow = Math.Min(startRow+BATCH_SIZE-1, sheet.LastRowNum);
// 处理当前批次
for(int i=startRow; i<=endRow; i++) {
// 解析逻辑...
}
// 手动清理内存
GC.Collect();
Resources.UnloadUnusedAssets();
}
另一个优化点是使用缓存池复用Cell对象,避免频繁创建销毁带来的GC压力。实测在MMO项目的NPC配置表处理中,这个优化让解析时间从3.2秒降到了1.8秒。
对于需要热更的游戏配置,可以采用"Excel→JSON→AB包"的流程:
关键代码示例:
csharp复制// Excel转JSON
JArray jsonArray = new JArray();
foreach(var row in sheet) {
JObject json = new JObject();
json["id"] = row.GetCell(0).NumericCellValue;
// 其他字段...
jsonArray.Add(json);
}
File.WriteAllText(jsonPath, jsonArray.ToString());
// 然后使用Unity的BuildPipeline打AB包
BuildPipeline.BuildAssetBundles(outputPath,
BuildAssetBundleOptions.ChunkBasedCompression,
BuildTarget.StandaloneWindows);
这种方案在我们公司的卡牌游戏中运行良好,策划可以随时调整卡牌数值,玩家下次登录就会自动获取最新配置,完全不需要发版本。
当Excel包含中文时,可能会遇到乱码。解决方法是在读取时指定编码:
csharp复制using(var fs = new FileStream(path, FileMode.Open, FileAccess.Read)) {
var workbook = new XSSFWorkbook(fs);
// 处理代码...
}
如果是从网络下载的Excel,可能需要先检查BOM头。有次我们接入运营商的配置表就栽在这个坑里,后来加了预处理代码:
csharp复制byte[] bytes = File.ReadAllBytes(path);
if(bytes.Length > 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) {
bytes = bytes.Skip(3).ToArray();
}
using(var ms = new MemoryStream(bytes)) {
var workbook = new XSSFWorkbook(ms);
// 处理代码...
}
Excel中的日期是个大坑,它实际上是用数字表示的(从1900年1月1日开始的天数)。正确解析方式:
csharp复制if(DateUtil.IsCellDateFormatted(cell)) {
DateTime date = cell.DateCellValue;
Debug.Log(date.ToString("yyyy-MM-dd"));
} else {
Debug.LogError("单元格不是日期格式!");
}
曾经有个活动配置因为日期解析错误导致全服活动提前一周开放,这个教训告诉我们:处理日期时一定要加上格式验证和异常捕获。
游戏配置通常分散在多个Sheet中,比如:
推荐使用字典来管理:
csharp复制var configDict = new Dictionary<string, ISheet>();
for(int i=0; i<workbook.NumberOfSheets; i++) {
ISheet sheet = workbook.GetSheetAt(i);
configDict[sheet.SheetName] = sheet;
}
// 然后按需获取
if(configDict.TryGetValue("角色成长曲线", out var growthSheet)) {
// 处理成长曲线...
}
在最近的项目中,我们甚至开发了一个基于注解的自动映射系统,通过在类上标记[ExcelSheet("表名")]属性,自动匹配对应的Sheet页,大幅减少了模板代码。