微信小程序贪吃蛇源码详解:从状态机到渲染实战
发布时间:2026/9/15 13:25:00来源:尧图网络
简介一份基于微信小程序开发的贪吃蛇游戏完整源代码面向微信小程序初学者、前端开发者以及对休闲游戏实现感兴趣的学习者用来理解小程序项目结构、游戏循环与常见交互设计。资源包共22个文件压缩后约414KB主要包含JSON配置、JavaScript逻辑、WXML页面结构、WXSS样式文件以及PNG、GIF、WAV、MP3等图片与音频素材配置、代码、资源一应俱全。已有584人学习下载。源码从project.config.json、app.js等全局配置到pages/SnakeGame目录下的游戏页面完整实现了蛇的移动控制、食物随机生成、墙壁碰撞检测、分数累计等核心玩法并演示了wx.setStorageSync本地存储、wx.showModal弹窗提示等小程序API用法。通过学习这份代码读者既能快速上手小程序开发流程也能借鉴游戏状态管理和触摸事件处理等实现思路适合作为课程设计或入门练手的实践参考资料。1. 贪吃蛇微信小程序源代码里真正撑起这个游戏的是三处设计拆这套贪吃蛇微信小程序源代码的时候我最关注的是它怎么用最朴素的 WXML 页面把“蛇”这种连续移动的对象画出来。SnakeGame-master 里没有游戏引擎也没有复杂动画库只有 project.config.json、app.json 等根配置和 pages 下的一套页面游戏循环写在 pages/SnakeGame/index.js 中坐标数组代表蛇身setTimeout 驱动每一帧更新。这种写法和 C 语言版贪吃蛇基本同构把终端画布换成 setData把随机刷食物换成空位数组随机取值。适合两类人读刚写完第一个小程序、想补上游戏状态管理的新手以及想搞清楚 WXML 渲染下刷新频率怎么控制的开发者。下面按工程骨架、核心循环、系统接口、可测试性四条线来拆。2. 工程骨架project.config.json、app.json 与 app.js 怎么把游戏“注册”进微信从 zip 解压后的目录看根目录依次是 project.config.json、app.json、sitemap.json、app.js、app.wxss、pages 和 utils。这个排布是微信开发者工具的标准识别方式先读 project.config.json 认项目和基础库再读 app.json 注册页面app.js 承担生命周期app.wxss 只写公共样式。把这一层理解透后续调地图尺寸、游戏速度时才知道参数改在哪个文件。2.1 project.config.json先调好 appid、libVersion 和域名校验开关project.config.json 是工具层配置不进入小程序包体积但它决定开发者工具能不能正确编译。用游客模式运行源码时appid 通常是 touristappid不需要换成自己申请的 AppID如果要把项目发布到线上再替换成真实 appid。下面是一份适用于这套源码的基础配置{ description: 贪吃蛇微信小程序源代码, compileType: miniprogram, projectname: SnakeGame-master, appid: touristappid, libVersion: 2.20.1, setting: { es6: true, postcss: true, minified: true, urlCheck: false } }重点参数有三个。libVersion 指定基础库版本源码里如果用到了高版本 API基础库太低会在运行时报 xxx is not a functionurlCheck 是 wx.request 的域名校验开关开发联调时设 false提交审核前必须恢复 true。compileType 保持 miniprogram即使做游戏也不要改成 gamegame 类型对应的是小游戏包二者页面模型完全不同。建议把这些参数整理成一张表改动前先对着表确认。配置项作用调试时建议appid标记小程序归属测试号填 touristappid游客模式可直接运行libVersion决定可用 API 和样式特性先用 2.20.1 起步compileTypeminiprogram 代表普通小程序不要改成 gameurlCheck控制 wx.request 域名校验本地 false上线 true2.2 app.json 的页面注册顺序为什么不把 SnakeGame 放在根目录app.json 是微信小程序的全局配置pages 数组第一项就是编译后进入的首页。这套源码只有一个页面所以把 pages/SnakeGame/index 放在第一位打开工具就会直接进入游戏页。很多初学者会把 index.wxml 放到根目录但微信规定的页面路径必须存在 pages 数组里并且不写文件后缀。{ pages: [ pages/SnakeGame/index ], window: { navigationBarTitleText: 贪吃蛇, navigationBarBackgroundColor: #111111, navigationBarTextStyle: white }, style: v2, sitemapLocation: sitemap.json }navigationBarTitleText 是顶部导航标题navigationBarBackgroundColor 决定标题栏底色这两个参数影响首屏观感。style 写成 v2 表示使用新版组件样式老项目如果出现样式异常可以先去掉这一项再对比。sitemapLocation 指向 sitemap.json微信索引页面时使用源码提供的 sitemap.json 一般保持默认即可不需要修改。2.3 全局层放什么app.js 的 globalData 与 app.wxss 的资源协调app.js 和 app.wxss 是整个小程序的公共层。以这份源码为例app.js 里适合放设备信息、历史最高分这类所有页面都可能读取的数据而不是放游戏定时器定时器生命周期和页面一致放全局反而容易出现页面销毁后还在回调的问题。常见做法是这样App({ globalData: { statusBarHeight: 0, bestScore: 0 }, onLaunch() { const info wx.getSystemInfoSync() this.globalData.statusBarHeight info.statusBarHeight || 20 this.globalData.bestScore wx.getStorageSync(bestScore) || 0 } })wx.getSystemInfoSync 在新项目中也可以换成 wx.getWindowInfo返回值里 statusBarHeight 字段一致。拿到的 statusBarHeight 用于页面顶部避让刘海屏bestScore 在启动时从本地存储读一次避免每个页面重复读 Storage。app.wxss 里通常只放 button 重置、页面底色、公共 flex 工具类游戏里的蛇身网格样式写在 pages/SnakeGame/index.wxss否则全局样式表会随着页面增多越来越难维护。3. 核心循环状态机、蛇头移动、食物生成和越界检测进入 pages/SnakeGame/index.js第一眼能看到的状态常量、Page 数据字段和定时器调度把游戏分成了四个阶段初始化、运行、暂停、结束。这个状态机就是整个游戏循环的骨架后续加暂停按钮、加结束重开都是在状态分支上补一个入口。3.1 数据模型先行用坐标数组表示蛇身用状态常量约束流程相比用一个嵌套对象描述整条蛇坐标数组更直观数组下标 0 是蛇头最后一项是蛇尾。每次移动时在头部插入新坐标没有吃到食物就把尾部 pop 掉数组长度不变吃到食物则只插不删长度加 1。页面里这样定义初始数据const STATE { INIT: 0, RUNNING: 1, PAUSED: 2, OVER: 3 } Page({ data: { state: STATE.INIT, snake: [ { x: 4, y: 4 }, { x: 3, y: 4 }, { x: 2, y: 4 } ], direction: right, nextDirection: right, food: { x: 8, y: 8 }, score: 0, speed: 150 }, start() { clearTimeout(this.timer) this.setData({ state: STATE.RUNNING, score: 0, snake: [{ x: 4, y: 4 }, { x: 3, y: 4 }, { x: 2, y: 4 }], food: this.createFood(), direction: right, nextDirection: right }) this.scheduleMove(this.data.speed) }, scheduleMove(delay) { clearTimeout(this.timer) this.timer setTimeout(() { if (this.data.state STATE.RUNNING) { this.move() } }, delay) } })state 用数字常量而不是字符串能避免拼写错误data.state 在调试面板里一眼就能看出当前是运行中还是已结束。speed 表示两次移动的间隔毫秒数150 对应每秒约 6.7 格玩家吃食物后把 speed 减小就能实现经典加速玩法。提示scheduleMove 里的箭头函数保留了 this 指向即使 setTimeout 触发时页面已经切走也能通过 data.state 判断是否继续 move避免后台空转。3.2 单次移动的更新时序nextDirection 锁、unshift 和 pop 的执行顺序贪吃蛇最容易出 bug 的地方是转向玩家在一帧内连续滑动两次方向如果被直接改写蛇可能瞬间 180 度掉头撞死自己。这份源码用一个 nextDirection 字段解决触摸时只改 nextDirectionmove 开始时才把 nextDirection 同步给 direction保证每帧最多只接受一次转向。move() { const vectorMap { up: { x: 0, y: -1 }, down: { x: 0, y: 1 }, left: { x: -1, y: 0 }, right: { x: 1, y: 0 } } const snake this.data.snake.slice() const direction this.data.nextDirection const vector vectorMap[direction] const head { x: snake[0].x vector.x, y: snake[0].y vector.y } this.setData({ direction }) if (this.isCollide(head, snake)) { this.gameOver() return } snake.unshift(head) const ateFood head.x this.data.food.x head.y this.data.food.y if (ateFood) { this.setData({ snake, score: this.data.score 1, food: this.createFood(snake) }) } else { snake.pop() this.setData({ snake }) } this.scheduleMove(this.data.speed) }这段代码里的关键顺序是先复制蛇身数组再计算新蛇头然后做碰撞检测最后 unshift 或 pop。直接用 this.data.snake.push 会修改 Page.data 的原始引用setData diff 时可能拿不到正确的变化slice 复制后setData 才能安全地整体替换。vectorMap 把方向字符串映射成坐标增量如果写成 if/else 也能工作但 map 写法在后续增加斜向移动时更好扩展。3.3 食物生成与死亡判定避免随机取点造成死循环食物生成最常见的错误写法是 while(true) 随机选坐标选中蛇身体就重新选。蛇身越长随机命中蛇身的概率越高极端情况下死循环会直接把主线程卡死。更好的做法是先把所有空格子收集到一个数组再从数组里随机取一项createFood(snake) { const gridSize 20 const empty [] for (let x 0; x gridSize; x) { for (let y 0; y gridSize; y) { if (!snake.some(part part.x x part.y y)) { empty.push({ x, y }) } } } if (empty.length 0) { return null } return empty[Math.floor(Math.random() * empty.length)] }, isCollide(head, snake) { const gridSize 20 if (head.x 0 || head.y 0 || head.x gridSize || head.y gridSize) { return true } return snake.some((part, index) index 0 part.x head.x part.y head.y) }isCollide 检查两类死亡越界和自撞。注意 index 0因为碰撞检测发生在蛇头还没插入数组前数组第一项就是当前蛇头如果从 index 0 开始比较每次都会误判。地图用 20×20 还是 30×30由 WXML 布局和手机屏幕宽度共同决定地图越大createFood 的循环次数越多但 20×20 只在蛇身接近满格时才需要遍历 400 次性能没有问题。地图模型移动更新碰撞查询适用场景坐标数组unshift 加头、pop 去尾遍历蛇身比较坐标蛇身短、逻辑简单二维网格同时更新头尾两个格子直接读 grid[y][x]需要墙体、道具等地图元素4. 把玩法接到小程序能力上触摸、渲染和本地存储游戏逻辑在 index.js 里跑但真正让玩家产生手感的是三个系统能力滑动方向识别、WXML 渲染蛇身、本地存储和弹窗反馈。这三个环节分别对应触摸事件、view 节点、Storage 和 Modal API。4.1 touchstart/touchend 计算滑动方向用阈值过滤误触方向控制不建议用上下左右四个按钮滑动操作在手机上更自然代码量也没有多多少。做法是在容器上绑定 touchstart 和 touchend记录起始坐标与结束坐标的差值再判断横向还是纵向滑动onTouchStart(e) { this.touchStartX e.changedTouches[0].clientX this.touchStartY e.changedTouches[0].clientY }, onTouchEnd(e) { const dx e.changedTouches[0].clientX - this.touchStartX const dy e.changedTouches[0].clientY - this.touchStartY const THRESHOLD 12 let next this.data.nextDirection if (Math.abs(dx) Math.abs(dy) Math.abs(dx) THRESHOLD) { next dx 0 ? right : left } else if (Math.abs(dy) THRESHOLD) { next dy 0 ? down : up } if ((this.data.direction right next left) || (this.data.direction left next right) || (this.data.direction up next down) || (this.data.direction down next up)) { return } this.setData({ nextDirection: next }) }THRESHOLD 是滑动触发的最小像素距离太大会让快速转向失灵太小会在手指轻微抖动时误改方向12 到 16 之间比较合适。判断相反方向的逻辑放在触摸层不是放在 move 层这样即使玩家连续滑动两次也只更新 nextDirection而真正生效的方向由 move 里的 direction 锁控制。如果是横屏模式坐标差值需要额外考虑旋转角这套源码采用的是竖屏布局直接用 clientX、clientY 就能工作。提示changedTouches 取数组下标 0保证多指触控时只用最后离开屏幕的那根手指避免两只手同时操作导致方向抖动。4.2 WXML 渲染蛇身setData 全量更新下的刷新节奏页面渲染部分用 view 节点画蛇身不使用 canvas理由是 snake 状态本身是数组WXML 的 wx:for 可以直接展开成多个 view。渲染模板可以这样组织view classboard stylewidth: {{boardSize}}px; height: {{boardSize}}px; view wx:for{{snake}} wx:keyindex classsnake-node styleleft: {{item.x * cellSize}}px; top: {{item.y * cellSize}}px; /view view classfood-node styleleft: {{food.x * cellSize}}px; top: {{food.y * cellSize}}px;/view view classmask wx:if{{state 0}} bindtapstart点击开始/view view classmask wx:elif{{state 3}}得分 {{score}}/view /viewcellSize 通常来自 wx.getWindowInfo 获取的 windowWidth 除以网格数比如 375 除以 20 得到 18.75使用 px 布局可以保证不同设备上格子大小一致。蛇身的绝对定位依赖 .board 设置相对定位和固定宽高JavaScript 侧的 left/top 就是蛇头所在的像素坐标。注意不要给蛇身节点加 CSS transitionmove 的间隔是 150ms 一次过渡动画生效时蛇身中间位置会不可控会出现穿模和拖影。4.3 最高分、游戏结束弹层与音频资源的生命周期分数存储和结束反馈是这套源码中直接调用微信 API 的部分。最高分建议用 wx.setStorageSync 同步写入数据量只有几个字节同步 API 比异步更简单弹窗用 wx.showModal比 showToast 更适合游戏结束这种需要玩家确认的场景。结束逻辑可以写成这样gameOver() { clearTimeout(this.timer) const best wx.getStorageSync(bestScore) || 0 if (this.data.score best) { wx.setStorageSync(bestScore, this.data.score) } this.setData({ state: STATE.OVER }) wx.showModal({ title: 游戏结束, content: 得分 this.data.score 最高分 Math.max(this.data.score, best), showCancel: false, success: () { this.setData({ state: STATE.INIT, snake: [{ x: 4, y: 4 }, { x: 3, y: 4 }, { x: 2, y: 4 }], score: 0 }) } }) }clearTimeout 放在第一行保证弹窗弹出后定时器不再触发下一次 move。showModal 的 success 回调只在用户点击确定后执行这是重置游戏数据的合适时机如果直接在 gameOver 里重置弹窗期间 snake 数组已经提前被重置玩家会看不到结束画面。music 目录里的音频一般在 onLoad 里创建 wx.createInnerAudioContext 播放页面 onUnload 时要调用 destroy否则音频实例会残留并继续播放。微信 API在本项目中的用途使用注意wx.getStorageSync读取历史最高分同步阻塞适合小数据wx.setStorageSync写入新最高分写入频率低无需节流wx.showModal游戏结束提示success 回调再重置数据wx.createInnerAudioContext播放 music 目录音频页面卸载时 destroywx.request扩展排行榜功能需要合法域名对应 urlCheck5. 把蛇的移动逻辑抽进 utils再在 Node 里验证这套源码最适合复用的点是把移动逻辑抽成不依赖 Page 的纯函数放到 utils/snake.js 后用 Node 直接验证。const GRID_SIZE 20 const VECTOR { up: { x: 0, y: -1 }, down: { x: 0, y: 1 }, left: { x: -1, y: 0 }, right: { x: 1, y: 0 } } function createFood(snake) { const empty [] for (let x 0; x GRID_SIZE; x) { for (let y 0; y GRID_SIZE; y) { if (!snake.some(p p.x x p.y y)) empty.push({ x, y }) } } return empty.length ? empty[Math.floor(Math.random() * empty.length)] : null } function nextSnake(snake, direction, food) { const head { x: snake[0].x VECTOR[direction].x, y: snake[0].y VECTOR[direction].y } if (head.x 0 || head.y 0 || head.x GRID_SIZE || head.y GRID_SIZE) return { dead: true } if (snake.some(p p.x head.x p.y head.y)) return { dead: true } const next [head].concat(snake) if (head.x food.x head.y food.y) return { dead: false, snake: next, food: createFood(next) } next.pop() return { dead: false, snake: next, food } } module.exports { nextSnake, createFood }页面中调用时只保留状态切换和渲染const { nextSnake } require(../../utils/snake.js) const result nextSnake(this.data.snake, this.data.nextDirection, this.data.food) if (result.dead) { this.gameOver() return } this.setData({ snake: result.snake, food: result.food, score: this.data.score (result.snake.length - this.data.snake.length) }) this.scheduleMove(this.data.speed)验证不吃的场景直接在终端跑node -e const srequire(./utils/snake); console.log(JSON.stringify(s.nextSnake([{x:5,y:5},{x:4,y:5}],right,{x:7,y:5})))输出里 snake 变为 [{x:6,y:5},{x:5,y:5}]food 不变dead 为 false。分数增长用长度差以后加多倍道具也成立。在开发者工具里给调用处打断点watch 表达式写 result.snake.map(p p.x , p.y)每帧都能看到蛇身快照真机掉帧就把 speed 从 150 调到 200无需改逻辑。这样就完成了从“能玩”到“能测试”的改造。本文还有配套的精品资源点击获取
网站建设高端定制企业官网