资讯动态

微信小程序游戏开发实战:Canvas渲染与Java后端联调

发布时间:2026/10/2 18:14:52 来源:尧图企业网站定制
简介这份微信小程序源码是一套可直接运行的「飞翔的小鸟」完整Demo面向正在学习小程序开发、想通过小游戏案例掌握Canvas绘图与前后端联调的初学者和进阶者。项目以微信小程序原生框架实现游戏主逻辑借助Canvas完成小鸟、管道与背景的绘制和碰撞检测同时附带Java后端代码便于理解前后端数据交互的基本流程。压缩包共35个文件约284KB包含7个js逻辑脚本、4个xml与2个wxml页面结构、3个wxss样式、3个json配置以及png、jpg图片素材和md说明文档另有java后端文件与license目录按pages、images、utils、java等模块划分结构清晰。目前已有541人学习下载。读者可据此快速跑通一个完整小游戏案例掌握Canvas渲染、页面生命周期、资源加载与后端接口对接等关键知识点也可在此基础上替换素材、调整难度参数作为课程设计或练手项目的参考模板。1. 飞翔的小鸟小程序从 Canvas 渲染到 Java 后端的完整闭环微信小程序里做游戏很多人第一反应是“小游戏”类目但用普通小程序页面配合 Canvas 2D 接口同样能跑出流畅的帧动画。这个“飞翔的小鸟”完整 demo 的价值不在于游戏本身多复杂而在于它把前端渲染循环、碰撞检测、分数持久化、Java 后端接口串成了一条可复现的链路。你如果是刚接触微信小程序游戏开发或者想找一个前后端联调的练手项目这套结构比单纯看文档要直观得多。它适合两类人一是想理解 Canvas 在小程序里怎么驱动逐帧动画的前端二是想用 Java 写一套轻量接口给小程序调用的后端。下面按“先跑通渲染、再接通后端、最后处理坑”的顺序拆开讲。2. Canvas 渲染循环与小鸟运动逻辑requestAnimationFrame 在小程序里的正确用法2.1 为什么不用 setInterval 而用 Canvas 自带的帧回调微信小程序的 Canvas 2D 接口提供了requestAnimationFrame它和浏览器端的同名 API 行为接近但有一个关键差异小程序的 Canvas 节点需要通过SelectorQuery获取并且要等节点挂载完成才能拿到上下文。很多新手直接写setInterval每 16 毫秒重绘结果在低端安卓机上帧率抖动严重因为setInterval不保证与屏幕刷新同步而requestAnimationFrame会把回调排到下一次重绘之前。我一般会这样初始化// pages/game/game.js Page({ onReady() { const query wx.createSelectorQuery() query.select(#gameCanvas) .fields({ node: true, size: true }) .exec((res) { const canvas res[0].node const 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.canvas canvas this.ctx ctx this.startLoop() }) }, startLoop() { const loop () { this.update() this.render() this.canvas.requestAnimationFrame(loop) } this.canvas.requestAnimationFrame(loop) } })这段代码里dpr的处理是必须的否则在 iPhone 上画面会发虚。canvas.requestAnimationFrame是 Canvas 节点自身的方法不是全局的这一点和 Web 端不同。update负责改小鸟的 y 坐标和管道位置render负责清屏和重绘。逻辑说明每帧先更新物理状态再绘制最后递归调用帧回调。参数上dpr直接取系统信息不需要手动传canvas.width设的是物理像素ctx.scale之后所有绘制坐标按逻辑像素走。2.2 小鸟重力与管道碰撞的参数怎么调小鸟的下落用重力加速度模拟跳跃给一个瞬时反向速度。我常用的参数是重力0.5跳跃初速度-8管道移动速度2管道间距150。这些值不是固定的但如果你把重力设到1以上小鸟会像石头一样砸下去手感很“玄学”。碰撞检测用矩形相交判断小鸟的碰撞盒要比视觉图片小一圈否则玩家会觉得“明明没碰到却死了”。// 物理更新片段 update() { this.bird.vy 0.5 // 重力 this.bird.y this.bird.vy // 跳跃时设置 this.bird.vy -8 // 管道左移 this.pipes.forEach(pipe { pipe.x - 2 }) // 碰撞小鸟矩形与管道矩形相交 const birdRect { x: this.bird.x - 12, y: this.bird.y - 12, w: 24, h: 24 } for (const pipe of this.pipes) { if (this.isRectIntersect(birdRect, pipe.topRect) || this.isRectIntersect(birdRect, pipe.bottomRect)) { this.gameOver() } } }参数说明vy是垂直速度每帧累加重力bird.x固定不变管道从右往左移动。碰撞盒的w和h比图片实际尺寸小 4 到 6 像素这是血泪经验——视觉上小鸟的翅膀和嘴尖容易造成误判。isRectIntersect就是标准的 AABB 检测四个条件同时满足才算相交。2.3 分数显示与本地缓存的衔接分数用ctx.fillText直接画在 Canvas 上比用view覆盖层更省性能。每通过一个管道分数加一同时用wx.setStorageSync写本地缓存。这里注意不要每帧都写缓存只在分数变化时写。缓存键名用bestScore读取时给默认值0。addScore() { this.score const best wx.getStorageSync(bestScore) || 0 if (this.score best) { wx.setStorageSync(bestScore, this.score) } }逻辑上本地缓存只存最高分当前分数在内存里维护。参数上wx.setStorageSync是同步接口写频繁会卡顿所以只在超越最高分时写一次。如果你要做排行榜这一步就要换成调 Java 后端接口。3. Java 后端接口设计用 Spring Boot 给小程序提供分数上报与排行榜3.1 最小可用的 Spring Boot 工程结构后端不需要复杂一个ScoreController加一个内存存储或 SQLite 就够学习版用。我一般用 Spring Boot 2.7 加 Maven依赖只引spring-boot-starter-web和spring-boot-starter-jdbc数据库用 H2 文件模式省去安装 MySQL 的麻烦。工程结构如下src/main/java/com/example/bird/ BirdApplication.java controller/ScoreController.java model/ScoreRecord.java repository/ScoreRepository.java src/main/resources/ application.yml schema.sqlapplication.yml里配 H2 的路径和端口server: port: 8080 spring: datasource: url: jdbc:h2:file:./data/birddb driver-class-name: org.h2.Driver username: sa password: sql: init: mode: alwaysschema.sql建一张表CREATE TABLE IF NOT EXISTS score_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, openid VARCHAR(64) NOT NULL, score INT NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );参数说明openid是小程序用户唯一标识学习版可以先用假数据代替score存整数create_time自动生成。H2 文件模式会在项目根目录下建data文件夹重启不丢数据。3.2 分数上报接口的请求体与校验小程序端用wx.request发 POST后端用PostMapping接收。请求体用 JSON字段就两个openid和score。后端要做基本校验分数不能为负openid 不能为空。RestController RequestMapping(/api/score) public class ScoreController { Autowired private ScoreRepository scoreRepository; PostMapping(/submit) public MapString, Object submit(RequestBody ScoreRequest request) { MapString, Object result new HashMap(); if (request.getOpenid() null || request.getOpenid().isEmpty()) { result.put(code, 400); result.put(msg, openid 不能为空); return result; } if (request.getScore() 0) { result.put(code, 400); result.put(msg, 分数不能为负); return result; } scoreRepository.save(request.getOpenid(), request.getScore()); result.put(code, 0); result.put(msg, ok); return result; } }逻辑说明先校验参数再写库最后返回统一结构。参数上ScoreRequest是一个简单的 POJO包含openid和score两个字段。注意不要用RequestParam接收 JSON要用RequestBody。返回的code为 0 表示成功非 0 表示失败小程序端根据这个字段判断。3.3 排行榜查询与分页参数排行榜按分数降序取前 50 条用 SQL 的ORDER BY score DESC LIMIT 50。如果要做分页加page和size参数但学习版固定 50 条就够了。GetMapping(/rank) public MapString, Object rank() { ListScoreRecord list scoreRepository.findTop50(); MapString, Object result new HashMap(); result.put(code, 0); result.put(data, list); return result; }ScoreRepository里用JdbcTemplate查询public ListScoreRecord findTop50() { String sql SELECT openid, score, create_time FROM score_record ORDER BY score DESC LIMIT 50; return jdbcTemplate.query(sql, (rs, rowNum) - { ScoreRecord record new ScoreRecord(); record.setOpenid(rs.getString(openid)); record.setScore(rs.getInt(score)); record.setCreateTime(rs.getTimestamp(create_time)); return record; }); }参数说明LIMIT 50控制返回条数如果数据量大可以改成LIMIT ?, ?做分页。create_time返回给前端用于显示时间。注意 H2 的TIMESTAMP类型和 Java 的Timestamp对应。4. 前后端联调与避坑小程序请求 Java 接口的常见翻车点4.1 避坑一开发环境请求不到 localhost现象小程序开发者工具里wx.request请求http://localhost:8080报错提示不在合法域名列表。原因小程序默认校验请求域名开发阶段可以勾选“不校验合法域名”但真机调试必须用内网 IP 或备案域名。解决在开发者工具右上角“详情”里勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”真机调试时把localhost换成电脑的内网 IP比如http://192.168.1.100:8080并确保手机和电脑在同一局域网。4.2 避坑二Canvas 在 iOS 上导出白图现象用canvas.toDataURL导出图片时iOS 上得到一张白图。原因iOS 的 Canvas 实现要求绘制操作必须在requestAnimationFrame回调内完成且导出前需要调用ctx.draw()或等待一帧。解决把导出逻辑放到requestAnimationFrame里或者用wx.canvasToTempFilePath代替toDataURL后者是小程序封装好的接口兼容性更好。4.3 避坑三Java 后端返回中文乱码现象小程序收到的msg字段中文显示为问号。原因Spring Boot 默认的StringHttpMessageConverter字符集是 ISO-8859-1。解决在application.yml里加spring.http.encoding.charset: UTF-8和spring.http.encoding.force: true或者直接在RequestMapping上加produces application/json;charsetUTF-8。4.4 避坑四分数上报重复提交现象玩家连续点击导致同一局分数上报多次。原因小程序端没有做防抖或状态锁。解决在gameOver时设一个this.reported true标志上报成功后不再重复发请求后端也可以加一个基于openid和create_time的去重逻辑比如 5 秒内相同 openid 只保留最高分。4.5 避坑五H2 数据库文件锁冲突现象重启 Spring Boot 时报Database may be already in use。原因上一个进程没有正常关闭H2 文件锁没释放。解决在application.yml的 URL 后加;AUTO_SERVERTRUE允许多进程访问或者开发阶段直接用内存模式jdbc:h2:mem:birddb重启数据清空适合学习版。5. 进阶技巧用 Canvas 离屏渲染优化管道绘制与帧率当管道数量增多时每帧重绘所有管道会浪费性能。我一般用离屏 Canvas 预渲染管道图片主循环只做drawImage。具体做法创建一个离屏 Canvas画好一根管道的上下两部分然后每帧根据管道位置把离屏 Canvas 的内容贴到主 Canvas 上。这样每帧的绘制指令从几十条降到几条低端机上帧率能稳在 60。// 预渲染管道 const offscreen wx.createOffscreenCanvas({ type: 2d, width: 52, height: 320 }) const offCtx offscreen.getContext(2d) // 绘制管道渐变和管口 const gradient offCtx.createLinearGradient(0, 0, 52, 0) gradient.addColorStop(0, #5cb85c) gradient.addColorStop(1, #3d8b3d) offCtx.fillStyle gradient offCtx.fillRect(0, 0, 52, 320) // 主循环里 this.ctx.drawImage(offscreen, pipe.x, pipe.topY)参数说明wx.createOffscreenCanvas的width和height按管道实际尺寸设不要设太大否则占内存。drawImage的第二个和第三个参数是目标坐标。注意离屏 Canvas 只需要创建一次不要每帧创建。另一个技巧是限制帧率上限。虽然requestAnimationFrame会跟着屏幕刷新走但你可以用时间戳判断如果距离上一帧不足 16 毫秒就跳过更新只做渲染。这样在高刷屏手机上不会让小鸟飞得太快。let lastTime 0 const loop (timestamp) { if (timestamp - lastTime 16) { this.canvas.requestAnimationFrame(loop) return } lastTime timestamp this.update() this.render() this.canvas.requestAnimationFrame(loop) }这个习惯我用了很久尤其是做微信小程序游戏开发时不同设备的刷新率差异很大不加限制的话120Hz 屏幕上的小鸟速度会是 60Hz 的两倍玩家体验完全不一样。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑