微信小程序游戏开发实战:彩色消除的完整实现与优化

发布时间:2026/8/31 3:55:02
微信小程序游戏开发实战:彩色消除的完整实现与优化 你决定要做一个微信小程序名字都想好了叫“彩色消除”。听起来很简单不就是消消乐吗但等你真正打开开发者工具新建项目开始写第一行代码时你会发现事情远没有想象中那么顺利。方块要怎么生成如何判断三个颜色连在一起点击交换后不匹配要不要回弹动画怎么和消除逻辑配合为什么模拟器上好好的一到真机上就卡这些问题会一个接一个冒出来。我见过不少同学被“彩色消除”这个项目卡住。不是因为算法太难而是因为没有先想清楚这类小程序游戏的核心本质是什么。彩色消除最容易被低估的地方不是“消除”这个动作而是它把游戏状态、渲染、交互和微信小程序的生命周期全部揉在一起。单次跑通很容易但你要做出一个流畅、稳定、能上线的版本真正考验的是你把一个看似简单的规则拆解成一套可维护的数据和渲染流程的能力。这篇文章我就用“彩色消除”这个项目作为例子拆解一套从零开发微信小程序游戏的实际路径。它不是教学演示代码而是一份可以抄作业的实践路线。我会尽量把每一步的“为什么”也讲清楚因为只抄代码你下次换个需求还是会卡住。1. 先想清楚彩色消除到底要做什么——最小可行版本怎么拆1.1 从用户视角理解游戏核心如果只看“彩色消除”这个名字用户对它的预期非常直接屏幕上有一堆彩色方块我点击或者交换相邻方块如果三个或更多同色方块连在一起它们就消失上面的方块掉下来新的方块补上我再继续操作。但作为开发者你不能一上来就埋头写代码。你需要先把用户感受翻译成技术需求用户点击的响应要快不能有延迟感。交换、消除、下落、补充这四段动画要连贯不能一顿一顿。每次操作之后棋盘上的方块必须是合法的、可玩的状态不能出现消除后空位没填上也不能出现死局。分数、步数或时间的反馈要清晰让用户知道自己在游戏里处于什么阶段。这些要求听起来很基本但每一个都对应着一个常见的坑。比如“动画连贯”如果你每次交换都直接 setData 一次数组用户会感觉到明显的卡顿比如“合法状态”如果你只做了消除检测没有做“无可行交换”检测玩家很快就会遇到无法继续操作的局面。所以在写代码之前先写一个功能清单。我当时给自己定的最小可行版本是生成一个 8×8 的彩色方块网格。支持点击两个相邻方块交换位置。交换后如果形成三个及以上同色连线则消除。消除后上方方块下落空白位置补新方块。每次消除加分显示当前分数。游戏能正常结束或重置。至于什么关卡、道具、特效、排行榜都可以放到后面。先跑通核心循环这是最重要的。1.2 把游戏拆成状态机比直接写流程更可靠很多新手写小游戏喜欢把逻辑全部写在事件回调里点击了做这个交换了做那个消除了再做下一个。这样写到后面代码会乱成一团因为你很难判断“当前到底能不能点击”“动画是不是还在播放中”。更靠谱的做法是先把游戏看作一个状态机。彩色消除的核心状态可以分成这几个空闲Idle用户可以进行点击或交换操作。交换中Swapping两个方块正在交换位置此时不能接收新的操作。消除中Clearing检测到匹配正在执行消除动画。下落中Refilling方块下落和补充新方块的过程中。结算中GameOver无可行交换或达到目标条件。每个状态之间是互斥的。只有在“空闲”状态下才允许用户交互其他状态都要锁定输入。这样做的好处是你不用在点击回调里判断一堆 if 条件只需要判断“当前状态是否空闲”如果不是就忽略这次操作。状态机的实现也很简单。你可以定义一个变量gameState用字符串常量表示当前状态。在关键动作开始前把状态切走在动作结束后再切回空闲。这样整个游戏流程就变成了一条清晰的链路点击交换 → 检测匹配 → 消除 → 下落补充 → 回到空闲。1.3 不要一开始就追求“好看”先把数据结构做对彩色消除游戏里界面上的每一个彩色方块本质上是一个数据对象。你需要用一个二维数组来表示整个棋盘。数组的每个元素可以是数字、字符串或对象代表颜色。我建议使用数字编号而不是直接用颜色字符串比如const COLORS { 0: #FF585D, 1: #FFB03A, 2: #7BC67E, 3: #4DC3FF, 4: #B78BFF, 5: #FF8FB3 };这样网格数据就是一个二维数字数组方便比较和运算。渲染时再通过编号查表得到具体颜色。为什么这么设计因为消除算法的核心是“比较颜色是否相同”。如果直接存颜色字符串比较时就会涉及字符串对比虽然也能做但数字比较更快也更不容易出错。更重要的是当你后续需要扩展不同类型的方块比如特殊方块、障碍方块时数字编号可以很方便地扩展比如 6 代表炸弹7 代表冰块而颜色字符串就会越来越难维护。2. 技术选型小程序里做游戏为什么不能用传统游戏引擎2.1 先分清彩色消除是“小程序页面”还是“小游戏”微信生态里有两类形态一类是“小程序”一类是“小游戏”。光看名字“彩色消除”可能会有歧义。如果你选择注册“小程序”那你本质上还是在用 WXML WXSS JavaScript 构建页面只是页面里可以嵌入 Canvas、交互事件等。如果你注册“小游戏”那就完全使用 Canvas 和游戏循环没有 WXML 和 WXSS。我这里讨论的是用“小程序”方案实现彩色消除。原因很简单第一小程序账号和开发环境更容易获得第二小程序可以复用页面的基础能力比如登录、分享、支付第三对于轻量级消除游戏用小程序页面 Canvas 完全够用不需要引入小游戏的框架复杂度。但这也带来了一个重要选择游戏画面到底用 DOM 节点拼还是用 Canvas 绘制2.2 DOM 节点和 Canvas怎么选对于 8×8 的棋盘你可以用 WXML 的 view 组件通过 flex 布局生成 64 个方块每个方块用 CSS 设置背景色。点击事件也可以直接绑定到每个 view 上。听起来很简单对不对但问题是消除游戏会频繁改变网格内容。如果用 DOM 节点你每次需要更新网格时很可能要重新渲染很多 view。在小程序中setData 更新大数组会触发视图层重新渲染很容易导致性能瓶颈。尤其是动画期间如果你连续 setData真机上可能直接卡成幻灯片。所以我更推荐用 Canvas 2D 作为游戏主渲染层。Canvas 的绘制完全由 JavaScript 控制你可以自由地绘制方块、处理动画不需要频繁更新 WXML 节点。Canvas 的性能在小程序游戏场景里是被验证过的很多类似的小游戏都是这么实现的。2.3 小程序 Canvas 2D 怎么用目前微信小程序推荐的 Canvas 用法是 Canvas 2D 接口也就是在 WXML 中声明一个 canvas并指定type2d然后在逻辑层通过节点查询拿到 canvas 的 node 对象。常见结构是这样的canvas type2d idgameCanvas stylewidth: 100%; height: 100%;/canvasconst 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); // 这里可以开始绘制 });注意这里的canvas.width和canvas.height设置的是画布物理像素大小而样式上的宽高分是逻辑像素。如果不乘以pixelRatio在 Retina 屏上会模糊。这个例子是常见写法不同基础库版本可能略有差异落地前建议先在真机上验证一下。我开始写的时候也遇到画布模糊的问题排查后才发现是 DPR 没有处理。2.4 不要一上来就给自己挖“框架”的坑有的人习惯先引入一个游戏框架或者自己封装一套渲染引擎。对于彩色消除这种小项目我不建议过度设计。先把ctx.fillRect、drawImage、requestAnimationFrame这几个基础 API 用熟练你就能画出一个完整的棋盘。框架反而会让问题变得复杂比如兼容性、包体积、调试难度。真正需要抽象的是游戏逻辑层和渲染层分离。比如你写一个Grid模块负责数据写一个Renderer模块负责画图写一个Game类负责控制状态。这样即使以后改成小游戏逻辑层也能复用。3. 核心机制彩色消除的匹配算法和方块数据结构3.1 初始化网格别让一开始就出现“自动消除”初始化棋盘时最直接的做法是每个格子随机生成一个颜色编号function createGrid(rows, cols) { const grid []; for (let r 0; r rows; r) { grid[r] []; for (let c 0; c cols; c) { grid[r][c] Math.floor(Math.random() * 6); } } return grid; }但如果运气不好棋盘一开始就会出现三连或四连游戏还没开始就自动消除体验会非常奇怪。所以在随机填充后必须检查整个棋盘如果有匹配就重新生成直到整个棋盘没有任何匹配。工程上更高效的做法是逐格生成生成时避免和左边两个、上边两个形成同色。比如function randomColorExclude(nonColors) { let color; do { color Math.floor(Math.random() * 6); } while (nonColors.indexOf(color) 0); return color; } function createGridNoInitialMatch(rows, cols) { const grid []; for (let r 0; r rows; r) { grid[r] []; for (let c 0; c cols; c) { const forbidden []; if (c 2 grid[r][c - 1] grid[r][c - 2]) { forbidden.push(grid[r][c - 1]); } if (r 2 grid[r - 1][c] grid[r - 2][c]) { forbidden.push(grid[r - 1][c]); } grid[r][c] randomColorExclude(forbidden); } } return grid; }这样生成的棋盘天生不会出现连续三个相同的方块。这个方法比“生成后检测再重新生成”更省时间也更稳定。3.2 匹配检测从行和列两个方向扫描所谓“消除”本质是找到棋盘上所有水平或垂直方向连续相同颜色数量大于等于 3 的连线。最常见、最容易理解的实现就是分别按行和按列扫描。逻辑是这样的对每一行从左到右遍历记录当前颜色和连续长度。当遇到颜色变化或到达行尾时如果连续长度 3就把这段连续位置的索引加入待消除集合。对每一列做同样处理。因为一个方块可能同时属于一行和一列的匹配比如十字形所以建议用一个Set来存储待消除方块的坐标避免重复。伪代码示例function findMatches(grid) { const rows grid.length; const cols grid[0].length; const matches new Set(); // 按行扫描 for (let r 0; r rows; r) { let c 0; while (c cols) { let end c 1; while (end cols grid[r][end] grid[r][c]) end; if (end - c 3) { for (let i c; i end; i) matches.add(r , i); } c end; } } // 按列扫描类似 // ... return matches; }这段代码不难但有一个细节容易漏你需要在“消除前”用当前棋盘判断并且在“消除后、下落前”再判断一次因为方块下降后可能又形成新的匹配。这种连环消除在消消乐里非常常见也是游戏爽感的重要来源。3.3 消除后的下落和补充数据的重排消除完成后棋盘上会出现空洞。最简单的做法是从上往下把每一列的空洞往下移动然后补充新方块到顶部。一个常见的实现方式是按列逐列处理。对于第c列从下往上遍历用一个writeIdx指向当前位置应该填入的位置for (let c 0; c cols; c) { let writeIdx rows - 1; for (let r rows - 1; r 0; r--) { if (!removed.has(r , c)) { grid[writeIdx][c] grid[r][c]; writeIdx--; } } // 补充新方块到顶部 while (writeIdx 0) { grid[writeIdx][c] Math.floor(Math.random() * 6); writeIdx--; } }这里要注意新补充的方块也可能形成新的匹配所以需要在整体下落之后再次执行findMatches。如果还有匹配就继续消除。如果没有才回到空闲状态。这个“消除 → 下落 → 再检测”的循环是整个游戏的核心循环。你可以把它封装成一个函数比如processBoard()然后配合动画层播放。3.4 死局检测没有可交换但没消除的棋盘怎么处理一个容易忽略的问题是棋盘可能变成“无解状态”没有任何两个相邻方块交换后能形成三消。如果玩家发现怎么也消不掉就会认为游戏出 bug 了。所以在每次回到空闲状态前建议检测当前棋盘是否存在至少一个可交换操作。检测方法是遍历所有相邻交换组合交换后做一次匹配检测如果有匹配则恢复原状并返回 true。这个检测的计算量是(rows*(cols-1) (rows-1)*cols) * 匹配检测对于 8×8 来说非常快。如果找不到可交换操作可以自动洗牌或重新生成棋盘。对玩家来说看到一个“自动重排”的动画总比死局好。这个细节很影响口碑但很多入门教程里不会提到。4. 交互与动画让人感觉到“消除”的反馈不是靠颜色4.1 点击、交换、回弹手势状态管理很多初学者在写点击交互时会直接判断点击的是哪个方块。但彩色消除的常见玩法是点击第一个方块选中再点击第二个相邻方块交换。如果你的目标是做“交换两个方块”可以这样处理第一次点击记录当前方块的坐标并给出选中效果比如高亮边框。第二次点击判断新点击的方块是否和上一次选中的方块相邻。如果相邻则尝试交换。如果不相邻则把新方块作为新的选中对象。交换前需要先对格子数据做一次“静态匹配检测”。即在交换后的数组上调用findMatches如果有匹配就真正执行交换否则动画回弹。这里的关键是“先试算再操作”不是先把 UI 改了再判断。比如你交换了grid[r1][c1]和grid[r2][c2]然后检测匹配。有匹配保留交换没有匹配再换回来。这个过程中可以配合动画表现但逻辑上一定是“数据先行”。4.2 消除动画与帧循环别让动画和逻辑抢时间如果你用 Canvas 绘制游戏那么动画最自然的方式是使用requestAnimationFrame。在小程序里canvas 节点提供自己的requestAnimationFrame方法或者你也可以用全局的setInterval模拟帧循环。但我不建议直接在帧循环里写业务逻辑。更好的做法是给动画建立“进度”概念交换动画记录progress从 0 变到 1根据进度插值计算两个方块当前绘制位置。消除动画把要消除的方块记录在数组中每个方块有life每帧减少当life 0时移除。下落动画每个格子记录从原位置到目标位置的位移同样通过进度来插值。这样动画和逻辑就能解耦。逻辑层负责告诉渲染层“现在有哪些方块要动、去哪里”渲染层只负责根据进度绘制。比如你先计算出下落前后的网格状态然后生成一个动画队列每一帧更新绘制。小程序 Canvas 的性能上限比 DOM 高但你仍然不能无限制地频繁重绘。一个常见的优化是只重绘当前帧变化的内容或至少不要使用ctx.clearRect清空整个画布再全部填充。但对于 8×8 的网格清屏重绘的开销其实不大所以不用太早优化。先跑通再考虑脏矩形之类的优化。4.3 音效和视觉反馈可以从简但必须有很多开发者会觉得“音效这种小事最后加也没关系”。但实际上音效和反馈是消除游戏体验里非常重要的一部分。如果没有音效和动画反馈用户点击方块和消除时会感觉游戏“死气沉沉”。即使你没有设计资源也可以用非常基础的方式选择方块时绘制一个高亮边框。交换不合法时做一个短暂的回弹动画。消除时让方块闪烁一下或缩小消失。新方块补充时从上一格滑落到目标位置。加分时在 Canvas 上绘制弹出的分数文字。音效方面微信小程序支持wx.createInnerAudioContext可以播放短音频。你可以准备几个简单的 MP3 文件比如“点击”“消除”“失败”放在项目的assets/audio目录下。第一次在真机上使用音频时还需要在用户点击后先调用一次播放以通过用户手势启动音频因为很多浏览器和 WebView 有自动播放限制。4.4 一个常见隐患动画未结束就接收新一轮输入这是很多休闲游戏最常见的 bug。玩家快速点击上一轮下落动画还没播完你已经交换了新的方块导致棋盘数据错乱。解决方式就是前面说的状态机。在进入交换、消除、下落任一阶段时把gameState设为busy在processBoard()全部结束后再设为idle。同时所有输入回调的第一行都检查if (this.gameState ! idle) return;这一行代码能避免大量死锁和错乱问题比任何“防抖”都有效。5. 微信小程序特有的坑屏幕适配、性能、生命周期和发布5.1 不同机型的画布尺寸换算彩色消除需要在不同尺寸的手机上看起来正常。你不能把方块大小写成固定像素比如 40px而要根据屏幕宽高动态计算。常见做法是以屏幕宽度为基准留出左右边距然后计算出每个格子的宽度和高度。格子一般用正方形所以可以取const screenWidth wx.getSystemInfoSync().windowWidth; const margin 16; const gridWidth screenWidth - margin * 2; const cellSize Math.floor(gridWidth / cols); const gridHeight cellSize * rows;然后让 Canvas 的高度至少包含gridHeight再加上一个分数显示区域。你还可以用wx.getWindowInfo()获取更安全的窗口信息注意有些接口已经改名真机测试时最好打印一下。如果屏幕高度不够尤其是横屏适配或全面屏底部安全区需要额外处理。彩色消除一般做成竖屏问题较小但也要注意底部 Home 指示条可能遮挡画布所以页面底部要加安全区 padding或者使用safe-area-inset-bottom。5.2 setData 性能瓶颈在游戏场景中怎么绕普通小程序页面里setData是更新界面的唯一标准方式。但在游戏 Canvas 中你完全可以不依赖 setData 来刷新画面。我最初的版本会在每次格子变化时调用setData({ grid: newGrid })结果在真机上非常卡。后来发现Canvas 只需要在初始化时通过setData设置一下画布尺寸或者少量配置之后画面更新全部走ctx绘制几乎不卡。这其实是一个思维转变页面结构是静态的游戏画面是动态的动态的内容交给 Canvas动静分离。如果要在 Canvas 内显示分数可以把分数画在 Canvas 内而不是用一个text节点。这样分数更新时就不需要 setData。当然如果你想要无障碍访问或方便自动化测试用 WXML 节点显示分数也可以但为了性能我不会在主循环里频繁 setData 分数而是每 100 毫秒或每秒同步一次。5.3 音频、背景、分包和体验版发布说起发布有四个高频痛点。第一个是音频资源路径。在开发版里本地音频可以直接引用相对路径。但正式发布后如果音频放在代码包里包体可能会变大。建议把音频文件上传到自己的服务器或对象存储然后在代码里配置合法域名。记住微信小程序正式环境不能直接请求 IP 地址也不能请求未备案的域名必须在后台配置 request、downloadFile 等合法域名。第二个是包体积。一个小程序主包默认限制是 2MB如果你的彩色消除项目里放了很多图片、音频很容易超限。解决办法是使用“分包”。你可以把游戏页面放到主包把其它不常用的页面比如设置、帮助、积分商城放到分包里。对于纯 Canvas 游戏包体积一般不会很大但也要注意不要随意放几张高清大图就超了。第三个是体验版二维码和真机调试。开发完成后你可以在微信开发者工具里点击“预览”生成一个体验版二维码扫码后就能在真机上打开。这里容易遇到的坑是预览版域名校验问题、手机和电脑不同步、代码包上传失败等。遇到问题时先看开发者工具的控制台日志再看真机调试Remote Debugging模式逐条排查网络请求和 JS 报错。第四个是常见真机报错。比如net::ERR_CONNECTION_RESET这通常意味着请求被重置常见原因包括合法域名未配置、SSL 证书过期、代理或网络环境异常、服务端连接数限制。不要一上来就怀疑代码先检查网络请求面板看是哪个请求失败再逐层排查。另外要注意开发者工具里勾选“不校验合法域名”只在开发时有效真机上必须配置合法域名。5.4 生命周期管理切后台再回来游戏不能崩微信小程序有完整的生命周期。onHide会在用户切到后台时触发onShow在回到前台时触发。对于游戏类小程序你需要在这两个生命周期里做好处理onHide停止游戏循环记录当前时间。onShow恢复游戏循环可选做离开时长校验比如超过一定时间就暂停或重开。如果不处理onHide你的动画循环可能一直在后台跑浪费电量甚至因为某些系统限制被冻结导致回来时画面卡死。所以建议在onHide中取消动画帧在onShow中重新启动。另外onUnload时要清理所有监听和定时器避免内存泄漏。比如requestAnimationFrame的回调要保存 id卸载时调用wx.offWindowResize等接口清理事件监听。6. 从“跑通”到“可上线”的工程化补充6.1 游戏状态持久化和分数排行彩色消除如果只是一个“玩完就关”的页面用户留存会很低。你可以做一个本地最高分记录用wx.getStorageSync和wx.setStorageSync保存。这很简单不需要服务器。如果你想做全局排行榜那就需要后端支持。完整方案通常涉及用户登录通过wx.login获取 code传给后端换取 openid。后端存储用户分数提供排行榜接口。小程序端通过wx.request请求排行榜数据使用 Canvas 或 WXML 展示。但要提醒自己不要一上来就做完整后端。可以先做本地最高分再逐步接入云开发。微信云开发提供了免鉴权数据库和云函数对小型项目非常友好。你可以用云存储存头像用云函数写排行榜逻辑不用自己买服务器。6.2 接入微信登录和分享时注意什么如果你的彩色消除需要微信登录可以参考 open-type 或wx.getUserProfile等能力但注意这些 API 政策和权限一直在调整具体要以微信官方文档为准。更稳妥的做法是一开始不强制登录只用设备本地数据等用户玩出好成绩以后再引导进行登录和分享。分享也很有讲究。你可以通过wx.shareAppMessage让用户点击按钮分享给好友。分享卡片要设计得能吸引点进来比如“你能超过我的分数吗”这种文案。但要注意微信官方对于诱导分享有严格要求不要用“分享才能解锁”或“分享后复活”这类诱导机制否则很容易被封禁。6.3 一个可以复用的测试清单我每次开发完一个小程序游戏都会按以下清单自测这能避免很多低级问题初始棋盘是否无自动消除。点击非相邻方块是否正确切换选中而不是报错。交换后不匹配是否回弹。交换后匹配是否消除、下落、补充并处理连环消除。快速点击、连续点击是否出现状态错乱。切后台再回前台动画是否恢复分数是否保留。在不同尺寸机型标准屏、全面屏、折叠屏下棋盘是否完整显示。真机上画布是否模糊。真机上音频能否播放。请求的接口域名是否已配置合法域名。主包体积是否超限。体验版二维码能否正常打开。这个清单相当于一个回归测试集。每改一个功能跑一遍能省很多上线后的麻烦。6.4 长期维护版本更新和后续演进彩色消除看似简单但它可以承载很多扩展方向。比如你可以加入“限时模式”“步数模式”“特殊道具”“关卡地图”甚至使用 WebGL 做更华丽的粒子特效。微信小程序 Canvas 2D 目前支持 WebGL 渲染模式但复杂度会高很多不是必须的。更重要的是把代码结构保持干净。我建议把游戏核心逻辑做成一个独立的类比如Game它不直接依赖小程序 API。然后在页面Page里创建 Game 实例注册渲染器、事件和生命周期。这样以后如果你想换成小游戏或者其他平台逻辑层可以基本不动。这个思路也可以迁移到其它小游戏项目。你会发现很多时候卡住你的不是某一个具体特效而是“状态管理”“数据与渲染分离”“生命周期处理”这些底层的工程意识。彩色消除只是一个很好的训练场把这些练好了你再做其他小游戏都会顺手很多。最后回到那句话彩色消除不算难但要做一个让人愿意持续玩的版本靠的不是花哨的样式而是扎实的数据结构和流程控制。希望你写完这个项目再看任何小游戏都能一眼看出它的状态机、数据流和渲染层是怎么组织起来的。