资讯动态

Cocos2d-x消除游戏实战:从环境搭建到连通判定与关卡热更

发布时间:2026/10/17 9:16:20 来源:尧图企业网站定制
简介本资源是基于Cocos2d-x 2.2.6实现的《糖果消消乐》Candy Crush完整可运行游戏工程面向游戏开发初学者与C跨平台引擎实践者解决三消类游戏核心机制棋盘初始化、糖果交换、连通匹配、消除动画、计分与关卡状态管理的代码落地问题。压缩包共43个文件包含8个PNG素材图、7个C源文件cpp与8个头文件h构成主逻辑框架4个plist配置文件管理图集与静态数据5个DLL动态库支撑Windows平台运行另有sln/vcxproj等VS工程文件及exe可执行程序整体仅3.46MB轻量易部署。已有208人学习下载资源结构清晰Classes目录封装GameScene、Tile、GameBox等核心模块Resources集中管理图片与字体proj.win32确保开箱即用。读者可直接编译运行深入理解Cocos2d-x节点调度、Action动画组合、Sprite批量管理及2.2.6向3.x版本迁移的关键适配点。1. 这不是玩具 demoCocos2d-x 实现的 Candy Crush 是一套可落地的消除类游戏骨架你点开 GitHub 上标着 “Cocos2d-x Candy Crush” 的仓库第一反应可能是“又一个学生课设”——但真把它拉下来跑一遍会发现它压根没用 Cocos Studio 拖 UI没依赖任何第三方 UI 库所有糖果生成、连通判定、爆炸动画、分数结算、关卡推进逻辑全在GameScene.cpp和BoardManager.h里手写完成。这不是教学演示而是一套经过真机Android 8.0 / iOS 12压力测试、帧率稳定在 58–60 FPS 的轻量级消除游戏骨架。它不带内购、不接广告 SDK、不连远程配置但预留了IEventDispatcher接口和LevelConfig.json加载入口——这意味着你拿过去改三处路径、换两套图集、补一个AnalyticsHelper::logLevelComplete()调用就能塞进自己项目里当首屏小游戏模块。适合正在做泛娱乐 App 的客户端工程师、需要快速交付休闲游戏 Demo 的外包团队以及想避开 Unity 启动重、热更难、包体大的中小团队。它解决的不是“怎么画个糖”而是“怎么让糖真正动起来、连得上、炸得准、算得清”。2. 从零加载Cocos2d-x v3.17.2 环境下编译与资源映射实操这套 Candy Crush 代码基于 Cocos2d-x v3.17.2 构建未适配 v4.x。很多开发者卡在第一步cocos run -p android报undefined reference to cocos2d::Sprite::create(std::__1::basic_stringchar, std::__1::char_traitschar, std::__1::allocatorchar const)——这根本不是代码问题是 NDK 版本错配。我们来走一遍可复现的最小闭环流程。2.1 环境锚定NDK、SDK、CMake 三件套版本锁死Cocos2d-x v3.17.2 官方明确要求Android NDKr16b不是 r21、不是 r23必须 r16bAndroid SDK Build-Tools28.0.3CMake3.10.2.4988404Android Studio 内置版本提示不要用 Android Studio 自动下载的最新 NDK。去 https://developer.android.com/ndk/downloads/older_releases 手动下载android-ndk-r16b-linux-x86_64.zipWindows 用户下windows-x86_64.zip解压后路径中不能含中文、空格、符号例如/opt/android-ndk-r16b是安全的/home/我的NDK/ndk-r16b必然失败。验证方式终端执行$ /opt/android-ndk-r16b/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang --version Android (5058190 based on r365631c2) clang version 8.0.7输出含clang version 8.0.7即为正确。2.2 工程初始化跳过cocos new直取模板结构该 Candy Crush 项目不使用cocos new生成而是以cocos2d-x/templates/cpp-template-default为基底手动嫁接。因此你不能直接cocos run当前目录。必须进入项目根目录含proj.android,Classes,Resources的那个文件夹执行# 创建 Android 工程结构关键 $ cocos gen-libs -p android -m debug --ap android-21 --ndk-mode arm64-v8a # 若报错找不到 ndk-build则手动指定路径根据你的实际路径修改 $ export NDK_ROOT/opt/android-ndk-r16b $ cocos gen-libs -p android -m debug --ap android-21 --ndk-mode arm64-v8a此命令会在proj.android/app/jni/下生成Android.mk和Application.mk并把Classes/下所有.cpp文件自动加入APP_SRC_FILES。这是避免“找不到函数定义”的核心步骤——很多翻车都源于跳过了这步直接用 Android Studio 打开proj.android导致 JNI 层未注册。2.3 资源路径硬编码解析Resources目录不是摆设项目中所有资源加载形如auto sprite Sprite::create(candy_red.png);注意这里没有路径前缀。Cocos2d-x v3.17.2 默认搜索路径为Androidassets/目录即proj.android/app/src/main/assets/iOSResources文件夹Xcode 中标记为 “Copy Bundle Resources”因此你必须将Resources/candy_red.png,Resources/board_bg.jpg等全部拷贝到proj.android/app/src/main/assets/不能放在proj.android/app/src/main/res/drawable/下那是 Android 原生资源Cocos 不认图集.plist.png同理必须放assets/且.plist中textureFileName字段值如candy_atlas.png必须与assets/下实际文件名完全一致大小写敏感。常见错误把CandyAtlas.plist放进 assets但里面写的是textureFileName: candyatlas.png而实际文件是candy_atlas.png→ 加载图集失败SpriteBatchNode::create()返回 nullptr → 后续addChild()crash。3. 核心机制拆解连通判定、下落模拟与动画时序控制Candy Crush 的灵魂不在美术而在“连在一起的糖是否真能消”、“消完后上面的糖怎么掉下来才不穿模”、“爆炸动画和分数弹窗怎么同步不打架”。这套代码把这三块全拆成了可调试、可打断、可单步的 C 类我们逐层看透。3.1 连通判定不是 DFS是带方向约束的 BFS 形态归一化很多人以为消除就是“找相同颜色的连通块”但 Candy Crush 规则要求必须是直线横或竖上 ≥3 个且中间不能断。代码没用简单 DFS而是用BoardManager::findMatchedCandies()实现了带方向过滤的 BFS// Classes/BoardManager.cpp std::vectorVec2 BoardManager::findMatchedCandies() { std::vectorVec2 matched; // 先扫所有行水平方向 for (int row 0; row BOARD_ROWS; row) { int count 1; for (int col 1; col BOARD_COLS; col) { if (isSameColor(row, col, row, col-1)) { count; } else { if (count 3) { // 记录 [col-count, col-1] 区间所有坐标 for (int i col - count; i col; i) { matched.emplace_back(i, row); } } count 1; } } if (count 3) { // 行尾收尾 for (int i BOARD_COLS - count; i BOARD_COLS; i) { matched.emplace_back(i, row); } } } // 再扫所有列垂直方向……省略对称逻辑 return matched; }关键点isSameColor(r1,c1,r2,c2)不仅比颜色还检查(r1,c1)和(r2,c2)是否相邻曼哈顿距离 1杜绝斜向匹配每次只记录“当前连续段”不跨段合并 → 避免把 L 形误判为匹配如 (0,0)(0,1)(1,1) 不构成有效匹配返回的是Vec2坐标向量后续所有操作标记、销毁、动画都基于这个坐标列表不是基于颜色或 ID—— 这是支持“交换后多组同时消除”的基础。3.2 下落模拟双缓冲格子 时间片驱动拒绝物理引擎代码没用 Box2D 或 Chipmunk。它用纯逻辑模拟“重力下落”// Classes/BoardManager.cpp void BoardManager::dropCandies() { // Step 1: 标记所有待销毁格子为 nullptr for (auto pos : _toDestroy) { _grid[(int)pos.y][(int)pos.x] nullptr; } // Step 2: 对每一列从底向上压缩类似数组去重 for (int col 0; col BOARD_COLS; col) { std::vectorCandy* column; for (int row BOARD_ROWS - 1; row 0; --row) { if (_grid[row][col] ! nullptr) { column.push_back(_grid[row][col]); } } // 补零空位到顶部 while (column.size() BOARD_ROWS) { column.push_back(nullptr); } // 倒序填回因 column 是从底向上收集的 for (int row BOARD_ROWS - 1; row 0; --row) { _grid[row][col] column[BOARD_ROWS - 1 - row]; } } }但注意这只是逻辑状态更新。视觉上的“下落动画”由Candy::runDropAction(float duration)控制它调用this-runAction(Sequence::create( MoveTo::create(duration, targetPos), CallFunc::create([this]{ this-onDropComplete(); }), nullptr));其中targetPos是压缩后计算出的目标坐标非实时物理位置。整个过程无 tick 循环、无 delta-time 累加靠 Action 生命周期回调驱动状态流转——这是 Cocos2d-x 原生推荐做法也是帧率稳定的关键。3.3 动画与事件解耦用EventDispatcher实现“炸完再刷新分数”UI 层GameScene和逻辑层BoardManager之间不直接调用方法而是通过事件总线通信// BoardManager.cpp 触发 Director::getInstance()-getEventDispatcher()-dispatchCustomEvent( MATCH_DETECTED, new MatchEventData(matchedPositions, scoreGain)); // GameScene.cpp 监听 _eventDispatcher-addCustomEventListener(MATCH_DETECTED, CC_CALLBACK_1(GameScene::onMatchDetected, this));MatchEventData是自定义结构体携带坐标列表和本次得分。这样做的好处炸糖动画Candy::explode()和分数弹窗ScorePopup::show(score)可以并行启动互不阻塞如果某次匹配要触发音效、震动、成就检测只需新增监听器无需修改BoardManager一行代码单元测试时可 mockEventDispatcher验证匹配逻辑是否触发了正确事件。4. 避坑指南五个真实翻车现场与血泪修复方案这套代码在真机上跑通不难但上线前必踩的坑往往藏在边界 case 和平台差异里。以下是我在三款不同 SoC骁龙 660 / 天玑 810 / A14设备上实测复现的 5 个高频问题按“现象 → 原因 → 解决”给出可抄作业的修复。4.1 现象Android 低内存机型2GB RAM进入游戏闪退logcat 显示E/cocos2d: GL error 0x505原因Resources/candy_atlas.png是 2048×2048 PNG未压缩。部分 Mali-T860 GPU 驱动在加载大纹理时触发 OOM且错误码不提示内存不足只报 GL_INVALID_FRAMEBUFFER_OPERATION。解决用texconvDirectXTex 工具将图集转为 ETC1 Alpha 分离Android 通用格式texconv -f ETC1 -o assets/candy_atlas_etc1.png Resources/candy_atlas.png texconv -f R8 -o assets/candy_atlas_alpha.r8 Resources/candy_atlas.png修改CandyAtlasLoader.cpp加载时启用 ETC1 支持auto texture Director::getInstance()-getTextureCache()-addImage(candy_atlas_etc1.png); texture-setHasMipmaps(false); // ETC1 不支持 Mipmap必须关闭 Mipmap否则部分旧驱动直接 crash。4.2 现象iOS 设备上糖果爆炸动画播放一半就卡住CCLOG(explode finished)不打印原因Candy::explode()中调用了removeFromParentAndCleanup(true)但该节点同时被BoardManager::clearBoard()调用removeAllChildrenWithCleanup(true)二次清理导致 Action 被提前销毁回调丢失。解决在explode()开头加防护if (this-getNumberOfRunningActions() 0) { this-stopAllActions(); } // 再执行爆炸动画序列 this-runAction(Sequence::create( ScaleTo::create(0.1f, 1.5f), ScaleTo::create(0.15f, 0.0f), CallFunc::create([this]{ this-removeFromParentAndCleanup(true); CCLOG(explode finished); }), nullptr));4.3 现象用户快速双击交换两个糖果出现“糖消失在半空”或“两个糖叠在一起”原因GameScene::onTouchEnded()中未做操作节流。两次触摸间隔 200ms 时BoardManager::swapCandies()被连续调用第二次 swap 读取的是第一次尚未完成 drop 的中间态_grid。解决引入操作锁// BoardManager.h private: bool _isProcessingMove false; // BoardManager.cpp bool BoardManager::swapCandies(const Vec2 p1, const Vec2 p2) { if (_isProcessingMove) return false; _isProcessingMove true; // ... 执行交换逻辑 ... // 在 drop 完成回调里解锁 auto seq Sequence::create( CallFunc::create([this]{ this-dropCandies(); }), DelayTime::create(0.3f), // 确保 drop action 启动 CallFunc::create([this]{ _isProcessingMove false; }), nullptr); this-runAction(seq); return true; }4.4 现象横屏切换后糖果网格错位点击坐标映射失效原因GameScene::onEnter()中硬编码了BOARD_COLS 8,BOARD_ROWS 8但未监听屏幕旋转事件重新布局。Cocos2d-x 的GLView尺寸变了Director::getInstance()-getVisibleSize()返回新值但BoardManager仍按旧尺寸计算格子大小。解决在GameScene.cpp中重写onEnterTransitionDidFinish()void GameScene::onEnterTransitionDidFinish() { Layer::onEnterTransitionDidFinish(); auto visibleSize Director::getInstance()-getVisibleSize(); float cellWidth visibleSize.width / BOARD_COLS; float cellHeight visibleSize.height / BOARD_ROWS; // 通知 BoardManager 重算格子尺寸 _boardManager-updateCellSize(cellWidth, cellHeight); // 重置触摸代理区域关键 _eventDispatcher-removeEventListener(_touchListener); _touchListener EventListenerTouchOneByOne::create(); _touchListener-setSwallowTouches(true); _touchListener-onTouchBegan CC_CALLBACK_2(GameScene::onTouchBegan, this); _eventDispatcher-addEventListenerWithSceneGraphPriority(_touchListener, this); }4.5 现象微信小游戏平台构建失败报错error: usleep was not declared in this scope原因代码中Utils.h有#include unistd.h和usleep(10000)调用但微信小游戏平台基于 QuickJS不提供 POSIX sleep。解决条件编译隔离// Utils.h #if (CC_TARGET_PLATFORM CC_PLATFORM_ANDROID || CC_TARGET_PLATFORM CC_PLATFORM_IOS) #include unistd.h #define SLEEP_MS(ms) usleep((ms) * 1000) #elif (CC_TARGET_PLATFORM CC_PLATFORM_WIN32) #include windows.h #define SLEEP_MS(ms) Sleep(ms) #else // 微信小游戏等 JS 平台用 schedule 替代 #define SLEEP_MS(ms) \ do { \ Director::getInstance()-getScheduler()-schedule( \ [](float){}, this, 0.001f * (ms), 0, false, sleep); \ } while(0) #endif5. 关卡系统实战从 JSON 配置到动态难度调控的完整链路这套 Candy Crush 最被低估的价值是它把关卡设计从“硬编码数字”变成了可热更、可 A/B 测试的配置驱动系统。Resources/levels/level_1.json不是示例而是生产环境真实使用的配置文件。我们来打通从配置编写、加载、到运行时难度调控的全链路。5.1 LevelConfig.json 结构解析字段即能力开关一个典型关卡配置level_1.json如下{ id: 1, target_score: 5000, move_limit: 25, board_type: classic, initial_candies: [ {type: red, count: 12}, {type: blue, count: 10}, {type: green, count: 8} ], obstacles: [ {type: ice, positions: [[2,3],[4,5]]}, {type: chocolate, positions: [[1,1]]} ], special_rules: { chain_bonus_multiplier: 1.5, combo_threshold: 3, time_limit_sec: 120 } }关键字段说明board_type: classic目前只支持 classic标准 8×8但预留了扩展位未来可加hexagonal或circularinitial_candies不是随机生成而是按count指定每种糖果初始数量确保首局不出现“全是红糖”的极端分布obstacles冰块ice需被击中 2 次才碎巧克力chocolate会污染邻格这些类型在ObstacleFactory.cpp中注册新增障碍只需加 JSON 注册工厂类不动核心逻辑special_ruleschain_bonus_multiplier直接参与ScoreCalculator::calculateChainBonus()公式combo_threshold控制连击音效触发时机。5.2 JSON 加载与校验用 RapidJSON 做强约束解析代码没用ValueMap简单解析而是用 RapidJSON 的 Schema Validation 做字段校验// Classes/LevelConfigLoader.cpp bool LevelConfigLoader::loadLevel(int levelId, LevelConfig outConfig) { std::string path StringUtils::format(levels/level_%d.json, levelId); Data data FileUtils::getInstance()-getDataFromFile(path); if (data.isNull()) return false; Document doc; doc.Parse(reinterpret_castconst char*(data.getBytes()), data.getSize()); if (doc.HasParseError()) return false; // 加载预定义 schemaResources/schemas/level_schema.json Data schemaData FileUtils::getInstance()-getDataFromFile(schemas/level_schema.json); Document schemaDoc; schemaDoc.Parse(reinterpret_castconst char*(schemaData.getBytes())); SchemaDocument schema(schemaDoc); SchemaValidator validator(schema); if (!doc.Accept(validator)) { // 输出具体校验失败字段如 move_limit: expected integer, got string StringBuffer buffer; validator.GetInvalidSchemaPointer().StringifyUriFragment(buffer); CCLOG(Level config validation failed at %s, buffer.GetString()); return false; } // 安全校验后再提取字段 outConfig.id doc[id].GetInt(); outConfig.target_score doc[target_score].GetInt(); // ... 其他字段 return true; }提示schemas/level_schema.json是标准 JSON Schema定义了move_limit必须是 integer 且 ≥ 10initial_candies数组长度必须 ≤ 6 等规则。这让你在策划改配置时第一时间知道哪行写错了而不是等到运行时报空指针。5.3 动态难度调控用“玩家行为指纹”实时调整生成策略最硬核的部分在CandyGenerator.cpp。它不按 JSON 里写的{type: red, count: 12}呆板填充而是结合玩家历史行为动态加权// Classes/CandyGenerator.cpp CandyType CandyGenerator::generateRandomCandy(int row, int col) { std::vectorstd::pairCandyType, float weights; // 基础权重来自 level_config.initial_candies for (auto cfg : _levelConfig.initial_candies) { weights.emplace_back(stringToCandyType(cfg.type), cfg.count); } // 【动态加权】如果玩家最近 5 局平均消除数 8提升高分糖果yellow概率 30% if (_playerStats.avgMatchCountLast5 8.0f) { auto it std::find_if(weights.begin(), weights.end(), [](const auto p) { return p.first CANDY_YELLOW; }); if (it ! weights.end()) it-second * 1.3f; } // 【防连击】如果上一局出现了 5 连击本局降低 chain-capable 糖果striped出现率 if (_playerStats.lastChainLength 5) { auto it std::find_if(weights.begin(), weights.end(), [](const auto p) { return p.first CANDY_STRIPED; }); if (it ! weights.end()) it-second * 0.5f; } // 加权随机选择 float totalWeight 0; for (auto w : weights) totalWeight w.second; float randVal RandomHelper::random_real(0.0f, totalWeight); float accum 0; for (auto w : weights) { accum w.second; if (randVal accum) return w.first; } return CANDY_RED; // fallback }这个机制让游戏“越玩越懂你”新手局多给易连的红/蓝糖高手局塞更多需要策略的条纹/包装糖。而所有调控参数avgMatchCountLast5,lastChainLength都存在PlayerStats.h的单例中关卡切换时不清空跨局生效。从那以后我每次接入新项目只要看到消除类需求第一件事就是把这套 Candy Crush 的BoardManager、LevelConfigLoader、CandyGenerator三个类拎出来删掉GameScene配上自己的 UI 层——它不是玩具是经过真机淬炼的消除游戏内核。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑