
简介本资源是微信小游戏《果蔬去哪了》的完整开发包面向前端初学者及小程序游戏开发者提供可研读、可调试的休闲益智类小游戏实践范例。包内含738个文件总计6.79MB涵盖219张PNG游戏素材图、103个JS核心逻辑脚本、293个JSON配置与场景数据、4个Atlas图集及4个Plist动画资源描述文件辅以MP3音效、MP4演示视频和Cocos2d-js引擎相关脚本完整支撑从资源加载、事件响应、动画播放到UI渲染的全流程实现。已有616人学习下载适合通过真实项目理解微信小游戏架构、掌握WXML/WXSS/JS协同开发模式并深入学习消除类玩法逻辑、资源预加载策略与性能优化技巧。1. 这不是个“切水果”换皮项目《果蔬去哪了》用状态机驱动关卡逻辑靠 Canvas 帧同步实现 60fps 拖拽反馈专治微信小游戏里“点不中、拖不动、卡成PPT”的玄学翻车你试过在微信小游戏里拖拽一个番茄结果手指划出半屏它才慢悠悠挪一格或者刚点中香蕉屏幕闪一下就跳到下一关连动画都没播完这不是你手机卡——是很多“果蔬类”小游戏压根没做帧同步和输入缓冲。而《果蔬去哪了》源码包里那个game-core/目录下的DragManager.ts和LevelStateMachine.ts就是冲着这个黑匣子来的。它不靠wx.createCanvasContext硬怼而是用 requestAnimationFrame 驱动 Canvas 渲染循环把触摸坐标映射到游戏世界坐标系时做了双线性插值补偿关卡推进不用setTimeout打乱节奏全由状态机控制——准备态 → 拖拽中 → 松手判定 → 动画播放 → 结果校验 → 下一关加载每一步都有明确的 enter/exit 回调。适合想拿真实线上项目练手的开发者代码结构清晰、资源组织规范、没有魔改引擎、所有逻辑都在 TypeScript 里可读可调。如果你正卡在“本地跑得飞起真机上拖拽延迟半秒”或者“动画一多就掉帧”这份源码就是你的后悔药。2. 项目结构与技术栈从project.config.json到res/资源目录的完整路径信任链微信小游戏的工程结构看似简单实则暗藏陷阱。这份《果蔬去哪了》源码不是“导出即用”的压缩包而是一个经过真机压测、CI 构建验证过的完整工程目录。它没用任何第三方游戏引擎如 Cocos Creator 或 LayaAir纯原生微信小游戏 SDK TypeScript 开发这意味着你能看到每一行渲染逻辑、每一个资源加载时机、每一次触摸事件的生命周期处理。下面我带你一层层拆开它的骨架重点标出那些“改错一个路径就白忙半天”的关键节点。2.1 工程根目录project.config.json是信任起点不是摆设微信小游戏的构建流程高度依赖project.config.json中的配置项。这份源码里该文件不是默认模板而是经过线上环境校准的{ description: 果蔬去哪了 - 微信小游戏线上版本, setting: { urlCheck: false, es6: true, enhance: true, postcss: true, preloadBackgroundData: false, minified: true, newFeature: true, coverImage: res/images/cover.jpg, realtimeSearch: false }, compileType: game, libVersion: 2.28.4, appid: wx1234567890abcdef, projectname: guoshu-qunaile, isGameTourist: false, condition: { search: {current: -1, list: []}, conversation: {current: -1, list: []}, game: {current: 0, list: [{id: 0, name: 主游戏, path: pages/index/index}]}, miniprogram: {current: -1, list: []} } }提示libVersion字段必须与你本地开发者工具的「基础库版本」严格一致。线上版本用的是2.28.4如果你用2.30.0工具打开wx.getSystemInfoSync().SDKVersion返回值会触发兼容逻辑分支导致res/下的.webp图片加载失败——现象是所有果蔬图标显示为灰色方块。解决方法在开发者工具右上角「详情 → 项目设置 → 基础库版本」手动切回2.28.4或修改libVersion后重新构建。2.2 核心代码目录src/下的四层责任划分整个业务逻辑集中在src/目录采用分层设计拒绝“一个 ts 文件塞 800 行”src/app.ts小程序生命周期入口只做三件事——初始化全局资源管理器、注册触摸事件监听器、启动主游戏循环。没有业务逻辑。src/game/游戏运行时核心含GameLoop.tsrequestAnimationFrame 主循环、InputManager.ts触摸坐标归一化防抖、RenderManager.tsCanvas 清屏→绘制背景→绘制角色→绘制UI。src/core/领域模型层Fruit.ts定义果蔬实体含 type、score、state、positionLevelData.ts存储关卡配置JSON Schema 可校验CollisionDetector.ts实现 AABB 碰撞检测非物理引擎轻量级矩形重叠判断。src/utils/纯函数工具集PathResolver.ts封装资源路径拼接自动补res/前缀并处理平台差异Logger.ts重写了console.log在真机调试时输出带时间戳和模块名的日志[DragManager] touch start at (120, 340)。特别注意src/game/GameLoop.ts的帧同步设计// src/game/GameLoop.ts class GameLoop { private lastTime 0; private frameCount 0; private readonly TARGET_FPS 60; private readonly FRAME_INTERVAL 1000 / this.TARGET_FPS; // ≈16.67ms start() { const loop (timestamp: number) { const deltaTime timestamp - this.lastTime; if (deltaTime this.FRAME_INTERVAL) { this.lastTime timestamp; this.update(deltaTime); // 仅更新逻辑位置、状态 this.render(); // 仅触发 Canvas 绘制 this.frameCount; } requestAnimationFrame(loop); }; requestAnimationFrame(loop); } update(deltaTime: number) { // 所有状态变更放这里拖拽计算、碰撞检测、计时器减法 InputManager.update(); DragManager.update(deltaTime); CollisionDetector.checkAll(); } render() { // 所有绘制放这里清屏、drawImage、fillText RenderManager.clear(); RenderManager.drawBackground(); RenderManager.drawFruits(); RenderManager.drawUI(); } }这段代码决定了你能不能在低端安卓机上稳住 60fps。关键点在于update()不做任何drawImagerender()不做任何setPositiondeltaTime传入DragManager.update()后用于计算拖拽速度衰减避免快速滑动后惯性过大而不是简单用touchmove坐标覆盖位置。2.3 资源目录res/图片、动画、音效的物理存在方式微信小游戏对资源加载有硬性限制单个包体积 ≤ 4MB主包远程资源需域名备案。本项目采用“主包内嵌 按需加载”策略res/ ├── images/ # 所有静态图格式为 .webp比 png 小 40% │ ├── bg/ # 背景图按关卡编号命名bg_level_1.webp, bg_level_2.webp │ ├── fruits/ # 果蔬图统一尺寸 120x120pxapple.png, banana.png, tomato.png │ └── ui/ # UI 元素btn_start.png, icon_score.png, cover.jpg ├── animations/ # Lottie JSON 动画非 gif由 lottie-mini 小程序版解析 │ ├── explode.json # 消除爆炸效果 │ └── combo.json # 连击特效 ├── audio/ # 音效全部为 .mp3微信不支持 .ogg 真机播放 │ ├── drag.mp3 # 拖拽音效短促≤0.3s │ └── success.mp3 # 关卡成功音效 └── config/ # 运行时配置JSON 格式 └── level_config.json # 关卡参数maxTime、targetScore、fruitTypes 等注意animations/下的 Lottie JSON 文件不能直接用wx.createAnimation加载。项目使用了精简版lottie-mini已打包进src/libs/lottie-mini.js加载方式为import { loadAnimation } from ../libs/lottie-mini; const anim loadAnimation({ container: canvasElement, renderer: canvas, loop: false, autoplay: false, path: res/animations/explode.json }); anim.play(); // 手动触发3. 运行与调试从开发者工具真机预览到性能面板定位卡顿根源拿到源码后90% 的人卡在第一步为什么开发者工具里能跑扫二维码真机就白屏这不是代码问题是微信小游戏的运行时沙箱机制在作祟。下面给出一套经某公司上线项目验证的调试流水线覆盖从启动到压测的全链路。3.1 本地启动三步确认环境可信检查基础库版本打开微信开发者工具 → 右上角「详情」→「项目设置」→「基础库版本」必须设为2.28.4与project.config.json一致。这是硬性前提否则wx.getSystemInfoSync()返回的platform字段可能为devtools而非android/ios导致资源路径拼接错误。启动前清理缓存在开发者工具顶部菜单栏 →「工具」→「清除缓存」→ 勾选「编译缓存」和「微信登录态」点击「清除」。原因旧版本app.js缓存可能导致App.onLaunch未执行全局资源管理器未初始化后续ResLoader.loadImage(res/images/apple.png)返回null。首次运行必开「调试器」点击工具栏「调试器」按钮在 Console 面板粘贴以下命令确认核心模块加载成功// 在 Console 中执行 typeof gameCore ! undefined typeof gameCore.DragManager ! undefined // 应返回 true gameCore.RenderManager.canvasContext ! null // 应返回 true如果返回false说明src/app.ts未正确执行大概率是project.config.json的appid字段为空或格式错误必须是wx开头 16 位小写字母数字。3.2 真机预览扫码后必须做的三件事微信扫描预览码后不要急着玩先做这三件事打开「远程调试」在手机微信「发现」→「小程序」→ 找到刚预览的小程序 → 右上角「…」→ 「调试」→ 开启「vConsole」。此时手机屏幕底部会出现一个黑色控制台点击它切换到「Network」标签页。观察res/images/下的图片请求是否全部200。如果出现404说明PathResolver.resolve(images/fruits/apple.png)返回的路径错了——常见原因是res/目录被误放在src/外层或project.config.json中setting.coverImage的路径写成了images/cover.jpg漏了res/前缀。检查触摸事件是否注入在 vConsole 的「Console」标签页输入wx.onTouchStart wx.onTouchMove wx.onTouchEnd应返回三个函数对象。如果undefined说明src/app.ts中的wx.onTouchStart(() {...})未注册成功原因通常是App.onLaunch里异步操作如wx.login阻塞了事件注册。本项目已规避此问题但如果你后续加了登录逻辑务必用Promise.all([login(), initGame()])保证顺序。验证 Canvas 渲染在 vConsole 「Console」中执行gameCore.RenderManager.canvasContext gameCore.RenderManager.canvasContext.canvas.width 0若为false说明Canvas元素未正确创建。检查pages/index/index.wxml中是否遗漏了canvas canvas-idgameCanvas stylewidth:100vw;height:100vh;/canvas微信小游戏要求canvas-id必须与wx.createCanvasContext(gameCanvas)中的字符串完全一致且style中的宽高必须用vw/vh单位px在真机会失效。3.3 性能定位用「性能面板」揪出掉帧元凶当游戏运行卡顿时别猜用工具在开发者工具顶部菜单 → 「工具」→ 「性能面板」→ 点击「开始录制」→ 玩 10 秒 → 点击「停止」。查看火焰图Flame Chart重点关注Scripting区域。如果GameLoop.update占用超过 16ms说明逻辑计算过重如果GameLoop.render占用过高说明绘制操作太多。典型卡顿场景与解法现象快速连续拖拽多个果蔬时update时间飙升至 25ms。原因CollisionDetector.checkAll()对 N 个果蔬做 O(N²) 检测。解法在src/core/CollisionDetector.ts中启用空间分割Grid-based将屏幕划分为 10×10 网格每个果蔬只与同网格及相邻 8 个网格内的果蔬检测复杂度降至 O(N×k)k 为平均网格内果蔬数通常 ≤3。现象消除动画播放时render时间暴涨。原因Lottie 动画每帧都调用ctx.drawImage绘制大量小图。解法在src/game/RenderManager.ts的drawAnimations()方法中对正在播放的 Lottie 实例做帧率限流——if (frameIndex % 2 0) anim.goToAndPlay(frameIndex, true);强制 30fps 播放。4. 避坑指南真机调试中踩过的 5 个血泪坑附现场修复命令微信小游戏开发最反直觉的地方在于本地跑通 ≠ 真机能跑真机能跑 ≠ 线上稳定。下面这 5 个坑是某开发者在灰度发布前 48 小时连续排查出来的每一条都附带「现象 → 原因 → 解决」和可直接执行的修复命令。4.1 现象iOS 真机上所有果蔬图片显示为纯色方块Android 正常原因iOS 微信对.webp格式支持不一致部分系统版本如 iOS 15.4无法解码有透明通道的 webp。项目res/images/fruits/下的apple.webp使用了 Alpha 通道但未提供 fallback。解决将fruits/下所有.webp替换为带 alpha 的.png并修改PathResolver.ts的resolveImage方法优先尝试.png// src/utils/PathResolver.ts export function resolveImage(name: string): string { // 原逻辑return res/images/${name}.webp; // 改为 return res/images/${name}.png; // 强制用 png体积稍大但兼容性 100% }执行命令在项目根目录find res/images/fruits -name *.webp -exec convert {} -alpha on -background none -resize 120x120\! {}.png \; rm res/images/fruits/*.webp4.2 现象拖拽果蔬时手指离开屏幕后果蔬继续移动一段距离才停下原因DragManager.ts中的速度衰减算法未考虑设备 DPI。deltaTime计算正确但position.x velocity.x * deltaTime * 0.98的0.98是固定阻尼系数在高刷屏120Hz上衰减过慢。解决根据wx.getSystemInfoSync().pixelRatio动态调整阻尼// src/core/DragManager.ts const pixelRatio wx.getSystemInfoSync().pixelRatio; const DAMPING pixelRatio 2.5 ? 0.95 : 0.98; // 高 DPI 设备用更强阻尼 velocity.x * DAMPING; velocity.y * DAMPING;4.3 现象关卡结算界面文字模糊像被涂抹过原因res/images/ui/下的icon_score.png是 200×200px但在pages/index/index.wxml中用stylewidth:40px;height:40px;缩放显示微信小游戏对 Canvas 的drawImage缩放无抗锯齿。解决提供 2x 分辨率图并在wxml中用rpx单位!-- pages/index/index.wxml -- image src{{scoreIcon}} stylewidth:40rpx;height:40rpx;/image对应 JS 中// pages/index/index.ts this.setData({ scoreIcon: res/images/ui/icon_score2x.png // 2x 图尺寸 80×80px });4.4 现象连续玩 5 局后内存占用飙升至 300MB游戏崩溃原因Lottie 动画实例未销毁。每次消除调用loadAnimation()创建新实例但旧实例的destroy()未被调用Canvas 上下文持续占用内存。解决在src/game/AnimationManager.ts中维护动画实例池// src/game/AnimationManager.ts private static pool: Recordstring, any {}; static play(animationName: string, canvas: HTMLCanvasElement) { let anim this.pool[animationName]; if (!anim) { anim loadAnimation({ container: canvas, path: res/animations/${animationName}.json }); this.pool[animationName] anim; } anim.goToAndPlay(0, true); } // 播放完毕后自动回收Lottie 事件 anim.addEventListener(complete, () { setTimeout(() anim.destroy(), 100); // 延迟销毁确保帧结束 });4.5 现象微信后台切回前台时游戏状态丢失回到初始界面原因微信小游戏在后台时会冻结requestAnimationFrame但GameLoop未监听wx.onShow事件重置状态。解决在src/app.ts中添加生命周期监听// src/app.ts App({ onShow() { console.log([App] onShow - resume game loop); if (gameCore.GameLoop !gameCore.GameLoop.isRunning) { gameCore.GameLoop.start(); // 重启循环 gameCore.InputManager.reset(); // 重置触摸状态 } }, onHide() { console.log([App] onHide - pause game loop); if (gameCore.GameLoop gameCore.GameLoop.isRunning) { gameCore.GameLoop.stop(); // 暂停循环 } } });5. 核心模块拆解DragManager.ts与LevelStateMachine.ts的逐行实现逻辑这两个文件是《果蔬去哪了》区别于“玩具 demo”的关键。它们不是简单的“鼠标拖拽”封装而是针对触屏特性深度优化的状态协调器。下面我带你一行行读透重点看为什么这么写而不是“它写了什么”。5.1DragManager.ts触摸坐标的三次映射与防抖设计拖拽体验好不好70% 取决于坐标处理。本项目做了三层映射设备坐标 → Canvas 坐标微信touchstart事件的clientX/clientY是屏幕坐标需转为 Canvas 内部坐标考虑scale和offsetCanvas 坐标 → 游戏世界坐标Canvas 是像素单位游戏逻辑用逻辑单位如 1 单位 10px需缩放游戏世界坐标 → 网格坐标所有果蔬对齐 64×64 网格坐标需取整对齐避免亚像素渲染模糊。// src/core/DragManager.ts class DragManager { private currentFruit: Fruit | null null; private isDragging false; private lastTouchTime 0; private touchStartPos: { x: number; y: number } { x: 0, y: 0 }; onTouchStart(e: TouchEvent) { const touch e.touches[0]; // Step 1: 设备坐标 → Canvas 坐标 const rect gameCore.RenderManager.canvas.getBoundingClientRect(); const canvasX touch.clientX - rect.left; const canvasY touch.clientY - rect.top; // Step 2: Canvas 坐标 → 游戏世界坐标Canvas 逻辑宽高 750×1334适配 iPhone X const worldX (canvasX / rect.width) * 750; const worldY (canvasY / rect.height) * 1334; // Step 3: 防抖150ms 内重复 touchstart 忽略防误触 const now Date.now(); if (now - this.lastTouchTime 150) return; this.lastTouchTime now; // Step 4: 网格对齐找最近的果蔬64×64 网格 this.currentFruit gameCore.FruitManager.findNearestFruit(worldX, worldY, 64); if (this.currentFruit) { this.isDragging true; this.touchStartPos { x: worldX, y: worldY }; this.currentFruit.setState(FruitState.DRAGGING); // 触发拖拽开始音效 gameCore.AudioManager.play(drag); } } onTouchMove(e: TouchEvent) { if (!this.isDragging || !this.currentFruit) return; const touch e.touches[0]; const rect gameCore.RenderManager.canvas.getBoundingClientRect(); const worldX (touch.clientX - rect.left) / rect.width * 750; const worldY (touch.clientY - rect.top) / rect.height * 1334; // 关键不是直接赋值而是用速度插值避免跳跃感 const dx worldX - this.currentFruit.position.x; const dy worldY - this.currentFruit.position.y; // 限制最大位移防止甩出屏幕 const maxMove 200; // 逻辑单位 const distance Math.sqrt(dx * dx dy * dy); if (distance maxMove) { const ratio maxMove / distance; this.currentFruit.position.x dx * ratio; this.currentFruit.position.y dy * ratio; } else { this.currentFruit.position.x worldX; this.currentFruit.position.y worldY; } } onTouchEnd() { if (!this.isDragging || !this.currentFruit) return; // 松手时触发碰撞检测 const collisionResult gameCore.CollisionDetector.check(this.currentFruit); if (collisionResult.hit) { // 消除逻辑 gameCore.FruitManager.removeFruit(this.currentFruit); gameCore.AnimationManager.play(explode, gameCore.RenderManager.canvas); gameCore.ScoreManager.add(collisionResult.score); } else { // 归位用缓动动画回到初始网格点 const gridX Math.round(this.currentFruit.position.x / 64) * 64; const gridY Math.round(this.currentFruit.position.y / 64) * 64; gameCore.TweenManager.to(this.currentFruit.position, { x: gridX, y: gridY }, 300); } this.isDragging false; this.currentFruit null; } }参数说明maxMove 200单位是逻辑坐标1 单位 ≈ 10px防止用户快速滑动时果蔬飞出屏幕边界TweenManager.to(..., 300)300ms 缓动归位 easing 函数为easeOutQuad已在src/utils/TweenManager.ts实现比线性归位更自然findNearestFruit(..., 64)64 是网格大小函数内部遍历所有果蔬计算(x - worldX)² (y - worldY)²取最小值O(N) 但 N ≤ 20可接受。5.2LevelStateMachine.ts用状态机替代 if-else 嵌套的关卡流程传统写法if (gameState ready) { ... } else if (gameState playing) { ... } else if (gameState pausing) { ... }。本项目用状态机每个状态有明确的enter()、update()、exit()逻辑隔离可测试性强。// src/core/LevelStateMachine.ts type LevelState INIT | READY | PLAYING | PAUSED | FINISHED | FAILED; class LevelStateMachine { private state: LevelState INIT; private states: RecordLevelState, StateHandler { INIT: { enter: () { gameCore.LevelData.loadCurrent(); // 加载当前关卡配置 gameCore.FruitManager.spawnAll(); // 生成所有果蔬 gameCore.ScoreManager.reset(); // 重置分数 this.transition(READY); } }, READY: { enter: () { gameCore.UIManager.showReadyScreen(); // 显示“准备”倒计时 gameCore.Timer.start(3000, () this.transition(PLAYING)); // 3秒后进入游戏 } }, PLAYING: { enter: () { gameCore.Timer.start(gameCore.LevelData.timeLimit, () { this.transition(FAILED); // 时间到失败 }); }, update: (deltaTime: number) { // 每帧检查胜利条件 if (gameCore.ScoreManager.current gameCore.LevelData.targetScore) { this.transition(FINISHED); } } }, PAUSED: { enter: () gameCore.UIManager.showPauseScreen(), exit: () gameCore.UIManager.hidePauseScreen() }, FINISHED: { enter: () { gameCore.AudioManager.play(success); gameCore.UIManager.showWinScreen(); gameCore.Timer.start(2000, () { gameCore.LevelManager.nextLevel(); // 加载下一关 this.transition(INIT); }); } }, FAILED: { enter: () { gameCore.UIManager.showLoseScreen(); gameCore.Timer.start(2000, () this.transition(INIT)); } } }; transition(toState: LevelState) { if (this.state toState) return; const fromState this.state; if (this.states[fromState]?.exit) this.states[fromState].exit(); this.state toState; if (this.states[toState]?.enter) this.states[toState].enter(); } update(deltaTime: number) { if (this.states[this.state]?.update) { this.states[this.state].update(deltaTime); } } }设计价值transition()方法确保状态切换原子性先exit当前状态再enter新状态update()只在PLAYING状态被调用避免在READY或PAUSED时执行无关逻辑新增状态如RETRYING只需在states对象中添加一项无需改动其他状态逻辑。6. 二次开发实战如何新增“限时连击”玩法并接入微信排行榜现在你已经跑通、理解了核心模块下一步是动手改。我以“新增限时连击Combo玩法”为例演示从需求分析、代码改造、真机验证到上线的完整闭环。这不是理论是某开发者上周刚落地的功能。6.1 需求拆解什么是“限时连击”定义玩家在 1.5 秒内连续消除 ≥3 个果蔬触发连击特效额外加分规则连击计时器每次消除重置为 1.5 秒连击数1.5 秒内消除数量满 3 个触发combo动画分数加成baseScore × comboCountcomboCount 最高为 5防无限叠加数据上报连击次数计入微信「用户最高连击数」排行榜。6.2 代码改造三处关键注入点第一步在FruitManager.ts中暴露消除事件原代码中removeFruit()是私有方法需改为发布事件// src/core/FruitManager.ts class FruitManager { // ... 其他代码 removeFruit(fruit: Fruit) { const index this.fruits.indexOf(fruit); if (index ! -1) { this.fruits.splice(index, 1); // 新增发布消除事件 gameCore.EventBus.emit(fruit-removed, { fruit, timestamp: Date.now() }); } } }第二步新建ComboManager.ts管理连击逻辑// src/core/ComboManager.ts class ComboManager { private comboCount 0; private lastRemoveTime 0; private readonly COMBO_WINDOW_MS 1500; // 1.5秒窗口 private readonly MAX_COMBO 5; constructor() { // 订阅消除事件 gameCore.EventBus.on(fruit-removed, (e) this.onFruitRemoved(e)); } private onFruitRemoved(event: { fruit: Fruit; timestamp: number }) { const now Date.now(); // 判断是否在连击窗口内 if (now - this.lastRemoveTime this.COMBO_WINDOW_MS) { this.comboCount Math.min(this.comboCount 1, this.MAX_COMBO); this.lastRemoveTime now; // 满 3 个触发连击 if (this.comboCount 3) { this.triggerCombo(); } } else { // 窗口超时重置 this.comboCount 1; this.lastRemoveTime now; } } private triggerCombo() { // 播放连击动画 gameCore.AnimationManager.play(combo, gameCore.RenderManager.canvas); // 加分基础分 × 连击数 const baseScore 10; const bonus baseScore * this.comboCount; gameCore.ScoreManager.add(bonus); // 更新 UI 显示连击数 gameCore.UIManager.showComboText(×${this.comboCount}); } getComboCount(): number { return this.comboCount; } } // 导出单例 export const comboManager new ComboManager();第三步接入微信排行榜在src/app.ts的onLaunch中初始化排行榜// src/app.ts App({ onLaunch() { // ... 其他初始化 // 初始化排行榜需提前在微信公众平台配置 wx.getOpenDataContext().canvas; // 触发开放数据域初始化 // 上报连击数到排行榜 wx.setUserCloudStorage({ KVDataList: [{ key: max_combo, value: String(comboManager.getComboCount()) }], success: () console.log(max_combo saved to cloud), fail: (err) console.error(save max_combo failed, err) }); } });6.3 真机验证 checklist改完代码别急着提交按此清单逐项验证验证项操作预期结果失败排查连击计时器重置消除一个果蔬等 2 秒后再消除第二个comboCount为 1检查COMBO_WINDOW_MS是否写成150少个 0连击动画触发1.5 秒内快速消除 3 个果蔬屏幕中央显示×3文字 combo.json动画检查AnimationManager.play(combo)路径是否为res/animations/combo.json分数加成生效连击 3 次基础分 10 → 应得 30 分ScoreManager.current增加 30检查ScoreManager.add()是否传入bonus而非baseScore排行榜数据上报玩一局后在微信「发现」→「小程序」→「我的小程序」→「果蔬去本文还有配套的精品资源点击获取