资讯详情

资讯详情

Unity配置表全链路改造:自研读表工具从设计到落地

做游戏的人应该都有这种体验配置表这东西看着不起眼但你对它的容忍度直接决定了你晚上几点下班。我们团队上一套方案的链路是策划用 Excel 维护数值通过一个第三方导入工具转成 ScriptableObject 再在代码里引用这套流程跑了两年多稳定是稳定但到了要上热更、要多人协作、要频繁调数值的节点上各种问题全冒出来了。上周我们花了半天时间把 Unity 配置表从策划出表到客户端读取的整条链路全部换成了自研读表工具。这篇把从设计到落地再到踩坑的全过程整理出来给想动手整顿这套基建的团队做个参考。1. 为什么要动配置表这条链路先说旧方案的具体样子你就能理解我们为什么非要换。之前的流程是策划在 Excel 里维护一张张数值表填完之后跑一遍导出工具工具会读取 Excel 数据序列化成 ScriptableObject 文件放在 Unity 工程里程序员在脚本里直接拖引用或者通过 AssetDatabase 加载。这套流程在项目早期完全够用团队小、表少、改得也不频繁但到了运营期就变了。第一是合并冲突问题。多人同时改一张表Excel 文件本身是二进制格式或者你保存成了 xlsbGit 没法做细粒度合并两个人同时改了不同 Sheet 也会冲突解决冲突只能靠人肉重新填一遍。到了活动版本这种节点策划集中改表这种冲突几乎每周都要发生一次。第二是热更支持基本为零。ScriptableObject 是 Unity 资源虽然能打进 AssetBundle 里发出去但我们的热更流程走的是自研的 Lua 层方案客户端主体逻辑在 Lua 里跑旧方案等于在 C# 层多了一道转换数值读取要跨语言咖一层接口性能损耗倒是小事关键是链路上多一个环节排查问题的时候就得两头看。第三是改表的编译链路太长。策划改一个数值要经过导出、生成资源、等编辑器刷新、再看效果。如果导出工具本身对某些格子的处理有兼容性问题还得拉着程序一起看。半天时间就耗在这种来回上了。我们当时列了一个改造目标清单出表入口继续用 Excel这个不能动策划的熟练度在这里但导出产物改成自定义的紧凑二进制格式运行时由一个自研的读取器统一加载同时根据表结构生成强类型 C# 访问代码再暴露给 Lua 层。表格的版本信息、多语言字段、主键索引这些全部在设计里就要支持好不再靠打补丁。这个目标看着大但拆开之后其实都是很标准的工程问题难的是别在方案上反复纠结。我们当时定了一条原则能用代码生成解决的问题绝不在运行时做反射能用纯数据解决的绝不在代码里写死表结构。这两条原则在后面帮我们挡掉了不少坑。2. 方案选型为什么没有继续用现成工具市面上不是没有现成方案我们内部也讨论过要不要基于开源项目二次开发最后否掉了原因有三个。2.1 现成方案的核心问题很多第三方配置表工具的设计目标是“全平台通用”所以它们要兼容导出 Excel、Google Sheets、CSV 多套上游又要输出 C#、Java、C 多套代码还附带一个编辑器窗口。功能是齐全但代码量一大可定制性就下降。我们需要的是“Lua 层友好、支持热更、加载路径可控”这三件事现成工具要么不支持 Lua 代码生成要么生成出来的 C# 代码里耦合了它自己的运行时想绕都绕不开。另一个问题是黑盒依赖。第三方工具一旦版本升级或者作者停止维护整个链条就悬了。我们上一套脚本依赖一个更新不太活跃的插件遇到 Unity 版本升级就得等兼容性更新这种被动等的感觉很难受。自研工具虽然前期要投入一点精力但维护权完全在自己手里出问题随时能改。2.2 自研方案的核心权衡自研读表工具有三条路线可以走。第一条是 CSV 加运行时反射。把 Excel 导成 CSV运行时按列名反射绑定到类上。优点是开发最快缺点是反射有性能开销而且字段类型一旦对不上错误要到运行期才暴露。第二条是 Excel 直读。直接引用第三方库在运行时读 Excel 文件。这个方案最省事但最不推荐Excel 格式本身就不是为高性能读取设计的而且你的客户端还得打包一堆解析库进去热更包体积和加载时间都要付出代价。第三条是生成强类型代码加自定义二进制格式也就是我们最终选的方案Excel 作为编辑入口导出阶段把数据转成紧凑的二进制文件同时根据表头生成 C# 的强类型访问类运行时只加载自定义格式的二进制。选第三条的核心逻辑很简单配置文件读得勤、量又大所以解析开销要放在导出期而不是运行期字段名和类型对应这种错误要在编译期暴露而不是等玩家跑到某个界面才崩。代码生成把这两件事都解决了。还有个反直觉的原因二进制格式看着不直观调试的时候没有 JSON 那么好打开看。但我们用了一个折中方案——同时导出一份 JSON 格式的调试副本只在编辑器环境用正式包完全不打包进去。这样既保留了运行时的高效又没牺牲排查问题的便利性。3. 工具设计从表结构定义到强类型访问这一节是整个改造的核心我把设计分成四块表结构描述、代码生成器、运行时加载器、编辑器配套工具。四块之间是单向依赖表结构描述是入口代码生成器消费它产出 C# 和 Lua 代码运行时加载器消费生成的代码和二进制数据。3.1 表结构描述用 JSON 定义一切我们没有像很多工具那样约定一套 Excel 表头规范比如第一行类型、第二行注释之类而是单独搞了一个 schema 文件来描述每张表。这个 schema 本身是 JSON放在工程的一个固定目录下策划和程序都能看。一张怪物表的 schema 大概长这样{ name: Monster, file: monster, primaryKey: id, fields: [ { name: id, type: int }, { name: name, type: string, loc: true }, { name: modelId, type: int }, { name: skillIds, type: int[] }, { name: dropIds, type: int[], default: [] }, { name: rarity, type: enum_rarity } ] }这里解释几个关键字段。primaryKey 决定运行时的索引方式如果是单个主键生成的是 Dictionary[int, Monster]如果是组合主键比如活动 ID 加关卡 ID生成的是 Dictionary 套 Dictionary 的外层索引。类型系统上我们支持 int、float、string、bool、枚举、数组以及后来加的嵌套结构体。数组用 type 名字带方括号表示默认值走 default 字段这个字段很关键。loc 标记多语言字段。标记了 loc 的字符串字段导出的时候不会直接把 Excel 里的中文写进二进制而是写进一个语言 key实际文案由本地化系统在运行时根据当前语言环境去查表。这要求策划在 Excel 里填的先是 key翻译文案统一维护在另一个语言表里。我们把多语言从配置表里拆出去是因为数值表经常要改文案也经常要改两种改动频率混在一起会让表结构变动变得很频繁。枚举字段的处理也值得说。C# 里枚举本质上就是整数但我们不希望策划填 0、1、2 这种魔法数字所以在 schema 里允许用枚举类型同时在字段说明里维护一份枚举值映射表策划在 Excel 里可以直接填枚举名比如 hero、monster、npc导出的时候转成对应的整数值。这样表可读性好代码里写判断的时候也不用满屏找魔法数字。schema 本身还支持继承和多表合并比如公共字段表定义一份各子表继承。但这个功能我们实际用得不多因为表结构设计得足够扁平继承只会增加理解成本所以后来干脆不推荐了。3.2 代码生成器从 schema 到强类型类有了 schema代码生成器的工作就是把 JSON 描述转成两套代码C# 访问类以及 Lua 侧的轻量绑定。C# 侧生成的类和手写类长得几乎一样只是所有字段的读取都通过一个内部索引器。生成的代码核心部分示意如下public class Monster : IConfigRow { private ConfigTable _table; private int _index; public int Id _table.ReadInt(_index, 0); public string Name _table.ReadString(_index, 1); public int ModelId _table.ReadInt(_index, 2); public int[] SkillIds _table.ReadIntArray(_index, 3); public int[] DropIds _table.ReadIntArray(_index, 4); public Rarity Rarity (Rarity)_table.ReadInt(_index, 5); public void Bind(ConfigTable table, int index) { _table table; _index index; } }为什么不用属性直接存值而是每次调用都读表原因是内存。一个大型项目配置表动辄几万行每行数据如果都物化成独立对象光对象头和字段引用就能吃几十兆内存。我们让对象只保存一个索引数据都集中存储在二进制字节数组里按行偏移读取这样每行记录的开销就只是一个对象引用加一个整数索引。代码生成器本身是基于简单的模板拼接写的没有引入重型生成框架。核心逻辑是遍历 schema 字段按类型映射到对应的读取函数。读取函数的分派逻辑是一张映射表int 对应 ReadIntstring 对应 ReadStringint[] 对应 ReadIntArray枚举对应 ReadInt 再强转。Lua 侧的绑定代码类似只不过通过一个 metatable 把字段名映射到读取函数。这块说到运行时加载器再展开。3.3 运行时加载器二进制布局与读取运行时加载器是整个工具链里最需要注意性能的部分因为游戏运行中读配置表是高频率操作而且很多是每帧都可能触发的。二进制文件的布局我们设计成三段式头部、字符串池、行数据区。头部是一个固定长度的元信息块记录版本号、行数、每行字节数、字段数量以及各字段在行内的偏移量表。解析头部只需要一次小的内存拷贝然后在内存里缓存偏移表之后每次读取都直接定位到行数据区不做任何额外解析。字符串池的引入是为了节省内存。配置表里大量字段是重复出现的字符串比如名字、描述、图标路径如果每行都独立存一份字符串内存会爆炸。我们把所有字符串去重后存进一个池子行数据区里只存一个 int 索引读取时按索引取字符串。行数据区是固定宽度的存储。每一行的大小在导出时就算好了比如一行有 6 个字段其中两个 int、两个 int 数组、两个字符串索引那么每行长度是固定的。固定宽度带来的好处是可以用乘法直接定位到任意一行不需要逐行扫描随机访问时间复杂度做到 O(1)。数组字段的存储是长度前缀加连续数据。行内先存一个 int 表示数组长度再紧接着存数组元素。读取数组的时候需要先从行里取长度再取数据区域但因为我们知道数组在行内的偏移是固定的所以这个过程也很快。加载流程上我们提供了两种模式同步加载和异步加载。小表直接异步大表比如几万行的掉落表走异步加载先读字节流再在子线程里完成字符串池的构建最后在主线程做一次校验。校验内容主要是版本号和行数一致性避免热更时新旧文件混用。这里有一个值得说的设计选择我们放弃了按 ID 直接内存映射而是先用头部信息构建主键索引字典。主键索引的构建发生在加载阶段构建完之后通过主键查找的路径是“字典定位行索引 行内偏移读取字段”不会出现遍历全表的情况。对于组合主键我们按第一层键做字典第二层用有序数组加二分查找因为一层的数量通常不会太大二分查找足够快。3.4 编辑器配套工具导出、预览、热重载工具链里最容易忽略的是编辑器配套。很多自研方案只有命令行导出策划用起来很痛苦最后又退回 Excel 裸奔了。我们把配套拆成三块。第一块是导出菜单。在 Unity 编辑器里加了一个窗口展示所有配置表和对应的 schema 状态点击导出会依次执行读取 Excel、校验数据合法性、生成二进制、生成 C# 和 Lua 代码。导出过程中会打印每条校验错误如果有错误就中断不让坏数据混进产物。第二块是预览功能。导出的二进制文件可以在编辑器里直接打开用表格形式展示内容方便程序员排查问题。这个预览器本质上是把运行时加载器在编辑器里跑了一遍再加一层 UI 展示代码量不大但排障价值很高。第三块是热重载。编辑器运行时如果配表改了重新导出后加载器可以通过一个回调刷新已加载的配置。这功能对玩法验证帮助巨大——以前改个数值要重启游戏进场景现在导出完自动刷几秒就能看到效果。热重载的代价是做数据比对如果表结构变了比如加了字段就会强制重启加载但结构没变的情况覆盖了绝大多数调试场景。4. 半天迁移真实操作流程标题说半天实际上我们从项目冻结到全量切换完成整个流程不到四个小时。半天能完成的核心原因是前期把 schema 建好迁移本身只是导出和替换调用的体力活。4.1 迁移前准备我们用了两天时间把项目里所有配置表按模块梳理了一遍一共分四类角色数值表、关卡配置表、物品掉落表、活动运营表。每一类都先写好 schema 并完成测试导出确保策划的 Excel 能直接跑通。这里的准备工作主要花在了“Excel 数据规范化”上。原来的表里很多格子带着多余的空格、公式残留、合并单元格这些在我们新的导出校验里会直接亮红。所以准备工作其实就是把 Excel 数据洗了一遍去掉合并单元格把公式替换成纯数值删掉空行。这部分工作可以和 schema 编写并行不占迁移当天的资源。4.2 按模块切换的步骤当天我们按“角色数值 → 物品掉落 → 关卡配置 → 活动运营”的顺序切换每切一个模块做一次全量回归。切换一张表的具体步骤固定是四步在 schema 目录里新建对应的 JSON 描述文件确认字段名和类型和 Excel 表头一致。跑导出检查校验日志有没有 warning 或 error重点看类型不匹配和空值。替换代码里的访问方式。旧代码走 ScriptableObject 的字段引用新代码通过配置管理器按主键查询。这一步我们做了一个兼容层把旧的 API 函数签名保持相同所以调用处不需要大规模重写。跑一遍该模块的核心玩法流程确认数值表现和旧版本一致。第四步的验证是最容易翻车的环节。我们提前准备了一份“数值快照”就是在改造前把所有配置表的关键数值打印成 JSON 存档切完表之后重新打印一次用工具做 diff误差为零才算通过。这个快照对比比人工目测靠谱得多强烈建议想做类似改造的团队先存一份。4.3 回归测试与切流所有表切完之后我们做了三小时的集中回归跑主线关卡、测掉落概率、看活动商店的展示以及最关键的——在各档性能机上跑帧率对比。因为新工具的字符串池和固定行宽设计实际帧率表现比旧方案还快了一点主要体现在战斗内频繁读取技能表、掉落表的场景。热更链路我们也做了专项验证新配置打进热更包客户端启动后从远端下载并替换本地配置进游戏后数值表现正常。这一步的意义不只是功能验证而是确认加载器对“文件被替换”这个状态有正确的容错处理——旧版本号文件被新包覆盖后不会出现缓存了旧索引再去读新文件导致的越界。5. 迁移过程中踩过的坑自研工具最不缺的就是坑我们这半天加后续两天修了十几个问题挑几个典型的分享。5.1 Excel 导出的字符编码问题第一个坑出现在 Windows 环境下Excel 导出的 CSV 文件默认是 GBK 编码但我们读取时用 UTF-8 解析结果中文字符串全变成了乱码。排查了很久才发现问题不在解析逻辑而在编码。后来我们把导出流程做成了两步Excel 先存成 xlsx 格式由导出工具用专门的库读取这个库对编码处理更标准而不是先转 CSV。这之后乱码问题再没出现过。5.2 空值和默认值的语义模糊第二个坑是空值处理。Excel 里一个格子是真空和填了 0 是完全不同的语义技能 ID 为空表示这个角色没有技能填 0 在某些表里会被当成“普通攻击”的 ID。如果导出器把空值统一转成 0就会引入隐蔽的逻辑错误。我们的解决办法是 schema 里允许每个字段显式声明空值策略。默认策略是“空值报错”强制策划填值允许为空的字段要写 allowNull导出时空值会被转成一个特殊的负一哨兵值有 default 字段的则用默认值补齐。这样空值语义完全由表结构定义不会出现歧义。这里踩过最痛的一次有一张道具表的新字段忘了加 allowNull上线前策划批量填表时留了十几个空格导出一路绿灯结果玩家拿到道具后客户端读到一个负一索引直接内存越界崩溃。加了空值校验之后这类问题再没有出现。5.3 数组读取的边界条件第三个坑是数组字段的边界。二进制存储里数组长度是一个 int但如果 Excel 里的单元格填了带空格的内容导出时数组解析会多出一个空字符串长度就不对了。读取端虽然做了长度校验但在某些旧缓存文件和新格式混用的情况下会出现长度索引指向了错误的数据区域。我们的修复方案是在导出端做严格过滤数组里的空元素统一剔除同时在文件头部格式里加了一个魔数校验加载时先检查魔数不匹配直接报错而不是尝试解析。魔数是个看似简单但极其有效的保护强烈建议所有自研二进制格式都加上能省下无数个“为什么加载出来全是大数字”的排查时间。5.4 热重载对编辑器状态的影响第四个坑是编辑器热重载。我们做热重载的时候没有考虑到某些系统在配置表刷新后还持有旧对象的引用。比如一个跑了一半的战斗流程角色身上已经绑定了旧版本的技能配置对象热重载刷新后这个对象指向的内存已经被回收了下次访问就会空引用。解决办法是热重载时主动通知所有注册的监听者让它们清缓存、重建引用。虽然不能在战斗中途安全热重载但至少不会崩。这个功能后来我们就明确限定在“非战斗状态”使用也算一种妥协。5.5 组合主键的索引构建效率最后一个想说的问题是组合主键的构建。最早实现里组合主键的索引是一个嵌套字典外层 key 到内层字典再到值。当一张表有上千条数据时构建索引的时间能到几十毫秒放到启动加载流程里虽然不致命但配合多张表一起加载就明显卡顿。优化方式是把内层字典换成有序数组加二分查找因为内层的数据量通常只有几十条线性表比哈希表更省内存也更快。这个优化让建索引的总耗时降到了原来的三分之一。6. 配置表全链路改造后的真实收益最后说说收益以及我们现在回头看会想调整的点。最直观的收益是改表效率。策划改一个数值导出加热重载再进游戏看效果全过程从一个小时缩短到两三分钟。这个效率提升对运营期的内容更新至关重要活动版本一天要调七八轮数值以前是排队等程序帮忙看现在策划自己就能完成调试闭环。第二是类型安全。所有字段类型在编译期就被代码生成器固定下来了访问代码里完全不存在字符串键查表除了 Lua 层保留的少量动态访问以前那种“字段名拼错了没报错跑到线上才发现读出来是 0”的坑基本被根除。第三是热更包体积。字符串池加固定行宽存储同样数据量的配置表二进制产物比之前 ScriptableObject 的资源文件小了约七成。多语言字段拆分出去之后语言包可以独立更新不用每次改文案都拖上整个配置表下发。第四是排障速度。编辑器预览器和数值快照 diff 这两个工具让定位“配置没生效”到底是表填错了、导错了、还是加载缓存错了的时间从小时级降到了分钟级。如果现在再让我做一次我会把 schema 的版本管理做得更严格让表结构变更也能走代码评审流程而不是靠自觉。另外Lua 层的绑定可以做一层缓存把频繁访问的字段的偏移量缓存起来进一步压榨读取性能。这些都是锦上添花的事核心链路本身已经足够稳健。最后分享一个小技巧像这种基建替换不要追求把全项目一次切成新方案而是保证新旧链路能并存一段时间。我们在切表期间保留了旧导出功能任何一张表出问题都能一键回退到旧资源这个保险让团队敢于快速推进切换最终才能把整个迁移压缩在半天里完成。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →