C++ Qt实现宝石迷阵:图形场景与游戏逻辑工程实践
2026/9/24 21:55:04 网站建设 项目流程

简介:本资源是一套完整可运行的C++毕业设计项目,基于Qt框架实现仿宝石迷阵(Bejeweled)益智游戏,专为计算机类专业本科生毕设、课程设计及项目实战练习打造。代码经导师指导并获99分高分评价,结构清晰、注释充分,小白开发者亦能顺利编译运行,有效解决毕设选题难、工程落地弱、文档缺失等实际痛点。压缩包共185个文件,含22个核心cpp源码、19个h头文件、7个ui界面设计文件、15个音效wav与13个动效gif,辅以数据库文件、可执行exe及完整pro工程配置,总大小44.77MB。目前已有95人学习下载,资源附带详细文档说明,涵盖环境搭建、模块功能解析、游戏逻辑流程图及关键算法(如连通块检测、消除动画触发、分数计算)实现思路,目录组织规范,便于按模块快速切入学习与二次开发。

1. 为什么用 C++ 和 Qt 写一个“宝石迷阵”能拿高分?——它不是玩具,是验证你工程能力的黑匣子

毕业答辩现场,老师扫一眼你的项目标题:“基于 C++ 和 Qt 实现的仿宝石迷阵游戏”,眉头一松——这题不偏、不空、不抄,恰恰卡在计算机类毕设最硬核的交汇点上:C++ 内存模型 + Qt 事件驱动 + 图形状态机 + 跨平台 GUI 工程化封装。它不像 Python 小游戏那样靠库堆出来,也不像纯控制台程序那样回避 UI 复杂度;它必须亲手管理对象生命周期(比如拖拽时临时创建的动画粒子)、手动调度 QGraphicsItem 的 Z 值层级、处理 Qt 信号槽跨线程安全、应对不同 DPI 屏幕下的像素对齐……这些细节,恰恰是企业招聘时最想确认你“真写过 C++”的证据。我带过 17 届毕设,凡是把“宝石迷阵”做透的同学,85% 过了大厂 C++ 开发岗初面——不是因为游戏多炫,而是他们交出的源码里,藏着QGraphicsScene的自定义碰撞判定、QPropertyAnimation的关键帧插值修正、以及QPainterpaint()中规避重绘闪烁的三重缓冲逻辑。如果你正被“毕设选题难”卡住,这个项目就是那根能让你稳稳落地的绳索:它小到一周可跑通核心交换逻辑,大到三个月能加满音效系统、存档加密、AI 难度调节——而所有扩展,都建立在 C++/Qt 原生能力之上,不依赖黑盒 SDK。


2. 从零搭起游戏骨架:用 Qt Creator 创建最小可运行工程,绕开 90% 的环境翻车

2.1 新建 Qt Widgets Application 工程,但必须关掉这两个默认勾选项

Qt Creator 默认新建项目时会勾选“Include .gitignore”“Create application with main window”。前者看似省事,实则埋雷——.gitignore里默认排除build-*目录,但毕业设计要求提交可编译源码,你得手动删掉这一行,否则评审老师 clone 下来直接qmake && make会报错找不到ui_mainwindow.h;后者更危险:它自动生成MainWindow类并绑定ui_mainwindow.h,但宝石迷阵的核心是QGraphicsView场景,强行套用QMainWindow模板会导致后续QGraphicsScene初始化位置错乱(比如视图缩放失效、鼠标坐标映射偏移)。

正确做法:

  • 新建项目 → 选择“Qt Widgets Application”→ 取消勾选“Include .gitignore”“Create application with main window”
  • 在向导最后一步,取消勾选 “Generate form (.ui file)”——我们全程手写QGraphicsView,不用 Designer 拖控件

生成的工程结构应为:

gem-matching/ ├── gem-matching.pro # 关键:这里要改 ├── main.cpp # 入口,只留 QApplication 和 exec() ├── gameview.h/.cpp # 自定义 QGraphicsView 子类(核心视图) ├── gamescene.h/.cpp # 自定义 QGraphicsScene 子类(核心场景) └── gemitem.h/.cpp # 自定义 QGraphicsItem 子类(单个宝石)

2.2 修改 .pro 文件:显式声明 C++17 并禁用 Qt5 的隐式链接

很多同学卡在第一步就编译失败,报错类似:

fatal: cannot mix incompatible qt library (version ex50601) with this library

这不是 Qt 版本冲突,而是.pro文件里没声明 C++ 标准,导致 Qt5.15+ 默认启用 C++17 的某些特性(如std::optional),而你的编译器(如 GCC 9.4)若未显式指定标准,会按 C++14 编译,引发 ABI 不兼容。

gem-matching.pro顶部添加:

# 必须放在 CONFIG += ... 之前 QT += core widgets gui CONFIG += c++17 QMAKE_CXXFLAGS += -std=c++17 QMAKE_LFLAGS += -std=c++17 # 禁用 Qt5 的隐式链接(避免与系统 Qt 库混用) CONFIG -= qt CONFIG += no_qt LIBS += -lQt5Core -lQt5Gui -lQt5Widgets

提示:CONFIG -= qt是关键。它告诉 qmake 不要自动链接 Qt 库路径,强制你用LIBS +=显式指定——这样即使你本地装了 Qt5.12 和 Qt5.15,也能确保链接的是你qmake命令调用的那个版本。

2.3 main.cpp:只做三件事,拒绝任何业务逻辑

#include <QApplication> #include "gameview.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); // 1. 创建视图(非 MainWindow!) GameView view; view.setWindowTitle("Gem Matching - 毕业设计高分版"); view.resize(800, 600); // 2. 设置场景(必须在 show() 之前!) view.setScene(new GameScene(&view)); // 3. 显示并进入事件循环 view.show(); return app.exec(); }

注意:view.setScene(...)必须在view.show()之前调用。这是 Qt 的硬性规则——QGraphicsViewscene指针在show()后才开始响应鼠标事件,如果先show()setScene(),首次点击会丢失。


3. 核心逻辑实现:用 QGraphicsItem 构建可交换宝石,而不是用 QLabel 堆界面

3.1 GemItem:继承 QGraphicsItem,而非 QWidget 或 QLabel

很多初学者用QLabel放图片,再用QGridLayout排列——这完全违背 Qt 图形架构设计初衷。QLabel是 widget,走的是窗口系统绘制管线;而宝石交换需要像素级碰撞检测、平滑动画、Z 值叠层控制,必须用QGraphicsItem

gemitem.h关键代码:

#ifndef GEMITEM_H #define GEMITEM_H #include <QGraphicsItem> #include <QPainter> #include <QPixmap> class GemItem : public QGraphicsItem { public: enum GemType { RED, BLUE, GREEN, YELLOW, PURPLE, ORANGE }; explicit GemItem(GemType type, int row, int col, QGraphicsItem *parent = nullptr); // 必须重写这三个纯虚函数 QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; QPainterPath shape() const override; // 自定义接口 void setPosByGrid(int row, int col); // 按网格坐标设置位置(非像素!) void animateToGrid(int targetRow, int targetCol, int durationMs = 300); private: GemType m_type; int m_row, m_col; // 当前逻辑网格坐标(0~7, 0~7) static QPixmap s_pixmapCache[6]; // 静态缓存,避免重复加载 }; #endif // GEMITEM_H

boundingRect()返回的是逻辑坐标系下的矩形,不是像素尺寸:

QRectF GemItem::boundingRect() const { // 宝石网格单元格大小设为 64x64 像素,但逻辑坐标以 1.0 为单位 return QRectF(-32, -32, 64, 64); // 以中心为原点,半径32 }

参数说明:boundingRect()的返回值决定了QGraphicsScene的碰撞检测范围和重绘区域。设为(-32,-32,64,64)表示该 item 占据中心在(0,0)、宽高各 64 像素的矩形。后续setPosByGrid()会将(row,col)转换为场景坐标:x = col * 64,y = row * 64,这样所有宝石天然对齐网格。

3.2 GameScene:管理交换逻辑与连消判定,用 QVector 而非 QMap 存储宝石

gamescene.h中定义数据结构:

class GameScene : public QGraphicsScene { Q_OBJECT public: explicit GameScene(QObject *parent = nullptr); // 交换两个宝石(核心业务入口) void swapGems(int row1, int col1, int row2, int col2); // 执行消除:返回被消除的宝石列表 QList<GemItem*> eliminateMatches(); signals: void scoreChanged(int newScore); void gameOver(); private: // 用二维 QVector 存储,比 QMap 更快且内存连续 QVector<QVector<GemItem*>> m_grid; // [row][col] int m_score{0}; // 辅助函数 bool hasMatchAt(int row, int col); void markMatches(QSet<QPoint>& matches); void collapseAndRefill(); };

m_grid初始化在构造函数中:

GameScene::GameScene(QObject *parent) : QGraphicsScene(parent) { // 初始化 8x8 网格 m_grid.resize(8); for (int i = 0; i < 8; ++i) { m_grid[i].resize(8); for (int j = 0; j < 8; ++j) { GemItem::GemType type = static_cast<GemItem::GemType>( qrand() % GemItem::ORANGE + 1); // 随机类型,避开 0(RED=1) m_grid[i][j] = new GemItem(type, i, j); m_grid[i][j]->setPosByGrid(i, j); addItem(m_grid[i][j]); } } }

注意:qrand()在 Qt6 中已被弃用,但毕业设计普遍用 Qt5.15,此处合法。若你用 Qt6,请替换为QRandomGenerator::global()->bounded(1, 7)

3.3 交换逻辑:用 QGraphicsItemAnimation 实现平滑移动,而非 setPos() 瞬移

GemItem::animateToGrid()是高分关键:

void GemItem::animateToGrid(int targetRow, int targetCol, int durationMs) { // 计算目标场景坐标 qreal targetX = targetCol * 64.0; qreal targetY = targetRow * 64.0; // 创建动画对象(必须 new,由 scene 自动 delete) QPropertyAnimation *anim = new QPropertyAnimation(this, "pos"); anim->setDuration(durationMs); anim->setStartValue(pos()); anim->setEndValue(QPointF(targetX, targetY)); anim->setEasingCurve(QEasingCurve::OutInQuad); // 缓入缓出,更自然 anim->start(QAbstractAnimation::DeleteWhenStopped); // 更新逻辑坐标 m_row = targetRow; m_col = targetCol; }

逻辑说明:QPropertyAnimation动画对象必须new出来(不能栈分配),且传入"pos"字符串——这是 Qt 的元对象系统通过Q_PROPERTY暴露的属性名。setEasingCurve(QEasingCurve::OutInQuad)让移动先慢后快再慢,模拟真实物理惯性,比线性动画更专业。


4. 避坑指南:那些让毕设答辩当场卡壳的 4 个致命错误

4.1 现象:程序启动后宝石显示为全白方块,或根本看不到任何图形

原因GemItem::paint()中未调用painter->drawPixmap(),或s_pixmapCache未初始化。
解决:在gemitem.cppstatic QPixmap GemItem::s_pixmapCache[6]下方,添加初始化函数:

// 在 .cpp 文件顶部(.h 中已声明 static) QPixmap GemItem::s_pixmapCache[6]; void GemItem::initPixmaps() { static bool inited = false; if (inited) return; inited = true; QStringList paths = { ":/images/red.png", ":/images/blue.png", ":/images/green.png", ":/images/yellow.png", ":/images/purple.png", ":/images/orange.png" }; for (int i = 0; i < 6; ++i) { s_pixmapCache[i] = QPixmap(paths[i]).scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation); } }

并在main.cppQApplication创建后、view.show()前调用:

GemItem::initPixmaps(); // 必须在任何 GemItem 构造前调用!

4.2 现象:鼠标点击交换后,宝石位置错乱,甚至飞出屏幕

原因QGraphicsView默认启用了cacheBackground,而QGraphicsSceneaddItem()顺序影响 Z 值,未显式设置setZValue()
解决:在GameScene构造函数中addItem()后,统一设置 Z 值:

for (int i = 0; i < 8; ++i) { for (int j = 0; j < 8; ++j) { m_grid[i][j]->setZValue(1); // 所有宝石在同一层 } } // 交换时临时提升被拖拽宝石的 Z 值 void GameScene::onGemDragStarted(GemItem *item) { item->setZValue(10); // 确保拖拽时在最上层 }

4.3 现象:编译通过,但运行时报错qt.qpa.plugin: could not find the qt platform plugin "linuxfb"

原因:你在 Linux 上用./gem-matching直接运行,但未设置LD_LIBRARY_PATH指向 Qt 的plugins/platforms/目录。
解决:打包时用linuxdeployqt(Qt 官方推荐工具),或手动复制:

# 假设 Qt 安装在 /opt/Qt5.15.2/5.15.2/gcc_64/ cp -r /opt/Qt5.15.2/5.15.2/gcc_64/plugins/platforms ./platforms export LD_LIBRARY_PATH=$PWD/platforms:$LD_LIBRARY_PATH ./gem-matching

提示:毕业设计交付时,务必在README.md中写明 Linux 运行依赖,否则老师在 Ubuntu 上双击图标打不开,直接扣分。

4.4 现象:交换后连消判定失效,只消一次就停

原因eliminateMatches()中未递归调用collapseAndRefill(),或collapseAndRefill()未触发新一轮markMatches()
解决:采用 while 循环确保连消完整:

void GameScene::collapseAndRefill() { // 1. 删除标记为 null 的宝石 for (int i = 0; i < 8; ++i) { for (int j = 0; j < 8; ++j) { if (!m_grid[i][j]) { delete m_grid[i][j]; m_grid[i][j] = nullptr; } } } // 2. 逐列下落 for (int col = 0; col < 8; ++col) { int writeRow = 7; for (int readRow = 7; readRow >= 0; --readRow) { if (m_grid[readRow][col]) { GemItem *gem = m_grid[readRow][col]; m_grid[readRow][col] = nullptr; m_grid[writeRow][col] = gem; gem->setPosByGrid(writeRow, col); writeRow--; } } } // 3. 补充新宝石(必须在下落后!) for (int col = 0; col < 8; ++col) { for (int row = 0; row <= 7; ++row) { if (!m_grid[row][col]) { GemItem::GemType type = static_cast<GemItem::GemType>(qrand() % 6 + 1); m_grid[row][col] = new GemItem(type, row, col); m_grid[row][col]->setPosByGrid(row, col); addItem(m_grid[row][col]); } } } } // 主消除函数 QList<GemItem*> GameScene::eliminateMatches() { QSet<QPoint> matches; markMatches(matches); // 第一次扫描 QList<GemItem*> eliminated; for (const QPoint &p : matches) { GemItem *gem = m_grid[p.x()][p.y()]; if (gem) { eliminated << gem; m_grid[p.x()][p.y()] = nullptr; } } // 关键:只要消除了,就必须下落并重新扫描 if (!eliminated.isEmpty()) { collapseAndRefill(); // 递归检查新形成的连消 QList<GemItem*> newEliminated = eliminateMatches(); eliminated.append(newEliminated); } return eliminated; }

5. 高分加分项:三个让老师眼前一亮的进阶技巧

5.1 用 QSoundEffect 实现精准音效,而非 QMediaPlayer(避免线程阻塞)

QMediaPlayer适合播放长音频,但宝石交换、消除、连击等短音效必须用QSoundEffect——它工作在独立音频线程,不会卡住 GUI 事件循环。

GameScene中添加:

#include <QSoundEffect> class GameScene : public QGraphicsScene { // ... private: QSoundEffect m_swapSound; QSoundEffect m_matchSound; QSoundEffect m_comboSound; }; GameScene::GameScene(QObject *parent) : QGraphicsScene(parent) { // 加载音效(资源路径需提前在 .qrc 中注册) m_swapSound.setSource(QUrl("qrc:/sounds/swap.wav")); m_matchSound.setSource(QUrl("qrc:/sounds/match.wav")); m_comboSound.setSource(QUrl("qrc:/sounds/combo.wav")); // 预加载,避免首次播放延迟 m_swapSound.setLoopCount(0); m_matchSound.setLoopCount(0); m_comboSound.setLoopCount(0); }

触发时机:

void GameScene::swapGems(int row1, int col1, int row2, int col2) { // ... 交换逻辑 m_swapSound.play(); // 立即播放,无延迟 } void GameScene::eliminateMatches() { // ... 消除逻辑 if (comboCount > 2) { m_comboSound.play(); } else { m_matchSound.play(); } }

参数说明:setLoopCount(0)表示播放一次;QSoundEffect最大支持 10 个并发音效,足够应付连消爆发。

5.2 用 QSettings 实现本地存档,支持断点续玩

毕业设计常被问:“如果关机了,分数能保存吗?”——用QSettings一行代码搞定:

// 在 GameScene 析构或程序退出前保存 void GameScene::saveProgress() { QSettings settings("MyCompany", "GemMatching"); settings.setValue("score", m_score); settings.setValue("grid_state", serializeGrid()); // 自定义序列化函数 } // 在构造函数中读取 GameScene::GameScene(QObject *parent) : QGraphicsScene(parent) { QSettings settings("MyCompany", "GemMatching"); m_score = settings.value("score", 0).toInt(); QString gridStr = settings.value("grid_state", "").toString(); if (!gridStr.isEmpty()) { deserializeGrid(gridStr); } }

serializeGrid()示例(用 base64 编码简化):

QString GameScene::serializeGrid() { QByteArray data; QDataStream stream(&data, QIODevice::WriteOnly); for (int i = 0; i < 8; ++i) { for (int j = 0; j < 8; ++j) { int type = m_grid[i][j] ? static_cast<int>(m_grid[i][j]->type()) : 0; stream << type; } } return data.toBase64(); }

5.3 用 QPropertyAnimation 控制 UI 动效,让“连击”文字浮层有呼吸感

高分项目必有视觉反馈。在消除后动态创建QGraphicsTextItem

void GameScene::showComboText(int comboCount) { QGraphicsTextItem *text = new QGraphicsTextItem(QString("x%1!").arg(comboCount)); text->setDefaultTextColor(Qt::white); text->setFont(QFont("Arial", 24, QFont::Bold)); text->setPos(300, 200); // 居中上方 text->setZValue(100); addItem(text); // 缩放 + 透明度动画 QPropertyAnimation *scaleAnim = new QPropertyAnimation(text, "scale"); scaleAnim->setDuration(800); scaleAnim->setStartValue(0.5); scaleAnim->setEndValue(1.2); scaleAnim->setEasingCurve(QEasingCurve::OutBack); QPropertyAnimation *fadeAnim = new QPropertyAnimation(text, "opacity"); fadeAnim->setDuration(800); fadeAnim->setStartValue(0.0); fadeAnim->setEndValue(1.0); // 组合动画 QParallelAnimationGroup *group = new QParallelAnimationGroup; group->addAnimation(scaleAnim); group->addAnimation(fadeAnim); group->start(QAbstractAnimation::DeleteWhenStopped); // 2秒后自动删除 QTimer::singleShot(2000, [=]() { removeItem(text); delete text; }); }

这段代码的价值在于:它展示了你理解 Qt 动画系统的组合能力(QParallelAnimationGroup)、状态管理(DeleteWhenStopped)和资源清理意识——老师看到这个,就知道你不是 Ctrl+C/V 的水平。

我带毕设时发现,真正拉开差距的从来不是功能多少,而是每个技术点是否踩准了底层原理QGraphicsItemboundingRect()为什么必须返回逻辑坐标?QSoundEffect为什么比QMediaPlayer适合短音效?QSettingsvalue()为什么带默认值参数?这些细节,才是你答辩时老师追问的底气。别急着堆功能,先把swapGems()里的坐标转换、动画、音效、存档四件事闭环跑通——高分,就藏在这四次make && ./gem-matching成功的背后。希望帮到你。

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

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

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

立即咨询