简介:微信小游戏《果蔬去哪了》是一款休闲益智类产品,这份源码与素材包适合正在学习微信小游戏开发、或希望借助Cocos Creator完整工程快速上手小游戏项目的开发者。压缩包共738个文件、约6.79MB,包含png游戏贴图、js逻辑脚本、json配置文件、mp3音频、atlas/plist图集以及cocos2d-js-min.js引擎文件,清晰展示了从图像资源、动画序列到程序代码的整体组织方式。该资源已有615人学习下载,源码覆盖游戏框架、点击消除事件处理、CSS3/JavaScript动效、计分与关卡逻辑、UI布局、本地数据存储、错误处理、性能优化、资源预加载与释放等关键实现,并预留了网络通信接口便于扩展。配合全部图片、音效与动画素材,开发者可以一步步还原《果蔬去哪了》的完整玩法;无论初学者还是有经验的开发者,都能从这套工程中提炼可复用的设计思路,并在此基础上改造出属于自己的微信小游戏。
1. 先给《果蔬去哪了》源码包定性:能上线的工程和教学 Demo 差在哪
第一次拿到《果蔬去哪了》这套微信小游戏线上源码和资源时,我第一反应不是急着打开工具,而是先审一遍它够不够「线上」两个字。线上和教学 Demo 的区别,就在三样东西上:一个规范的小游戏入口 game.js 和配套的 game.json;一套组织清晰的图片、音频资源;以及配置上能让你绕开开发环境直接上真机验证的工程设置。三者缺一,就只能算 Demo,补齐了才谈得上下一步。这篇文章按我的接手顺序来写:先读包结构,再本地跑通,接着动资源改玩法,最后把上线前那些高频坑提前踩一遍。适合正在做果蔬、消除、配对这类休闲小游戏的新团队,也适合想拿现成工程练手微信小游戏架构的开发者——结构和资源都是能复用的一等公民。
2. 源码包结构拆解:从入口文件到资源目录,先画出代码地图
拿到源码包,别急着双击 project.config.json 往编辑器里拖。我习惯先把整个目录摊开看一遍,因为线上工程的坑大多藏在组织方式里——入口文件、资源目录、配置字段,任何一样放错位置,开发者工具都能给你报一个看似无关的错。这一章只干一件事:建立代码地图,让你在改任何东西之前先知道它躺在哪个文件里。
2.1 目录地图:每个文件该放什么、不该放什么
这类线上源码包的骨架一般长这样,我建议你把包里的实际目录和它对一遍,重点看入口和资源位置:
果蔬去哪了/ ├─ game.js # 小游戏入口,全局只有一个 ├─ game.json # 小游戏配置:横竖屏、网络超时、分包声明 ├─ project.config.json # 开发者工具工程配置,不含业务逻辑 ├─ js/ │ ├─ main.js # 启动顺序与场景切换 │ ├─ scene/ │ │ ├─ bootScene.js # 加载资源用的启动场景 │ │ ├─ gameScene.js # 主玩法场景 │ │ └─ overScene.js # 结算场景 │ ├─ utils/ │ │ ├─ atlas.js # 合图(雪碧图)加载与取帧 │ │ └─ audio.js # 音频统一封装 │ └─ conf/ │ └─ levels.js # 关卡配置:出哪些果蔬、限时多少 └─ res/ ├─ images/ # 独立散图,UI 按钮之类 ├─ atlas/ # 合图:fruits.json + fruits.png 成对出现 └─ audio/ # BGM、点击、答对、答错音效看目录先找两个东西:game.js 和 game.json。game.js 是微信小游戏唯一确定的入口文件,开发者工具编译时首先读它;game.json 决定运行环境,比如横屏还是竖屏、要不要状态栏、哪些目录是分包。res 目录的命名基本能看出资源是散图还是合图——散图适合 UI 和数量少的图标,果蔬这类大量重复出现的游戏元素基本都走合图,后面第 4 章会专门说。
另外,如果目录里出现 stage/ 这类分包目录,不要把它当成普通资源文件夹。它的意思是某些场景要延后加载,降低首包体积。改错这里,提审时包体可能直接超上限。
2.2 入口与主循环:GameApp、场景切换和时间步长
小游戏没有 DOM,没有 document.getElementById,页面渲染全靠 Canvas。所以你看到的入口代码不会有 index.html,而是从 game.js 开始挂一个全局对象:
// game.js:小游戏的唯一入口,被开发者工具直接执行 const { GameApp } = require('./js/main.js'); // 小游戏的启动时机要等 onShow 之后 Canvas 才完全就绪 GameApp.getInstance().onLaunch();require 是 CommonJS 模块写法,微信小游戏支持。GameApp 用单例模式,防止开发工具热重载时重复初始化。main.js 内部通常还会做一次兼容降级——在基础库版本不支持 requestAnimationFrame 时退回 setTimeout,避免低端机白屏:
// js/main.js:启动顺序与主循环 const raf = typeof requestAnimationFrame === 'function' ? (cb) => requestAnimationFrame(cb) : (cb) => setTimeout(() => cb(Date.now()), 16); class GameApp { constructor() { this.scene = null; this.lastTime = 0; } onLaunch() { this.bindEvents(); } bindEvents() { // 切后台再切回来,dt 会被拉大,必须截断 wx.onShow(() => { this.lastTime = 0; this.loop(); }); } loop() { const now = Date.now(); const dt = Math.min(now - this.lastTime, 50); // 上限 50ms 防止瞬移 this.lastTime = now; this.scene && this.scene.update(dt); this.scene && this.scene.render(); raf(() => this.loop()); } }这段代码最值得抄的是 dt 截断。微信小游戏切后台回前台,Date.now() 的差值可能是几秒甚至几十秒,如果你直接拿它去做移动步长,果蔬会瞬移到屏幕外;截断到 50ms 后最多跳一帧,体验不会出现断层。「果蔬去哪了」这类点选或配对玩法的对象位置大多由场景管理,update 和 render 分离,正好方便分析和改动。
画面切换通常再包一层场景管理器,把 boot、game、over 三个场景注册成表,切换时不销毁对象而是替换引用:
// 场景注册表:新增场景只需在表里加一行 const scenes = { boot: BootScene, game: GameScene, over: OverScene, }; function switchTo(name, params) { const SceneCls = scenes[name]; if (!SceneCls) throw new Error('scene not found: ' + name); const app = GameApp.getInstance(); app.scene && app.scene.onExit && app.scene.onExit(); app.scene = new SceneCls(params); app.scene.onEnter && app.scene.onEnter(); }场景名约定成 boot、game、over 是这类项目的惯用套路,好处是新增玩法场景时不需要改动主循环,只往表里加一行。参数 params 一般用来传关卡号或者上次得分,switchTo 的调用方不必关心场景内部怎么消费这些参数。
2.3 资源约定:图片、音频、关卡 JSON 各自的位置和命名习惯
| 目录 | 内容 | 接手时的两个注意点 |
|---|---|---|
| js/conf/levels.js | 关卡配置 | 找「出题」「时限」字段,别在场景代码里瞎搜 |
| res/images/ | 独立散图 | 换图时保持同名或同步改引用 |
| res/atlas/ | 合图 json + png | 只改 png 会错位,必须重新生成 json |
| res/audio/ | 音效与 BGM | mp3 优先,避免冷门编码 |
命名习惯上,我强烈建议保持英文小写加下划线,像 a_fruit_apple.png 这样。不要出现中文路径,尤其是食材名用拼音命名的情况,部分安卓机型的文件系统对中文路径支持有坑,线上包最容易在这里翻车。资源的相对引用路径建议统一以 / 开头,比如 '/res/atlas/fruits.png',这是小游戏环境的标准写法。
3. 本地跑通最小路径:导入项目、改三处配置、把首屏跑出来
结构认完了,剩下就是把它在微信开发者工具里跑起来。这一步大多数人栽在「目录选错」和「配置文件没改」上,下面按我自己的顺序来。先别管玩法逻辑,目标是看到首屏果蔬画面稳定出现,并且点击有反馈。
3.1 导入的正确姿势:选目录、选 AppID、开调试
导入步骤按下面走,缺一步都可能给你埋个后期雷:
- 打开微信开发者工具,项目类型选「小游戏」,不是「小程序」,这两者的编译目标不一样。
- 目录选到包含 game.json 的那一层,既不是它上一级,也不是它下一级。选错层级是最常见的导入失败原因。
- AppID 没有的话直接选「测试号」,不影响本地跑通;有游戏类 AppID 就填自己的。
- 导入后,点「详情 — 本地设置」,勾上「不校验合法域名」。这一步只影响开发期,正式上线以管理后台配置为准。
选 AppID 这里有个细节:测试号模式下 wx.login 拿不到真实 openid,本地模拟没问题,但如果你要接排行榜、云存储这类能力,后面得换成正式游戏 AppID 再联调。项目名建议在 project.config.json 里改成英文小写下划线,中文项目名在某些版本的工具里会引发路径转义问题。
3.2 两个配置文件的三处必改项
先把工程配置打开,确认下面三个字段:
{ "appid": "touristappid", "compileType": "game", "projectname": "guoshu_qu_na_le", "libVersion": "latest" }appid 默认是 touristappid,表示游客测试号,发布前要改成你自己的。compileType 必须是 game,如果这里是 miniProgram,工具会按小程序逻辑编译,报一堆莫名其妙的错。projectname 最好用英文小写下划线,避免部分机器上的路径问题。
再看 game.json,这是小游戏运行时的总开关:
{ "deviceOrientation": "portrait", "showStatusBar": true, "networkTimeout": { "request": 5000, "connectSocket": 5000 }, "subpackages": [ { "name": "stage", "root": "stage/" } ] }deviceOrientation 控制横竖屏,果蔬题材休闲游戏默认 portrait 竖屏,追求沉浸感的改成 landscape 也行。showStatusBar 决定顶部是否显示系统状态栏,部分工具版本在 true 时会出现刘海屏适配问题。subpackages 声明分包后,root 字段指向的目录必须真实存在,否则编译直接报错,不存在就整段删掉。
3.3 首屏验证清单:看到什么算通了
跑通后别急着改代码,先对照这份清单确认环境干净:
- Console 没有红色 error,最多有 deprecated 警告。
- bootScene 日志能看到资源加载进度,说明启动链路正常。
- 模拟器里出现果蔬标题或开始按钮,点击后有音效反馈。
- 切一次后台再回来,画面没有黑屏或跑飞。
常见误导是开发者工具里一切正常,真机白屏。原因是本地调试勾了「不校验合法域名」,同时资源路径用了绝对地址,真机环境解析时机不一样。这个坑在第 5 章展开,这里先记住:首屏验证只代表本地通了,不代表真机通。
4. 资源与玩法改造:合图加载、音效适配、判定与数值调参
这一章是动手改代码的重头戏。很多人拿到源码后第一件事是换素材、调手感,结果换完图错位、音效不响、难度失衡。其实根源都在三个地方:资源的加载方式、音效的持有方式、判定代码的耦合位置。逐个拆开看。
4.1 合图与单图替换:怎样换素材并且不踩到合图坑
Canvas 2D 里每调一次 drawImage 就是一次贴图操作,全用散图会大量占用渲染时间;把几十个果蔬合成一张大图,运行时按小图坐标裁剪,性能会好一个量级。这就是 res/atlas/ 下 fruits.json + fruits.png 成对出现的原因。合图 JSON 一般长这样:
{ "frames": { "apple.png": { "frame": { "x": 2, "y": 2, "w": 64, "h": 64 } }, "tomato.png": { "frame": { "x": 68, "y": 2, "w": 64, "h": 64 } } } }读取合图并绘制小图的封装大概是这样:
// atlas.js:读取合图 json,建立 name -> 小图矩形 的映射 function loadAtlas(jsonPath, imagePath) { return new Promise((resolve, reject) => { const img = wx.createImage(); img.onload = () => resolve({ img, frames: require(jsonPath).frames }); img.onerror = reject; img.src = imagePath; // 小游戏环境的相对路径 }); } // 渲染时按帧信息裁剪:sx/sy/sw/sh 取自合图 function drawFrame(ctx, atlas, name, dx, dy) { const f = atlas.frames[name].frame; ctx.drawImage(atlas.img, f.x, f.y, f.w, f.h, dx, dy, f.w, f.h); }注意 f.x、f.y 是原图在大图里的像素坐标,不是 UI 坐标。换素材时最常见的错误是只替换 fruits.png,不重新生成 fruits.json,结果旧坐标裁出新图的其他部分,果蔬串了脸。正确做法是用工具重新导出合图,保证 json 和 png 成对更新。如果只换一两张素材,也可以临时改成散图加载,用 wx.createImage 直接读 res/images 下的同名文件,代价是 draw 次数增加,低端机上会掉帧。
4.2 音效适配:格式选择、预加载与播放回调用法
wx.createInnerAudioContext 创建的音频实例,模拟器上怎么播都正常,真机上问题一堆。格式上 mp3 覆盖面最大,但部分老安卓对高码率 mp3 解码有问题,常见做法是统一转成 128kbps,并把短音效在场景加载时预创建实例,避免每次点击现场 new 一个:
// audio.js:统一音频管理,避免每次播放都 new 实例 const cache = {}; function preload(name, src) { const audio = wx.createInnerAudioContext(); audio.src = src; audio.onError((e) => console.warn('audio error', name, e)); cache[name] = audio; } function play(name) { const audio = cache[name]; if (!audio) return; audio.stop(); audio.play(); }preload 的第二个参数 src 建议写成 '/res/audio/pick.mp3' 这样的绝对相对路径,不要用 './res/audio/pick.mp3',后者在部分基础库版本下解析不一致。play 之前先调 stop 是为了快速连点时不叠音,音效手感会干净很多。真机无声时先看 onError 输出,再查音频格式,最后查基础库版本——顺序反了容易被误导。
4.3 玩法判定的常见形态:点选型的命中判定与状态流转
处理这类源码时,我一般先判断它属于三种形态里的哪一种:找茬点选型、配对消除型、归类放置型。三种玩法的代码核心都围绕「果蔬对象的 type 和 state」两个字段,区别只在点击后怎么走状态。下面是点选型最常见的一套骨架:
// 每个果蔬对象的核心字段 const item = { id: 7, type: 'apple', // 素材类型,用于判定对不对 state: 'idle', // idle 待点选 / picked 已选中 / settled 已消除 x: 120, y: 300, w: 60, h: 60 }; // 命中测试:触摸点是否落在果蔬矩形内 function hitTest(tx, ty, item) { return tx >= item.x && tx <= item.x + item.w && ty >= item.y && ty <= item.y + item.h; } // 点击处理:点对进 picked,点错震动提示 function handleTap(touch) { const hit = scene.items.find((it) => hitTest(touch.x, touch.y, it)); if (!hit) return; if (hit.type !== level.wantedType) { wx.vibrateShort({ type: 'light' }); return; } hit.state = 'picked'; checkWin(); }level 是当前关卡对象,由 switchTo 从 levels.js 传进来,wantedType 表示本关要点的果蔬类型。改成配对消除型时,把「点对即 picked」换成「两个已 picked 的 item 比较 id,相同才 settled」;归类放置型则看触摸结束位置是否落在目标区域。改判定逻辑时最忌讳直接在 render 里写状态判断,状态流转只放在 update 和事件函数里,画面表现只根据 state 映射。
4.4 难度曲线:时间、目标数量、步数三个参数先调哪个
| 参数 | 位置 | 手感影响 | 第一次调整建议 |
|---|---|---|---|
| 限时 | levels.js 每关 time 字段 | 直接决定紧张感 | 从原值 +15 秒开始试 |
| 目标数量 | levels.js 每关 target 字段 | 决定单局时长 | 每次加减 1~2 个 |
| 步数或点错惩罚 | levels.js 或场景内常量 | 决定容错空间 | 点错扣时间改成扣步数 |
我一般先调限时,跑三局感觉节奏,再动目标数量。三个参数一起改,调崩了根本分不清是哪个改过头。数值改完记得冷启动验证一遍,热重载和真机冷启动的计时基准不一样,启动速度差甚至会吃掉一秒限时。
5. 避坑清单:5 个线上小游戏高频翻车点与排查顺序
这一章直接给结论。以下五个问题是我在接手类似源码包时遇到次数最多的,每条都按现象—原因—解决来写,你可以把它当成排查手册用。
5.1 编译报错找不到 game.js:几乎都是目录层级选错
现象:导入后工具提示 game.js 不存在,或者编译立刻失败。
原因:双击解压后目录多套了一层,比如真正项目在 guoshu_qu_na_le/ 里,你导入的是外层文件夹;也可能误把 js 目录当成了根目录。工具只认 game.json 所在的层级。
解决:打开文件管理器,找到包含 game.json 的那一层重新导入。拿不准就搜 game.json,它旁边必须躺着 game.js 和 project.config.json。
5.2 真机白屏模拟器正常:资源 onload 和绝对路径两大元凶
现象:开发者工具里画面正常,手机预览白屏,Console 可能没有任何报错。
原因:图片没等 onload 完成就开始 drawImage。开发者工具本地解码极快,真机解码慢,首帧绘制时 image 还没就绪,整个场景画了个寂寞。另一个常见原因是 src 写了带 ./ 的相对路径,真机解析时机不一致。
解决:所有资源加载改成 Promise 化,启动场景等全部 onload 后再切主场景:
loadAtlas('/res/atlas/fruits.json', '/res/atlas/fruits.png') .then((atlas) => { bootScene.init(atlas); }) .catch((e) => console.error('atlas load failed', e));init 必须发生在 onload 之后,任何提前引用 atlas.img 的代码都是定时炸弹。
5.3 模拟器有声音真机无声:音频实例生命周期与格式问题
现象:模拟器点击果蔬有音效,手机上一声不吭。
原因:三个候选——音频实例被垃圾回收、src 相对路径解析失败、音频编码格式真机不支持。
解决:把音频实例存进模块级 cache 持有引用,参考第 4.2 节的封装;src 统一改成 /res/audio/xxx.mp3 绝对相对路径;格式统一转 128kbps 的 mp3。修复后用 onError 回调确认是否还有报错,不要凭感觉重试。
5.4 模拟器能联网真机超时:合法域名白名单没配
现象:排行榜或资源请求在模拟器正常,真机预览全部 timeout。
原因:本地设置里勾了「不校验合法域名」,这个开关只在开发者工具生效。真机上 wx.request 强制要求域名是 HTTPS,且已加入后台的 request 合法域名白名单。
解决:上线前到管理后台把请求域名加进白名单,或者改用云托管免域名配置。提审前拿预览版真机跑一遍网络请求,这是唯一靠谱的验证方式。
5.5 改了资源不生效:工具缓存与合图 json 同步问题
现象:替换了图片、改了 json,重新编译画面还是旧素材。
原因:开发者工具的本地缓存没清,或者是合图只换了 png 没换 json,坐标裁出来还是旧内容。
解决:先清缓存——工具栏「清缓存 — 清除全部」,再重新编译。如果换的是合图里的小图,必须用工具重新导出合图,确保 json 和 png 成对更新。改文件名强制刷新也是土办法,但记得把代码里的引用一起改掉。
6. 上线前最后一关:帧率自检、资源体积与埋点开关
6.1 提审前十分钟的四个检查
| 检查项 | 做法 | 通过标准 |
|---|---|---|
| 帧率 | 打开工具帧率面板或代码统计 | 稳定在 55 以上,不出现周期性卡顿 |
| 包体 | 详情面板看上传大小 | 首包不超上限,分包配置对得上 |
| 域名 | 管理后台核对白名单 | 所有 wx.request 均为 HTTPS 已备案域名 |
| 真机 | 一台低端安卓加一台老旗舰机 | 无白屏、无内存持续上涨 |
6.2 实测帧率:用 60 行代码代替肉眼
模拟器帧率说明不了问题,低端机才是试金石。我提审前会往主循环开头临时挂一段诊断代码:
let last = 0, frames = 0, fps = 0; function measure() { const now = Date.now(); if (last > 0) fps = 1000 / (now - last); last = now; if (++frames % 60 === 0) console.log('fps=', fps.toFixed(1)); requestAnimationFrame(measure); } measure();连续跑五秒,看日志里 fps 的波动幅度。如果个别帧掉到 30 以下,多半是合图没用上或者每帧 new 了临时对象,回到第 4 章的思路排查。跑完这段代码记得删掉,它本身挂在主循环外,会额外触发一次渲染调度。
6.3 我的收尾习惯
交付前我一定完整跑三局,从冷启动开始,记录启动耗时、首次音效延迟、切后台再回前台的表现。这个习惯救过我几次,教训是同一个:热重载正常不代表冷启动正常,模拟器流畅不代表低端机流畅。我现在的规矩是,任何一次数值改动或资源替换,都跟一局完整测试,而不是看一眼首屏就收工。这套工程本身不难,难的是把每一个变量都验证到位。希望帮到你。
本文还有配套的精品资源,点击获取