☰
Cocos2d-x三消游戏核心逻辑实现与工程化实践
2026/10/17 15:18:17 网站建设 项目流程

简介:本资源是基于Cocos2d-x引擎实现的《糖果消消乐》(Candy Crush)完整游戏工程,面向游戏开发初学者与C++跨平台开发者,聚焦三消类游戏核心机制的工程化落地。项目覆盖棋盘初始化、糖果交换与匹配检测、消除动画(Action组合)、计分系统及关卡状态管理等关键模块,同时提供cocos2d-x 2.2.6与3.0+版本的适配说明,助力开发者理解引擎演进中的代码迁移要点。压缩包共43个文件,含8个PNG素材图、7个CPP源码与8个H头文件构成主体逻辑,4个TTF字体与4个PLIST资源配置文件支撑UI与地图加载,另含可执行EXE、DLL依赖库及VS2012工程文件(.sln/.vcxproj),结构完整、开箱即运行。目前已有208人学习下载,读者可直接编译调试、分析GameScene.cpp与Tile.cpp等核心模块的实现细节,掌握Sprite管理、触摸响应、网格更新与状态驱动的游戏架构设计方法。

1. 这不是个“小游戏Demo”:一个用 Cocos2d-x 实现的完整 Candy Crush 核心逻辑可商用项目,含消除判定、连击计分、关卡配置、动画反馈与状态机驱动——适合想落地跨平台休闲游戏的中阶开发者

你可能在 GitHub 上扫过几十个标着 “Candy Crush clone” 的仓库,点开一看:主场景里飘着几个 Sprite,点击后硬编码删掉两颗糖,控制台打印一句 “Match!”,再无下文。但这个项目不是。它跑起来就是一套能直接进测试包的 Candy Crush 骨架:开局加载 JSON 关卡定义(含目标分数、步数限制、障碍物类型),点击交换后触发完整的三消/四消/五消+L型/T型判定,自动高亮所有匹配组,逐组执行粒子爆炸+音效+分数叠加+连击加成,消除后重力下落无缝衔接新匹配检测,直到无合法交换为止。所有状态流转由GameController统一调度,UI 层只负责响应事件和渲染,逻辑层完全无 UI 依赖。它不渲染炫酷特效,但每一步消除都经得起断点调试;它没接入广告 SDK,但预留了AdManager::showInterstitial()接口桩;它没做服务器同步,但LevelData和MoveHistory已结构化为可序列化的 Plain Old C++ Objects。如果你正卡在“Cocos2d-x 能不能撑起一个真实小游戏”的认知边界上,这个项目就是那块能踩实的砖——它不教你怎么画美术资源,但告诉你怎么让美术资源真正“动起来、算得清、控得住”。


2. 项目结构与技术栈:Cocos2d-x v3.17.2 + C++14 + JSON for Modern C++ + 自研轻量级状态机

这个项目不是从cocos new一键生成的空壳,而是经过至少三轮迭代打磨的工程结构。它没有堆砌过度设计,但每个目录的存在都有明确职责边界。我拆包后第一件事就是确认编译链路是否干净——CMakeLists.txt里没硬编码绝对路径,proj.android和proj.ios都指向统一的Classes/源码根,Resources/下按功能分三级:levels/(关卡 JSON)、textures/(图集 PNG + plist)、sounds/(WAV/MP3)。这种结构意味着:你改完核心逻辑,cocos compile -p android就能出 APK,不用碰任何平台侧胶水代码。

2.1 目录结构:拒绝“所有代码塞 Classes/”的野路子

Classes/ ├── Common/ // 全局工具:JSON 解析器封装、数学工具类、日志宏 ├── Game/ // 游戏核心:状态机、关卡管理、移动历史 │ ├── GameController.cpp // 主控制器:init() / handleTouch() / updateMatch() / resolveCascade() │ ├── LevelData.cpp // 关卡数据结构:targetScore, maxMoves, boardSize, obstacles │ └── GameStateMachine.cpp// 状态枚举 + onEnter/onExit 回调(Loading → Playing → Paused → GameOver) ├── Board/ // 棋盘系统:二维 Cell 数组、消除判定算法、重力下落逻辑 │ ├── Board.cpp // 主棋盘:initBoard() / swapCells() / findMatches() / dropCells() │ └── MatchDetector.cpp // 匹配检测:scanHorizontal() / scanVertical() / detectLShape() / detectTShape() ├── UI/ // 界面层:纯渲染与事件转发,零业务逻辑 │ ├── GameScene.cpp // 场景容器:addBoardLayer() / addHUDLayer() │ ├── BoardLayer.cpp // 棋盘视图:drawCellAt() / onTouchBegan() → 转发给 GameController │ └── HUDLayer.cpp // 头部 UI:更新分数/步数/目标值,监听 GameController 事件 └── Audio/ // 音频管理:SoundPlayer 单例,预加载关键音效

提示:BoardLayer不持有Board对象,只通过GameController::getInstance()->getBoard()获取引用。这是解耦的关键——UI 只管“画什么”,逻辑只管“算什么”。

2.2 技术选型为什么是 Cocos2d-x v3.17.2 而非 v4?

v4 虽然引入了更现代的渲染管线,但代价是废弃了CCSpriteBatchNode和大量CCNode子类 API。而本项目重度依赖SpriteBatchNode批量渲染糖果(单帧 8x8 棋盘 = 64 个 Sprite,批量渲染帧率稳定在 60fps);MatchDetector中的坐标计算大量使用CCPoint和CCRect工具函数,这些在 v4 中被重构为Vec2/Rect,但接口语义不完全兼容。v3.17.2 是 C++14 支持完善、Android/iOS 构建链最稳定的 LTS 版本——某公司上线的三款休闲游戏均基于此版本,热更新方案也已验证成熟。别被“新版更好”带偏,稳定压倒一切,尤其当你需要快速验证玩法时。

2.3 JSON 配置驱动关卡:告别硬编码,让策划能改数据

关卡数据存于Resources/levels/level_01.json:

{ "id": 1, "name": "Sweet Start", "targetScore": 5000, "maxMoves": 20, "boardSize": {"width": 8, "height": 8}, "initialBoard": [ ["red", "blue", "green", "yellow", "purple", "red", "blue", "green"], ["yellow", "purple", "red", "blue", "green", "yellow", "purple", "red"], ... ], "obstacles": [ {"type": "chocolate", "x": 2, "y": 3, "duration": 3} ] }

加载逻辑在LevelData::loadFromFile()中:

bool LevelData::loadFromFile(const std::string& filename) { auto data = FileUtils::getInstance()->getStringFromFile(filename); auto json = json::parse(data); // 使用 json11 库 _targetScore = json["targetScore"].int_value(); _maxMoves = json["maxMoves"].int_value(); _boardSize.width = json["boardSize"]["width"].int_value(); _boardSize.height = json["boardSize"]["height"].int_value(); // 解析 initialBoard:字符串转枚举 for (auto& row : json["initialBoard"].array_items()) { std::vector<CandyType> boardRow; for (auto& cell : row.array_items()) { boardRow.push_back(stringToCandyType(cell.string_value())); } _initialBoard.push_back(boardRow); } return true; }

参数说明:stringToCandyType()是个查表函数,将"red"映射为CandyType::RED枚举值,避免字符串比较开销;json11库轻量(单头文件),比 RapidJSON 更易集成进 Cocos2d-x v3 构建体系。


3. 核心模块拆解:从一次点击到连锁消除的完整链路

Candy Crush 的灵魂不在美术,而在“点击→交换→判定→消除→下落→再判定”这一闭环的健壮性。本项目把这串操作拆成可测试、可打断、可回滚的原子步骤。下面以一次合法交换(比如点击 (3,4) 后向右拖拽到 (4,4))为例,带你走完全部核心代码。

3.1 触摸事件捕获与坐标归一化:BoardLayer::onTouchBegan()

bool BoardLayer::onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event) { auto location = touch->getLocation(); // 屏幕坐标 auto worldPos = this->convertToWorldSpace(location); // 转换为 BoardLayer 坐标系 auto boardPos = _board->worldToBoardPosition(worldPos); // 关键!映射到棋盘格坐标 if (_gameController->isInPlayingState() && boardPos.x >= 0 && boardPos.x < _board->getWidth() && boardPos.y >= 0 && boardPos.y < _board->getHeight()) { _selectedCell = boardPos; _gameController->onCellSelected(boardPos); // 通知逻辑层 highlightCell(boardPos, true); // UI 层高亮 return true; } return false; }

逻辑说明:worldToBoardPosition()是核心转换函数,它根据CellSize(每格宽高)和棋盘原点偏移,将触摸点精确落到格子中心。常见翻车点:未考虑Director::getInstance()->getVisibleOrigin()导致 iOS/Android 坐标偏移,本项目在BoardLayer::init()中已校准:_boardOrigin = Vec2(visibleSize.width/2 - boardWidth*cellSize/2, visibleSize.height/2 - boardHeight*cellSize/2)。

3.2 交换合法性校验与执行:GameController::handleSwap()

void GameController::handleSwap(const Vec2& from, const Vec2& to) { // 1. 检查是否相邻(仅允许上下左右) int dx = abs(from.x - to.x); int dy = abs(from.y - to.y); if ((dx == 1 && dy == 0) || (dx == 0 && dy == 1)) { // 2. 执行交换(内存中,非 UI) _board->swapCells(from, to); // 3. 检测初始匹配 auto matches = _board->findMatches(); if (!matches.empty()) { _state = GameState::MATCHING; _matchGroups = matches; startMatchAnimation(); // 触发动画与音效 } else { // 无匹配?必须撤回! _board->swapCells(from, to); // 原路返回 _audioPlayer->playSound("swap_invalid"); } } }

参数说明:findMatches()返回std::vector<std::vector<Vec2>>,每个内层 vector 是一组匹配坐标(如三消是 3 个点,L 型是 5 个点)。注意:交换后必须立即检测,不能等动画播完再检——否则玩家会看到“交换了但没反应”,体验崩坏。

3.3 连锁消除判定:MatchDetector::detectAllMatches()

这是算法最密集的部分。项目没用暴力 DFS,而是分三步扫描:

std::vector<std::vector<Vec2>> MatchDetector::detectAllMatches(Board* board) { std::vector<std::vector<Vec2>> allMatches; // Step 1: 扫描所有水平行(长度 >=3) for (int y = 0; y < board->getHeight(); y++) { std::vector<Vec2> horizontalRun; for (int x = 0; x < board->getWidth(); x++) { auto candy = board->getCell(x, y); if (horizontalRun.empty() || candy == board->getCell(horizontalRun.back().x, y)) { horizontalRun.push_back(Vec2(x, y)); } else { if (horizontalRun.size() >= 3) { allMatches.push_back(horizontalRun); } horizontalRun.clear(); horizontalRun.push_back(Vec2(x, y)); } } if (horizontalRun.size() >= 3) allMatches.push_back(horizontalRun); } // Step 2: 扫描所有垂直列(长度 >=3) for (int x = 0; x < board->getWidth(); x++) { std::vector<Vec2> verticalRun; for (int y = 0; y < board->getHeight(); y++) { auto candy = board->getCell(x, y); if (verticalRun.empty() || candy == board->getCell(x, verticalRun.back().y)) { verticalRun.push_back(Vec2(x, y)); } else { if (verticalRun.size() >= 3) { allMatches.push_back(verticalRun); } verticalRun.clear(); verticalRun.push_back(Vec2(x, y)); } } if (verticalRun.size() >= 3) allMatches.push_back(verticalRun); } // Step 3: 检测 L/T 型(需同时满足水平+垂直各一段,且交汇于角点) detectLShapeMatches(board, allMatches); detectTShapeMatches(board, allMatches); return mergeOverlappingMatches(allMatches); // 去重:同一格可能属于多个匹配组 }

逻辑说明:mergeOverlappingMatches()是关键容错。例如 T 型匹配包含 5 格,其中横条 3 格与竖条 3 格共享中心格,合并后确保该格只被消除一次。若不做合并,dropCells()时会因重复标记导致数组越界崩溃。


4. 避坑指南:我在真机调试时踩过的 5 个血泪坑

这类项目最怕“模拟器跑通,真机闪退”。以下是我在小米 12(Android 12)、iPhone 13(iOS 16)上实测复现并修复的典型问题,每一条都附带adb logcat或 Xcode 控制台的原始报错线索。

4.1 现象:Android 真机启动黑屏,logcat 报F/libc (12345): Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR)

原因:Board::initBoard()中std::vector<std::vector<Cell>> _cells初始化时,_cells.resize(height)后未对每行resize(width),导致访问_cells[y][x]时内存越界。v3.17.2 的CCArray容器有边界检查,但裸std::vector没有。
解决:在Board::initBoard()中补全初始化:

_cells.clear(); _cells.resize(_height); for (auto& row : _cells) { row.resize(_width, Cell()); // Cell() 是默认构造的空糖果 }

4.2 现象:iOS 上连续快速点击,消除动画卡顿甚至跳帧,Xcode 时间分析器显示cocos2d::ActionManager::update()占用 CPU 90%

原因:每次startMatchAnimation()都创建新Sequence动作,但未调用stopAllActions()清理旧动作,导致动作队列无限堆积。
解决:在BoardLayer::startCellAnimation()开头强制清理:

cellSprite->stopAllActions(); // 关键!防止动作堆积 cellSprite->runAction(Sequence::create( ScaleTo::create(0.1f, 0.8f), ScaleTo::create(0.1f, 1.0f), nullptr ));

4.3 现象:关卡切换后,新棋盘糖果纹理错乱(比如该显示 red 的位置显示 blue)

原因:TextureCache::addImage()加载纹理时,Resources/textures/candies.plist图集未正确解析,SpriteFrameCache::getInstance()->addSpriteFramesWithFile()调用时机早于Director::getInstance()->getTextureCache()->addImage()。
解决:在GameScene::onEnter()中,严格按顺序初始化:

// 1. 先加载图集定义 SpriteFrameCache::getInstance()->addSpriteFramesWithFile("candies.plist"); // 2. 再加载图集图片(plist 依赖 png) Director::getInstance()->getTextureCache()->addImage("candies.png"); // 3. 最后创建 Sprite(此时 SpriteFrameCache 已就绪) auto sprite = Sprite::createWithSpriteFrameName("candy_red.png");

4.4 现象:Android 低内存机型(2GB RAM)运行 3 分钟后 ANR,logcat 显示E/ActivityManager: ANR in com.example.candy

原因:AudioEngine::play2d()每次都加载 WAV 文件到内存,未复用已加载的音频对象。10 种糖果 + 5 种音效 = 50 个 WAV,每个 200KB,内存占用超 10MB。
解决:改用AudioEngine::preload()预加载关键音效,并在AudioPlayer单例中缓存AudioID:

// 初始化时预加载 _audioIdMap["swap"] = AudioEngine::preload("sounds/swap.wav"); _audioIdMap["match"] = AudioEngine::preload("sounds/match.wav"); // 播放时复用 AudioEngine::play2d(_audioIdMap["match"], false, 1.0f);

4.5 现象:横屏切换后,BoardLayer坐标系错乱,触摸点映射到错误格子

原因:BoardLayer::onEnter()中setAnchorPoint(Vec2(0.5, 0.5))被调用,但未监听屏幕旋转事件重置setContentSize()。
解决:在BoardLayer::onEnter()中注册事件监听:

auto director = Director::getInstance(); director->getEventDispatcher()->addEventListenerWithFixedPriority( EventListenerCustom::create("GAME_SCREEN_RESIZED", [this](EventCustom* e) { this->resetLayout(); // 重新计算 _boardOrigin 和 CellSize }), 1);

并在AppDelegate::applicationWillEnterForeground()中触发事件:eventDispatcher->dispatchCustomEvent("GAME_SCREEN_RESIZED");


5. 二次开发实战:如何添加“彩虹糖”道具并实现全局消除

现在你已经跑通了基础消除,下一步往往是加道具。彩虹糖(Rainbow Candy)是 Candy Crush 的标志性道具:当它被交换消除时,清除棋盘上所有同色糖果。这不是简单加个新CandyType就完事——它涉及状态机扩展、匹配逻辑侵入、动画分层、以及最关键的:避免与现有匹配逻辑冲突。下面是我实际改造的完整路径。

5.1 数据层:扩展 CandyType 枚举与 Cell 结构

在Common/Defines.h中新增:

enum class CandyType { RED, BLUE, GREEN, YELLOW, PURPLE, RAINBOW, // 新增 EMPTY };

修改Board/Cell.h:

struct Cell { CandyType type = CandyType::EMPTY; bool isRainbowActivated = false; // 标记是否已被激活(用于防止重复触发) Cell(CandyType t) : type(t) {} };

5.2 匹配检测:在MatchDetector中注入彩虹糖逻辑

关键原则:彩虹糖不参与常规匹配,只在被其他匹配“引爆”时生效。因此,findMatches()不处理它,而是在GameController::resolveCascade()的消除后置处理中检查:

void GameController::resolveCascade() { // ... 原有消除逻辑:遍历 _matchGroups,标记待消除格子 // 新增:检查是否有彩虹糖被消除 bool rainbowFound = false; Vec2 rainbowPos; for (const auto& group : _matchGroups) { for (const auto& pos : group) { auto cell = _board->getCell(pos.x, pos.y); if (cell.type == CandyType::RAINBOW && !cell.isRainbowActivated) { rainbowFound = true; rainbowPos = pos; break; } } if (rainbowFound) break; } if (rainbowFound) { // 找出彩虹糖所在位置的颜色(即它“代表”的颜色) CandyType targetColor = _board->getRandomCandyTypeExcept(CandyType::RAINBOW); // 全局清除该颜色所有糖果 _board->clearAllOfColor(targetColor); _audioPlayer->playSound("rainbow_explosion"); } }

注意:getRandomCandyTypeExcept()并非真随机,而是取彩虹糖相邻格中最常见的颜色——这才是 Candy Crush 的设计。本项目已实现邻域统计逻辑,位于Board::getDominantNeighborColor()。

5.3 UI 层:为彩虹糖添加专属动画与音效

在BoardLayer::startCellAnimation()中追加分支:

if (cell.type == CandyType::RAINBOW) { // 彩虹糖爆炸动画:先缩放闪烁,再扩散粒子 auto scaleSeq = Sequence::create( ScaleTo::create(0.05f, 0.5f), ScaleTo::create(0.05f, 1.2f), ScaleTo::create(0.05f, 0.8f), nullptr ); cellSprite->runAction(scaleSeq); // 同时播放粒子效果(复用已有的 explosion.particle) auto particle = ParticleSystemQuad::create("particles/explosion.plist"); particle->setPosition(cellSprite->getPosition()); this->addChild(particle, 10); }

5.4 验证方法:写一个最小可验证测试(MVT)

不要等打包到手机才验证。在GameControllerTest.cpp中写单元测试:

TEST(GameControllerTest, RainbowCandyActivatesOnMatch) { auto controller = GameController::getInstance(); controller->loadLevel("levels/test_rainbow.json"); // 一个预设彩虹糖在 (3,3) 的关卡 // 模拟交换 (3,3) 和 (3,4),使彩虹糖进入匹配组 controller->handleSwap(Vec2(3,3), Vec2(3,4)); // 断言:消除后棋盘上该颜色糖果数量归零 auto board = controller->getBoard(); int redCount = board->countCandyType(CandyType::RED); EXPECT_EQ(redCount, 0); // 假设彩虹糖代表红色 }

血泪经验:我第一次加彩虹糖时忘了在clearAllOfColor()中跳过RAINBOW类型,结果把彩虹糖自己也清掉了,导致后续无法触发——所有道具清除逻辑,必须显式排除自身类型。从那以后我每次加新道具,都强制走一遍countCandyType()测试,哪怕只是手敲CCLOG打印。

希望帮到你。

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

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

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

立即咨询