21 KiB
关卡资源发布与维护
本方案仅接入微信小游戏。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] 日志。
生成资源(不上传、不部署)
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的资源可长期缓存。新增机关先发布兼容客户端,再提高其版本阈值对应的上限;旧版本继续原上限。
node packages/level-resources/pack.js --rollback build/wechatgame-levels/levels/v1 <历史release>
回滚命令校验历史清单引用的全部文件,生成rollback-.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]记录,并注明测试关卡、冷缓存/热缓存、设备和网络环境。
控制台开关及导出:
// 当前运行立即开启;设为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会在构建时重新生成,需要按本地服务地址重新设置。
npm run test:levels
测试覆盖增量发布、模拟B隔离、B34/B80路由、8秒兜底、222关修复筛选、旧配置对局隔离、全量门槛与重试、周更复用、版本回退、取消开始意图以及微信文件系统适配。上线前需完整构建微信并在开发者工具/真机验证网络故障、缓存空间不足与场景切换。