☰
cocos2d-x三消游戏开发实战:从棋盘状态机到连锁结算的完整实现
2026/10/29 20:14:04 网站建设 项目流程

简介:这份资源是面向 cocos2d-x 初学者与三消游戏开发爱好者的 Candy Crush 实战工程,基于 2.2.6 版本实现基础版糖果消除玩法,并兼顾 3.0 及以上版本的适配思路,可用于学习棋盘布局、相邻糖果交换、匹配消除判定、计分与关卡状态管理等核心机制。压缩包共 43 个文件,约 3.46MB,以 cpp 与 h 源码为主体,配合 png 贴图、plist 配置、ttf 字体及 dll 运行库,另有 sln 与 vcxproj 工程文件,可直接在 Windows 下打开编译调试。已有 210 人学习,适合希望从可运行工程入手理解三消游戏架构、并在此基础上扩展特殊糖果、道具系统或关卡编辑器的开发者参考。

1. 从一份“能跑起来的三消”说起:cocos2d-x 做 candy crush 到底要交付什么

三消游戏看起来简单,真动手做才发现坑不少:棋盘怎么初始化才保证有解、消除后怎么补位、连锁反应怎么算分、特殊糖果怎么触发、动画和逻辑怎么解耦。用 cocos2d-x 写一个 candy crush 类的三消 Demo,核心不是把界面画出来,而是把“棋盘状态机 + 匹配检测 + 消除补位 + 连锁结算”这条链路跑通。这套方案面向两类人:一是要做课程设计或毕设、需要一个完整可演示游戏原型的学生;二是想快速搭一个三消玩法骨架、后续再换皮做产品的开发者。下面按需求、设计、实现、验收的顺序,把这份 cocos2d-x 三消方案拆开讲清楚。

2. 需求与玩法边界:先定死规则,再谈代码

2.1 三消的核心规则清单

动手写代码之前,先把规则写成一张能对照验收的清单。candy crush 类玩法的规则看着多,真正影响代码结构的其实就下面这些:

规则项具体定义对代码的影响
棋盘尺寸8×8 网格,格子边长固定决定坐标换算与容器布局
糖果种类6 种基础颜色决定随机池与匹配判定
交换方式相邻格子交换,非法交换回弹需要交换合法性预判
匹配判定横或竖连续 3 个及以上同色核心检测算法
消除补位上方糖果下落,顶部补新糖果需要下落动画与状态同步
连锁结算补位后再次匹配继续消除需要循环结算直到稳定
特殊糖果4 连生成条纹、5 连生成炸弹匹配结果需要分类处理
计分基础分 × 连锁倍数结算逻辑与 UI 解耦

这张表的价值在于:每一条都能对应到后面一个模块或一个函数。规则没定死就开写,最常见的翻车是做到一半发现“特殊糖果的生成条件”和“匹配检测”耦合在一起,改一处崩一片。我一般会先把这张表跟需求方(或者导师)对一遍,确认没有歧义再进设计。

2.2 为什么选 cocos2d-x 而不是别的引擎

选型这件事,三消 Demo 用 cocos2d-x 是合理的:它是 C++ 为主的 2D 引擎,节点树、动作系统、精灵批处理这些能力对网格类游戏很友好,动作系统(Action)天然适合做交换、下落、消除的缓动动画,不用自己写补间。相比纯手写 OpenGL 或者用更重的 3D 引擎,cocos2d-x 在 2D 网格游戏上的开发效率明显更高,而且跨平台发布成熟,课程设计里“能打包出可执行文件演示”这一点很加分。

需要提前想清楚的是版本与语言绑定。cocos2d-x 有 C++ 原生接口,也有 Lua、JS 绑定。三消这种逻辑密集、性能要求不极端的游戏,用 C++ 写核心逻辑最直接,调试也方便;如果团队更熟脚本,Lua 绑定也能做,但要注意绑定层的对象生命周期管理,这是后面避坑章节要展开的点。选型结论:核心棋盘逻辑用 C++ 类封装,表现层用 cocos2d-x 的 Node/Sprite/Action,两者通过一个薄薄的控制器衔接。

2.3 交付物与验收口径

一份能交差的三消方案,交付物通常包括:可运行的工程(含资源)、棋盘与结算的核心类、一份规则说明、以及演示脚本(怎么操作能展示连锁和特殊糖果)。验收口径建议定成三条硬指标:任意一次合法交换后棋盘必然收敛到稳定态(无残留可消除组合);连锁结算的分数与消除数量对得上;特殊糖果在满足条件时必定生成且触发效果正确。这三条能覆盖大部分演示场景,也是答辩时最容易被追问的地方。

3. 总体架构设计:棋盘、结算、表现三层怎么切

3.1 三层结构与职责边界

整个工程我一般切成三层,边界清晰了后面才好改:

  • 数据层(BoardModel):只存棋盘状态,一个二维数组,每个元素记录糖果类型、是否特殊、特殊类型。不碰任何节点、不碰动画。
  • 逻辑层(MatchSolver / GameLogic):负责匹配检测、消除标记、下落补位、连锁循环、计分。输入是 BoardModel,输出是一串“操作指令”(消除哪些格、哪些格下落到哪、补哪些新糖果)。
  • 表现层(BoardView / Controller):把逻辑层产出的指令翻译成 cocos2d-x 的节点动作,播放动画,动画结束后回调逻辑层推进下一步。

这样切的好处是:逻辑层可以脱离引擎单独跑单元测试,棋盘收敛性、计分正确性都能在无渲染环境下验证;表现层只关心“怎么演”,换皮、换动画不影响规则。很多三消 Demo 翻车就翻在把匹配检测写进了触摸回调里,动画和逻辑搅在一起,连锁一多就状态错乱。

3.2 棋盘数据结构与坐标换算

棋盘用一维数组还是二维数组,是个小决策但影响手感。我倾向用一维std::vector<Cell>配index = row * cols + col,缓存友好,遍历快;对外仍提供at(row, col)的访问接口,读起来不别扭。每个 Cell 至少要有这些字段:

// 棋盘单元格定义:只描述数据,不持有任何渲染节点 struct Cell { int type = -1; // 糖果颜色类型,-1 表示空格 int special = 0; // 0 普通,1 条纹,2 炸弹 bool marked = false; // 本轮是否被标记消除 }; class BoardModel { public: static const int ROWS = 8; static const int COLS = 8; Cell& at(int r, int c) { return cells_[r * COLS + c]; } bool inRange(int r, int c) const { return r >= 0 && r < ROWS && c >= 0 && c < COLS; } private: std::vector<Cell> cells_ = std::vector<Cell>(ROWS * COLS); };

type用整数而不是枚举字符串,是为了随机生成和匹配比较都走整数,快且省事;special单独一个字段而不是塞进 type,是为了让“颜色匹配”和“特殊效果”两套判定互不干扰——条纹糖果仍然按颜色参与匹配,但消除时额外触发整行或整列。marked是每轮结算的临时标记,结算完统一清掉,避免状态残留。坐标换算统一走at(),表现层拿格子中心点时再用(col + 0.5) * tileSize换算,两边不各算各的,减少对不齐的玄学问题。

3.3 状态机:空闲、交换、结算、补位

三消最容易乱的地方是“玩家在动画播放中又点了一下”。解决办法是给游戏主循环一个显式状态机,触摸输入只在Idle状态被接受:

enum class GameState { Idle, // 等待玩家操作 Swapping, // 交换动画播放中 Resolving, // 匹配消除结算中 Falling, // 下落补位动画中 GameOver };

状态迁移规则:Idle收到合法交换 →Swapping;交换动画结束且形成匹配 →Resolving;消除动画结束 →Falling;下落结束再检测,有匹配回Resolving,无匹配回Idle。非法交换则播放回弹动画后直接回Idle。这个状态机是后面所有动画回调的“总闸”,每个回调进来先看当前状态对不对,不对就丢弃,能挡掉一大半时序 bug。

4. 核心玩法实现:匹配检测、消除补位与连锁结算

4.1 匹配检测算法:横竖扫描与结果分类

匹配检测是整套逻辑的心脏。做法不复杂:对每一行做一次连续同色扫描,对每一列再做一次,长度 ≥3 的段落标记出来。难点在“结果分类”——要区分 3 连、4 连、5 连,因为不同长度生成不同特殊糖果。我一般让检测函数返回一个结构化的结果,而不是直接改棋盘:

struct MatchGroup { std::vector<std::pair<int,int>> cells; // 参与匹配的格子 int length; // 连续长度 bool horizontal; // 横向还是纵向 }; // 扫描单行,收集所有长度>=3的连续同色段 std::vector<MatchGroup> scanLine(const BoardModel& b, int row, bool horizontal) { std::vector<MatchGroup> groups; int runStart = 0; for (int i = 1; i <= BoardModel::COLS; ++i) { int prevType = horizontal ? b.at(row, i-1).type : b.at(i-1, row).type; int curType = (i < BoardModel::COLS) ? (horizontal ? b.at(row, i).type : b.at(i, row).type) : -2; // 哨兵,强制收尾 if (curType != prevType) { int len = i - runStart; if (prevType >= 0 && len >= 3) { MatchGroup g; g.length = len; g.horizontal = horizontal; for (int k = runStart; k < i; ++k) g.cells.push_back(horizontal ? std::make_pair(row, k) : std::make_pair(k, row)); groups.push_back(g); } runStart = i; } } return groups; }

这里用哨兵值-2强制在行尾收尾,省掉循环外单独处理最后一段的重复代码,是个小技巧。prevType >= 0的判断是为了跳过空格(type 为 -1),否则空格连成一片会被误判成匹配。横竖两次扫描的结果要合并去重——一个格子可能同时属于横向和纵向的匹配组(十字交叉),去重后再统一标记消除,否则同一个格子会被结算两次,分数直接翻倍,这是很典型的翻车点。

4.2 消除与补位:下落逻辑与顶部补充

消除之后棋盘会出现空洞,补位分两步:先让每列的空格上方的糖果下落,再在顶部补新糖果。下落逻辑按列处理最直观:

// 对每一列做压缩:非空格子下沉,记录每个格子的下落距离 void collapseColumn(BoardModel& b, int col, std::vector<FallStep>& steps) { int writeRow = BoardModel::ROWS - 1; // 从底部往上填 for (int r = BoardModel::ROWS - 1; r >= 0; --r) { if (b.at(r, col).type >= 0) { if (r != writeRow) { b.at(writeRow, col) = b.at(r, col); b.at(r, col) = Cell{}; // 清空原位置 steps.push_back({r, writeRow, col}); // 记录下落,供表现层播动画 } --writeRow; } } // 剩余顶部空位补新糖果 for (int r = writeRow; r >= 0; --r) { b.at(r, col).type = randomType(); steps.push_back({-1, r, col}); // -1 表示新生成 } }

关键设计是FallStep这个中间产物:逻辑层只负责算出“哪个格子落到哪”,表现层拿着这份清单去播下落动画,播完再回调。逻辑和动画彻底解耦,动画慢一点快一点都不影响棋盘数据的正确性。writeRow从底往上走,保证同一列里下方的糖果先落位,不会出现“上面的糖果穿过下面的糖果”这种视觉穿模。新糖果的type从随机池取,但要注意:补位后如果立刻又形成匹配,属于正常连锁,不要试图在补位时“避免匹配”,那会让连锁玩法失效。

4.3 连锁结算循环与计分

连锁结算是把“检测 → 消除 → 补位”包成一个循环,直到棋盘稳定。计分用连锁倍数递增,第 n 次连锁的得分乘 n:

int chain = 0; while (true) { auto groups = findAllMatches(board); // 横竖扫描 + 去重 if (groups.empty()) break; // 稳定,退出 ++chain; int cleared = markAndClear(board, groups); // 标记并清空,返回消除数 int score = cleared * BASE_SCORE * chain; // 连锁倍数 totalScore += score; collapseAll(board, fallSteps); // 补位,产出动画清单 // 此处把 fallSteps 交给表现层播动画,动画结束后继续下一轮 }

循环的退出条件只有一个:findAllMatches返回空。这里有个必须注意的点——markAndClear要在collapseAll之前完成,且清空后type置 -1,否则下一轮扫描会把已消除的格子当成有效糖果。chain每轮自增,但只在“本轮确实有消除”时自增,空轮不计数。计分公式里BASE_SCORE建议做成可配置常量,方便调平衡;cleared * BASE_SCORE * chain这种乘法结构简单直观,答辩时也好解释。特殊糖果的触发可以在这个循环里扩展:如果被消除的格子里有特殊糖果,额外把整行或整列加入消除集合,再走一遍标记,形成“特殊糖果引爆”的二次结算。

4.4 特殊糖果的生成与触发

特殊糖果的规则要和匹配结果分类挂钩:4 连生成条纹糖果(消除整行或整列),5 连生成炸弹(消除周围一圈或同色全部)。生成时机是在markAndClear里,对每个MatchGroup判断长度,选组内一个格子(通常是交换的那个格子,或组中心)升级为特殊糖果,而不是直接清空。触发时机是在消除阶段,如果被标记消除的格子special != 0,就把它的效果范围并入消除集合。

这里的设计要点是“特殊糖果不参与普通匹配的颜色判定之外,还要能被引爆”。实现上给markAndClear加一个待处理队列:先把普通匹配的格子入队,出队时若发现是特殊糖果,把其影响范围再入队,直到队列空。这样条纹引爆条纹、炸弹引爆炸弹的链式反应天然支持,不用写嵌套递归。表现层对应地给特殊糖果加一个高亮或缩放动画,让玩家能看出“这个格子不一样”,否则演示时观众根本注意不到特殊糖果的存在。

5. 避坑与排查:三消 Demo 最容易翻车的五个点

5.1 棋盘初始就有匹配,一进游戏自动消一片

现象:游戏刚加载,还没操作,棋盘自己开始消除、加分。原因:初始化时纯随机填格子,没做“初始无匹配”约束。解决:生成每个格子时,检查它左边两个和上边两个是否同色,同色就重摇,保证初始棋盘稳定。这个检查很便宜,初始化时做一次即可。

5.2 交换后没有匹配却不回弹,棋盘卡死

现象:玩家交换两个不相邻同色糖果,没形成三连,但两个糖果停在交换后的位置不动了。原因:交换逻辑只处理了“形成匹配”的分支,漏了“无匹配则回退”的分支,或者回退动画的回调没把数据层换回去。解决:交换前先做一次“假设交换后是否形成匹配”的预判,不形成就直接播回弹动画、数据层不动;形成才真正交换。预判函数复用findAllMatches即可,成本很低。

5.3 连锁时分数翻倍或漏算

现象:一次消除分数明显偏高,或者连锁第二轮的分数没加上。原因:横竖扫描结果没去重,同一格子被两个MatchGroup各算一次;或者chain在空轮也自增,导致倍数错位。解决:合并匹配结果时用std::set按格子索引去重;chain只在cleared > 0时自增。这两个都是逻辑层单测能覆盖的,建议写个无渲染的测试用例专门跑连锁计分。

5.4 动画播放中重复点击导致状态错乱

现象:快速连点,糖果位置和数据显示对不上,甚至崩溃。原因:触摸回调没有状态门禁,动画未结束就接受新输入,两个结算流程并发改同一个棋盘。解决:所有触摸入口先判断state == Idle,非空闲直接 return;动画结束的回调里再推进状态机。这是第 3 章状态机的直接收益,别省这一步。

5.5 特殊糖果触发后棋盘不收敛

现象:特殊糖果引爆后,补位又生成新的特殊糖果,循环停不下来或卡在某一轮。原因:特殊糖果的生成条件在连锁中被反复满足,或者引爆范围计算有误导致同一格子反复入队。解决:给待处理队列加“已处理”标记,同一格子一轮内只处理一次;同时给连锁轮数设一个上限(比如 20 轮)作为兜底,超过就强制收敛并打日志,方便排查是规则问题还是死循环。

6. 验收与答辩:怎么证明这套三消真的能跑

验收这件事,别等到最后一天才想。我一般会在开发中期就搭一个“可复现演示脚本”,答辩时照着走一遍,比临场发挥稳得多。具体做法是准备三组固定操作序列:第一组演示基础三连消除和补位;第二组演示四连生成条纹糖果并触发整行消除;第三组演示连续两次连锁,展示倍数计分。每组操作前先把棋盘状态截图存档,操作后对照预期结果,这样评委问“你怎么保证连锁算对了”,你直接翻出前后对比。

逻辑正确性建议用无渲染单测兜底。把BoardModel和MatchSolver抽成不依赖 cocos2d-x 的纯 C++ 类,用固定棋盘输入跑断言:

// 固定棋盘:构造一个已知会触发一次三连的布局 BoardModel b = makeBoardFromString({ "AAB...", // 省略号代表其余格子,测试时补全 // ... }); auto groups = findAllMatches(b); assert(groups.size() == 1); // 只应检测到一组匹配 int cleared = markAndClear(b, groups); assert(cleared == 3); // 恰好消除 3 个

这类断言覆盖“匹配数量、消除数量、连锁轮数、总分”四个维度,跑一遍几秒钟,比手动点半天可靠。答辩时如果被问到边界情况,直接说“空格不参与匹配、十字交叉去重、连锁有轮数上限”这三条,基本能接住。

还有一个容易被忽略的验收点:性能。8×8 棋盘的全盘扫描是 O(rows×cols),连锁最多几十轮,单帧开销很小,但如果每轮都重建节点、不回收精灵,长时间演示会掉帧。做法是糖果节点池化复用,消除时回收、补位时取出,避免频繁new/delete。这个优化不复杂,但演示跑十分钟不卡,观感差别很明显。

血泪经验是:三消的 bug 大多不在算法本身,而在“逻辑和动画的时序”上。从那以后我每次做这类网格游戏,都强制先把状态机和纯逻辑层写完、单测跑绿,再接表现层动画,顺序反了就是给自己埋雷。希望这套拆解能帮到你,照着这个结构把棋盘、结算、表现三层搭起来,一份能演示、能讲清楚、能扛住追问的三消方案就成型了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询