ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

别踩白块微信小程序源码拆解:触摸交互与性能优化实战

2026/9/16 9:36:30 拓冰建站 浏览量
别踩白块微信小程序源码拆解:触摸交互与性能优化实战 简介面向毕业设计、期末大作业及微信小程序前端学习者这是一份可直接运行的小游戏源码完整实现“别踩白块”的经典玩法。压缩包共36个文件核心由JS逻辑、WXML页面结构、WXSS样式及JSON配置组成同时附有图片素材、README与说明文档整体仅142KB结构清晰可直接导入微信开发者工具流畅运行也方便按模块修改和调试。项目已有574人学习浏览适合作为课程设计、毕业设计或期末大作业的参考也适合小程序初学者作为练手项目。通过阅读源码可理解游戏页面渲染、点击事件响应、黑白块判定、计分及结束处理等关键实现还能在现有代码基础上扩展音效、提升关卡难度或加入排行榜快速打造出独具个性化的作品。1. 别踩白块微信小程序源码包值不值得拆开改拿到这份名为“别踩白块”的微信小程序毕设/期末大作业源码包先别急着拖进微信开发者工具。里面除了几个 js、wxml、wxss 文件还带了一张 qrcode.jpg 预览图和 README看起来是个成型的前端项目。这套源码最值得拆的地方是用一维数组管理棋盘、用触摸坐标换算命中黑块、用速度档位控制难度这三个点几乎覆盖了微信小程序交互小游戏最常见的工程写法。对做毕业设计或期末大作业的人来说直接改它比从零写省一半时间对前端工程师来说这也是研究 setData 性能边界的一个好样本。下面从文件结构开始拆讲清楚每个文件在游戏里承担什么职责以及哪些参数是真正能影响手感的。2. 源码拆解WXML 布局、触摸事件与方块定位2.1 单页面文件结构小游戏源码包的目录意义这个源码包的目录不算复杂app.json负责页面注册和窗口配置app.js创建全局 App 实例pages/index/目录下是游戏主页面的逻辑、结构和样式文件外加 README 和预览图。要特别注意虽然包名里写着“小游戏”但这份代码是标准的小程序页面工程不是微信小游戏的game.js工程。导入微信开发者工具时应选择“小程序”项目而不是“小游戏”项目否则会直接提示找不到game.json。{ pages: [pages/index/index], window: { navigationBarTitleText: 别踩白块, navigationBarBackgroundColor: #ffffff, navigationBarTextStyle: black, backgroundColor: #f7f7f7 } }navigationBarTitleText是顶部导航栏标题navigationBarBackgroundColor和navigationBarTextStyle控制导航栏底色与字体颜色。如果想把游戏做成沉浸式全屏可以在app.json里把navigationStyle改成custom但那样要自己处理状态栏高度对毕设项目来说不是必须项。第一次打开源码时优先检查pages路径是否和实际目录一致这是编译报错最集中的来源。2.2 方块定位绝对定位与百分比列宽的实际搭配棋盘区域用一个相对定位的view承载所有方块用绝对定位叠放。布局上最关键的一点是横向用百分比纵向用固定像素。横向固定四列每列宽度 25%列索引从 0 到 3算出来的left正好是0%、25%、50%、75%屏幕宽度变化时方块能自动拉伸。纵向的top和行高用像素因为触摸事件拿到的是clientX/clientY像素坐标直接除以行高就能反推行号不需要做rpx到像素的转换。view classboard bindtouchstartonBoardTouchStart view wx:for{{blocks}} wx:keyid classblock {{item.isBlack ? black : white}} styletop: {{item.y}}px; left: {{item.col * 25}}%; width: 25%; height: {{rowHeight}}px; >.board { position: relative; width: 100%; height: 100vh; overflow: hidden; background: #e0e0e0; } .block { position: absolute; box-sizing: border-box; border-bottom: 1px solid #ccc; } .block.black { background: #333; border-bottom: 1px solid #111; } .block.white { background: #fff; }绝对定位的方块全部叠放在.board内部overflow: hidden保证移出屏幕的方块不会产生滚动条。黑白块之间用border-bottom做分隔线视觉上比纯色块更接近常见的“别踩白块”界面。2.3 触摸坐标换算clientX/clientY 到方块下标的完整过程点击判定不用tap事件而是用touchstart。原因是tap在部分基础库版本上存在 300ms 延迟虽然微信开发者工具里不明显但在真机上容易感觉慢半拍。游戏需要连续快速点击用touchstart会更跟手。onBoardTouchStart(e) { if (this.data.gameOver) return; const touch e.touches[0]; const query wx.createSelectorQuery(); query.select(.board).boundingClientRect(rect { if (!rect) return; this.boardWidth rect.width; const x touch.clientX - rect.left; const y touch.clientY - rect.top; this.handleTap(x, y); }).exec(); }rect.left是棋盘相对页面左边的距离clientX是手指相对页面左边的距离两者相减才能得到手指在棋盘内的横坐标。用同样的方式算出纵坐标后交给handleTap做行号和列号换算。handleTap(x, y) { if (x undefined || this.data.gameOver) return; const col Math.floor(x / (this.boardWidth / 4)); const row Math.floor(y / this.data.rowHeight); const blocks this.data.blocks; const target blocks.find(b b.col col b.isBlack b.y y y b.y this.data.rowHeight ); if (target) { this.hitBlack(target, blocks); } else { this.endGame(); } }Math.floor把坐标映射成列数和行数。需要注意边界条件点击位置在棋盘外时x或y会出现负值find找不到目标黑块程序会走到endGame。所以前端项目里棋盘边界那 1 到 2 像素的误差也值得处理常见做法是加一个判断if (x 0 || y 0) return。触摸判定涉及的核心字段可以整理成下表方便调试时对照字段来源含义clientXtouch对象手指相对页面左边缘的距离clientYtouch对象手指相对页面上边缘的距离rect.leftboundingClientRect棋盘左边界rect.topboundingClientRect棋盘上边界rowHeightdata每个方块的高度也是行高2.4 方块数组设计为什么用一维数组而不是二维数组源码里管理棋盘的数据结构是一维数组每个元素包含行、列、是否黑块和当前 y 坐标。用一维数组的好处是坐标到数组索引的换算非常直接而且setData更新时可以整体替换不需要在二维数组里做深层遍历。initGame() { this.rowSeq 0; this.lastBlackCol -1; const blocks []; const rowHeight this.data.rowHeight; const boardHeight this.data.boardHeight; const rows Math.floor(boardHeight / rowHeight) 1; for (let row 0; row rows; row) { const blackCol this.getRandomBlackCol(); for (let col 0; col 4; col) { blocks.push({ id: ${this.rowSeq}-${col}, row: row, col: col, y: row * rowHeight, isBlack: col blackCol }); } this.rowSeq; } this.setData({ blocks, score: 0, speed: 1, gameOver: false }); }row字段在初始化时用来标记当前是从哪一行生成的真正渲染时只依赖y。随着游戏推进方块不断向上或向下移动row字段会失真所以核心逻辑中不能拿row计算位置。this.rowSeq是自增序列用来保证每个方块的id唯一避免wx:key报错。lastBlackCol记录上一次黑块所在的列便于生成随机行时避开连续同列。3. 核心循环方块生成、难度递增与失败判定的工程写法3.1 自动下落模式setInterval 驱动方块移动经典“别踩白块”是方块自动下落玩家点黑块消除点白块或漏点则失败。微信小程序里没有浏览器原生的requestAnimationFrame最稳妥的做法是用setInterval驱动一个 16ms 的循环近似 60 帧。每帧让所有方块的y增加一个速度值然后判断是否触底。startLoop() { if (this.timer) clearInterval(this.timer); this.timer setInterval(() { if (this.data.gameOver) return; const speed this.data.speed; const boardHeight this.data.boardHeight; const blocks this.data.blocks; const missed blocks.some(b b.isBlack b.y speed boardHeight); if (missed) { this.endGame(); return; } const nextBlocks blocks .map(b ({ ...b, y: b.y speed })) .filter(b b.y boardHeight); this.fillTopRows(nextBlocks); this.setData({ blocks: nextBlocks }); }, 16); }speed单位是像素每帧16ms一帧运行一次。boardHeight是棋盘可视区域高度任何黑块只要y speed越过底部判定为漏点游戏结束。filter把已经移出底部的方块清掉避免数组无限膨胀。missed判断一定要放在filter之前否则漏掉的黑块会先被过滤无法触发结束逻辑。3.2 顶部补行用黑块随机生成撑满棋盘下落模式最大的问题是顶部会持续出现空白区域。解决方法是每帧检查当前数组中最小的y如果大于 0 就插入一行新方块行首行放在最小 y 值再往上减一行高的位置这样棋盘永远处于“顶部有方块、底部有方块”的满格状态。fillTopRows(blocks) { if (blocks.length 0) return; const minY Math.min(...blocks.map(b b.y)); if (minY 0) { const blackCol this.getRandomBlackCol(); const newY minY - this.data.rowHeight; for (let col 0; col 4; col) { blocks.push({ id: ${this.rowSeq}-${col}, col: col, y: newY, isBlack: col blackCol }); } } }Math.min配合展开运算符取数组中的最小 y 值这是数组型方块逻辑里最简洁的写法。注意blocks是上一步map之后生成的新数组直接push不会影响this.data.blocks的引用最后统一用setData更新。这里每帧最多补一行不会出现一次性补满多行导致方块过分拥挤。关于黑块随机生成直接Math.floor(Math.random() * 4)生成本身没有问题但连续多行黑块落在同一列时玩家会出现“只用一根手指同一个位置连点”的机械操作游戏性变差。常见做法是避开上一次的列getRandomBlackCol() { let col Math.floor(Math.random() * 4); if (col this.lastBlackCol) { col (col 1) % 4; } this.lastBlackCol col; return col; }第一次调用时lastBlackCol为 -1可以直接取随机列。之后每次生成新行都会避开上一行的黑块列但不能避免隔一行同列。如果你想要更强的随机分布可以用while循环重新生成两到三次但对这个小游戏来说1取模已经足够。3.3 难度递增分数映射到速度档位游戏开始时速度设为 1每得一分会变快但线性加速到后期会很快突破人类反应极限。更实用的方案是分段映射定义一个难度表分数落在哪个区间就走哪个速度档位。分数区间速度像素/帧对应体验0-191起步期熟悉判定区域20-491.5明显加速需要预判50-992中等难度适合演示100-1992.5高速段落容易失误200 以上3极限档位作为展示加分项getSpeedByScore(score) { if (score 20) return 1; if (score 50) return 1.5; if (score 100) return 2; if (score 200) return 2.5; return 3; }这里的速度值不是任意取的。按 16ms 一帧计算速度 1 时 1 秒移动 62.5px速度 3 时 1 秒移动 187.5px。以 80px 的行高来看速度 3 时一个方块从出现到触底只需要不到 3 秒已经非常紧张。如果行高改成 100px同样的速度档位感觉会更快所以改rowHeight之后一定要同步测试速度手感。3.4 点击黑块删除方块并计入总分玩家点击黑块时handleTap已经在数组里找到了目标对象接下来要做的是把这个方块从数组中移除然后更新分数和速度。删除可以简单用splice但因为fillTopRows每帧都会执行顶部新行会持续补充被删掉的那一行不会造成棋盘空缺。hitBlack(target, blocks) { const idx blocks.findIndex(b b.id target.id); if (idx -1) return; blocks.splice(idx, 1); const newScore this.data.score 1; this.setData({ blocks: blocks, score: newScore, speed: this.getSpeedByScore(newScore) }); }splice会改变数组长度findIndex基于target.id找到精确位置后再删除。这里要避免两个连续点击同时命中同一个方块因为touchstart回调里的SelectorQuery是异步的连续点击可能拿到同一个target二次执行时blocks里已经找不到对应ididx返回 -1 直接退出。游戏结束时clearInterval和弹窗提示可以放在同一个endGame里endGame() { if (this.data.gameOver) return; clearInterval(this.timer); this.setData({ gameOver: true }); wx.vibrateShort({ type: medium }); }wx.vibrateShort是微信小程序的震动接口调用后手机有短促震动反馈。参数type支持heavy、medium、light三档真机上差异明显开发者工具里没有震动效果只能靠日志确认是否执行。4. 从源码到可运行项目微信开发者工具配置与真机调试4.1 导入项目选择“小程序”而不是“小游戏”解压源码后打开微信开发者工具选择“导入项目”目录指向解压出来的外层文件夹。很多同学在这里会踩坑因为文件名带“小游戏”就选了“小游戏”项目类型结果工具提示找不到game.json。这份源码是标准小程序页面工程必须选“小程序”。AppID 可以选“测试号”不影响本地编译和真机预览。# 检查解压目录结构是否满足小程序要求 find . -maxdepth 2 -type f | sort如果app.json不在导入目录的第一层工具会报“未找到 app.json”。常见情况是文件夹内还有一层同名目录处理方法是把内层目录作为导入根目录或者在工具里重新选择。4.2 基础库版本与增强编译的坑微信开发者工具默认使用最新基础库但源码如果是两三年前的工程可能使用了旧的wx:语法或者未开启增强编译。登录源码里的project.config.json检查libVersion字段如果版本过高部分 API 行为会有变化。比如旧版本里wx.createSelectorQuery()必须在wx.nextTick或页面onReady后执行新版本在touchstart里直接调用也能拿到结果。常见报错可以按下面这个表快速定位报错信息原因处理方式app.json: 未找到导入目录层级不对把包含app.json的目录作为根目录game.json not found项目类型错选为“小游戏”重新导入并选择“小程序”Bad attr wx:keywx:key绑定的字段不存在改为wx:keyid或确保每个对象有 idsetData is not a functionthis指向已脱离页面实例回调函数改用箭头函数或在开头const that thisCannot read property width of nullboundingClientRect未拿到节点检查.board类名是否存在4.3 修改刚进入的加载页面和启动窗口热搜里常提到的“修改刚进入的加载页面”在小程序里其实分两层。第一层是启动时系统展示的加载窗口由app.json的window配置控制修改backgroundColor和navigationBarBackgroundColor可以改变加载时的底色。第二层是页面内自己写的启动loading动效常见做法是在index.js的onLoad里加一个计时器先显示一个加载图等数据初始化完成后再切换到游戏画面。{ window: { navigationBarTitleText: 别踩白块, navigationBarBackgroundColor: #000000, navigationBarTextStyle: white, backgroundColor: #000000 } }把导航栏和全局背景都改成黑色会让游戏区域看起来像一个完整的“白块下落”界面。导航栏文字用白色避免黑底白字变成黑底黑字。注意navigationBarTextStyle只有black和white两个合法值不能填颜色代码。4.4 真机预览与 qrcode.jpg 的用途源码包里那张qrcode.jpg通常是开发者工具预览生成的二维码扫码后可以在手机上打开小游戏。自己重新预览时会生成新的二维码旧图只能作为项目展示截图不能直接用。真机预览时要注意手机和电脑不需要同一局域网但开发版预览需要微信登录授权如果扫码后白屏优先看调试器里的 console 报错。真机测试和开发者工具最大的差异是触摸系列事件。同一个touchstart回调在开发者工具里拿到的clientY和真机上完全一致但真机上手指有触点半径快速连点时很容易同时触发两个touchstart导致一次点击消掉两个黑块或误判。解决方法是加一个防抖时间戳记录上次点击时间如果两次点击间隔小于 80ms直接忽略第二次。onBoardTouchStart(e) { const now Date.now(); if (now - this.lastTapTime 80) return; this.lastTapTime now; // 后续触摸换算逻辑 }时间戳防抖不会影响正常连续点击因为一帧 16ms游戏最快的连续点击也很难低于 100ms 间隔。这个参数推荐保留在全局配置里后续如果改成 canvas 渲染也能继续复用。5. 把这个小游戏改造成毕设项目的几个进阶技巧5.1 用本地缓存保存最高分默认源码只在当前页面显示分数退出后归零。加一个最高分记录只需要在endGame里调用wx.getStorageSync和wx.setStorageSyncendGame() { if (this.data.gameOver) return; clearInterval(this.timer); const best wx.getStorageSync(bestScore) || 0; if (this.data.score best) { wx.setStorageSync(bestScore, this.data.score); } this.setData({ gameOver: true, bestScore: Math.max(best, this.data.score) }); // 震动反馈等逻辑 }getStorageSync返回的是 Previous 保存的原始值取不到时是空字符串所以要用|| 0兜底。更新最高分后Math.max保证显示不会倒退回旧值。这个改动虽然简单但在期末大作业的演示环节里能明显看出选手考虑了“状态持久化”这个点。5.2 用 canvas 重写渲染层降低 setData 压力view 版本在方块数量少时没有问题但如果把屏幕拉长、方块数量和行数翻倍setData每帧传输的 JSON 数据会变成性能瓶颈。一个更偏工程化的改法是保留现在的触摸判定逻辑把渲染层换成 canvas 2Dconst ctx wx.createCanvasContext(gameCanvas); ctx.setFillStyle(#333); ctx.fillRect(col * blockWidth, y, blockWidth, rowHeight); ctx.draw();canvas 重绘只改变画布像素不需要走setData的数据序列化流程。手游速度档位提升后canvas 方案能稳定跑满 60 帧而且不像 view 那样容易出现白屏闪烁。缺点是触摸坐标换算要自己结合canvas的宽高重新计算并考虑canvas内坐标和 CSS 像素的缩放比例。作为毕设项目可以在答辩演示时展示“view 版”和“canvas 版”两套代码对比运行帧率这种工程对比比单纯堆功能更能说明你对性能的理解。5.3 增加反馈机制震动、音效与视觉高亮wx.vibrateShort只能在用户主动点击后调用不能像音效一样反复播放。配合点击黑块可以在方块消掉前先做一个 200ms 的白色高亮动画setTimeout(() { blocks.splice(idx, 1); this.setData({ blocks }); }, 100);延迟 100ms 再删除配合wx.createInnerAudioContext播放短促的“嗒”声手感会明显更扎实。音效文件要放在utils或assets目录下并在app.json的usingComponents中不要做任何注册直接用InnerAudioContext创建即可。注意开发者工具中音频文件必须小于 1MB超了真机可能正常但工具里不播放。const audio wx.createInnerAudioContext(); audio.src /assets/tap.wav; audio.play();音效实例建议只创建一次并保存到this不要每次点击都新建否则连续点击会产生大量并发音频流导致真机卡顿。游戏结束时再destroy释放内存。这几个改造点组合起来已经足够把一个“能跑的基础源码”变成“能讲清楚设计取舍的毕业设计”。本文还有配套的精品资源点击获取