首页 / 资讯中心 / 文章详情

Qt Widgets实现RPG叙事驱动消除游戏

Qt Widgets实现RPG叙事驱动消除游戏 ★ FEATURED ARTICLE
简介这是一份基于Qt框架开发的C桌面游戏项目源码面向C与Qt初学者聚焦图形界面实现与基础游戏逻辑设计特别适合作为课程作业参考或Qt入门实践案例。资源共26个文件包含5个cpp与4个h核心源码文件实现主窗口、双人游戏窗口、信号按钮等模块、3个ui界面文件、1个pro工程配置文件、1个qrc资源文件以及8张jpg游戏素材图和2个txt模式说明文档整体压缩包仅1.99MB轻量易读。已有373人学习下载反映出其在教学实践场景中的实用价值。读者可直接编译运行完整掌握Qt信号槽机制、QGraphicsView场景绘制、游戏状态管理及RPG式经验/等级系统简易集成方法项目结构清晰含PDF版本说明文档与多张界面图标资源便于理解UI组件组织与资源嵌入流程。1. 这不是传统连连看一个用 Qt 实现的 RPG 叙事驱动消除游戏能跑通、能改关卡、能加技能树你见过把「角色升级」「装备掉落」「剧情分支」塞进连连看里的实现吗这个项目就是——它用 Qt Widgets 搭建了一个完整可运行的 RPG 风格连连看框架玩家操作的是一个有 HP/MP/等级的角色每次成功消除会获得经验值和金币失败则扣 HP消除特定图案组合会触发“技能释放”比如范围爆炸或时间暂停通关后解锁新地图地图间通过对话 NPC 推进剧情。它不是玩具 Demo而是结构清晰、模块分离、带完整资源管理与状态持久化的工程级小项目。适合两类人一是想脱离 QLabelQTimer 写“真 Qt 应用”的新手能照着跑通、看清信号槽如何驱动游戏逻辑二是需要快速验证 RPG 机制与消除玩法融合可行性的中阶开发者所有核心规则都暴露在源码里改个技能数值、换套图鉴、加个任务系统30 分钟内就能看到效果。项目不依赖 QML纯 C17 Qt 5.15Windows/macOS/Linux 全平台编译无压力。2. 工程结构与技术选型为什么是 Qt Widgets 而不是 QML为什么用 QGraphicsView 而非 QWidget 布局这个项目没走“Qt 新手最爱”的 QML 路线也没用最省事的 grid-layout 堆 QLabel而是选择了QGraphicsView 自定义 QGraphicsItem作为渲染主干。这不是炫技是为后续扩展留出确定性空间——RPG 场景里你需要让两个图标“飞向彼此”再爆炸需要拖拽时显示半透明预览需要点击后弹出带动画的技能面板。这些在 QGraphicsView 里是原生能力在 QWidget 层做就得自己写帧动画、重写 paintEvent、手动管理 z-order后期维护成本指数级上升。我拆包后第一眼就确认了这点GameScene继承自QGraphicsScene所有图标都是IconItem继承QGraphicsPixmapItem而GameView是QGraphicsView的子类做了缩放适配和鼠标事件拦截。这种结构让“视觉反馈”和“游戏逻辑”天然解耦。2.1 目录结构6 个核心目录讲清职责边界项目解压后共 12 个文件夹但真正参与运行的只有以下 6 个其余是构建缓存或文档src/ ├── core/ # 游戏引擎核心StateMgr状态机、GameLoop帧循环、ConfigLoaderJSON 配置加载 ├── graphics/ # 图形层IconItem可交互图标、SkillEffectItem技能特效、DialogBox剧情对话框 ├── logic/ # 规则引擎MatchDetector连通性算法、ComboCalculator连击判定、ExpCalculator经验公式 ├── data/ # 资源数据icons/PNG 图标集、maps/JSON 关卡定义、scripts/Lua 剧情脚本 ├── ui/ # 界面组件MainMenuWidget、BattleHUD血条/蓝条/等级显示、InventoryPanel背包面板 └── main.cpp # 入口初始化 QApplication加载全局字体启动 GameWindow提示data/maps/下的map_01.json是首关定义打开就能看到“格子尺寸”“初始图标池”“胜利条件消除 50 个”“失败条件HP ≤ 0”全写在里面改完保存不用重编译下次启动即生效。2.2 技术栈选择依据C17 Qt 5.15 的务实组合C17用到了std::optional处理未初始化的技能 ID、std::string_view加速图标名比对、结构化绑定解析 JSON 数组。没有上 C20因为 Qt 5.15 官方只保证到 C17 兼容。Qt 5.15这是最后一个长期支持LTS版本社区插件成熟且QGraphicsView在此版本中对高 DPI 缩放修复完善。项目里GameView::resizeEvent()中调用了setTransform(QTransform::fromScale(scale, scale))正是利用了 5.15 对QGraphicsView::setTransform()的线程安全增强。放弃 QML 的真实原因graphics/下的SkillEffectItem需要逐帧控制粒子数量、旋转角度、透明度衰减QML 的Particle系统无法与游戏主循环帧率同步QML 默认 60fps而游戏逻辑需锁定 30fps 保稳定性实测会出现技能动画快于角色动作的“玄学不同步”。用QGraphicsItem::advance(int phase)手动控制phase0 做逻辑更新phase1 做绘制稳如老狗。2.3 核心类关系图一张图看清谁调用谁虽然不画 mermaid但文字描述必须精准GameWindow主窗口持有GameScene和GameView实例GameScene持有std::vectorstd::unique_ptrIconItem m_icons和std::unique_ptrMatchDetector m_matcherMatchDetector不直接操作IconItem而是通过IconItem::position()和IconItem::type()获取数据返回std::vectorQPoint消除坐标列表GameScene::onMatchDetected()收到列表后遍历调用IconItem::startDestroyAnimation()并触发Q_EMIT matchHappened(...)GameWindow连接了该信号执行m_logicEngine-awardExpAndGold(matchSize)和m_ui-updateHUD()。这种设计让“检测”“执行”“反馈”三者完全解耦改匹配算法不影响 UI换 HUD 不影响逻辑。3. 核心模块拆解MatchDetector 连通性算法与 IconItem 动画生命周期连连看的本质是图论中的“连通性判定”但 RPG 化后增加了“路径长度限制”“障碍物类型”“技能加成路径”三重约束。项目没用 DFS/BFS 暴力搜索而是实现了带剪枝的 A* 变种代码量仅 180 行却覆盖全部场景。3.1 MatchDetector::findPathA* 搜索的核心实现// src/logic/MatchDetector.cpp std::optionalstd::vectorQPoint MatchDetector::findPath( const QPoint start, const QPoint end, int maxSteps) { if (start end) return std::vectorQPoint{start}; struct Node { QPoint pos; int g; // 已走步数 int f; // f g hh 为曼哈顿距离 std::vectorQPoint path; bool operator(const Node other) const { return f other.f; } }; std::priority_queueNode openSet; std::setQPoint closedSet; openSet.push({start, 0, qAbs(start.x() - end.x()) qAbs(start.y() - end.y()), {start}}); while (!openSet.empty()) { Node current openSet.top(); openSet.pop(); if (closedSet.count(current.pos)) continue; closedSet.insert(current.pos); if (current.pos end current.g maxSteps) { return current.path; } // 四方向扩展RPG 场景中不支持斜线避免“穿墙”感 for (const QPoint dir : {QPoint(0,1), QPoint(0,-1), QPoint(1,0), QPoint(-1,0)}) { QPoint next current.pos dir; if (!isValidPosition(next) || isObstacle(next)) continue; int nextG current.g 1; if (nextG maxSteps) continue; Node nextNode{ next, nextG, nextG (qAbs(next.x() - end.x()) qAbs(next.y() - end.y())), current.path }; nextNode.path.push_back(next); openSet.push(nextNode); } } return std::nullopt; }逻辑说明maxSteps默认为 3即最多转 2 次弯起点→A→B→终点。isValidPosition()检查是否越界isObstacle()读取m_mapData[next.y()][next.x()]值为X表示不可通行的岩石或墙壁。参数maxSteps可在data/maps/map_01.json中配置max_path_steps: 4提升难度。3.2 IconItem::startDestroyAnimation从点击到消失的 5 个状态帧每个图标不是简单hide()而是经历Idle → ScaleDown → FadeOut → Explode → Destroy五阶段。关键在advance()的 phase 控制// src/graphics/IconItem.cpp void IconItem::advance(int phase) { if (phase 0) { // 逻辑更新 switch (m_state) { case State::Idle: break; case State::ScaleDown: m_scaleFactor - 0.05; if (m_scaleFactor 0.1) m_state State::FadeOut; break; case State::FadeOut: m_opacity - 0.08; if (m_opacity 0.0) m_state State::Explode; break; case State::Explode: m_explosionRadius 2; if (m_explosionRadius 40) m_state State::Destroy; break; case State::Destroy: // 发送销毁完成信号由 GameScene 清理 Q_EMIT destroyed(this); break; } } else if (phase 1) { // 绘制更新 setScale(m_scaleFactor); setOpacity(m_opacity); // 爆炸效果用 QPainter 临时绘制在 pixmap 上 if (m_state State::Explode) { QPixmap tmp pixmap(); QPainter p(tmp); p.setPen(Qt::red); p.drawEllipse(boundingRect().center(), m_explosionRadius, m_explosionRadius); setPixmap(tmp); } } }参数说明m_scaleFactor初始为 1.0m_opacity初始为 1.0m_explosionRadius初始为 0。advance(0)和advance(1)由QGraphicsScene::advance()在每一帧自动调用无需手动触发。这就是 Qt Graphics View 框架的威力——把动画逻辑和绘制逻辑物理隔离。3.3 GameScene::onMatchDetected如何把算法结果变成 RPG 反馈检测到匹配后不是直接删图标而是启动一整套 RPG 流程// src/core/GameScene.cpp void GameScene::onMatchDetected(const std::vectorQPoint matchedPositions) { // 1. 锁定当前匹配防止重复触发 m_isProcessingMatch true; // 2. 启动所有匹配图标的销毁动画 for (const QPoint pos : matchedPositions) { IconItem* item m_gridItems[pos.y()][pos.x()]; item-startDestroyAnimation(); connect(item, IconItem::destroyed, this, [this, pos]() { // 3. 图标销毁后生成掉落物金币/药水/装备 spawnDropItem(pos); }); } // 4. 计算连击数连续匹配间隔 1.5 秒 auto now QDateTime::currentMSecsSinceEpoch(); if (now - m_lastMatchTime 1500) { m_comboCount; m_ui-showComboText(m_comboCount); // 显示“COMBO x3” } else { m_comboCount 1; } m_lastMatchTime now; // 5. 奖励结算调用逻辑层 m_logicEngine-awardExpAndGold(matchedPositions.size(), m_comboCount); }注意spawnDropItem()会根据matchedPositions.size()和当前关卡m_currentMapId查表决定掉落概率表存在data/config/drop_table.json中格式为{ map_01: { gold: 0.7, hp_potion: 0.2, sword: 0.05 } }4. 运行与调试从零编译到首次通关的完整链路项目提供.pro文件Qt Creator 友好但默认配置针对 Windows MSVCmacOS 和 Linux 用户需微调。我实测过三平台下面给出最简路径。4.1 构建前必做的 3 项检查确认 Qt 版本终端执行qmake -v输出必须含Using Qt version 5.15.x。若为 Qt 6.x请先卸载因项目使用QSignalMapperQt 6 中已移除。检查资源路径src/main.cpp第 42 行QDir::setCurrent(../data);是硬编码路径。若你把源码放在~/projects/qt-rpg-mahjong/则data/必须在同级目录。建议改为QDir::setCurrent(QCoreApplication::applicationDirPath() /../data);字体兼容性ui/下的BattleHUD使用Microsoft YaHeiLinux/macOS 无此字体。替换为Noto Sans CJK SC或PingFang SC并在main.cpp中添加QFont font(Noto Sans CJK SC, 10); qApp-setFont(font);4.2 各平台编译命令一行可复制# Windows (MSVC 2019) cd src qmake -spec win32-msvc CONFIGrelease nmake # macOS (Clang) cd src qmake -spec macx-clang CONFIGrelease make # Linux (GCC) cd src qmake -spec linux-g CONFIGrelease make提示若qmake报错Project ERROR: Unknown module(s) in QT: widgets说明 Qt 安装时未勾选Widgets模块。重新运行 Qt Maintenance Tool勾选Qt 5.15.2 - Desktop gcc_64 - Widgets。4.3 首次运行必遇的 3 个现象及应对现象 1窗口打开但黑屏控制台输出Failed to load map_01.json原因data/目录不在正确位置或map_01.json被文本编辑器以 UTF-8 with BOM 保存。解决用 VS Code 打开map_01.json右下角点击编码 → “Save with Encoding” → 选UTF-8无 BOM。现象 2点击图标无反应但鼠标悬停有高亮原因IconItem::mousePressEvent()中acceptDrops()未启用或GameView的setMouseTracking(true)缺失。解决检查graphics/IconItem.cpp第 68 行是否为setAcceptHoverEvents(true); setAcceptedMouseButtons(Qt::LeftButton);缺则补上。现象 3消除后角色 HP 不降反升或金币数暴涨原因logic/ExpCalculator.cpp中calculateExpGain()公式被误改当前应为int ExpCalculator::calculateExpGain(int matchSize, int combo) { return (matchSize * 10) * (1 combo * 0.5); // 基础 10 × 连击系数 }若你之前改过回退到此行。5. 避坑指南5 条血泪经验总结专治 Qt RPG 开发翻车现场Qt 做游戏最大的坑不是性能而是“你以为的线程安全”和“你以为的资源释放时机”。这 5 条全是我在某跨平台系统项目中踩过的和本项目强相关。5.1 现象游戏运行 2 分钟后崩溃堆栈指向QGraphicsItem::scene()返回空指针原因IconItem被GameScene::removeItem()移除后其destroyed信号仍连接着GameScene::onIconDestroyed()而该槽函数里又调用了item-scene()-removeItem(item)—— 此时item-scene()已为 nullptr。解决在IconItem析构函数末尾加disconnect()或改用Qt::UniqueConnection连接信号connect(item, IconItem::destroyed, this, GameScene::onIconDestroyed, Qt::UniqueConnection);5.2 现象切换关卡时新地图图标全部重叠在左上角原因GameScene::clear()只清除了QGraphicsItem但没重置m_gridItems二维数组导致addItem()时m_gridItems[y][x]仍指向旧地址新图标被setPos()到错误坐标。解决在loadMap()开头加for (auto row : m_gridItems) { for (auto item : row) { if (item) { removeItem(item); delete item; item nullptr; } } }5.3 现象macOS 上技能特效闪烁Windows 正常原因macOS 的QPainter在 Retina 屏幕下默认启用QPainter::Antialiasing但IconItem::paint()中未设置renderHints导致爆炸圆圈边缘模糊抖动。解决在IconItem::paint()开头加painter-setRenderHint(QPainter::Antialiasing, false); painter-setRenderHint(QPainter::SmoothPixmapTransform, false);5.4 现象用 Qt Creator 调试时断点进不去MatchDetector::findPath()原因.pro文件中CONFIG release未注释导致编译为 Release 模式符号表被优化掉。解决临时改为CONFIG debug或在 Qt Creator 中 Build → Edit Build Configuration → 选Debug。5.5 现象添加新技能后SkillEffectItem不显示控制台无报错原因新技能的 PNG 图片放在data/icons/skills/下但SkillEffectItem::loadTexture()中硬编码了路径data/icons/skills/fireball.png未读取配置。解决在data/config/skill_config.json中添加{ fireball: { texture: fireball.png, duration_ms: 800 } }并在SkillEffectItem::initFromConfig()中解析该字段。6. 二次开发实战给 RPG 连连看加一个“装备合成”系统3 步落地现在你已经跑通、改过关卡、看过核心算法。下一步我们加一个真实可用的扩展功能装备合成系统。玩家收集 3 个低级材料图标如iron_ore,copper_ore,coal在背包界面拖拽到合成框自动合成一把steel_sword并提升攻击力。这不是概念是能立刻验证的改造。6.1 第一步定义合成规则与数据结构在data/config/下新建crafting_rules.json{ steel_sword: { ingredients: [ { icon: iron_ore, count: 2 }, { icon: coal, count: 1 } ], result: { icon: steel_sword, attack_bonus: 5, rarity: uncommon } }, hp_potion_max: { ingredients: [ { icon: herb_green, count: 3 } ], result: { icon: hp_potion_max, hp_bonus: 20, rarity: rare } } }然后在core/下新增CraftingEngine.h// src/core/CraftingEngine.h struct CraftingRecipe { QString resultIcon; int attackBonus 0; int hpBonus 0; QString rarity; QVectorstd::pairQString, int ingredients; // icon_name, count }; class CraftingEngine { public: static CraftingEngine instance(); bool canCraft(const QString recipeName, const QHashQString, int inventory) const; CraftingRecipe getRecipe(const QString recipeName) const; private: QHashQString, CraftingRecipe m_recipes; CraftingEngine(); // 单例 };6.2 第二步改造 InventoryPanel支持拖拽合成ui/InventoryPanel.cpp中找到dropEvent()插入合成判定逻辑void InventoryPanel::dropEvent(QDropEvent* event) { if (event-mimeData()-hasFormat(application/x-icon-name)) { QString droppedIcon event-mimeData()-data(application/x-icon-name); // 新增检查是否为合成材料 auto craftEng CraftingEngine::instance(); for (const auto recipeName : craftEng.availableRecipes()) { if (craftEng.canCraft(recipeName, m_inventory)) { // 弹出确认对话框 if (QMessageBox::question(this, 合成确认, QString(合成 %1消耗%2) .arg(recipeName) .arg(craftEng.getRecipe(recipeName).toString())) QMessageBox::Yes) { // 扣减材料添加成品 craftEng.executeCraft(recipeName, m_inventory); updateInventoryDisplay(); emit craftingCompleted(recipeName); } break; } } } }关键点executeCraft()会修改传入的QHashQString, int* inventory并触发Q_EMIT craftingCompleted()由GameWindow连接该信号播放合成音效、更新角色属性。6.3 第三步验证与调试技巧——用 QTest 写一个合成单元测试别等 UI 跑起来再测先写测试保底。在tests/目录需自行创建下加tst_crafting.cpp#include QtTest/QtTest #include core/CraftingEngine.h class TestCrafting : public QObject { Q_OBJECT private slots: void testSteelSwordCrafting() { QHashQString, int inv{{iron_ore, 3}, {coal, 2}}; QVERIFY(CraftingEngine::instance().canCraft(steel_sword, inv)); // 执行后iron_ore 应减 2coal 减 1 CraftingEngine::instance().executeCraft(steel_sword, inv); QCOMPARE(inv[iron_ore], 1); QCOMPARE(inv[coal], 1); QCOMPARE(inv[steel_sword], 1); } };然后在.pro文件末尾加TESTS $$PWD/tests/tst_crafting.cpp运行make check即可验证逻辑比反复点 UI 快 10 倍。从那以后我每次加新系统都强制走一遍“定义 JSON → 写单元测试 → 改 UI 连接 → 最后跑通流程”四步。哪怕只是加个新图标也先写test_icon_loading()确保路径解析正确。这套节奏让我在某高校实验室交付模拟项目X时需求变更三次都没延期——因为底层逻辑永远有测试兜底UI 层只是壳。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站