资讯动态

奇葩连连看源码拆解:H5页游迁移微信小游戏的通关之路

发布时间:2026/9/17 12:31:00 来源:尧图企业网站定制
简介这是一份奇葩连连看微信小游戏完整源码适合初入微信小游戏开发、想了解完整游戏流程的读者参考。包内包含65个文件涵盖6个js逻辑脚本游戏主逻辑、消除判定、计时计分等、1个css样式文件、1个htm入口页面以及大量jpg、png图片素材游戏背景、方块图案、按钮与提示图标等整体压缩包仅225KB方便快速下载查看。已有1222人学习浏览。代码采用createjs引擎结合jQuery实现目录结构按页面、脚本、图片模块划分易于对照理解连连看的棋盘生成、消除判定与界面刷新过程同时附带免责声明与必要说明方便读者在本地运行调试、修改关卡或替换素材作为入门级微信小游戏实战参考非常合适。1. 微信小游戏源码里的奇葩连连看一套2013年CreateJS H5页游的承重结构奇葩连连看这套源码压缩包第一眼像网页素材库index.htm入口、createjs-2013.12.12.min.js、jquery-2.1.0.min.js以及三十多张按序号排的jpg牌面。但它真正值钱的不是UI是game_base.js和createjs_game.js里那套与DOM解耦的棋盘算法。项目标题里的“微信小游戏源码”说明它是按小游戏场景分享的实际结构是H5页游拷贝进微信小游戏运行需要先做Canvas化与去jQuery改造这一步恰是学习收益最大的地方。拆这份源码最有价值的部分是数据层牌面生成、连通判定、死局检测与洗牌全部放在game_base.js和createjs_game.js里页面只负责把结果画出来。适合三类人做连连看逻辑复刻、课程设计选型、需要把老H5棋盘游戏迁进微信小游戏的人。下面按启动链路、核心算法、微信环境适配、调试验证四个方向拆代码都以这套资源的实际结构和经典实现为准。2. 从index.htm到main.js奇葩连连看的资源分层与启动链路老H5项目最容易在入口处翻车。index.htm把CSS和全部脚本按顺序堆在头部任何一个库没加载成功后面的业务脚本全部白挂。先把压缩包当代码仓库而不是素材包看理清每一层文件的职责后面改棋盘行列、换牌面、接微信环境才不会改错文件。2.1 压缩包里的资源分层引擎、入口、业务脚本、素材文件在这个项目里的角色index.htm页面入口按顺序引入CSS与全部脚本main.js初始化逻辑创建棋盘配置并拼装数据层与渲染层game_base.js棋盘数据、牌面生成、连通判定、死局检测createjs_game.js基于CreateJS的渲染层用Canvas画棋盘与消除动画createjs-2013.12.12.min.jsEaselJS 2013年12月版本提供Stage、Shape、Bitmap、Tickerjquery-2.1.0.min.jsDOM操作与事件绑定主要在按钮和面板UI上jquery.cookie.js读写本地成绩比如bestscore、curscoreimg/*.jpg1.jpg到31.jpg牌面图以及按钮、背景、结算图表格的排序依据是脚本在index.htm里的加载顺序。createjs-2013.12.12.min.js是EaselJS的老版本API比后来的版本精简很多Stage放显示对象、Ticker跑主循环这套写法在2013到2015年的H5棋牌小游戏里非常常见。jquery-2.1.0.min.js和它同期页面里有大量DOM操作所以这份源码完整跑起来的前提是这几个基础库都不能缺。注意这里没有出现微信小游戏专用的game.js意味着保留的是网页版运行形态。要迁到微信小游戏不是改几个文件后缀而是把依赖document和jQuery的部分整体替换后面第4章会专门讲。2.2 index.htm脚本引入顺序决定能不能跑起来index.htm里脚本的引入顺序不能乱。比如第4个标签就要加载main.js而main.js里直接访问GameBoard构造函数这个函数定义在后面的game_base.js里。同步加载模式下浏览器执行到main.js时后面的脚本还没解析控制台会先报“GameBoard is not defined”而不是渲染错误。script srcjs/createjs-2013.12.12.min.js/script script srcjs/jquery-2.1.0.min.js/script script srcjs/jquery.cookie.js/script script srcjs/main.js/script script srcjs/game_base.js/script script srcjs/createjs_game.js/script依赖顺序是createjs和jquery必须在main之前因为main.js的构造函数里已经引用了它们game_base.js在main之后是因为main.js只在运行时访问GameBoard而GameBoard的定义通过函数声明提升在同文件内不受顺序影响但跨文件就必须先加载。createjs_game.js放最后因为它同时引用main.js创建的实例和game_base.js定义的数据结构。本地调试不要直接双击index.htm用file://打开。老项目里图片资源用相对路径读取浏览器对file协议下的静态资源限制比较多常见做法是在项目根目录起一个静态服务python3 -m http.server 8080然后访问http://localhost:8080/。这样图片加载、cookie读写、CreateJS的Image预加载都走正常HTTP流程比file://少踩很多莫名其妙的跨域问题。提示先跑起静态服务再打开页面如果控制台还有404优先检查img目录下jpg文件名是否和main.js里的资源表一致。这套源码的图片是按数字命名的改过文件名会导致牌面加载不出来。2.3 main.js初始化一张牌桌要做的事main.js按“配置、数据、渲染”三步启动。配置里最关键的是棋盘行列数和牌型种类数这两组数字的乘积必须严格相等否则prepare阶段生成牌面时会出现单牌或数组越界。// main.js 里典型的初始化流程实际变量名以源码为准 var gameConfig { rows: 8, // 棋盘行数 cols: 10, // 棋盘列数 typeCount: 20, // 参与游戏的牌面种类数 pairPerType: 4, // 每种牌面数量4表示两对 cellSize: 48 // 单格边长单位px }; var board new GameBoard(gameConfig); board.prepare(); // 生成牌面并打乱 var stage new createjs.Stage(gameCanvas); var gameView new GameView(stage, board); gameView.render();rows乘cols等于80typeCount乘pairPerType等于80两张表对得上棋盘才装得满。如果棋盘格数和牌面总数不一致prepare()里大概率会留下空格或多出牌面。我一般会把这条校验直接写进GameBoard构造函数调试时第一时间暴露配置错误而不是等渲染出来发现少了几张牌。prepare()内部做的事可以拆成三步先按typeCount生成牌型数组每个牌型出现pairPerType次再用随机洗牌打乱顺序最后按行优先填入棋盘数组。这套流程是连连看最基础的数据准备方式后面第3章的死局检测和洗牌都依赖这份二维数组。3. 连连看核心算法棋盘数组、连通判定与死局洗牌这是这套源码里含金量最高的部分。界面上的动画、按钮、倒计时都是表面工作能不能消、怎么消、无解时怎么办全部由game_base.js里的算法决定。把源码里那套判定按经典实现梳理成下面几个函数思路可以直接复用到任何语言。3.1 棋盘数据结构为什么是二维数组而不是对象数组game_base.js里的棋盘核心是一张二维数组行索引y、列索引xboard[y][x]等于0表示空格大于0表示某种牌面。之所以用二维数组不用对象数组是因为连连看算法绝大多数操作是整行整列的扫描二维数组按下标寻址循环里直接读board[y][x]成本最低。// 5行6列的示意棋盘0代表空位 var board [ [ 1, 2, 3, 4, 5, 6], [ 7, 8, 1, 2, 3, 4], [ 5, 6, 7, 8, 9, 10], [11, 12, 13, 14, 15, 16], [17, 18, 19, 20, 0, 0] ];这套源码的棋盘是完整矩形和传统连连看一致。注意边界处理双拐点扫描时路径可以绕到棋盘外也就是从最上一行往上一格、穿过“虚拟的-1行”再绕回来。常见实现是把棋盘声明成(rows2)乘(cols2)的数组四周留一圈0这样所有边界情况都统一走clear函数不需要在算法里堆一堆if判断是否越界。源码里30多张jpg的牌面id和图片路径是一对一映射牌型id只存数字渲染时才查图片数据层不关心图片长什么样。3.2 直连与单拐点判定先写两个clear函数连通判定最基础的操作是“判断两点之间是否全空”。直连要求同一行或同一列中间所有格子都为0单拐点是在两个交叉点里找一个路径中转站。下面这段先定义行和列的空白判断// 判断从x ax 到 x bx第y行之间的格子是否全空不含两端 function clearHoriz(ax, bx, y) { var minX Math.min(ax, bx); var maxX Math.max(ax, bx); for (var x minX 1; x maxX; x) { if (board[y][x] ! 0) return false; } return true; } // 判断从y ay 到 y by第x列之间的格子是否全空不含两端 function clearVert(ay, by, x) { var minY Math.min(ay, by); var maxY Math.max(ay, by); for (var y minY 1; y maxY; y) { if (board[y][x] ! 0) return false; } return true; }这两个函数是所有连接判定的地基。明确“不含两端”这一点很重要端点位置放的是两张待消除的牌判断路径时如果含端点必然撞牌永远返回false。起点和终点是相邻格时for循环一次都不执行直接返回true这种情况对应两张牌上下或左右紧挨着。单拐点的逻辑是在a和b的连线矩形里试两个交叉点(a.y, b.x)和(b.y, a.x)。A点先走水平再走垂直或者先走垂直再走水平两条线段的交叉处必须恰好是空格// 判断a点到b点能否通过一个拐点连通 function oneCorner(a, b) { // 候补1a - (a.y, b.x) - b拐点在(a.y, b.x) if (board[a.y][b.x] 0 clearHoriz(a.x, b.x, a.y) clearVert(a.y, b.y, b.x)) { return true; } // 候补2a - (b.y, a.x) - b拐点在(b.y, a.x) if (board[b.y][a.x] 0 clearVert(a.y, b.y, a.x) clearHoriz(a.x, b.x, b.y)) { return true; } return false; }这里第一个候补的拐点是(a.y, b.x)与a同一行与b同一列。A到拐点走水平线拐点到b走竖直线。第二个候补反过来的道理一样。两个候选交叉点必须为0因为交叉点不是端点牌如果那里已经有牌路径就被挡住了。单拐点是最常见的消除路径很多新手实现漏掉“交叉点为空格”这个条件导致路径从牌中间穿过去。3.3 双拐点扫描与死局检测把三条线段拆开验证双拐点比单拐点多一条中间线段做法是逐行逐列扫描。固定A点后扫每一行yA点向下到(y, a.x)是竖直线、中间行从(a.x)到(b.x)是水平线、再从(y, b.x)向上到B点是竖直线三段全空就成立。反过来再扫每一列。这就是经典的两层循环双拐判定function twoCorner(a, b) { var rows board.length; var cols board[0].length; // 垂直扫描固定a.y和b.y试探中间行y for (var y 0; y rows; y) { if (y a.y || y b.y) continue; // 退化成单拐或直连跳过 if (clearVert(a.y, y, a.x) clearHoriz(a.x, b.x, y) clearVert(y, b.y, b.x)) { return true; } } // 水平扫描固定a.x和b.x试探中间列x for (var x 0; x cols; x) { if (x a.x || x b.x) continue; if (clearHoriz(a.x, x, a.y) clearVert(a.y, b.y, x) clearHoriz(x, b.x, b.y)) { return true; } } return false; }这里有一个关键点中间行y允许等于棋盘边界内的任意一行包括第一行和最后一行。因为路径可以在棋盘外面绕EaselJS渲染层不会管虚拟边界只要逻辑判定通过、连线绘制时把路径画到棋盘外框再回来就行。如果棋盘数组没有外扩一圈这段代码里y从0扫到rows-1就能覆盖“从上方绕行”的场景如果数组外扩了扫描范围改成从0到rows1配合周边0值一起判定。死局检测依赖这个判定函数遍历所有剩余牌对只要有一对能连通就说明还有得玩// 检测棋盘是否还存在可消除的一对 function hasAnyMove() { var rows board.length; var cols board[0].length; for (var y 0; y rows; y) { for (var x 0; x cols; x) { if (board[y][x] 0) continue; // 只和后面的牌配对避免重复检测同一对 for (var yy y; yy rows; yy) { for (var xx (yy y ? x 1 : 0); xx cols; xx) { if (board[yy][xx] 0) continue; if (board[yy][xx] ! board[y][x]) continue; if (canLink({x: x, y: y}, {x: xx, y: yy})) { return true; } } } } } return false; }canLink内部把直连、oneCorner、twoCorner三种情况用或运算串起来。这段代码的复杂度在最坏情况下是O(n²)数量级但连连看棋盘一般就8乘10或10乘14实时检测完全够用。hasAnyMove返回false时触发洗牌洗牌前先把剩余牌收集起来重新打散再填回棋盘之后再次调用hasAnyMove确认新棋盘有解最多重试30次避免洗出连续死局。3.4 洗牌与开局有解的实现边界一个经常被忽略的点保证开局有解与保证过程有解是两件事。随机洗牌只保证牌面分布均匀不保证任意时刻都存在可消对死局检测解决的是过程兜底反向构造才能让开局固定有解。反向构造的做法是从空棋盘出发随机选一对空格验证这对位置本身连通再把两张牌放进去重复直到满盘。这种方式能保证每一步落子都合法但会牺牲部分随机性牌面分布看起来不如纯洗牌自然。课程设计里我一般推荐“纯洗牌加死局重排”因为它代码量少且和真实产品行为一致。如果要做得更细可以在洗牌时优先把剩余牌往棋盘中心区域放边缘留空这样可连通的概率更高触发死局重排的次数会明显下降。4. H5奇葩连连看迁到微信小游戏去DOM化与Canvas适配要点index.htm的存在是最大的适配线索。微信小游戏运行时没有完整的浏览器DOM没有document对象jQuery依赖的DOM API基本不可用。但这套源码的数据层和渲染层分得比较开game_base.js可以整体复用主要工作量在main.js和createjs_game.js这两层的对接上。4.1 微信小游戏运行时与H5的差异对照H5里这份源码的用法微信小游戏环境的替代方案$(.btn).click(handler)canvas添加touch事件或手动计算点击坐标$.cookie(bestscore, score)wx.setStorageSync(bestscore, score)new Image() 加载牌面wx.createImage()HTMLAudioElement播音乐wx.createInnerAudioContext()document.getElementById(gameCanvas)wx.createCanvas()获取主画布表格左侧是源码里真实出现过的用法右侧是迁到微信小游戏时的对应API。以jquery.cookie.js为例它底层依赖document.cookie微信小游戏环境没有cookie概念直接用wx.setStorageSync替换之后bestscore和curscore两个成绩值的读写都要同步换否则排行榜面板会一直读到旧值或0。game_base.css在Canvas化之后不再负责业务渲染它只作用于HTML页面上的按钮和文本在微信小游戏里这些UI要重新用Canvas绘制。4.2 CreateJS老版本在微信环境的两个坑createjs-2013.12.12.min.js本质是Canvas封装库微信小游戏环境能跑但不建议直接原样引入。老版本内部会访问window、navigator、document.createElement(canvas)微信开发者工具的模拟环境做了部分兼容真机上容易在初始化阶段静默失败。最稳妥的做法是保留createjs_game.js里的Stage、Bitmap、Ticker逻辑把图片加载全部替换成wx.createImage。// H5里的预加载方式 var img new Image(); img.src img/1.jpg; // 微信小游戏环境 var img wx.createImage(); img.onload function () { var bitmap new createjs.Bitmap(img); stage.addChild(bitmap); }; img.src img/1.jpg;第一个坑是canvas尺寸。浏览器里Canvas大小由style和width属性决定微信小游戏里主画布默认铺满窗口舞台尺寸要用wx.getSystemInfoSync拿到windowWidth和windowHeight再设置否则老游戏按800像素设计的布局在小屏手机会被截掉一圈。第二个坑是Ticker的帧回调在微信小游戏里Ticker正常运行但如果页面切后台开发者工具会暂停JS执行恢复后时钟状态要重新同步尤其是clock.png相关的倒计时逻辑。4.3 替换jQuery和cookie成绩与分享改造main.js里操作DOM的部分是最先需要改的。成绩面板、按钮事件、暂停菜单全部改成Canvas文本和触摸坐标判定// 原来的写法 $.cookie(bestscore, score, { expires: 30 }); $(#scorePanel).text(score); // 微信小游戏写法 wx.setStorageSync(bestscore, score); scoreText.text score ; // createjs.Text 对象注意页面里还有两个入口按钮toplistbtn.png排行榜按钮和lowkeysharebtn.png分享按钮。网页版分享走浏览器转发链路微信小游戏里要接wx.shareAppMessage分享参数用query字段传递这样好友点进小游戏时可以在onLoad里拿到分享来源。上架前再核对平台最新的著作权登记与类目要求这份源码明确标注仅用于学习参考直接商用前需要自测素材版权、自绘牌面图片并补齐对应资质材料。5. 调试技巧控制台自动通关脚本与棋盘透视验证调试老H5最简单的方法不是打断点而是把棋盘数据暴露到window上用控制台脚本自动寻找可消除的一对。这相当于给棋盘开一个逻辑透视不用肉眼看动画判断算法对不对。5.1 把棋盘挂到window让控制台替你验证消除路径在main.js创建棋盘之后加一行window.__board board或者直接在game_base.js里找到棋盘二维数组赋值的位置挂上去。暴露之后控制台就能直接读取牌面数据// 控制台运行寻找第一对可消除的牌 function findPair(board) { var rows board.length; var cols board[0].length; for (var y 0; y rows; y) { for (var x 0; x cols; x) { if (board[y][x] 0) continue; for (var yy y; yy rows; yy) { for (var xx (yy y ? x 1 : 0); xx cols; xx) { if (board[yy][xx] 0) continue; if (board[yy][xx] ! board[y][x]) continue; // 调用源码里的连通判定这里假设暴露为 canLink if (canLink({x: x, y: y}, {x: xx, y: yy})) { return [{x: x, y: y}, {x: xx, y: yy}]; } } } } } return null; }找到可消对之后把它接入游戏自身的点击处理函数用setTimeout模拟两次点击就能看到完整的消除动画。源码里点击牌的handler函数名需要到main.js里确认找到后挂到window上例如window.game.onPick。自动通关脚本每次消除前先跑findPair没有结果就说明通关或死局function autoResolve() { var pair findPair(window.__board); if (!pair) { console.log(剩余牌:, countRemain(window.__board)); return; } window.game.onPick(pair[0]); setTimeout(function () { window.game.onPick(pair[1]); setTimeout(autoResolve, 100); }, 40); } autoResolve();执行过程中如果动画中途停下来说明findPair选出的对子在游戏自身判定里不可消两边对连通规则的理解不一致。这时把pair和board当前状态打出来手动调canLink走一遍能很快看出是边界条件没处理还是外扩一圈的问题。还有一个衍生技巧把findPair的返回值改成从剩余牌里随机挑可消对就能做成一个低速自动化演示用来验收消除动画和得分逻辑。把棋盘数组挂到window这一招之后调试任何老H5棋盘游戏都能直接套用。本文还有配套的精品资源点击获取

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

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

免费获取报价