ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

游戏开发中源码与JSON配置脱节:从截图回归到数据校验的实战指南

游戏开发中源码与JSON配置脱节:从截图回归到数据校验的实战指南 1. 事故现场怪物刷新正常掉落表却全空了1.1 一个本该五分钟结束的回归任务先说个我印象很深的真实案子。项目是一个卡牌加推图的手游关卡属于典型的>// 一个典型的反序列化片段看起来人畜无害 public class WaveConfig { public string spawnType { get; set; } public int delayedSpawnDuration { get; set; } public Liststring dropPoolIds { get; set; } } // 如果 JSON 里没有 delayedSpawnDuration 这个 key // Newtonsoft.Json 不会报错而是直接给 0 WaveConfig wave JsonConvert.DeserializeObjectWaveConfig(jsonText);一旦dropPoolIds或者关联的掉落池 ID 因为某种原因没解析出来它就不是 0 而是空列表。空列表的表现可能是不掉落任何东西也可能是掉落列表为空导致后续逻辑直接 return。这种错误靠人眼盯截图是绝无可能发现的。2.3 版本管理与字段演进的历史包袱还有一个绕不开的坑JSON 配置在版本管理里的变迁史。游戏项目的配置目录往往特别大一个中大型关卡版本可能有上千个 JSON 文件。每次策划调整数值diff 里就是一堆 key 的增删改。如果团队没有统一的配置迁移规范就会出现一种很普遍的现象某个 JSON 文件还是三个月前的结构但源码已经迭代了三轮。平时这些文件因为没被策划打开过还维持着旧结构没人发现问题一旦某次玩法反向读取了这个旧结构bug 就炸了。我后来在排查类似问题时几乎都会先问一句这个 JSON 最近一次改动是什么时候源码里对应类的最后提交又是什么时候两者时间差越大风险越高。3. 排查链路从 JSON 解析到字段比对3.1 第一步拉齐所有 JSON跑一遍结构校验那次掉落全空的 bug我的排查路径其实是这样的。先把出问题的关卡 JSON 拉下来用脚本解析一遍打印出每层 key 和类型。然后对照源码里对应的配置类逐个字段比对。这一步用肉眼做不现实因为一个波次配置可能有几十个字段所以我把校验做成了自动化写一个 Python 脚本用 JSON Schema 定义好这个关卡配置文件应有的结构和类型然后用库去 validate 所有关卡 JSON。import json import jsonschema schema { type: object, required: [levelId, waves, dropPools], properties: { levelId: {type: string}, waves: { type: array, items: { type: object, required: [spawnType, monsterIds, dropPoolIds], properties: { spawnType: {enum: [normal, delayedSpawn, boss]}, delayedSpawnDuration: {type: number, minimum: 0}, dropPoolIds: {type: array, items: {type: string}} } } }, dropPools: { type: object, additionalProperties: { type: array, items: {type: string} } } } } with open(level_1001.json, r, encodingutf-8) as f: data json.load(f) errors list(jsonschema.iter_validate(data, schema)) for e in errors: print(f校验失败: {e.json_path} - {e.message})这个脚本跑完结果很清晰dropPools里引用的一个池子在 JSON 里根本不存在。也就是说波次 XML 引用了掉落池 A但掉落配置模块里只定义了池子 B。源码没问题JSON 结构也没语法错误问题出在引用关系的完整性上——这是 JSON Schema 只靠结构校验不出来的需要业务层的断言。3.2 第二步用代码级断言校验引用完整性和业务规则结构校验能抓漏字段、类型错、枚举值非法但抓不了引用了一个不存在的 ID这类业务规则问题。这时候就得把校验逻辑从通用 schema 检查升级成业务规则检查。我写了一套针对关卡配置的断言每条都对应一个曾经踩过的坑每个波次引用的怪物 ID必须存在于怪物表 JSON 中。每个掉落池引用的物品 ID必须存在于物品表 JSON 中。delayedSpawnDuration必须大于等于 0且不能超过关卡总时长的 50%。波次数量必须和源码里的WaveCount常量一致这个很关键因为很多波次循环逻辑是用常量写的。每个配置类对应的 JSON 文件key 的数量不能比源码属性少防止漏加新字段。def validate_level_references(level_data, monster_table, item_table): errors [] for wave_index, wave in enumerate(level_data[waves]): for monster_id in wave[monsterIds]: if monster_id not in monster_table: errors.append(f第{wave_index 1}波引用了不存在的怪物: {monster_id}) for pool_id in wave[dropPoolIds]: if pool_id not in level_data[dropPools]: errors.append(f第{wave_index 1}波引用了不存在的掉落池: {pool_id}) else: for item_id in level_data[dropPools][pool_id]: if item_id not in item_table: errors.append(f掉落池 {pool_id} 引用了不存在的物品: {item_id}) return errors这套断言的思路很简单把策划配置合法和程序代码正确之间的契约变成机器可读、可自动跑的检查。跑完之后测试同学就不用靠肉眼去对配置表格了。3.3 第三步别忘了默认值静默替换这是最容易忽略的一层。结构校验和引用校验都过了运行时还可能出错原因就是反序列化默认值。举个例子源码里WaveConfig有个字段bossStageTransition用于 Boss 转阶段时切换到哪个事件。如果某个波次 JSON 没写这个字段解析出来就是null。代码里如果写成if (config.bossStageTransition ! null) { ... }倒还好最多不触发转阶段但如果写成if (config.bossStageTransition.stateName berserk) { ... }直接就在空引用上崩溃了。所以在排查对不上 JSON的问题时我会额外关注三类字段新增字段老 JSON 没有代码里有没有兜底逻辑可空字段空值会被下游怎么处理是优雅跳过还是直接抛异常枚举字段JSON 里写了delayedSpawn代码枚举里有没有这个值一旦拼写不一致解析库通常选择给一个默认枚举值而不是告诉你有问题。我在那个案子里最后定位到的根因其实就属于第三类掉落表里某个dropType字段拼成了common_drop而源码枚举叫CommonDrop大小写不一致。反序列化库把未知枚举值静默转成了默认项默认项恰好对应无掉落。整个过程没有任何报错没有任何红色日志纯粹是数据静默错位。4. 回归测试的重心调整从视觉回归到数据回归4.1 截图回归的边界它只能证明画面没坏我不是说截图回归没用。恰恰相反对于 UI 布局、美术表现、特效叠层这类问题截图回归效率很高成本也低。但必须明确它的能力边界截图只能验证渲染结果无法验证数据状态。举几个我踩过的例子怪物刷新点坐标被 JSON 里的负数覆盖怪物刷到了场景边界外截图上根本看不到那只怪但玩家能听到声音、能锁定仇恨。掉落概率被配成了 0打死怪不掉东西截图上干干净净看起来很对实际上玩家一关白打。关卡通关后跳到结算界面由于某个奖励字段缺失结算面板显示 0 金币。截图能看出显示 0但看不出应该是 100。这类问题有一个共同点画面输出是正常的数据计算是错误的。截图回归的天然盲区就在这里。4.2 数据回归怎么做跑一遍配置加载 逻辑仿真我的做法是把数据回归做成一个不依赖引擎渲染的测试套件。具体来说在关卡加载流程里抽出一个纯逻辑层——负责解析 JSON、构造关卡运行时对象、计算掉落结果、结算奖励。这层逻辑不碰渲染、不碰音效、不碰输入只做数据计算。测试时直接对这层做单元测试和集成测试输入一份 JSON 配置断言输出的运行时对象和结算结果。def test_wave_config_matches_source_constants(): level load_level(level_1001.json) assert len(level.waves) source_constant(level_1001_wave_count) assert level.waves[1].spawn_type delayedSpawn assert level.waves[1].delayed_duration 3.0 def test_drop_reward_not_empty(): level load_level(level_1001.json) rewards simulate_level_clear(level, rng_seed42) assert len(rewards.items) 0, 通关奖励为空检查掉落池配置这么做的好处是回归速度极快。一个关卡的完整数据仿真可能在毫秒级跑完几百个关卡配置打包成一个测试任务几十秒就出结果。比起进游戏、加载关卡、打一遍、截图、肉眼对比这套流程可以天天跑、每次提交都跑。4.3 把数据校验接入 CI让回归自动化光有测试脚本还不够关键是要让它跑在正确的时间点。我见过不少团队写了校验脚本但只在发版前手动跑一次这种节奏太慢了问题发现得越晚定位成本越高。我推荐的节奏是三层提交时用 git hook 跑增量校验只检查本次提交改动的 JSON 和相关源码类。几十个文件几秒钟出结果开发在本地就能发现问题。合并时在 CI 流水线里跑全量配置校验和数据仿真确保分支合并后所有关卡配置仍然自洽。发版前再跑一次完整的配置加载 掉落仿真 结算仿真输出一份所有关卡的关键指标通关时长、预期奖励、波次数量对照表。# 流水线示意伪代码 stages: - validate_config - simulate_levels - report validate_config: script: - python scripts/validate_all_levels.py artifacts: paths: - validation_report.json simulate_levels: script: - python scripts/simulate_all_levels.py --seed 42 artifacts: paths: - simulation_report.json report: script: - python scripts/generate_regression_report.py这套 CI 跑起来之后团队最大的变化就是对不上 JSON的问题不再靠人肉发现而是提交的那一刻就被流水线拦住。截图回归依然存在但它的角色从唯一防线降级成了渲染表现兜底。5. 一份可以直接抄的关卡回归检查清单5.1 提交前开发侧自查如果你的项目也是源码 JSON 配置的架构下面这份清单可以直接贴在开发文档里新增源码字段时是否同步更新了对应 JSON如果没有程序对默认值有没有兜底修改枚举值或字符串常量时是否全局搜索了 JSON 里旧的枚举值大小写、下划线、缩写风格是否一致是否对修改涉及的所有关卡跑过一遍结构校验和引用校验是否更新了 JSON Schema如果没有明天下一个人还会踩同一块石头。5.2 测试执行中回归测试侧自查测试同学在回归关卡时除了打关卡看表现建议加上这几步进关卡前先加载一份配置校验报告确认本次改动涉及的关卡没有结构错误和引用错误。打完关卡后不要只看掉落动画和结算界面打开后台日志确认掉落物品 ID 列表和期望值一致。尝试用测试指令强制触发特殊波次、Boss 转阶段等分支逻辑确认这些分支的 JSON 配置也加载正常。保留一份上一版本的完整配置基线比对两个版本的字段 diff重点看被删除、被改名、被改类型的字段。5.3 发布前发布负责人侧自查到了发版前已经有自动化流水线之后还差最后一道人工检查确认配置版本和代码版本对应关系。这里有个常见事故开发分支上源码已经改到 V2.3但配置分支还停在 V2.2发布时忘记把配置一起合入主干。结果线上运行的是新逻辑读旧 JSON各种字段对不上表现全部异常。所以发布前一定要核对两个版本号最好在发布流水线里加一个校验步骤比对代码分支和配置分支的版本标识。6. 最后再分享一个排查小技巧回到开头那个掉落全空的案子最后帮我们快速定位的其实是一个很土的办法在反序列化时开启严格模式把未知字段和类型不匹配全部变成警告日志而不是静默忽略。var settings new JsonSerializerSettings { MissingMemberHandling MissingMemberHandling.Error, // 缺失字段直接报错 NullValueHandling NullValueHandling.Ignore };虽然这种做法在上线环境里太严格配置稍有历史包袱就会崩但在测试环境里开着没问题。测试包一旦遇到字段缺失直接弹错误弹窗问题当场暴露根本不用等到打完一整关才发现掉落空了。我在项目里就是这么干的测试环境开严格模式线上环境保持宽容模式两端各取所需。排查源码和 JSON 对不上这类问题最难的不是修 bug而是意识到截图看起来没问题不等于数据没问题。把视角从画面转到数据流之后你会发现这类隐患其实都可以在提交阶段被机器拦住。希望这套思路对你有用。
返回列表