微信小游戏贪吃蛇源码解析:Canvas渲染、触摸输入与游戏循环优化

微信小游戏贪吃蛇源码解析:Canvas渲染、触摸输入与游戏循环优化 简介这款微信小游戏贪吃蛇项目源码适合微信小游戏初学者入门参考。资源以原生小游戏方式实现经典贪吃蛇玩法使用微信开发者工具导入小游戏模板即可直接编译运行便于对照学习游戏循环、触摸事件、画布绘制等核心逻辑。压缩包共12个文件以8个JS脚本为主承担游戏主逻辑、工具函数与模块拆分3个JSON文件分别用于项目配置与游戏配置1个Markdown文档提供简要说明整体仅18KB结构清晰易读。已有562人学习/下载。源码虽小但五脏俱全从界面渲染到手势控制均有对应实现可作为第一个微信小游戏练手项目也可在此基础上扩展关卡、计分与音效。无论想了解Canvas绘图、触摸响应还是游戏状态机这份源码都能提供直观示例帮助快速迈入小游戏开发门槛。1. 拿到贪吃蛇微信小游戏源码先别急着跑一份贪吃蛇微信小游戏的项目源码打开开发者工具能跑起来只是第一关。真正要把它改造成能交付、能上架、能适配不同屏幕比例的东西得先看清微信小游戏的运行模型渲染不走 DOM直接操作 Canvas输入不是键盘而是触摸事件主循环没有浏览器里的 setInterval 兜底requestAnimationFrame、帧间隔、碰撞检测全部要自己拼。这份源码的价值不在「能用」而在它把微信小游戏的最小骨架摆在了桌面上——入口文件、game.json 配置、全局画布、游戏循环、触摸响应五件事缺一件后面都要返工。适合想弄懂小游戏底层结构、又不想从空工程搭起的开发者。2. 用微信小游戏 API 搭出贪吃蛇的网格、移动与触控输入为什么网页版贪吃蛇源码不能直接搬进微信小游戏因为运行环境没有 document、window所有原生能力都收敛到 wx.* 系列 API。画面输出靠 wx.createCanvas()第一次调用拿到的是上屏画布后续调用返回离屏画布输入靠 wx.onTouchStart / onTouchEnd没有 keydown。搞清楚这两点整个源码的骨架就清楚了。2.1 先对齐 game.json 与入口文件每个微信小游戏工程必须有 game.json 和 game.js。game.json 声明设备方向、渲染模式与网络超时game.js 是入口初始化逻辑直接写顶层没有 DOMContentLoaded 这类生命周期。{ deviceOrientation: portrait, showStatusBar: false, networkTimeout: { request: 10000 } }deviceOrientation 设为 portrait 强制竖屏贪吃蛇单手操作比横屏顺手showStatusBar 关闭系统状态栏避免游戏顶部多出一条非绘制区域networkTimeout 只影响 wx.request本地玩法用不到但显式声明能避免真机网络慢时默认超时太久。整套源码的启动路径就是微信加载 game.json 拿到配置再执行 game.js。2.2 创建全局画布并适配逻辑分辨率屏幕尺寸要用 wx.getWindowInfo() 取返回的是逻辑像素。物理像素适配是新手最容易漏掉的一步漏了之后同一套代码在 iPhone 和 Android 上清晰度差别很明显。const { windowWidth, windowHeight, pixelRatio } wx.getWindowInfo(); const canvas wx.createCanvas(); canvas.width windowWidth * pixelRatio; canvas.height windowHeight * pixelRatio; const ctx canvas.getContext(2d); ctx.scale(pixelRatio, pixelRatio);先给画布分配物理像素尺寸再通过 ctx.scale 让后续所有绘制命令按逻辑坐标走。如果不做这一步网格线的位置会偏移半个物理像素正方形格子边缘出现锯齿。概念来源作用windowWidthwx.getWindowInfo()逻辑宽度UI 布局与坐标计算基准pixelRatio同上物理像素与逻辑像素比值常见为 2 或 3canvas.width手写赋值物理像素宽度决定绘制精度ctx.scale手写调用让后续 draw 命令映射到逻辑坐标系后续所有绘制坐标都以 windowWidth、windowHeight 为边界不要在绘制函数里再乘一次 pixelRatio。2.3 蛇身数据结构与自撞检测蛇身最基本的存法是数组每移动一步把新头插入头部没吃到食物就弹出尾部。但数组头部插入是 O(n) 操作贪吃蛇格子少时无所谓地图放大到 40×40 以上、帧率拉到 60fps 时就能感觉到偶发卡顿。我一般用固定长度的类型化数组加头索引来存蛇身把运行时内存分配降到零。const mapSize 22; const capacity mapSize * mapSize; const segX new Int32Array(capacity); const segY new Int32Array(capacity); let headIndex 0; let len 4; let dir { x: 1, y: 0 }; let pendingDir null; function setDirection(name) { const map { up: { x: 0, y: -1 }, down: { x: 0, y: 1 }, left: { x: -1, y: 0 }, right: { x: 1, y: 0 } }; const d map[name]; if (!d) return; if (d.x dir.x 0 d.y dir.y 0) return; pendingDir d; } function moveSnake() { if (pendingDir) { dir pendingDir; pendingDir null; } const newX segX[headIndex] dir.x; const newY segY[headIndex] dir.y; if (newX 0 || newX mapSize || newY 0 || newY mapSize) { gameOver(); return; } for (let i 0; i len; i) { const idx (headIndex - i capacity) % capacity; if (segX[idx] newX segY[idx] newY) { gameOver(); return; } } headIndex (headIndex 1) % capacity; segX[headIndex] newX; segY[headIndex] newY; if (isFood(newX, newY)) { len; spawnFood(); } draw(); }逻辑说明segX、segY 一次性分配 capacity 个格子移动时头索引顺时针推进旧尾巴位置自然被新头覆盖。自撞检测用(headIndex - i capacity) % capacity从当前头向前回绕 len 段只检查存活部分避免把已被覆盖的旧数据也算进去。setDirection 里过滤 180 度反向是必须的否则连续快速操作会让蛇头直接穿进自己身体。绘制函数保持简单先 fillRect 清屏再画蛇身和食物绘制也要靠 headIndex 回绕读取。function draw() { ctx.fillStyle #1a1a2e; ctx.fillRect(0, 0, windowWidth, windowHeight); ctx.fillStyle #e94560; for (let i 0; i len; i) { const idx (headIndex - i capacity) % capacity; ctx.fillRect(segX[idx] * CELL_SIZE, segY[idx] * CELL_SIZE, CELL_SIZE - 1, CELL_SIZE - 1); } ctx.fillStyle #f5c542; ctx.fillRect(food.x * CELL_SIZE, food.y * CELL_SIZE, CELL_SIZE, CELL_SIZE); }每个格子画成 CELL_SIZE 减 1 像素留出 1px 间隙视觉上比紧贴的方块更容易区分边界。CELL_SIZE、food 等变量在初始化函数里赋值全局只维护一套。2.4 触摸输入滑动与虚拟方向键两条路微信小游戏没有键盘输入要么靠整屏滑动要么靠屏幕上的虚拟方向键。滑动适合追求手感的老玩家虚拟按键适合教学关。源码里最稳的做法是一套输入适配器同时接两种来源统一走 setDirection。let startTouch null; wx.onTouchStart((e) { startTouch e.touches[0]; }); wx.onTouchEnd((e) { if (!startTouch) return; const t e.changedTouches[0]; const dx t.clientX - startTouch.clientX; const dy t.clientY - startTouch.clientY; if (Math.abs(dx) 20 Math.abs(dy) 20) return; if (Math.abs(dx) Math.abs(dy)) { setDirection(dx 0 ? right : left); } else { setDirection(dy 0 ? down : up); } startTouch null; });onTouchStart 记录起点onTouchEnd 计算位移。20 像素阈值用来区分点击与滑动防止玩家手指轻微移动就误换方向。视觉方向键则是把四个按键的区域坐标写死在 onTouchStart 里命中检测后调 setDirection两者共用同一套方向过滤逻辑。提示多指触控时 e.touches[0] 只取第一个触点若玩家手指换了一根startTouch 可能失效。需要在 onTouchStart 里记录 identifieronTouchEnd 按 identifier 匹配避免滑动中断后方向错乱。3. 游戏循环、固定时间步进与真机性能参数调试贪吃蛇的核心循环只有一句话每过固定间隔蛇移动一格每帧画面重绘。但三种写法的效果差别不小。setInterval(fn, ms)写起来最快但定时器受运行环境调度影响低电量模式下会被合并成 1000ms 触发一次蛇直接瞬移。requestAnimationFrame(fn)微信小游戏支持跟随屏幕刷新率逻辑却和设备帧率绑死60Hz 和 120Hz 设备上蛇速不一样。逻辑帧与渲染帧分离移动逻辑固定间隔渲染跟随刷新率。这是 2D 游戏里最常见的做法也是这套源码里推荐的主循环结构。3.1 用 requestAnimationFrame 搭出主循环const TICK_MS 120; let lastTick 0; let paused false; function frame(timestamp) { if (!paused timestamp - lastTick TICK_MS) { moveSnake(); lastTick timestamp; } draw(); requestAnimationFrame(frame); } requestAnimationFrame(frame);timestamp 由系统传入单位毫秒。移动和绘制分离是关键改动moveSnake 只更新数据draw 只负责画两者不互相调用。TICK_MS 决定速度120 表示每 120ms 走一格要提供三档难度就把它做成变量在设置面板里改。暂停不能取消 requestAnimationFrame否则再恢复时会有一帧空档正确做法是用 paused 标志跳过移动但继续渲染挂起画面里的暂停按钮还能保持闪烁。3.2 从 setInterval 迁移到 RAF 的坑从网页版贪吃蛇源码迁移时最容易踩的坑是canvas 绘制代码原样保留但把 setTimeout 换成 setInterval 后真机上速度忽快忽慢。原因是安卓部分机型的省电策略会把 timer 聚合到 1000ms 一次然后一次性补跑多次逻辑。requestAnimationFrame 的 timestamp 是单调递增的不受补帧影响在开发者工具里不勾选「帧率模拟」就能复现这个差异。3.3 帧率与绘制开销的排查参数微信小游戏没有网页端那种打开即用的 Performance 面板对游戏循环的逐帧追踪要自己埋点。wx.getPerformance() 是最直接的入口。const perf wx.getPerformance(); const observer perf.createObserver(() { const entry perf.getEntriesByName(frame)[0]; const cost entry ? Math.round(entry.duration) : 0; if (cost 33) { console.warn(frame cost too high:, cost, ms); } });getEntriesByName(frame) 返回最近一帧渲染耗时。超过 33ms 说明这一帧已经跌破 30FPS优先检查绘制函数是不是逐格 fillRect 导致状态切换过多或者是 fillText 每帧重复绘制静态文本造成不必要的重排。现象常见原因排查手段帧率掉到 20FPS每格单独 fillRect状态频繁切换先整体填充背景再只画蛇和食物偶发卡顿每帧 new 临时对象触发 GC用类型化数组消息体复用对象池真机发烫后台仍持续渲染onHide 里取消 RAFonShow 时重新启动3.4 真机预览与调试入口工具右上角「预览」生成二维码后真机上默认没有 vConsole报错看不见。要打开调试面板在 game.js 顶层调用wx.setEnableDebug({ enableDebug: true });这个接口只能在开发者工具开启调试模式时生效正式发布包默认关闭。调试面板打开后能看到每帧耗时、console 输出与网络请求。上线前记得在打包脚本里剔除该调用否则正式版会请求失败并打印一条无害但刺眼的报错。提示调试模式下 setEnableDebug 在部分基础库版本中会因重复调用报错用 try/catch 包住避免初始化流程被打断。4. 微信小游戏源码交付竖屏适配、分包与著作权检查「项目源码」交付意味着别人要能接得住代码能跑只是起点竖屏设置、首包体积、资质材料缺一不可。这三个问题在贪吃蛇这种小体量项目里尤其典型。4.1 横竖屏参数与安全区适配从网页版改来的源码默认横屏贪吃蛇却更适合竖屏。game.json 里 deviceOrientation 设为 portrait 解决的是屏幕旋转设计分辨率还要跟着改。const MARGIN_TOP 80; const cols Math.floor((windowWidth - 2 * PADDING) / CELL_SIZE); const rows Math.floor((windowHeight - MARGIN_TOP - 2 * PADDING) / CELL_SIZE);CELL_SIZE 建议取 16 的倍数375px 宽的常见逻辑分辨率下能整除设成 18 这种数会让最后一列蛇头画到边界外。PADDING 不能写死刘海屏、挖孔屏的安全区各不相同。wx.getWindowInfo() 拿不到安全区要读 wx.getSystemInfoSync() 的 safeArea 字段把 PADDING 替换成 safeArea 的 top 与 bottom 偏移。Web 端那套 safe-area-inset 的 CSS 环境变量在这里完全无效。4.2 首包体积限制与资源降级微信小游戏主包限额 4MB、总包 20MB具体数值以微信公众平台最新公告为准。贪吃蛇本体不大但加入高清背景图、音效和字体后很容易逼近主包上限。素材超限时最常见的做法是把新手引导、设置页拆到分包。{ subpackages: [ { name: tutorial, root: sub/tutorial/, independent: false } ] }wx.loadSubpackage({ name: tutorial, success() { /* 进入新手引导 */ }, fail(err) { /* 降级为简单文字说明 */ } });分包里的代码无法被主包直接同步 require加载是异步的必须在 success 回调里再切换场景提前切会白屏。贪吃蛇这种轻量项目我更倾向不做分包背景色用代码绘制、图片压成 webp、音效用短 ogg省下来的体积比分包调度更可控。4.3 交付前的著作权与合规检查「项目源码」交付还有一个隐形坑代码能跑接手方却卡在提审。微信小游戏上架必须选择游戏类目通常需要软著或版号信息具体以微信公众平台当前要求为准。给客户做定制交付时交付清单里应包含源码版权归属的书面说明并明确建议启动软著登记的时间点而不是等开发完才想起来。技术侧能提前做的是把 game.json 的类目信息与性能要求对齐提审前用工具「上传」功能生成体验版确认后台日志无异常报错。5. 进阶技巧用操作序列给贪吃蛇做一局回放给贪吃蛇加分回放功能顺便验证碰撞逻辑是我推荐这份源码改造时优先做的一个小功能。回放不复杂只记录两样东西时间戳和方向。其他一切状态——蛇长、食物位置、得分——都由游戏逻辑在回放时重算。5.1 记录操作序列const replayOps []; let replayStart 0; function onDirection(d) { replayOps.push({ t: Date.now() - replayStart, dir: d }); } function startRecord() { replayOps.length 0; replayStart Date.now(); }5.2 回放只跑逻辑跳过渲染function replayFrame(timestamp) { const cursor timestamp - replayStart; while (idx replayOps.length replayOps[idx].t cursor) { moveSnake(replayOps[idx].dir); idx; } draw(); requestAnimationFrame(replayFrame); }回放时把 setDirection 换成直接改写 dir 的 moveSnake 调用因为回放不需要再走触摸输入和 180 度过滤操作序列里存的本来就是已经过滤后的合法方向。5.3 用回放做的两个验证第一验证边界碰撞是否可复现。玩家反馈「明明没撞墙却死了」回放一遍就能发现高速连按时上一帧的方向指令还没被移动消耗下一帧又补了一条反向指令蛇头同一回合被翻转 180 度直接撞到自己。setDirection 里过滤反方向的代码不能放在 pendingDir 赋值之后必须放在消费端上一行让两条连续指令之间只有一条生效。回放数据会直接暴露这种时序 bug。第二验证食物生成的公平性。有些源码生成食物时只在当前空格子里随机看似公平但重放十局就会发现食物频繁出现在蛇头下一步会经过的路径上。回放逻辑与正常逻辑共用 spawnFood数据和得分完全一致任何与正常局不一致的地方都是 bug。回放数据还可以存到 wx.setStorageSync 或上传后端用于在线排行榜的作弊检测服务器把玩家分数与操作序列回放一遍对比最终蛇长是否一致这是成本最低的防注入手段。想让回放更贴近实战可以加一条只在回放模式生效的快进键按住时暂停 draw 只跑 moveSnake几百毫秒就能审完一整局。本文还有配套的精品资源点击获取