Files
MatchMaster/docs/level-resources.md
2026-11-03 20:41:22 +08:00

162 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 关卡资源发布与维护
本方案仅接入微信小游戏。Creator预览及其他平台仍读取本地custom Bundle。源JSON保留在assets/custom/Json;真实B准备好后放入assets/custom/JsonB,不使用历史实验的关卡替代新方案。
## 业务配置与资源清单
login按请求version匹配服务端levelPolicy.ts中的最低版本阈值,返回maxLevel、bLevelCount(N)和levelGroup。N只对被明确分配到B的玩家生效:前N关使用B,其后使用A。已结束的new1_50不会自动重新启用。帮助与无限模式固定使用A。
字段缺失或非法时,客户端采用同一资源环境、内容世代及客户端版本下最后有效值;无历史值采用随包默认主线上限2040、N=0。上限下降不会裁剪玩家已通关进度。业务配置只在启动登录时应用,切前台不重新拉取。manifest第一次成功获取后固定到本次登录;首次获取失败可自动重试,后续不轮询新版本。清单已成功获取但尚无所需资源时,可通过“重新登录”重试重新检查配置。
manifest仅描述兼容资源范围和hash。其覆盖范围可以大于业务开放范围;不会因为资源提前上传而提前开放。业务要求超出清单覆盖时,只阻止真正缺失的关卡,不缩减上限、不跨组替换、不自动通关。
## 构建配置
packages/level-resources/publication.json:
- contentVersion:客户端支持的资源内容世代。新增不兼容机关时更新世代并发布新客户端,同时保留旧世代CDN。
- aResourceCount、bResourceCount:发布的连续A/B源文件数量,属于资源范围,不替代login的开放范围。
- builtinBCount:K,默认50;构建时内置min(K,实际B源数量),A固定内置100。已安装客户端的包内文件不会因N变化而增加或删除。
- defaultMaxLevel:首次无有效登录上限时的随包默认值。
- cacheBudgetBytes:默认33554432,软预算。
正式首期bResourceCount=0,没有真实B文件,B实验关闭。模拟B只允许测试产物:复制A内容但保留独立B身份并标记simulatedB,正式微信环境拒绝加载模拟内置资源或测试manifest。
custom在微信配置为远程Bundle,抖音配置不变。Creator 2.4.15完整构建微信,填写HTTPS资源服务器地址。构建扩展自动生成levels-local内置目录、注入加载器、移除custom子包声明,并排除remote和subpackages/custom的微信上传。首次接入必须完整构建,单独运行资源发布命令不能更新客户端代码。
微信构建后处理统一由 `packages/activity-bundles/main.js` 注册:先生成活动资源,再执行关卡打包,任一步失败都会使构建失败。`level-resources/main.js` 不再单独注册写入钩子。更新构建扩展后须重启 Creator,让新的统一入口生效;构建成功时控制台应同时出现 `[activity-bundles]` 和 `[level-resources]` 日志。
## 生成资源(不上传、不部署)
```powershell
npm run levels:publish
npm run levels:publish:test
```
正式默认产物在build/wechatgame-levels/levels/v1;测试在build/wechatgame-levels/levels-test/v1。微信开发/体验版使用资源服务器的levels-test/v1,正式版使用levels/v1。测试/正式缓存及不同内容世代按完整资源地址隔离。测试资源服务器需配置微信request/downloadFile合法域名。
真实B只读取JsonB。声明了80关却缺少B80会终止发布,不拿A填补缺失的真实B。测试模拟数量可通过--simulate-b=34等参数调整,必须同时使用--test。
每组按100关生成ZIP;A另有全量ZIP,每关另有独立JSON。所有JSON及ZIP以SHA-256命名,内容不变则复用原文件;同一源生成确定性的包。先生成并校验所有资源,最后写manifest.json;publication-report.json列出变化的关卡和包,manifests保存历史清单。
每周新增关卡:增加连续源文件,调整发布资源范围,运行命令;客户端通常只需新增/修复文件或受影响小包,不固定重下全量ZIP。小包、总包、单关共用文件缓存。
发布顺序:上传不可变资源并验证可访问 → 最后上传manifest.json → 更新服务端对应版本的maxLevel/N。manifest应禁用CDN缓存;带hash的资源可长期缓存。新增机关先发布兼容客户端,再提高其版本阈值对应的上限;旧版本继续原上限。
```powershell
node packages/level-resources/pack.js --rollback build/wechatgame-levels/levels/v1 <历史release>
```
回滚命令校验历史清单引用的全部文件,生成rollback-<release>.json供审核后上传为manifest.json,不覆盖当前本地发布清单,也不更改用户进度。保留旧hash文件供仍持有旧清单的会话使用。
## 登录、修复与预下载
从关卡manifest请求开始计算8秒总预算。在最终进度确定后,普通主线优先准备当前至后两关;超时且当前存在校验可用的旧资源则进入Home,后台继续修复。没有当前可用资源则保留登录页并提供重试。JSON文件内容完整不代表没有关卡设计问题,因此已知待修复但结构完整的旧配置仍符合约定的临时兜底条件。
主线后台修复仅针对已在本地、尚未通过的关卡。222关玩家跳过旧200与未缓存320,修复缓存中的250;帮助等实际读取旧200时仍检查版本。距离下一百关包约10关开始预下载,同时考虑内置范围和N边界(例如B内置50时提前准备B51)。接近主线上限10关时后台补齐全量A池。
每次开局读取自己的JSON对象,后台落盘不会替换进行中的棋盘。独立下载的单关文件先校验大小/hash/结构,再原子安装和更新索引;失败保留旧版本。缓存文件损坏不能当作可用旧资源。
首页开局和连续通关优先使用已显示的转场承载JSON和图片准备等待,不叠加Loading动画;只有没有可见转场时才打开现有Loading。资源准备完成后沿用原流程进入GameScene,不新增固定动画等待,也不串行化已有场景预加载。有效本地资源仍允许离线开局;没有可用本地资源时,前台读取最多等待60秒(包含manifest、下载排队、下载及安装后读取)。已检测到离线或等待中断网立即失败,请求明确失败也可提前结束,不必等满60秒;底层单次网络请求保留原有超时。
关卡读取失败通过wx.showModal显示“关卡读取失败,请连接网络!”,只显示“确定”。弹窗出现即作废本次开局回调,确认后返回HomeScene;原本就在首页则关闭Loading/转场并恢复开始按钮。断网下再次点击会再次提示;联网后点击会重新准备关卡,资源就绪才进入GameScene。后台预下载保持离线暂停和联网恢复,迟到下载可缓存,但不能自动恢复已取消的开局。登录8秒放行和无限模式总包准备规则保持原样。
整包采用“整包验证一次 → 解压 → 直接安装”:校验ZIP大小和SHA-256,检查归档路径及文件列表,原生解压后只列一次文件名检查解压是否完整,写一份包含文件名和大小的cache-index.json,再把整个解压目录一次rename到packs/<组>/<包hash>-<唯一标识>。安装期间不读取关卡JSON或内部manifest内容,不逐关计算hash、解析JSON,也不把文件读入内存后重写。单关版本记录来自该ZIP对应的外部manifest。源JSON结构由发布工具校验,实际开局仍检查当前关配置。
临时解压目录保留到安装完成、失败或取消后再清理。目录移动失败时不暴露部分安装;移动成功时目录和目录索引同时提交,重启可从目录索引恢复,即使逻辑index尚未写入也无需重新下载。单关修复仍写入files/<组>,读取时优先使用同hash的独立覆盖文件;兼容升级前的平铺缓存。全部所需文件安装成功之前不把完整无限池标为就绪。
## 无限模式
必须准备完整兼容manifest的A池,包括提前发布但未开放的资源;实际随机范围仍限制在登录开放范围内。已由小包收齐并校验可用的文件视为完整,无需额外重复下载all.zip。旧配置修复失败时沿用与主线相同的临时兜底规则。
无限玩家8秒后可进入Home,无资源准备进度条。点击开始时若未齐,提示“正在准备关卡数据”;后台以2、5、10、30、60秒退避自动重试,断网/切后台暂停,恢复后继续。准备完成后,仅当仍在同一Home且未操作其他功能时自动进入。重复点击共享任务;离开页面、打开其他功能或切后台会取消这次进入意图,资源任务可在恢复后继续。
32MiB为常态缓存目标。清理过期hash和临时文件,超额时淘汰不受保护的旧关卡;当前/近期资源及完整无限池受保护,可突破软预算。设备实际空间不足时不会缩小随机池或误判就绪,会继续准备并重试。
## 验证
### 首页/进关性能日志
完整构建微信后,开发版和体验版默认开启性能诊断,统一过滤`[LevelPerf]`。正式版默认关闭。诊断不改变资源策略,不打印玩家标识、登录响应或关卡JSON正文。不要仅复制错误日志;需保留从启动到地图就绪之后约5秒的完整`[LevelPerf]`记录,并注明测试关卡、冷缓存/热缓存、设备和网络环境。
控制台开关及导出:
```js
// 当前运行立即开启;设为false可以对照诊断自身的开销。
window.__levelPerf.setEnabled(true);
// 保存到本地,下次启动也开启(需要捕获完整启动过程时使用)。
wx.setStorageSync('level-resources-perf', true);
// 返回最近400条记录的纯文本;可复制控制台结果用于分析。
window.__levelPerf.dump();
// 关闭当前诊断并关闭下次启动诊断。
window.__levelPerf.setEnabled(false);
wx.setStorageSync('level-resources-perf', false);
// 恢复默认规则后重新启动:开发/体验开启,正式关闭。
wx.removeStorageSync('level-resources-perf');
```
`session`区分登录,`spanId`关联同一阶段的begin/end,`tMs`是从资源运行时初始化开始的毫秒时间,`ms`是阶段经过时间,`phase`表示最近的页面/进关时间点。并行阶段耗时不能相加。日志主要看:
| 日志 | 定位范围 |
| --- | --- |
| login.toHome / login.levelGate | 首页总等待、关卡门槛、8秒旧资源兜底 |
| login.publicUIBundle / login.activityState | UI Bundle、活动状态的并行等待 |
| scene.HomeScene.loadActivate / HomeScene.onLoad.sync / HomeScene.start.sync | 首页场景加载激活与生命周期同步初始化 |
| HomeScene.warmup / 首页当前关图片预热等阶段 | 首页预热及与后台关卡扫描的重叠 |
| entry.levelJSON / level.load / entry.applyLevelData | 关卡配置读取、命中本地还是下载、应用数据 |
| scene.GameScene.loadActivate / map.initMap.sync / map.ready | 场景加载激活、同步地图生成、地图最终就绪 |
| scene.GameScene.resourceLoad | 从调用loadScene前到进入runSceneImmediate,包含Bundle场景资源/依赖加载与解析,不能等同纯网络或纯磁盘耗时 |
| summary.metrics / summary.scene 中的 scene.resource.fetch | 当前场景资源的文件获取,含引擎文件缓存、下载调度和资源集合拆分;慢样本含 UUID、路径、Bundle |
| scene.resource.download / scene.resource.parse | 下载器调用至回调、解析器调用至回调的耗时;前者也可能命中平台缓存,后者包含异步解析等待,均不能直接视为纯网络或CPU耗时 |
| scene.resource.dependencies | 某资源的依赖任务批次从启动到完成;含依赖数量、最多3个依赖标识,以及可追踪到的父资源 |
| scene.progress | 最多每秒一次进度(完成时另报),包含动态完成数/总数及最近进度资源;不能当作单文件耗时 |
| scene.GameScene.switch.sync | runSceneImmediate同步切换总耗时,包含下列子阶段及常驻节点处理、事件和回调 |
| scene.GameScene.initialize.sync | Scene._load场景初始化 |
| scene.GameScene.releaseAssets.sync | 引擎释放旧场景自动管理的资源 |
| scene.GameScene.destroyOldScene.sync / deferredDestroy.sync | 旧场景destroy标记,以及延迟销毁执行(包含onDestroy) |
| scene.GameScene.activate.sync | 新场景激活,包含组件onLoad/onEnable;不包含后续调度的start及异步任务 |
| prefetch.scan / pack.scan / pack.strategy | 扫描关卡数、待修复数、缺失数、选择ZIP还是独立文件 |
| zip.fetchVerifyExtract / zip.install / zip.directoryInstalled | ZIP下载、整包校验及解压;整目录一次移动安装(members为整包文件数,installed为本次逻辑索引补齐数) |
| images.initialize / images.group | 图片版本准备、公共图集与普通方块两组的等待 |
| summary.metrics中的images.asset / images.engineLoad | 单文件来源、查找缓存、准备下载、引擎加载和解析校验的汇总 |
| summary.metrics中的images.download / images.createFrame / images.reuse | 下载排队与网络等待、落盘、SpriteFrame创建、共享任务等待或内存复用的汇总 |
| download / cache.collect | 慢请求/失败请求、缓存清理规模 |
| summary | 每约2秒汇总热路径count/totalMs/maxMs/bytes/errors,以及仍在等待的阶段 |
| eventLoopLag | 250ms诊断定时器额外延迟达到100ms;提示主线程/事件循环停顿,不等于GPU帧耗时 |
汇总重点比较`json.sha256`、`json.decode`、`json.parseValidate`、`state.write.index`、`fs.writeStateSync`、`fs.unzip`、`network.queueWait`和`network.download`。同步项可帮助识别CPU/同步IO开销;异步项统计的是等待耗时,且嵌套统计存在重叠。缓存文件读取失败也会计入errors,首次下载时缓存不存在属于正常情况。字节数表示累计处理/请求字节,重复校验会重复累计,不代表唯一文件大小或实际网络流量。
诊断在切后台期间不报告事件循环延迟,日志只保留最近400条,不逐关打印高频读写。开发者工具、断点和控制台输出本身也可能带来延迟,最终判断应结合真机日志及关闭诊断后的对照。
GameScene细分诊断针对当前Cocos 2.4.15调用链,在开启性能日志时临时包装运行时方法,不修改引擎文件或替换加载流程。每个阶段仅一对begin/end;正常完成、加载失败、切换拒绝或同步异常后恢复方法。关闭诊断时不安装包装。用spanId配对,requestId记录在begin中。switch.sync包含其子阶段,不能把总计和子阶段再次相加;scene.GameScene.loadActivate在启动回调处结束,switch.sync还包含回调尾部与场景启动事件,两者边界略有不同。
`scene.resource.*`为高频细节,例外地不逐资源输出begin/end:每约2秒在summary中汇总count/totalMs/maxMs/errors,每类最多保留3个最慢样本。pending包含仍在等待的资源标识;每条细节携带scene/requestId。只标记GameScene正式loadScene及其传播下来的依赖任务,其他后台加载不计入;URL去掉查询参数和片段。fetch包含download,dependencies包含子依赖的获取和解析,不能把这些指标直接相加。结束或异常时会恢复包装并汇总剩余样本;traceClosed表示诊断结束时资源仍未回调,不表示资源加载成功或失败。
`prefetch.scan.mode=metadata`表示只比较内置清单、缓存索引与远端manifest的hash,不读取JSON。相同登录/组别/进度的并发调用合并为一个任务,manifest和缓存版本未变化时跳过重复预取(metrics中的prefetch.joined、prefetch.unchangedSkipped)。变化的本地未通关资源仍异步修复;实际开局读取、校验和损坏恢复保留。登录配置尚未到达的启动初始化记录entry.levelJSON.deferred,继续初始化同步字段,不提前读取关卡。
`pack.scan.mode=metadata`也只检查索引:匹配内置清单,或在运行时目录索引中查找对应组别、关卡和hash;不逐文件readFile、hash或解析JSON。重启时每个已安装目录只恢复一次cache-index.json,独立覆盖文件通过文件名索引发现,不能仅凭逻辑使用记录认定已安装。旧版本仍存在时按原规则安排单关修复,缺失时下载补齐。已安装包在地图就绪后的再次检查,即使scanned=100,也不应贡献100次file.readCache/json.sha256/json.parseValidate。索引代表安装记录,不代表持续检测磁盘损坏;实际开局仍完整校验,索引所指文件缺失或损坏时按单关恢复,下载失败则保持原有失败/重试处理。
### 性能修复后的复测
关卡缓存通过平铺文件名和整包目录索引判断文件是否存在;索引不代表文件有效,实际使用仍校验大小、hash和JSON结构。同一次查找不重复尝试相同hash。成功安装后更新索引,文件消失或清理成功时移除;独立JSON下载使用临时文件加重命名,整包使用整个解压目录一次重命名,不读取已知不存在的目标,也不删除已被重命名的临时路径。
维护按索引记录的JSON大小判断预算,低于32MiB软预算的80%时跳过逐文件stat和清理;无法从清单或旧索引获得大小的遗留文件需要一次stat补齐。接近预算后只对独立文件stat,整包直接使用目录索引总大小;按目录回收整包,任一关受保护则保留整个目录。重叠包可回收多余目录,但须保留所有有效受保护文件的另一份副本,独立修复文件优先。安装期间跳过维护,安装开始时等待已发出的单个清理IO完成,再停止该轮后续清理;运行中的维护合并,清单和缓存版本不变时10秒内跳过重复维护。保护条件在删除前再次确认。登录进入首页、首页资源预热及进关到地图就绪期间暂停后台关卡扫描、修复安装和清理;已经发出的操作允许完成,后续步骤等待释放。当前关必需资源可提高优先级继续准备,明确等待无限池的请求也可继续,避免互相等待。首页预热的15秒兜底和页面销毁会释放暂停凭证。
manifest请求使用文本响应并在游戏侧JSON.parse,随后仍执行完整清单校验。观察`manifest.parseLocal`和`manifest.validate`确认原先的秒级校验是否消失;该收益需微信环境实测,不能用Node测试耗时替代。
复测428关、仅缓存428–430的场景,重点对比:`source.absentSkipped`应替代缺失文件readFile;`cache.plan.files`应接近本地文件数;`cache.maintenanceSkipped`记录重复维护跳过;`background.hold/release`应覆盖进关关键阶段。正常安装的临时文件unlink失败应消失,`map.ready.totalEntryMs`用于比较整体进关耗时。
整包安装的新指标:ZIP只贡献一次`download.sha256`,`zip.directoryInstalled.moves`应为1,即使缺97关也只移动一次目录;额外一次writeFile用于目录索引,不写回关卡JSON。`cache.maintenanceSkipped.reason`为belowBudget或installing时不应出现该轮逐文件stat。其他独立JSON下载、缓存检查及实际开局仍可产生readFile/hash/writeFile,不应把整个日志的这些计数误认为必须全部归零。
图片日志只对`images.initialize`和`images.group`输出begin/end;frame、asset、engineLoad、createFrame、reuse、download不逐文件打印,而是并入每约2秒的summary。`summary.metrics`保留各阶段count/totalMs/maxMs/errors,以及asset按source.local/temporary/remote/memory分类的统计。`summary.images.assets`和downloads分别保留本窗口最慢3条,failures最多保留3条失败样本;总失败数量以metrics.errors为准。仍在等待的阶段继续出现在pending中,避免卡住时失去观测。
asset的细分统计及样本包含lookupMs(含同步文件存在性检查)、cachePrepareMs(下载/排队准备)、engineLoadMs(Cocos loadRemote从调用到回调,包含本地读取/解码/引擎资源创建)、validateMs(plist坐标解析或PNG尺寸校验)。download包含queueMs、networkMs和installMs。引擎加载耗时不能直接等同GPU上传时间;结合images.group、images.createFrame和eventLoopLag定位长等待。并发文件与阶段互相嵌套,不能直接累加。
修改构建扩展后先重启Creator或重新加载扩展,再完整构建微信。扩展每次构建会重新加载发布脚本,避免旧脚本遗漏新模块(例如perf)。本地调试用的产物testUrl会在构建时重新生成,需要按本地服务地址重新设置。
```powershell
npm run test:levels
```
测试覆盖增量发布、模拟B隔离、B34/B80路由、8秒兜底、222关修复筛选、旧配置对局隔离、全量门槛与重试、周更复用、版本回退、取消开始意图以及微信文件系统适配。上线前需完整构建微信并在开发者工具/真机验证网络故障、缓存空间不足与场景切换。