
简介这套数字合十消消乐源码基于 Cocos Creator 2.X 制作是一套完整可运营的休闲游戏工程适合具备基础编程知识、想学习 Cocos 游戏项目结构的开发者阅读也可以在原功能之上继续增加玩法或接入区块链模块。工程代码采用 TypeScript 编写阅读和修改都比较方便。资源包共包含 668 个文件压缩包大小 5.05MB主要类型有场景预制体、动画、音效、脚本、图集和大量图片素材目录组织清晰下载解压后即可直接打开预览并体验数字合十消除的核心玩法。目前已有 423 人学习下载。通过这份源码可以学习消消乐游戏中数字匹配判定、动画触发、界面切换、关卡状态管理等常见模块的写法同时较为规范的文件划分也有助于快速定位逻辑适合用于个人练习、面试展示或者作为后续开发更多休闲游戏的基础工程。1. 数字合十消消乐不是三消拿到 Cocos Creator 2.x 源码先看这三处数字合十和传统三消最大的区别在判定方式不靠同色三连而是把相邻一组数字累加成 10 后整组消除再下落补位。玩法带一点 2048 的数值策略节奏还是消消乐的短局制源码包通常几百 KB 到几 MB。拿到标注 Cocos Creator 2.x、完整可运营的数字合十消消乐源码别急着双击 .fire 场景先核对三处creator 版本号是否匹配、assets 里 scripts 和 resources 是否完整、结算与重开流程是否闭环。版本对不上场景打开脚本全变 Missing Script资源缺了白屏流程不闭环就谈不上运营。下面按我验收这类源码小游戏的习惯把棋盘判定、跑通构建、手感参数和打包收尾讲清楚。2. 合十消除的判定内核棋盘状态、数字池与连消检测这一章拆源码里最核心的算法。消消乐这类棋局游戏写坏的地方往往不在动画而在数据层和显示层互相搅在一起。先把棋盘抽象出来后面调参数和接玩法都省事。2.1 棋盘二维数组与显示层解耦一份能改的源码数据层和显示层必须是分开的。常见做法是维护一个 rows × cols 的二维数组存数字0 表示空格所有 UI 节点只负责读数组做渲染不在 Sprite 节点上记数字。否则消除判定时要反向遍历场景树棋盘一大每步都要卡一下。// BoardModel.js —— 只维护数据不碰任何显示节点 cc.Class({ extends: cc.Component, properties: { rows: { default: 8 }, cols: { default: 8 }, }, onLoad() { this.grid []; for (let r 0; r this.rows; r) { this.grid[r] new Array(this.cols).fill(0); } this.initPool(); }, initPool() { // 数字池按出现次数做权重写死在配置里比生成时再随机更可控 this.pool [1,1,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9]; }, randomValue() { return this.pool[Math.floor(Math.random() * this.pool.length)]; }, reset() { for (let r 0; r this.rows; r) { for (let c 0; c this.cols; c) { this.grid[r][c] this.randomValue(); } } }, });说明properties 里的 rows 和 cols 在编辑器里直接改数据层自动适配randomValue 从预先定义的池子里取比每次 random(1,9) 更容易控制难度。reset 只重置数据显示层由外部统一刷新这就是解耦的意义。2.2 数字池与生成权重控制“无解”率数字池直接决定难度。1~9 均匀分布是新手最常写的但很容易出现全盘找不到合十组的死局5 只能靠另一个 5 来凑1 和 9 一旦隔着两个格子就够不着。合十游戏的无解率比三消高因为任意一个数字的互补对象是唯一的——9 只配 18 只配 2。我给这类项目定的经验值如下难度档数字池分布次数无解率倾向适配场景休闲1×6, 2×4, 3×3, 4×3, 5×2, 6×3, 7×3, 8×4, 9×6偏低前 5 关新手教学普通1×4, 2×4, 3×4, 4×3, 5×3, 6×3, 7×4, 8×4, 9×4中等主线关卡困难1×2, 2×2, 3×3, 4×5, 5×5, 6×5, 7×3, 8×2, 9×2偏高需要道具或洗牌的后期关原理数字池越偏向两端1、9 多互补配对越容易越往中间堆4、5、6组合越依赖同值配对和跨格连接。调难度时先改池子不要先改判定规则——池子影响全局判定规则改了会连累计分逻辑。2.3 找连通的合十组限定深度 DFS 与剪枝合十消除的判定就是在棋盘上找一组四向连通的格子数字和恰好等于 10。因为数字最小值是 1和的上限是 10递归深度最多 10 层这个规模用深度优先搜索完全够。关键是两个剪枝超过 10 直接放弃、访问过的格子不回访。const DIRS [[-1, 0], [1, 0], [0, -1], [0, 1]]; // 从 (r, c) 出发找一条数字和为 10 的连通路径找不到返回 null function searchTen(grid, rows, cols, r, c, visited, sum, path) { const value grid[r][c]; if (value 0 || sum value 10) return null; const nextPath path.concat([{ r: r, c: c }]); if (sum value 10) return nextPath; for (let i 0; i DIRS.length; i) { const nr r DIRS[i][0]; const nc c DIRS[i][1]; if (nr 0 || nr rows || nc 0 || nc cols) continue; const key nr * cols nc; if (visited.has(key)) continue; visited.add(key); const found searchTen(grid, rows, cols, nr, nc, visited, sum value, nextPath); if (found) return found; visited.delete(key); } return null; } // 全盘扫描返回第一组可消除的格子没有则返回 null function hasAnyTen(grid, rows, cols) { for (let r 0; r rows; r) { for (let c 0; c cols; c) { const key r * cols c; const visited new Set([key]); const group searchTen(grid, rows, cols, r, c, visited, 0, []); if (group) return group; } } return null; }参数说明sum 记录当前路径累计值path 存整条路径的坐标消除时靠它定位格子visited 用一维 key r * cols c 压缩坐标。searchTen 返回第一条和为 10 的路径hasAnyTen 做全盘扫描并负责判“无解”。每次递归复制 path 是为了方便直接返回整条路径深度不超过 10开销可接受。这段代码放进 utils/TenUtil.js 中用 module.exports 导出和界面组件彻底分开。2.4 消除后的下落与补位格子消掉后要下落补位常见错误是从上往下遍历列结果把已经下落的数字再搬一次。正确顺序是从底行往上处理遇到非空格就写到当前可写的行最后把列顶部的空位填新数字。// grid 是二维数组newTileValue 是生成新数字的函数 function applyFall(grid, rows, cols, newTileValue) { for (let c 0; c cols; c) { let writeRow rows - 1; for (let r rows - 1; r 0; r--) { if (grid[r][c] 0) { if (writeRow ! r) { grid[writeRow][c] grid[r][c]; grid[r][c] 0; } writeRow--; } } // 此时 writeRow 指向最后一个空位从这里往上全部补新数字 for (let r writeRow; r 0; r--) { grid[r][c] newTileValue(); } } }说明writeRow 是“下一个可写入的行号”从底部往上递减保证数字按原顺序从下往上堆。补位从 writeRow 往上填因为 writeRow 以上的格子要么是原空位、要么是被移走的空格。下落动画放后面做先保证数据正确界面只是数据的投影。3. 在 Cocos Creator 2.x 把源码跑起来版本核对、场景挂载与构建源码能改和能跑是两件事。这一章按“打开工程 → 看场景 → 构建验证”的顺序走一遍重点放在版本和挂载关系上这两个地方是源码包最常见的坑。3.1 先对版本2.4.x 与 2.0.x 的工程结构差异Cocos Creator 2.x 从 2.0 到 2.4 经历了多次资源格式调整。2.4.x 打开 2.2/2.3 的工程会提示升级升级后 .meta 文件保留资源 UUID 不变2.0 的工程则可能连首屏加载都过不去。先确认项目根目录里能暴露版本信息的文件# 项目根目录执行package.json 里 creator.version 是构建器使用的版本 cat package.json | sed -n 1,20p # 如果没有 package.json再看 settings 目录下的工程设置 ls settings/ cat settings/project.json 2/dev/null | head -n 20拿到版本后下载对应版本的 Cocos Creator 2.x 编辑器打开工程。规则是编辑器版本不低于源码构建版本2.4.x 尽量用同一个补丁号。打开后重点看 Console 有没有 Missing Script 报错有就说明脚本的 .meta 丢了或者脚本文件名改了。.meta 不能删删了 UUID 全变场景里的挂载关系全部断裂。3.2 场景里常见的节点层级与脚本挂载这类源码小游戏的结构只要健康场景树基本长这样Canvas 下挂 Board放格子底板和 BoardModel 脚本、UI 层放分数和步数 Label、OverPanel 是结算弹窗。看清楚挂载关系比看懂每个脚本更重要。GameRoot 负责把它们串起来// GameRoot.js —— 场景根控制器只做协调不存业务数据 const { hasAnyTen } require(TenUtil); cc.Class({ extends: cc.Component, properties: { boardModel: cc.Node, scoreLabel: cc.Label, stepLabel: cc.Label, }, start() { this.stepLeft 30; this.score 0; this.newGame(); }, newGame() { let model this.boardModel.getComponent(BoardModel); model.reset(); this.refresh(); }, refresh() { let model this.boardModel.getComponent(BoardModel); let group hasAnyTen(model.grid, model.rows, model.cols); this.highlightGroup(group); // 有解就高亮提示无解走洗牌逻辑 this.scoreLabel.string 分数 this.score; this.stepLabel.string 步数 this.stepLeft; }, });说明Cocos Creator 2.x 的 JS 脚本走 CommonJSTenUtil 和 GameRoot 放同一目录require 的路径相对脚本所在目录。getComponent(BoardModel) 用的是类名字符串脚本文件和类名对不上时返回 null报错点正好指向这一行。习惯做法是给 BoardModel 加一个静态单例避免到处 getComponent。3.3 构建 web 版验证逻辑再走 Android 包先用 web-mobile 构建目标验证逻辑最快浏览器 Console 能直接打断点。确认无误后切 Android 构建构建面板填包名、应用名、版本号构建产物是完整的 Android Studio gradle 工程。三种目标各有各的验证重点构建目标产物主要验证点web-mobile网页玩法逻辑、性能、存档读写androidAPK包名、SDK 回调、签名微信小游戏小游戏包平台 API 差异、分包体积3.3.1 Web 构建前必查三项第一首场景有没有在项目设置里指定构建后白屏多半是首场景没设置。第二resources 目录的资源有没有被动态加载代码引用2.x 里 resources 下的资源按路径加载路径写错运行期才报。第三浏览器预览时清一次缓存Creator 2.x 的资源缓存偶尔会让旧脚本残留表现为改了代码不生效。3.3.2 Android 构建的常见报错NDK 路径对不上是最高频的报错Cocos Creator 2.4.x 建议配 NDK r21 系列其次 gradle 第一次构建要下载依赖网络不好会卡在 Could not download。再就是包名构建面板里包名必须以字母开头不能带中文和连字符。构建完成后不要直接装 debug 包给测试签名不一致会导致覆盖安装失败。4. 把消消乐玩法调到能留人步数、目标分与动画节奏的参数表改源码最难的不是消除逻辑是数值。短局制游戏的留存很大程度取决于“每局步数刚好够、但差一点要动用道具”。这章给一套可以直接抄的参数表和对应代码。4.1 步数与目标分联动先定步数再定目标分。经验公式是目标分约等于单步平均得分乘步数再乘 0.7。单步平均得分取测试局的中位数别用理论最优值玩家不会每步都打出最大消除。关卡表关卡棋盘步数目标分数字池16×6251500休闲57×7283000普通108×8305000普通158×8358000困难实现上把关卡配置单独放一个数组运营要调难度时只改表不动代码// LevelConfig.js —— 关卡配置表运营直接改这份 const LEVELS [ { id: 1, rows: 6, cols: 6, steps: 25, target: 1500, pool: easy }, { id: 5, rows: 7, cols: 7, steps: 28, target: 3000, pool: normal }, { id: 10, rows: 8, cols: 8, steps: 30, target: 5000, pool: normal }, { id: 15, rows: 8, cols: 8, steps: 35, target: 8000, pool: hard }, ]; function getLevel(id) { return LEVELS.find(item item.id id) || LEVELS[0]; }pool 字段在 BoardModel 里映射到三套权重数组。步数和目标分一旦要做在线配置本地留一份默认值兜底网络请求失败也能开局。4.2 消除与下落的动画节奏消除动画太长拖节奏太短又没有爽感。给这个项目定的一套默认值消除时 0.12 秒内从 1 缩到 0.6 同时淡出下落按每格 0.08 到 0.12 秒逐列级联别把整列做成一条 tween 一起动。级联的关键是每颗棋子按目标行与当前行的差值乘固定系数得出 delay// fallMap: 节点 - { targetRow, delay }由数据层下落结果算出来 function playFall(node, cellSize, fallMap) { const move fallMap.get(node); if (!move) return; node.stopAllActions(); node.runAction(cc.sequence( cc.delayTime(move.delay), cc.moveTo(0.1, node.x, -move.targetRow * cellSize) )); }参数说明delay 取 0.02 乘以下落格数最多不超过 0.3 秒避免最上层棋子等太久moveTo 时长 0.1 秒按格子大小调整64 像素格子用 0.1 正合适。下落结束后停 0.1 秒再补位玩家才能看清补位后的棋盘。所有动画参数集中在一个配置对象里和关卡表放同一个文件方便回归测试。4.3 无解检测与洗牌兜底hasAnyTen 返回 null 就是无解。处理顺序是先重洗当前棋盘再检测还无解就重新生成整套数字仍无解就给保底分直接结算别让玩家卡死。function ensureSolvable(model) { for (let i 0; i 2; i) { if (hasAnyTen(model.grid, model.rows, model.cols)) return true; model.reshuffle(); // 打乱所有非空格子保留数字分布 } if (!hasAnyTen(model.grid, model.rows, model.cols)) { model.reset(); // 换一整套数字池 } return hasAnyTen(model.grid, model.rows, model.cols); }说明reshuffle 只重排现有数字不产生新值所以无解率由数字池决定两次重洗仍无解说明这一池的分布太差直接 reset。给无解触发次数计数连续触发三次以上的关卡说明数字池配错了回到 4.1 调池子而不是调代码。4.4 连击奖励与分数曲线分数曲线决定玩家追求“多消除”还是“快消除”。用消除格子数的平方做底数连击再乘倍率玩家会主动找大组而不是单点凑 10function settleScore(tileCount, combo) { const base 10 * tileCount * tileCount; const multiplier 1 0.25 * (combo - 1); return Math.round(base * multiplier); }参数说明tileCount 是这次消掉的格子数4 格是 160 分6 格是 360 分差距肉眼可见combo 表示连续消除次数1 秒内再次消除才累加超时重置为 1。combo 系数 0.25 偏保守想刺激连击可以调到 0.5但 4.1 的目标分公式也要同步系数否则后期关卡分数溢出。5. 可运营版本的收尾存档容错、广告位与 Android 打包验证源码能玩只是开始能运营要过三关存档不丢、广告位能换、包能装上。这章只讲最省事的做法。5.1 存档要带版本号和字段校验cc.sys.localStorage 在 2.x 全平台通用但旧版本存档字段缺失会直接炸掉新逻辑。读存档必须包 try-catch字段类型验一遍const SAVE_KEY sumten_slot1; function saveSnapshot(level, stepLeft, score) { const data { level, stepLeft, score, ts: Date.now() }; cc.sys.localStorage.setItem(SAVE_KEY, JSON.stringify(data)); } function loadSnapshot() { const raw cc.sys.localStorage.getItem(SAVE_KEY); if (!raw) return null; try { const data JSON.parse(raw); if (typeof data.level ! number || typeof data.stepLeft ! number) { cc.sys.localStorage.removeItem(SAVE_KEY); return null; } return data; } catch (e) { cc.sys.localStorage.removeItem(SAVE_KEY); return null; } }保存时把 key 写成带槽位和版本的形式如 sumten_slot1_v2后续加每日奖励、体力系统时不用迁移老数据直接换 key。5.2 广告位先做桥接层再谈 SDK可运营版本最常见的需求是看广告换步数。做法是在 Cocos 层包一层桥接玩法代码只调这层不直接依赖任何一家 SDK 的全局对象// AdBridge.js —— 原生广告的桥接层预览环境直接放行 cc.Class({ extends: cc.Component, statics: { showRewardVideo(onSuccess, onCancel) { if (cc.sys.platform cc.sys.ANDROID window.jsb) { // 示意代码AdUtil 是原生工程里自建的类包路径按实际结构填 jsb.reflection.callStaticMethod( org/cocos2dx/javascript/AdUtil, showRewardVideo, (Ljava/lang/String;Ljava/lang/String;)V, ad_unit_id, reward_callback ); } else { cc.warn(预览环境不弹广告直接回调奖励); onSuccess onSuccess(); } }, }, });注意Java 层的 ad_unit_id 和回调函数名要通过原生工程配置不同 SDK 的回调线程不同JS 层回调里不要再动节点动作用调度器把逻辑切回主线程再刷新界面。验证广告回调要装真机包浏览器预览直接走放行分支不会暴露原生层的时序问题。5.3 打包前对着这份清单过一遍检查项要求说明包名首字母开头的域名反写上线后不能改改了无法覆盖更新版本号三位段递增商店审核用 versionCode不能回退签名 keystore独立证书不进版本库用 debug 证书打 release 包会被应用市场打回minSdk21 以上2.4.x 默认模板的最小值建议保留构建产物android 目录的 gradle 工程直接在 Android Studio 里改原生代码装包后先用 adb 看日志确认 JS 层没报错再测存档和广告回调adb shell am start -n com.yourgame.sumten/org.cocos2dx.cpp.AppActivity adb logcat -s Cocos2dX:V | grep -Ei error|exception最后留一个验收顺序新装包跑通首局杀进程重开验证存档恢复切后台 30 秒回前台验证不黑屏广告位预览放行和真机各测一轮再把静默安装、飞行模式断网、弱网加载三个场景加进去。这个顺序跑完源码包能不能直接拿去运营基本就有数了。本文还有配套的精品资源点击获取