简介:这是一份基于QT与C++实现的打地鼠游戏完整源码,面向计算机相关专业的毕业设计、课程设计以及入门级项目开发学习者。项目在QT环境下编译运行,核心依托QT信号与槽机制完成界面与业务逻辑的关联,并支持动态调整地鼠出现速度等参数来改变游戏难度,适合用来理解事件驱动编程与图形界面交互的基本思路。压缩包共19个文件,约151KB,包含4个cpp源文件、3个h头文件、1个ui界面文件、1个pro工程文件与1个qrc资源文件,另有jpg、png图片素材及README说明文档,覆盖了从工程配置、界面布局到游戏逻辑与素材加载的完整结构。目前已有147人学习参考。源码经过严格测试,读者可在此基础上直接运行体验,也可通过修改参数、替换素材或扩展计分与关卡机制来完成二次开发,是学习QT项目组织与C++面向对象实践的实用参考。
1. 从零拆解一个 QT+C++ 打地鼠游戏:它到底能帮你拿到什么
如果你正在准备毕业设计或课程设计,手里只有一个模糊的方向,那"基于 QT+C++ 开发的打地鼠游戏"这个题目大概率已经在你候选清单里出现过。它看起来简单,但真正动手时你会发现:QT 界面设计怎么和游戏逻辑解耦?定时器怎么控制地鼠出现的节奏?鼠标点击怎么精确命中目标?这些问题不解决,代码写到一半就会变成一锅粥。我见过太多人从 GitHub 抄一份源码,改个标题就交差,结果答辩时被问"你的信号槽是怎么连接的"直接卡壳。这篇笔记不讲空话,我会把整个项目的技术选型、模块划分、核心代码、参数调优和踩坑记录全部摊开,让你既能自己跑通,也能在答辩时讲清楚每一行代码为什么这么写。适合有 C++ 基础、刚接触 QT 的在校生,也适合想用一个小项目练手 QT 信号槽和事件循环的开发者。
2. 技术选型与工程结构:为什么 QT+C++ 是打地鼠的最优解
2.1 为什么不用 Python 或 Java,偏要选 QT+C++
打地鼠游戏的核心需求其实就三条:界面要能实时刷新、鼠标点击要低延迟响应、定时器要稳定触发。Python 的 tkinter 做界面刷新会卡顿,Java Swing 写起来代码量大且部署麻烦。QT+C++ 的组合在这三点上都有天然优势:QT 的 QGraphicsView 框架自带场景管理和碰撞检测,QTimer 的精度在毫秒级,C++ 的信号槽机制让界面和逻辑彻底解耦。
从毕业设计的角度看,QT+C++ 还有一个隐性好处:答辩老师对这套技术栈的认可度高。它涉及的知识点足够多——面向对象、事件驱动、内存管理、UI 布局——但又不会像深度学习那样难以讲清楚。你可以在论文里写清楚每个类的职责、每个信号槽的连接关系,这些都是实打实的工作量。
另一个现实因素是环境配置。QT Creator 在 Windows 和 Ubuntu 上都有成熟的安装包,配合 MSVC 或 MinGW 编译器,从下载到跑通第一个窗口程序不超过半小时。相比之下,如果你选 JavaFX 或 Electron,光是配环境就能耗掉一整天。
2.2 工程目录怎么划分才不会被老师挑刺
我一般会把项目拆成四个模块:core(游戏核心逻辑)、ui(界面文件)、resources(图片和音效)、utils(工具类)。这样划分的好处是,答辩时你可以指着目录说"这是 MVC 模式的变体,core 对应 Model,ui 对应 View,信号槽就是 Controller"。
具体目录结构如下:
MoleGame/ ├── MoleGame.pro # QT 工程文件 ├── main.cpp # 程序入口 ├── core/ │ ├── gamecontroller.h/cpp # 游戏控制器,管理状态和定时器 │ ├── mole.h/cpp # 地鼠对象,继承 QGraphicsItem │ └── scoremanager.h/cpp # 分数管理,读写本地记录 ├── ui/ │ ├── mainwindow.h/cpp # 主窗口 │ ├── mainwindow.ui # QT Designer 布局文件 │ └── gameoverdialog.h/cpp # 游戏结束弹窗 ├── resources/ │ ├── images/ # 地鼠、锤子、背景图 │ ├── sounds/ # 击中音效、背景音乐 │ └── resources.qrc # QT 资源文件 └── utils/ └── config.h # 全局常量配置在.pro文件里,你需要把QT += widgets和QT += multimedia加上,后者用于播放音效。如果你用的是 QT 5.15.2 配 MSVC2019_64,记得在.pro里加CONFIG += c++17,否则有些新特性用不了。
提示:资源文件
.qrc的路径不要用中文,否则在某些 QT 版本下会出现资源加载失败的问题。这是血泪经验,我当初用"地鼠图片"做文件夹名,调试了两个小时才发现是编码问题。
2.3 核心类的职责划分与信号槽设计
GameController是整个项目的中枢。它持有QTimer对象,负责控制地鼠出现的频率和停留时间。Mole类继承自QGraphicsItem,重写paint()和boundingRect()方法,同时开启QGraphicsItem::ItemIsSelectable标志来接收鼠标事件。
信号槽的连接关系是这样的:Mole被点击时发出moleClicked(bool hit)信号,GameController接收到信号后更新分数,并通过scoreChanged(int newScore)信号通知MainWindow刷新界面显示。这种设计的好处是,Mole不需要知道分数存在哪里,MainWindow也不需要知道地鼠是怎么被点击的。
// gamecontroller.cpp 中的关键连接代码 connect(mole, &Mole::moleClicked, this, &GameController::onMoleClicked); connect(this, &GameController::scoreChanged, mainWindow, &MainWindow::updateScoreLabel); connect(this, &GameController::gameOver, mainWindow, &MainWindow::showGameOverDialog);参数说明:moleClicked的bool参数表示是否击中(有些地鼠是炸弹,点中扣分)。scoreChanged传递整数分数。gameOver不带参数,触发弹窗即可。
3. 从零跑通最小可玩版本:代码、参数与调试步骤
3.1 环境搭建与工程创建的具体命令
先确认你的 QT 版本和编译器。打开 QT Creator,点击"文件 → 新建文件或项目 → Application (Qt) → Qt Widgets Application"。项目名填MoleGame,路径不要有中文和空格。在"构建套件"页面,Windows 用户选Desktop Qt 5.15.2 MSVC2019 64bit,Ubuntu 用户选Desktop Qt 5.15.2 GCC 64bit。
创建完成后,在.pro文件末尾追加:
QT += widgets multimedia CONFIG += c++17 RC_ICONS = resources/images/appicon.icomultimedia模块用于QSoundEffect播放击中音效。RC_ICONS是 Windows 下设置 exe 图标的写法,Ubuntu 下不需要这行。
如果你在 Ubuntu 下编译时报错cannot find -lGL,执行sudo apt install libgl1-mesa-dev即可。这是 QT 图形渲染的依赖库,新手很容易卡在这里。
3.2 地鼠类的实现:paint 与鼠标事件
Mole类继承QGraphicsItem,核心是三个方法:boundingRect()返回地鼠的矩形区域,paint()绘制地鼠图片,mousePressEvent()处理点击。
// mole.h class Mole : public QGraphicsItem { public: Mole(QGraphicsItem *parent = nullptr); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; void setMoleType(MoleType type); // 普通地鼠或炸弹 signals: void moleClicked(bool hit); protected: void mousePressEvent(QGraphicsSceneMouseEvent *event) override; private: MoleType m_type; QPixmap m_pixmap; };// mole.cpp 中的 paint 实现 void Mole::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option); Q_UNUSED(widget); painter->drawPixmap(0, 0, m_pixmap.scaled(80, 80, Qt::KeepAspectRatio, Qt::SmoothTransformation)); } void Mole::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (event->button() == Qt::LeftButton) { bool hit = (m_type == MoleType::Normal); emit moleClicked(hit); event->accept(); } }逻辑说明:paint()里的scaled()参数控制地鼠显示大小,80x80 是经过测试后比较合适的值,太小点不中,太大遮挡其他地鼠。mousePressEvent只响应左键,避免右键误触。emit moleClicked(hit)把是否击中的判断交给控制器处理,Mole类本身不关心分数变化。
参数说明:boundingRect()必须返回和paint()绘制区域一致的矩形,否则点击检测会偏移。我一般写成return QRectF(0, 0, 80, 80);,和缩放后的尺寸保持一致。
3.3 游戏控制器的定时器与难度曲线
GameController用两个QTimer:一个控制地鼠出现间隔,一个控制地鼠停留时间。难度曲线通过动态调整这两个定时器的间隔来实现。
// gamecontroller.cpp 中的定时器初始化 m_spawnTimer = new QTimer(this); m_hideTimer = new QTimer(this); m_spawnTimer->setInterval(1000); // 初始 1 秒出现一只 m_hideTimer->setInterval(1500); // 初始停留 1.5 秒 connect(m_spawnTimer, &QTimer::timeout, this, &GameController::spawnMole); connect(m_hideTimer, &QTimer::timeout, this, &GameController::hideMole); // 难度递增:每得 10 分,出现间隔减少 50ms,最低 400ms void GameController::updateDifficulty() { int newInterval = qMax(400, 1000 - (m_score / 10) * 50); m_spawnTimer->setInterval(newInterval); }逻辑说明:spawnMole()在场景中随机位置创建一个Mole对象,并启动m_hideTimer。如果m_hideTimer超时时地鼠还没被点击,就调用hideMole()将其从场景中移除。updateDifficulty()在每次得分后调用,实现难度递增。
参数说明:setInterval()的单位是毫秒。初始 1000ms 是给新手的反应时间,最低 400ms 是经过测试后普通玩家能反应的极限。如果你想让游戏更简单,可以把最低值调到 600ms。
注意:
QTimer的精度受系统调度影响,实际间隔可能比设定值大 10-20ms。对于打地鼠游戏来说这个误差可以接受,但如果你要做音游,就需要换用QElapsedTimer做补偿。
3.4 主窗口的布局与分数显示
MainWindow使用QGraphicsView作为游戏区域,上方用QLabel显示分数和剩余时间。布局用QVBoxLayout嵌套QHBoxLayout实现。
// mainwindow.cpp 中的布局代码 QWidget *centralWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget); QHBoxLayout *infoLayout = new QHBoxLayout(); m_scoreLabel = new QLabel("分数: 0", this); m_timeLabel = new QLabel("时间: 60s", this); infoLayout->addWidget(m_scoreLabel); infoLayout->addStretch(); infoLayout->addWidget(m_timeLabel); m_scene = new QGraphicsScene(this); m_view = new QGraphicsView(m_scene, this); m_view->setFixedSize(800, 600); m_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); mainLayout->addLayout(infoLayout); mainLayout->addWidget(m_view); setCentralWidget(centralWidget);逻辑说明:setFixedSize(800, 600)固定游戏区域大小,避免窗口缩放导致地鼠位置错乱。关闭滚动条是因为场景大小和视图大小一致,不需要滚动。addStretch()把分数和时间标签推到两端。
参数说明:QGraphicsScene的场景范围用setSceneRect(0, 0, 800, 600)设置,和视图大小保持一致。地鼠的随机位置用QRandomGenerator::global()->bounded(0, 720)生成,留出 80 像素的边距防止地鼠被裁切。
4. 避坑与排查:那些让我熬夜的翻车现场
4.1 地鼠图片显示为黑块或无法加载
现象:编译通过,但游戏运行时地鼠位置显示为黑色方块,控制台输出QPixmap: It is not safe to use pixmaps outside the GUI thread。
原因:图片资源没有正确添加到.qrc文件中,或者QPixmap在非 GUI 线程中被创建。QT 的图形资源必须在主线程加载。
解决:在 QT Creator 中右键项目 → "Add New" → "Qt" → "Qt Resource File",把resources文件夹添加进去。然后在代码中用QPixmap(":/images/mole.png")加载,注意路径前的冒号。如果仍然失败,检查.qrc文件是否被添加到了.pro的RESOURCES变量中。
4.2 点击地鼠没有反应或点击位置偏移
现象:鼠标明明点在地鼠上,但分数不增加,或者点击地鼠旁边的空白区域反而得分。
原因:boundingRect()返回的矩形和paint()绘制的区域不一致。常见情况是paint()里用了scaled()缩放图片,但boundingRect()返回的是原始尺寸。
解决:确保boundingRect()返回的矩形和paint()中drawPixmap()的目标矩形完全一致。我一般会定义一个常量MOLE_SIZE = 80,两处都用这个常量。
// 统一尺寸定义 constexpr int MOLE_SIZE = 80; QRectF Mole::boundingRect() const { return QRectF(0, 0, MOLE_SIZE, MOLE_SIZE); } void Mole::paint(...) { painter->drawPixmap(0, 0, m_pixmap.scaled(MOLE_SIZE, MOLE_SIZE, ...)); }4.3 定时器叠加导致地鼠越出越多
现象:游戏运行一段时间后,屏幕上同时出现十几只地鼠,而且越来越多。
原因:spawnMole()中每次创建新地鼠时都new了一个QTimer,但没有在hideMole()中停止和销毁。旧定时器还在运行,导致地鼠不断累积。
解决:QTimer应该在GameController构造函数中创建一次,而不是每次生成地鼠时创建。hideMole()中只移除Mole对象,不操作定时器。
// 正确做法:定时器只创建一次 GameController::GameController(QObject *parent) : QObject(parent) { m_spawnTimer = new QTimer(this); m_hideTimer = new QTimer(this); // ... 连接信号槽 } // 错误做法:每次生成地鼠都 new 一个定时器 void GameController::spawnMole() { QTimer *timer = new QTimer(this); // 这行会导致定时器泄漏 // ... }4.4 游戏结束后分数没有重置
现象:点击"重新开始"后,分数标签仍然显示上一局的分数,而且新地鼠出现时分数继续累加。
原因:GameController的m_score变量没有在startGame()中重置为 0,或者MainWindow的m_scoreLabel没有刷新。
解决:在startGame()中显式重置所有状态变量,并发出scoreChanged(0)信号强制刷新界面。
void GameController::startGame() { m_score = 0; m_timeLeft = 60; emit scoreChanged(0); emit timeChanged(60); m_spawnTimer->start(); m_gameTimer->start(1000); // 倒计时定时器 }4.5 音效播放导致界面卡顿
现象:每次击中地鼠时,游戏界面会卡顿 0.5 秒左右,连续点击时卡顿更明显。
原因:使用了QMediaPlayer播放音效,它每次播放都需要重新加载文件。应该改用QSoundEffect,它专为低延迟音效设计。
解决:在.pro中添加QT += multimedia,然后用QSoundEffect加载音效文件。
QSoundEffect *hitSound = new QSoundEffect(this); hitSound->setSource(QUrl::fromLocalFile(":/sounds/hit.wav")); hitSound->setVolume(0.8); // 击中时调用 hitSound->play();参数说明:setVolume()的范围是 0.0 到 1.0。音效文件建议用.wav格式,.mp3在某些 QT 版本下解码会有延迟。
5. 进阶技巧:让这个项目从"能跑"变成"能打"
5.1 用状态机管理游戏生命周期
新手最容易犯的错误是把所有逻辑塞进MainWindow,用一堆if-else判断当前是"开始界面"还是"游戏中"还是"结束界面"。我建议引入一个简单的状态机,用枚举定义GameState,在GameController中统一管理状态切换。
enum class GameState { Ready, Playing, Paused, GameOver }; void GameController::setState(GameState newState) { if (m_state == newState) return; m_state = newState; switch (m_state) { case GameState::Playing: m_spawnTimer->start(); m_gameTimer->start(1000); break; case GameState::Paused: m_spawnTimer->stop(); m_gameTimer->stop(); break; case GameState::GameOver: m_spawnTimer->stop(); m_gameTimer->stop(); emit gameOver(m_score); break; default: break; } }这样做的好处是,所有状态切换集中在一处,调试时只需要看setState()的调用日志就能定位问题。答辩时你也可以把这个设计作为亮点讲——"我用了状态机模式来管理游戏流程"。
5.2 用 QSettings 保存最高分记录
毕业设计里加一个"最高分记录"功能,工作量不大但能体现完整性。QT 自带的QSettings可以跨平台读写配置文件,Windows 下写注册表,Ubuntu 下写~/.config目录。
// 保存最高分 QSettings settings("MyCompany", "MoleGame"); int highScore = settings.value("highScore", 0).toInt(); if (m_score > highScore) { settings.setValue("highScore", m_score); } // 读取最高分 int highScore = settings.value("highScore", 0).toInt(); m_highScoreLabel->setText(QString("最高分: %1").arg(highScore));参数说明:QSettings的构造函数接受组织名和应用名,这两个名字决定了配置文件的存储路径。value()的第二个参数是默认值,当键不存在时返回。
5.3 用 QPropertyAnimation 做地鼠冒头动画
静态显示地鼠太生硬,加一个从下往上冒头的动画会提升不少质感。QPropertyAnimation可以对QGraphicsItem的pos属性做插值。
QPropertyAnimation *animation = new QPropertyAnimation(mole, "pos"); animation->setDuration(200); animation->setStartValue(QPointF(x, y + 80)); animation->setEndValue(QPointF(x, y)); animation->setEasingCurve(QEasingCurve::OutBounce); animation->start(QAbstractAnimation::DeleteWhenStopped);参数说明:setDuration(200)表示动画持续 200ms。OutBounce缓动曲线会让地鼠冒头时有一个回弹效果,更生动。DeleteWhenStopped确保动画结束后自动释放内存。
5.4 打包发布:让老师双击就能玩
答辩时如果老师让你现场演示,你总不能打开 QT Creator 编译一遍。用windeployqt工具可以把依赖的 DLL 文件自动拷贝到发布目录。
# Windows 下在 QT 命令行工具中执行 windeployqt --release --no-translations MoleGame.exe执行后,把MoleGame.exe和生成的 DLL 文件夹一起打包成 zip。Ubuntu 下用linuxdeployqt做类似的事情。注意,打包后的文件夹里不要有中文路径,否则在某些电脑上会报错。
我一般会在答辩前一天,把打包好的游戏拷到 U 盘里,在自己的电脑上跑一遍,再去答辩教室的电脑上跑一遍。这个习惯帮我避免过两次现场翻车——一次是教室电脑没装 VC++ 运行库,另一次是屏幕分辨率不同导致窗口显示不全。
希望帮到你。
本文还有配套的精品资源,点击获取