Qt实战:RPG式连连看源码解析,回合制消除与战斗结算
简介这份资源是使用Qt框架与C编写的RPG风格连连看桌面游戏项目源自大学课程作业适合刚接触Qt、希望用完整小项目练手的初学者也适合想了解如何把经典消除玩法与角色成长机制结合的开发者参考。压缩包共26个文件约1.99MB包含5个cpp源文件、4个头文件、3个ui界面文件以及qrc资源文件、pro工程配置和若干jpg图片素材覆盖游戏逻辑、界面布局与资源打包等环节。已有372人学习下载说明其作为入门案例具备一定参考价值。项目通过QGraphicsView绘制游戏面板、QTimer实现计时并借助信号与槽处理交互RPG元素则体现在经验值、等级提升等设定上。读者可从中学习Qt的UI设计、事件处理、资源管理与简单游戏逻辑的整合方式理解一个可运行小游戏的完整目录结构与代码组织思路。1. 用 Qt 写一个 RPG 式连连看这份源码能让你把「消除」玩成回合制第一次拿到这个压缩包时我以为就是个普通的连连看换皮——点两个相同图标消掉顶多加个计时条。解压翻完代码才发现它把 RPG 的回合制骨架塞进了消除玩法里每次连线成功算一次「攻击」连击数决定伤害倍率棋盘上的图标被赋予了属性消除顺序会影响后续可消除的路径。换句话说这不是「连连看加了个血条」而是用 Qt 的图形视图框架重新组织了一套回合结算逻辑。这份源码适合两类人一类是想学 Qt Widgets 和 QGraphicsView 实战的开发者项目里窗口管理、自定义图元、信号槽通信、资源打包都有完整实现另一类是手里有消除类玩法原型、想加一层 RPG 数值系统的独立开发者可以直接把战斗结算模块拆出来复用。下面按「能干什么 → 怎么跑 → 怎么改」的顺序拆一遍。2. 项目结构与技术选型QGraphicsView 撑起的棋盘和回合系统2.1 为什么用 QGraphicsView 而不是 QWidget 网格常见的连连看 Demo 喜欢用 QGridLayout 铺一堆 QPushButton点哪个改哪个的图标。这种做法写起来快但一旦要加动画、拖拽、路径高亮就非常别扭——每个按钮都是独立控件坐标系和绘制时机不好统一。这个项目选了 QGraphicsScene QGraphicsView 的路线棋盘上的每个格子是一个自定义 QGraphicsItem连线路径是一条 QGraphicsPathItem消除时的缩放淡出是 QPropertyAnimation 直接作用在图元上。这样做的直接好处是所有格子共享一套场景坐标判断两个格子是否可连只需要比较网格行列号不需要去问控件「你在屏幕哪个位置」动画和绘制分离改视觉不用动逻辑。代价是要自己管理图元的创建和销毁内存泄漏的风险比 QWidget 方案高一些后面避坑章节会讲。2.2 目录结构逐个文件说明解压后大致是这样一个布局不同 Qt Creator 版本生成的 .pro 文件内容略有差异但结构一致RPGLinkGame/ ├── RPGLinkGame.pro # qmake 工程文件 ├── main.cpp # 程序入口QApplication 初始化 ├── mainwindow.h/.cpp # 主窗口负责场景切换和菜单 ├── gamescene.h/.cpp # 核心游戏场景继承 QGraphicsScene ├── gameboard.h/.cpp # 棋盘逻辑生成、洗牌、可连判定 ├── tileitem.h/.cpp # 单个格子图元继承 QGraphicsObject ├── battlemanager.h/.cpp # 回合与伤害结算 ├── playerdata.h/.cpp # 玩家属性、等级、技能数据 ├── resources.qrc # 图片、音效资源清单 └── images/ # 图标素材main.cpp只做三件事创建 QApplication、设置高分屏属性、显示 MainWindow。真正值得注意的是gamescene和gameboard的分工——场景管「怎么画、怎么动」棋盘管「能不能消、消了之后补什么」。这个边界划得比较干净二次开发时想换一套消除规则基本只动 gameboard 就行。2.3 核心类职责与信号槽连接GameBoard不继承任何 Qt 类是一个纯逻辑类对外暴露这几个方法// gameboard.h 关键接口 class GameBoard { public: void initBoard(int rows, int cols, int types); // 初始化棋盘 bool canConnect(const QPoint a, const QPoint b); // 判定两点可连 bool removePair(const QPoint a, const QPoint b); // 消除并返回是否成功 void shuffleRemaining(); // 无解时重排 QVectorQVectorint grid() const; // 供场景读取 private: QVectorQVectorint m_grid; // 0 表示空正数表示图标类型 int m_rows, m_cols; };canConnect是连连看的灵魂它要判断两个格子之间是否存在一条最多拐两次弯的路径。项目里用的是经典的「直线 一次拐点 两次拐点」三段式判定先查同行同列直连再查一个拐点最后查两个拐点。判定通过后返回路径上的拐点坐标交给GameScene画连线。GameScene在构造时把GameBoard的指针存下来格子点击事件由TileItem发出信号场景接收后记录第一次选中的格子第二次点击时调用canConnect// gamescene.cpp 中处理点击的核心片段 void GameScene::onTileClicked(TileItem *tile) { if (!m_firstTile) { m_firstTile tile; tile-setSelected(true); // 高亮第一个选中格 return; } if (m_firstTile tile) { // 重复点同一个取消选中 tile-setSelected(false); m_firstTile nullptr; return; } QPoint a m_firstTile-gridPos(); QPoint b tile-gridPos(); if (m_board-canConnect(a, b)) { drawPath(m_board-lastPath()); // 画出连线 m_board-removePair(a, b); m_battle-resolveAttack(a, b); // 交给战斗管理器结算 refreshBoard(); // 刷新图元显示 } else { m_firstTile-setSelected(false); // 连不上清掉选中态 } m_firstTile nullptr; }这里m_battle-resolveAttack就是 RPG 味道的来源。它接收两个坐标根据消除的图标类型、当前连击数、玩家攻击力算一次伤害然后更新血条。信号槽的连接在MainWindow里完成GameScene发出pairRemoved(int combo)信号BattleManager的槽函数接收后累加连击超时未消除则重置连击。2.4 资源打包与 .pro 配置.pro文件里除了常规的QT widgets还加了QT multimedia用于播放消除音效。资源通过resources.qrc打包进可执行文件避免运行时找不到图片。如果你要替换素材改完 qrc 后记得在 Qt Creator 里右键资源文件重新执行一次 qmake否则新加的图片不会编译进去——这个坑我踩过不止一次。3. 从零跑起来编译环境、依赖和第一次运行3.1 环境准备与版本选择项目基于 Qt 5 的 Widgets 模块用 qmake 构建。Qt 5.12 到 5.15 都能直接打开Qt 6 需要改几处 API比如QMediaPlayer的用法在 Qt 6 里变了。如果你机器上只有 Qt 6建议装一个 Qt 5.15 的套件省得改代码。需要装的组件Qt Creator、对应版本的 Desktop 套件MinGW 或 MSVC 都行、multimedia 模块。Windows 上用 MinGW 最省事Linux 下装qtbase5-dev和qtmultimedia5-dev即可。3.2 编译步骤用 Qt Creator 打开.pro文件选择套件直接点构建。命令行方式# Linux / macOS 下用 qmake 构建 mkdir build cd build qmake ../RPGLinkGame.pro make -j4 ./RPGLinkGame# Windows MinGW 下 mkdir build cd build qmake ..\RPGLinkGame.pro mingw32-make -j4 release\RPGLinkGame.exe如果编译报错cannot find -lQt5Multimedia说明 multimedia 模块没装回到 Qt 维护工具里勾上再试。运行后应该能看到一个带血条和连击数的棋盘窗口点击两个相同图标能连的话会画出一条折线并播放音效。3.3 运行时常见现象说明第一次运行可能会觉得窗口偏小这是因为MainWindow里写死了初始尺寸。棋盘行列数在GameBoard::initBoard的调用处配置默认是 8 行 10 列。想改难度改这两个数字就行但要注意图标类型数不能超过可用素材数量否则会出现空格子。音效播放依赖系统解码器Linux 下如果没声音装一下libqt5multimedia5-plugins。这个不是代码问题是运行环境缺插件。4. 核心模块拆解可连判定、回合结算和动画刷新4.1 可连判定的三段式实现连连看的判定逻辑说穿了就是「两点之间能否用不超过两段的折线连起来且折线不穿过非空格子」。项目里的实现分三步走每一步都返回路径拐点// gameboard.cpp 判定核心简化后 bool GameBoard::canConnect(const QPoint a, const QPoint b) { m_lastPath.clear(); // 第一步同行或同列中间全空 if (checkLine(a, b)) { m_lastPath a b; return true; } // 第二步一个拐点拐点坐标为 (a.x, b.y) 或 (b.x, a.y) QPoint c1(a.x(), b.y()); QPoint c2(b.x(), a.y()); if (isEmpty(c1) checkLine(a, c1) checkLine(c1, b)) { m_lastPath a c1 b; return true; } if (isEmpty(c2) checkLine(a, c2) checkLine(c2, b)) { m_lastPath a c2 b; return true; } // 第三步两个拐点沿 a 的行/列向外扫描 // ... 遍历 a 所在行和列的空格作为第一个拐点再判第二个 return false; }checkLine检查两点在同一行或同一列时中间所有格子是否为空。isEmpty判断某个坐标是否在棋盘范围内且值为 0。第三步的双拐点判定是性能瓶颈最坏情况下要遍历整行整列但棋盘就几十个格子实际跑起来没有卡顿感。注意m_lastPath在每次判定前必须清空否则上一次的路径会污染这次的结果。这个 bug 在调试时表现为「偶尔画出一条莫名其妙的线」查了半天才发现是没清路径。4.2 回合结算与连击系统BattleManager是 RPG 化的关键。它维护玩家血量、敌人血量、当前连击数和一个连击超时计时器。每次消除成功调用resolveAttack// battlemanager.cpp void BattleManager::resolveAttack(const QPoint a, const QPoint b) { m_combo; m_comboTimer-start(3000); // 3 秒内没新消除就断连 int baseDamage 10; int typeBonus m_board-tileType(a) * 2; // 图标类型影响伤害 int totalDamage (baseDamage typeBonus) * m_combo; m_enemyHp - totalDamage; emit damageDealt(totalDamage, m_combo); // 通知 UI 飘伤害数字 if (m_enemyHp 0) { emit enemyDefeated(); nextRound(); // 进入下一回合 } }连击倍率是线性的m_combo直接乘在伤害上。想改成指数增长或者加个上限改这一行就行。m_comboTimer是一个 QTimer超时后把m_combo重置为 0这就是「回合感」的来源——你必须在限定时间内连续消除否则连击断掉伤害回落。4.3 图元刷新与消除动画消除一个格子不是简单地把图元删掉而是先播放一个缩放淡出动画动画结束后再移除图元并更新棋盘数据。TileItem继承自 QGraphicsObject这样才能用 QPropertyAnimation 驱动它的opacity和scale属性// tileitem.cpp 播放消除动画 void TileItem::playRemoveAnimation() { QPropertyAnimation *anim new QPropertyAnimation(this, opacity); anim-setDuration(200); anim-setStartValue(1.0); anim-setEndValue(0.0); connect(anim, QPropertyAnimation::finished, this, [this]() { emit removeFinished(this); // 通知场景可以删了 }); anim-start(QAbstractAnimation::DeleteWhenStopped); }DeleteWhenStopped让动画对象在结束后自动释放省去手动 delete。场景收到removeFinished信号后从QGraphicsScene里移除该图元并 delete 掉同时把棋盘对应位置置 0。这里要注意顺序先删图元再改数据还是反过来取决于你的刷新逻辑但一定要保证两者同步否则会出现「格子没了但还能点」的幽灵状态。5. 避坑与排查编译、运行、逻辑三层问题定位5.1 编译报错找不到 multimedia 模块现象qmake 阶段提示Unknown module(s) in QT: multimedia。原因是安装 Qt 时没勾选 multimedia 组件。解决方式是打开 Qt 维护工具找到对应版本的 Additional Libraries勾上 Qt Multimedia重新安装后再构建。如果不想装也可以把音效相关代码注释掉.pro里的QT multimedia去掉程序照样能跑只是没声音。5.2 运行后棋盘是空的或者全是同一个图标现象窗口出来了但格子里没有图标或者所有格子长得一样。原因通常是资源路径不对。resources.qrc里的前缀和代码里引用的路径必须完全一致比如 qrc 里写的是:/images/tile1.png代码里就不能写成:/img/tile1.png。另一个可能是图标类型数超过了实际素材数量initBoard随机生成时取到了不存在的类型。检查images目录下的文件数量和initBoard第三个参数是否匹配。5.3 点击两个可消的格子却没反应现象明明两个图标相同、中间也没挡着点了就是消不掉。原因多半是canConnect里的边界判断有问题。棋盘外围通常要留一圈「虚拟空格」因为连连看的路径允许绕到棋盘外面走。如果isEmpty只检查棋盘内部绕外的路径就判为不可连。解决方式是在棋盘数据外围包一圈 0行列索引整体偏移 1判定时把范围放宽到-1到rows。5.4 消除后出现残影或点击错位现象格子消掉了但原来位置还能点或者新补上来的格子和显示对不上。这是图元删除和棋盘数据更新不同步导致的。常见做法是动画结束后先从场景移除图元再更新m_grid最后统一调一次refreshBoard重建所有图元。不要试图只更新局部局部更新在连连看这种会连锁补位如果实现了下落逻辑的场景里很容易出错。5.5 连击计时器导致程序退出时崩溃现象关窗口时偶尔崩溃调试信息指向 QTimer。原因是BattleManager里的m_comboTimer在对象销毁后还在跑触发了已释放对象的槽函数。解决方式是在BattleManager的析构函数里m_comboTimer-stop()或者把 timer 的 parent 设为this让 Qt 的对象树自动管理生命周期。6. 二次开发把战斗数值和棋盘规则拆开改这份源码最值得复用的地方是它把「棋盘逻辑」和「战斗结算」做成了两个独立类中间只靠信号槽和几个接口方法通信。这意味着你可以单独换掉其中一块另一块基本不用动。如果你想改玩法比如把「消除加伤害」改成「消除攒能量能量满了放大招」只需要动BattleManager。GameScene在消除成功后调用的还是resolveAttack你在这个方法里把伤害计算换成能量累加再发一个energyFull信号给 UI 就行。棋盘那边完全无感。如果你想改棋盘规则比如加入「重力下落」——消除后上方的格子掉下来——那要动的是GameBoard::removePair和refreshBoard。removePair里除了把两个位置置 0还要把上方格子依次下移refreshBoard则要根据新的m_grid重建图元位置。这里有个技巧不要逐个移动图元而是全部删掉重建虽然性能差一点但逻辑简单不容易错。棋盘就几十个格子重建的开销可以忽略。验证改动是否正确的办法很朴素在removePair和resolveAttack里各加一行qDebug()打印当前棋盘状态和伤害值跑几局看输出是否符合预期。我一般还会在canConnect返回 false 的时候把m_lastPath也打出来确认判定路径确实为空而不是画了线但没消掉。还有一个容易忽略的点PlayerData里的等级和技能数据目前是写死的没有存档功能。想加存档用QSettings或者QJsonDocument序列化都行前者更省事后者更灵活。我倾向于用 JSON因为技能树这种嵌套结构用 QSettings 的扁平键值对表达起来很别扭。从那以后我每次改这类消除游戏的判定逻辑都强制先在纸上把「直线、一拐、双拐」三种情况的坐标关系画一遍再写代码不然光靠调试器单步跟很容易在双拐的循环边界上绕晕。希望这份拆解能帮你少走点弯路。本文还有配套的精品资源点击获取