简介:这款微信小程序完整demo实现经典“飞翔的小鸟”小游戏,前端利用canvas渲染动态画面与碰撞检测,后端提供java接口支持,非常适合小程序开发者、前端初学者作为实战练习。资源包共35个文件,压缩后仅284KB,包含png/jpg图片、js逻辑脚本、wxml页面、wxss样式、json配置以及java后端代码,覆盖了小程序项目的主要构成,目录层次清晰,便于逐模块拆解学习。目前已有541人学习下载,说明该demo对入门小程序游戏开发有实际参考价值。通过源码可学习canvas绘制游戏场景的完整流程,掌握小程序端请求java后端接口的基本方式,还能参照README和配置文件快速复用项目骨架,适合在基础上构建自己的小程序交互小游戏。
1. 飞翔的小鸟微信小程序demo:canvas游戏与java后端,拆包实测能学到什么
做小程序的人手里多少都囤过几个完整demo,但真正能一路跑通、还改得动的没几个。这份“飞翔的小鸟”是微信小程序原生工程,游戏画面全程用canvas实现,后端配了java示例,打包标记是“适用1221(学习版)”。它的价值不在游戏本身,而在“小程序生命周期 + canvas游戏循环 + wx.request联调java接口”这条完整链路,一个人照着一套demo就能把三块基础补上。适合第一次想在微信小程序里做canvas项目的开发者,也适合后端想补小程序前端知识的java工程师。目录里能看到标准的app.json、pages、utils、config.js和java后端目录,README和LICENSE都齐全,作为学习版够完整,比零散看教程省事很多。
2. 整体结构与启动:从目录解剖到小程序端跑起来
2.1 目录结构:先看懂这份demo的骨架
对照工程看,这是标准的原生小程序结构,没有用uniapp、Taro这类跨端框架。根目录有app.js、app.json、app.wxss三个全局文件,pages/ 存放游戏页面,utils/ 是工具模块,images/ 提供游戏素材,config.js 承担运行配置,java/ 是后端示例工程。文件不算多,正好适合学习——多一个文件就多一层干扰。
app.json负责页面注册与窗口配置,打开能看到页面路径列表;app.js里是App()入口,做启动时数据初始化;app.wxss定义全局样式。pages/下的游戏页面分成四个文件:.wxml描述结构、.wxss控制样式、.js写游戏逻辑、.json配置本页面窗口。config.js里一般放“后端接口地址”这类需要按环境改的配置,建议你拿到手先打开它看一遍,后面联调全靠这个文件。
images目录里是小鸟、管道、背景等素材图,注意素材图的透明背景和尺寸直接影响到canvas里的绘制位置。utils/里面通常封装了一个request函数,把wx.request包了一层,统一处理baseUrl和错误回调。java/目录从命名看是个独立的java工程,README.md里有这个学习版的使用说明,LICENSE则可以确认代码能不能改、能不能用于自己的学习项目。
2.2 导入微信开发者工具:基础库版本是关键
下载解压后,用微信开发者工具导入项目,这一步有讲究。打开工具后选择“导入项目”,目录选中解压出来的文件夹,AppID可以先用“测试号”,不需要自己注册,等后面要真机预览再换正式的。导入成功后先别急着点编译,进“详情 - 本地设置”,把调试基础库切换到最新稳定版。
这套demo内部标记1221,按学习版的一般整理习惯,它是针对当时基础库环境做的适配。如果开发者工具的默认基础库版本太低,pages里的js调用某些API时会直接报“xxx is not a function”,表现是页面白屏、控制台一堆红色报错,但代码本身没问题。我一般会把基础库选到2.10以上,Canvas 2D接口和同层渲染特性才靠得住。
第一次编译如果报“未找到入口app.json”,多半是解压产生了嵌套目录,工程根目录没选到。解决办法很简单:把解压出来的最内层文件夹整个剪切到上层,让app.json直接位于项目根路径。
2.3 把java后端跑起来:一个可以改端口的学习版
从微信小程序的视角看,这个java后端就是“分数上报 + 排行榜查询”的接口服务。常见做法是Spring Boot直接起一个web应用,最终只需要保证config.js里的baseUrl能访问到它。如果你用IDEA导入,选Maven项目,等依赖下载完,找到标注@SpringBootApplication的主类直接运行;如果用命令行,就在java目录下跑mvn spring-boot:run,前提是本机装好了JDK和Maven。
这里有个新手常踩的坑:Spring Boot默认端口是8080,如果本机8080被占用了,后端起不来。这时去src/main/resources/application.yml里改端口,比如改成8090,同时同步修改小程序端config.js里的baseUrl端口,两边必须一致。后端起来的标志是控制台能看到Tomcat started,此时用浏览器访问一下接口地址,能返回JSON就说明联调环境基本就绪。
3. canvas游戏核心逻辑:绘制、帧循环、碰撞检测三块硬骨头
3.1 用Canvas 2D还是旧版CanvasContext?
飞翔的小鸟这类游戏,核心就是“每帧刷新画面”。在微信小程序里实现它要分新老两套API。旧版用wx.createCanvasContext,操作指令发给原生canvas再统一渲染,API有点别扭;新版是通过canvas.getContext('2d')拿到标准Canvas 2D上下文,与浏览器写法一致,还支持canvas.requestAnimationFrame,游戏循环更平滑。
从这份demo的“canvas实现”描述和基础设施来看,按Canvas 2D读源码是主流做法,也建议你这么学。要拿到2d上下文,得先通过wx.createSelectorQuery().select('#gameCanvas')找到canvas节点,再调用node.getContext('2d')。注意这个小程序里的获取节点过程是异步的,所以初始化代码要放在onReady回调里,而不是onLoad,否则节点还没渲染出来,拿到的对象是null,后面所有绘制都会静默失败。
let canvas = null let ctx = null initCanvas() { wx.createSelectorQuery() .select('#gameCanvas') .fields({ node: true, size: true }) .exec((res) => { if (!res || !res[0]) return canvas = res[0].node ctx = canvas.getContext('2d') const dpr = wx.getSystemInfoSync().pixelRatio canvas.width = res[0].width * dpr canvas.height = res[0].height * dpr ctx.scale(dpr, dpr) this.startGameLoop() }) }这段代码的逻辑是:先查找到canvas节点,拿到节点对象和尺寸;再把canvas内部宽高乘上设备像素比dpr,防止高分屏上画面发虚;最后调用ctx.scale(dpr, dpr)让后续绘制的坐标仍以逻辑像素为准。dpr这一步很容易漏,漏了之后在iPhone上画面会糊,很多“游戏能跑但画质差”的问题都出在这。
3.2 游戏循环:requestAnimationFrame驱动每帧绘制
游戏循环的结构在demo里通常是这样的:每帧调用一次绘制函数,先清空画布,再依次画背景、管道、小鸟,最后做物理更新。把“更新逻辑”和“绘制逻辑”分开写,是这类canvas小游戏最值得模仿的写法,后面加功能不打架。
startGameLoop() { const draw = () => { ctx.clearRect(0, 0, gameWidth, gameHeight) this.drawBackground() this.drawPipes() this.drawBird() this.update() canvas.requestAnimationFrame(draw) } draw() }clearRect把上一帧画面整个擦掉,否则新画面会叠在一起产生拖影;drawBackground先把背景铺满,这样擦除后的瞬间不会闪白;drawPipes根据管道数组逐个画管道,drawBird把小鸟画在当前位置;update函数专门负责“挪位置”——小鸟下落、管道左移、碰撞检测、计分判断都放这里。最后通过canvas.requestAnimationFrame(draw)注册下一帧,和浏览器的requestAnimationFrame行为一致,页面切后台时系统会自动暂停循环,不会白耗性能。
判断游戏是否卡顿,可以看帧耗时。真机上如果每帧耗时偏高,先看是不是画了太多圆角、阴影这类消耗大的API;canvas游戏里fillRect和drawImage是最便宜的,shadowBlur和globalAlpha这种要少用,这是血泪经验。
3.3 物理参数与手感调整
飞翔的小鸟手感好不好,全看几个数字参数。demo里的重力、跳跃速度、管道速度通常在游戏初始化阶段定义为常量,改它们就能改变难度,不需要动结构代码。
| 参数名 | 作用 | 调整方向 |
|---|---|---|
| gravity | 每帧加到小鸟垂直速度上的重力值 | 越大下落越快,游戏越难 |
| jumpVelocity | 点击时给小鸟的向上速度 | 绝对值越大跳得越高 |
| pipeSpeed | 管道水平左移速度 | 越大画面越快 |
| pipeGap | 上下管道之间的缺口高度 | 越小越难钻 |
| pipeInterval | 相邻两组管道的间隔 | 越小管道越密 |
物理更新逻辑在源码里大致是这样:
update() { if (this.gameState !== 'playing') return this.bird.vy += this.gravity this.bird.y += this.bird.vy this.pipes.forEach(pipe => { pipe.x -= this.pipeSpeed }) }这里有个细节:bird.vy是垂直速度,每一帧先给它加上重力,再把更新后的速度加到y坐标上,构成一个加速下落的效果——所以小鸟不动时会越落越快,这才是抛物线手感,而不是匀速掉。调整参数时要注意,gravity和jumpVelocity必须成对调,只改其中一个,手感会变得很怪,要么跳不起来,要么一碰就掉地。
3.4 碰撞检测与得分逻辑
碰撞检测在demo里用的是矩形相交判定,不需要像素级精度。小鸟用一个矩形表示,管道实际是两个矩形:上方管道底部到缺口顶部、下方管道顶部到缺口底部。只要小鸟矩形和任一管道矩形相交,游戏结束。
rectCollide(a, b) { return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y }这段代码是标准的AABB碰撞检测,四个条件同时满足才说明相交:a的左边在b右边左边、a的右边在b右边左边、a的上边在b下边之上、a的下边在b上边之下。建议不要把小鸟矩形设成整个图片尺寸,图片四周通常有透明留白,视觉没撞上但矩形碰了会提前死亡,很挫败。我把小鸟碰撞矩形缩到图片尺寸的70%居中,手感会宽容很多。
得分逻辑也有讲究:不是碰到管道就加分,而是当小鸟穿过管道缺口、小鸟x坐标超过管道右边缘时加1分。源码里用一个scored标记防止同一根管道重复计分,因为管道有多根在屏幕上同时移动,不加标记会在一帧内反复加好几分。
if (pipe.x + pipe.w < this.bird.x && !pipe.scored) { pipe.scored = true this.score++ wx.setStorageSync('score', this.score) }判断条件里的pipe.x + pipe.w是管道右边缘位置,当它小于bird.x说明小鸟已经整个飞过管道,此时加一次分。用wx.setStorageSync存分数是学习版常用的方式,分数能保存在本地缓存里,下次打开游戏还能读出来。排行榜功能则由java后端接口负责,第4章展开。
4. java后端在学习版里的角色:计分配置与接口联调
4.1 后端在整个demo里承担什么?
单机玩这个游戏根本不需要后端,分数自己存本地就行。这份demo专门配一个java后端,目的是补全“微信小程序通过wx.request请求服务器接口”这条完整链路。从学习版定位看,后端基本就干两件事:接收前端提交的成绩、按分数倒序返回排行榜。
很多自学小程序的人卡在第一步:前端的页面、样式都写好了,但不知道怎么跟服务器对话。这份资源把后端源码一并给你,意味着你可以在本机跑通一个“小程序前端 -> java接口 -> 数据返回”的最小闭环。理解了这条链路,以后接登录、接业务数据都是同一套路。
后端工程里的核心模块,按常见做法是这样组织的:一个Controller接收HTTP请求,一个Service处理业务逻辑,一个简单的存储结构保存分数(学习版一般用内存Map或者本地文件,不会上数据库)。这样做的好处是去掉数据库依赖,开箱即跑,零配置。
4.2 接口设计与请求流程
接口设计在源码里通常长这样:POST /api/score/save用来提交分数,GET /api/score/rank用来拉排行榜。以Spring Boot为例,Controller代码结构大致如下:
@RestController @RequestMapping("/api/score") public class ScoreController { @PostMapping("/save") public Result save(@RequestBody ScoreDto dto) { scoreService.save(dto.getUserId(), dto.getScore()); return Result.success("保存成功"); } @GetMapping("/rank") public List<ScoreVo> rank(@RequestParam(defaultValue = "10") int limit) { return scoreService.getTop(limit); } }@RequestBody把前端传过来的JSON自动映射成ScoreDto对象,不用手动解析;@RequestParam(defaultValue = "10")表示limit参数可传可不传,不传默认取前10名。学习版为了减少复杂度,通常不会把接口拆得很细,两个接口配一个内存存储就够了,重点是让你看懂请求从哪进、结果从哪出。
小程序端的请求调用在utils里封装,demo中大致是这个模式:
const config = require('../config.js') function submitScore(score) { return new Promise((resolve, reject) => { wx.request({ url: `${config.baseUrl}/api/score/save`, method: 'POST', data: { userId: 'test001', score: score }, header: { 'content-type': 'application/json' }, success: (res) => resolve(res.data), fail: (err) => reject(err) }) }) }这里值得看的是baseUrl的拼法,config.js里定义一份,所有请求统一引用,换环境时只改一个文件。header里指定content-type为application/json,是为了让java后端能正确把body反序列化成DTO。如果不加这个header,小程序默认发的是表单格式,Spring Boot的@RequestBody会解析失败,直接报400错误。
4.3 联调时最容易忽视的环境问题
前后端联调,十次有八次问题出在网络环境,而不是代码。开发工具里,默认情况下wx.request到http接口会报“url not in domain list”,需要手动打开“不校验合法域名”开关:详情 -> 本地设置 -> 勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”。注意这个勾选只对开发工具当前项目生效,真机预览时体验版必须配https域名,这是微信的硬性规定。
另一个高频问题是本机调试时baseUrl写127.0.0.1或localhost。在开发者工具里这样写没问题,因为工具跑在你的电脑上;一旦切到真机预览,手机访问127.0.0.1访问的是手机自己,不是电脑。正确做法是把config.js里的baseUrl改成电脑的局域网IP,比如192.168.1.105:8090,并且保证手机和电脑在同一WiFi下。我在联调时每次都先ping一下这个IP通不通,再往下排查。
防火墙也是个隐性坑。Windows本机开着防火墙时,java服务的端口默认不对外放行,手机请求会一直超时。遇到这类情况,先把防火墙关掉测试,如果能通再把端口加白名单。
5. 避坑与排查:跑不起来、黑屏、连不上后端的5个真实问题
5.1 导入后报“AppID无效”或“基础库不匹配”
现象:工程导入后还没看代码,编译就报错,提示AppID无效,或者某个API调用时提示undefined。
原因:这份demo里的AppID是作者自己项目的,拿到你这里自然无效;基础库版本低于源码使用的Canvas 2D等接口时,对应方法还没被注入,也会报undefined。
解决:在导入项目时AppID直接选“测试号”,不要用别人的;进入“详情 - 本地设置”,把调试基础库切换到最新稳定版。这两步做完,大多数导入阶段的报错都能消掉。
5.2 canvas黑屏,页面出来了但画面不渲染
现象:开发者工具里能看到页面结构或分数数字,但整个游戏区域是黑色的,重启也没用。
原因:最典型的两个——初始化代码放在onLoad里执行,此时canvas节点还没渲染完成,查询节点返回null;或者同一份代码里混用了旧版wx.createCanvasContext和新版getContext('2d'),两个上下文互不兼容,绘制指令被吞掉。
解决:把canvas初始化逻辑挪到onReady回调里执行,或者用setTimeout延迟50毫秒再查询节点;代码里统一只用Canvas 2D一套接口,不混用。查这个问题时看控制台有没有报错,如果没有任何报错但画面不存在,先打日志确认ctx是否为null。
5.3 开发者工具正常,真机上小鸟和背景出不来
现象:开发者工具里游戏画面完整,一切正常;用真机预览,小鸟看不到或者背景图空白。
原因:图片素材文件名大小写不一致,Windows上开发工具不敏感,但真机文件系统严格区分大小写,引用路径对不上就加载失败;另一种情况是图片路径写成了网络URL,而真机downloadFile域名白名单里没有该域名,被拦截。
解决:打开images目录,挨个核对代码里引用的文件名和实际文件名,大小写必须完全一致;本地图片确保打包进小程序包内,网络图片要么换成包内资源,要么去公众平台配置合法download域名。改完清缓存重新编译,别在旧缓存上反复试。
5.4 点击屏幕小鸟不动,像卡死一样
现象:游戏画面正常滚动,管道在移动,但手指点屏幕小鸟没有任何反应,直接坠落。
原因:事件根本没绑上。有些版本的同层渲染canvas对bindtap响应不稳定,触摸事件被原生组件层截获;还有人把bindtap绑到了外层view,canvas层级覆盖导致点击事件到不了触发区域。
解决:在canvas标签上直接绑定bindtouchstart,不要用bindtap,也不要绑到外层容器。事件处理函数里只做一件事:把bird.vy重置为负的跳跃速度。touchstart在手指按下瞬间就触发,比tap少300毫秒左右的延迟,游戏响应更快。改完真机验证,开发者工具里tap和touchstart差别不明显,真机上才能感到差距。
5.5 分数提交失败,wx.request一直报fail或超时
现象:本地游戏能玩,分数也出来了,但点击提交后控制台打印request:fail,或者一直转圈没响应。
原因:排查按这个顺序来——开发工具没开“不校验合法域名”;java后端没启动成功;baseUrl指向了127.0.0.1而你在真机预览;三个原因各占三成。
解决:先确认后端控制台能看到Tomcat started,浏览器直接访问接口地址能返回JSON;再确认开发工具的本地设置里勾选了不校验合法域名;最后在config.js里把baseUrl改成电脑的局域网IP。如果还是不通,在浏览器里安装一个跨域测试插件去请求后端,先排除前端问题,再回头查防火墙和端口占用。“连不上后端”这类问题最忌讳乱猜,按“前端 -> 网络 -> 后端”的顺序一层层排除,十分钟内能定位。
5.6 一份高效排查顺序
把这5个问题串起来看,我总结了一套自己一直在用的排查流程。先看控制台有没有红字报错,有报错按报错信息搜;没报错但画面不对,就调canvas初始化位置和上下文;画面正常但交互没反应,查事件绑定方式;功能完整但请求失败,从域名校验、后端进程、网络IP三层顺序查。这套流程帮我省下大量“玄学调参”的时间。
6. 进阶用法:换皮改参数,把demo变成你自己的第一个canvas游戏
6.1 换一套素材,五分钟让游戏“变脸”
拿到demo先别急着写新功能,第一步做“换皮”。把images目录下的小鸟图片换成你自己准备的png,管道图片、背景图也一并替换。替换素材时注意两个点:素材必须是透明背景PNG,否则游戏画面上会出现白底方块,很出戏;小鸟图片尺寸不要超过100x100像素,canvas区域就那么大,图太大不仅遮挡视线,缩放绘制还会增加每帧耗时。
替换后用真机预览一局,感受画面是否正常适配。这一步虽然不带任何逻辑改动,但能让你完整走一遍“改资源 -> 重新编译 -> 真机验证”的流程,后续改代码时节奏感会好很多。
6.2 用手感参数调出你自己的难度曲线
拿到demo默认参数玩一下,你会发现手感偏难或偏简单。这时候改动第3.3小节的参数表就行。我一般先调pipeGap,它是决定游戏“能不能过”的关键参数,建议初始设在150像素到180像素之间;再调pipeSpeed,它决定“有没有时间反应”,0.5则趁早放弃,直接调低到2左右。
调参改的就是几个数字,但每次只改一个变量,然后跑一局感受,再改下一个。同时改多个参数会让手感变化无法归因,出了bug也说不清是哪行代码引起的。调完参数后把一组自己满意的数值记下来,我通常在config.js里加一段注释,标注“舒适难度:gravity=0.5, jump=-8, pipeSpeed=2.2”。
6.3 把本地分数接到排行榜,跑通真正的联调
最有成就感的一步,是让游戏结束时自动把分数提交到java后端,然后从后端拉取排行榜展示。在游戏结束的代码分支里调用第4.2小节的submitScore函数,分数打完就传;页面再提供一个“排行榜”按钮,点击后请求GET /api/score/rank接口,把返回结果渲染到页面上。这一步跑通了,这个demo就不再是“一个单机小网页”,而是前后端联通的小程序应用了。
验证方法很简单:连玩三局,让每次分数都不同,然后刷新排行榜看顺序是否正确、分数是否匹配。之后再开一个新会话提交一个低分,确认排名被正确挤到后面。这里最容易翻车的点是排行榜刷新时机,我习惯在每次游戏结束后强制刷新一次,避免页面停留太久展示旧数据。
从那以后,我每次拿到这类canvas游戏demo,都强制走一遍“换素材 -> 调参数 -> 接接口”的步骤,先把工程跑熟再动结构代码,踩坑率直线下降。希望帮到你。
本文还有配套的精品资源,点击获取