
简介基于C语言与Qt5框架实现的打地鼠游戏完整源码源自课程期末大作业并获98分高分适合C语言初学者、Qt入门者以及需要课程设计或期末项目参考的高校学生。代码注释详细从随机生成地鼠、鼠标点击判定到锤子动画切换均有清晰实现界面布局完整并内置多张游戏图片素材功能完善、操作简单新手也能快速读懂。资源包共22个文件以cpp源文件、h头文件、ui界面文件、pro工程配置和qrc资源文件为主另有png、jpg等图片素材整体仅226KB轻量便于部署。目前已有50人浏览学习。解压后可直接用Qt5打开工程编译运行还可按注释调整游戏难度或替换素材作为高分课程设计模板非常实用。1. 用 Qt5 做打地鼠一份 98 分大作业到底哪里值钱很多同学看到“基于 C 语言的 Qt5 实现打地鼠”第一反应是这不过是个鼠标点点戳戳的小游戏能有多难但真把这份源码拆开你会发现它把 Qt5 里最容易被老师追问的三块硬骨头全占了QGraphicsScene 场景管理、QGraphicsItem 自定义图元、QTimer 驱动状态切换的游戏循环。对课程设计和期末大作业来说功能做完只是及格线真正拉开分差的是“项目里有没有能讲出原理的模块”。这套源码能拿 98 分靠的不是地鼠画得好看而是场景、图元、事件处理分层清晰每个类职责单一注释也跟得上。适合两类人一是期末大作业想快速交付、不想从零搭框架的二是一直搞不懂 Qt 图形视图框架Graphics View到底怎么落地的新手。下面我按工程结构、核心机制、踩坑记录和验证扩展四条线把它讲透。2. 拆解 beatMouse 工程每个文件在 Qt5 游戏里负责什么2.1 从 main.cpp 到 mainwindow程序入口与 UI 骨架先看工程最外层。源码里beatMouse.pro是 qmake 工程文件能看出来作者是 Qt 老手依赖收得干净。拿到手第一件事不是急着跑而是打开 pro 确认 Qt 模块是不是齐全。这份工程依赖widgets和multimedia按 Qt5 的标准写法应该长这样QT core gui widgets multimedia TARGET beatMouse TEMPLATE app SOURCES main.cpp \ mainwindow.cpp \ myscene.cpp \ myitem.cpp \ handler.cpp HEADERS mainwindow.h \ myscene.h \ myitem.h \ handler.h FORMS mainwindow.ui RESOURCES img.qrc逻辑说明QT 后面列的是编译期要链接的 Qt 模块缺widgets会直接报QtWidgets/QWidget: No such file or directory缺multimedia则是在代码里用到 QSound 时链接失败。TARGET决定生成的可执行文件名想改成自己学号命名就改这里。参数说明CONFIG c11可以加在 pro 里源码里如果用了 lambda 或auto就必须有这个配置不加也不报错但如果本机 Qt 版本是 5.6 之前默认标准可能是 C98碰到花式语法会编不过。再来看程序入口main.cpp。这个文件就是标准 Qt5 应用的骨架没有任何业务逻辑纯粹把界面拉起来#include QApplication #include mainwindow.h int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); }逻辑说明QApplication负责整个程序的事件循环所有鼠标点击、键盘按键、定时器超时都由它统一分发到各个 widget。wc.show()把主窗口显示出来exec()进入事件循环程序停在这里直到窗口关闭。参数说明argc/argv透传命令行参数调试时可以带上-style fusion切换 UI 风格方便排查样式相关的问题。mainwindow.ui是可视化界面文件真正的主窗口布局在 XML 里定义。Qt5 直接用QUiLoader加载或者由 ui 编译器在构建时生成ui_mainwindow.h在mainwindow.cpp里通过ui-xxx访问控件。这份源码的主窗口里通常有开始按钮、分数标签、倒计时标签和一个QGraphicsView视图区域游戏场景就嵌在这个 View 里。有一点很容易踩ui 文件里的对象名不能乱改改了之后代码里ui-label_score这类引用就跟不上。2.2 myitem 与 myscene地鼠、锤子与它们的舞台myscene.h和myitem.h是这份源码的精髓。MyScene继承QGraphicsScene负责管理场景里所有图元MyItem继承QGraphicsObject负责绘制地鼠和锤子。我拆工程时习惯先看类继承关系因为 Qt 里类的父子关系决定了事件如何传递。class MyScene : public QGraphicsScene { Q_OBJECT public: explicit MyScene(QObject *parent nullptr); protected: void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; private: QTimer *m_timer; int m_score; int m_timeLeft; };逻辑说明MyScene重写了三个鼠标事件按下时判断锤子是否敲在地鼠身上移动时让锤子图元跟随鼠标松开时把锤子抬起。分数和时间计数放在场景层因为只有场景能全局感知所有图元的状态。参数说明QGraphicsSceneMouseEvent里最关键的是scenePos()它返回鼠标在场景坐标系里的位置button()判断左键还是右键。建议先打印scenePos()再和地鼠的boundingRect()做比较这是碰撞检测的第一道门。MyItem则更直接class MyItem : public QGraphicsObject { Q_OBJECT public: explicit MyItem(QGraphicsItem *parent nullptr); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; protected: void mousePressEvent(QGraphicsSceneMouseEvent *event) override; private: bool m_isBeaten; int m_type; // 0地鼠 1锤子 2特效 };逻辑说明paint()里用QPainter绘制图元外观地鼠就用drawPixmap贴chui.png对应的鼠图锤子贴chui_up.png或chui_down.png。boundingRect()告诉场景这个图元占用的矩形区域Qt 的碰撞检测items()函数会遍历所有图元先用 boundingRect 做粗碰撞再调shape()做精细碰撞。参数说明如果boundingRect()返回的区域明显大于实际图片你会遇到“锤子离地鼠还有半个身位就判定打中”的错觉如果小于图片又会觉得打中了却没反应。所以boundingRect()要和图片的可见主体尺寸保持一致缩放系数建议按图片实际像素除以 2 来做。2.3 img.qrc 与资源打包图片为什么不走绝对路径游戏用到的资源全部集中在img.qrc代码里统一用:/前缀访问。这是 Qt 的资源系统编译时把图片二进制直接编进可执行文件。好处很明显换机器跑不用带着图片目录也不会出现相对路径对不上的问题。坏处是改图片要重新编译。这份源码的 qrc 文件长这样RCC qresource prefix/ filepic/background.png/file filepic/bg.png/file filepic/bg1.png/file filepic/chui.png/file filepic/chui_up.png/file filepic/chui_down.png/file filepic/beat.png/file /qresource /RCC逻辑说明prefix/表示资源挂载到根路径所以代码里写:/pic/chui_up.png。如果 prefix 改成/images那代码里就要写:/images/pic/chui_up.png。建议保持前缀简单别在 qrc 里嵌套过多目录层否则代码里每个路径都要跟着改。参数说明qrc 里路径是大小写敏感的Windows 上尤其容易犯错——文件名是Chui.png代码里写chui.png在 Linux 下编译直接被 RCC 拒掉在 Windows 下可能侥幸通过但换到 Linux 就翻车。2.4 handler.cpp事件处理器到底在忙什么handler.cpp是这份源码里容易被忽略但点名率很高的文件。从命名能看出来它是个独立的事件处理器职责是把输入事件翻译成游戏动作。我见过的课程设计里大多数人把逻辑直接塞在MyItem::mousePressEvent里但这套源码单独拆了一个 handler把“打中判定”和“分数结算”从图元里剥出来逻辑更清晰答辩时也更好讲。void Handler::handleHit(MyItem *target, int mouseX, int mouseY) { if (!target || !target-isActive()) { return; } QRectF r target-sceneBoundingRect(); if (r.contains(QPointF(mouseX, mouseY))) { target-setBeaten(true); m_scene-addScore(10); m_scene-playHitEffect(); } }逻辑说明这是一个典型的命中判定函数先用sceneBoundingRect()拿目标实际占用区域用contains()判断鼠标坐标是否落在区域内。命中后把地鼠标记为被打状态加 10 分再让场景播放打击特效。参数说明setBeaten(true)会触发update()重绘配合一个 300ms 的 QTimer 让地鼠变灰消失addScore(10)是自定义的信号入口最终会更新到主窗口的分数标签。这里的 10 分是单次命中分数想调成 5 或 20改 handler 和 scene 里对应的常量即可。把逻辑抽到 handler 而不是写死在地鼠类内部还有个实际好处后续如果加“连击加成”“限时翻倍”只需要在 handler 里加状态判断不需要动地鼠的绘制代码。这个设计思路答辩时能直接作为“模块解耦”的加分例。3. 场景与 Item 的协同让锤子真的“砸”到地鼠3.1 QGraphicsScene 坐标体系scenePos 与 item 坐标的换算很多第一次用 Graphics View 框架的人卡在“为什么我鼠标点到了items()却返回空”。根源在于坐标系没理清。QGraphicsScene 里同时存在三套坐标场景坐标scene coordinates、图元坐标item coordinates、视图坐标view coordinates。鼠标事件里event-scenePos()拿到的是场景坐标而event-pos()是图元坐标两者相差图元自身的位置偏移item-pos()。所以碰撞判断统一用场景坐标最省事QGraphicsItem *hitItem nullptr; QListQGraphicsItem * list scene-items(event-scenePos(), Qt::IntersectsItemShape); for (QGraphicsItem *it : list) { MyItem *mo qgraphicsitem_castMyItem *(it); if (mo mo-type() TYPE_MOLE) { hitItem mo; break; } }逻辑说明scene-items()是场景级碰撞查询第一个参数是坐标点第二个参数是命中模式。Qt::IntersectsItemShape表示用图元的真实形状做相交判断比Qt::IntersectsItemBoundingRect更精确。参数说明qgraphicsitem_cast要把 QGraphicsItem 转成 MyItem前提是自定义类实现了int type() const并返回唯一值。没实现 type() 的qgraphicsitem_cast会返回空指针所以这是必写的接口。实战里常见问题是地鼠在动画移动时boundingRect也随着位置变化。我一般会在地鼠的advance()或者定时器回调里强制调用prepareGeometryChange()告诉场景这个图元的几何区域要变了场景会清理旧的索引区域重新注册新的。少调这一步碰撞查询用的就是旧矩形结果自然是“打不中”。3.2 锤子三态动画与鼠标事件绑定锤子图元在源码里是跟着鼠标走的三张图chui_up.png、chui_down.png、chui.png对应三种状态待击、敲下、静止。核心逻辑在mouseMoveEvent里void MyScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { QPointF scenePos event-scenePos(); if (m_hammerItem) { m_hammerItem-setPos(scenePos.x() - m_hammerOffsetX, scenePos.y() - m_hammerOffsetY); } QGraphicsScene::mouseMoveEvent(event); }逻辑说明锤子图元的位置直接绑定到鼠标的scenePos()减去偏移量的目的是让锤柄握在鼠标光标的位置锤头对准被击目标。m_hammerOffsetX/Y是半锤子图像的宽高通常是图片中心点。参数说明如果锤子反应迟钝多半是这个 move 事件被 view 吞了需确认QGraphicsView的setMouseTracking(true)已开启否则只有按下鼠标时才有 move 事件。按下和释放事件里切换锤子图片是关键void MyScene::mousePressEvent(QGraphicsSceneMouseEvent *event) { m_hammerItem-setPixmap(QPixmap(:/pic/chui_down.png)); emit hammerStrike(event-scenePos()); QGraphicsScene::mousePressEvent(event); } void MyScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { m_hammerItem-setPixmap(QPixmap(:/pic/chui_up.png)); QGraphicsScene::mouseReleaseEvent(event); }逻辑说明按下瞬间换成锤子落下图片同时发出hammerStrike信号释放时换回抬手图片。这里有个技巧信号里带上scenePos()handler 接到信号后直接做碰撞判断而不是要QGraphicsSceneMouseEvent减少了模块耦合。参数说明为了让画面更跟手建议在mousePressEvent里直接调用一次items()碰撞检测释放时不再重复检测否则一次击中会统计两次分数。3.3 分数、时间与开始结束状态机游戏循环靠QTimer驱动。源码里场景持有一个定时器每隔一段时间随机生成一只地鼠同时另一个定时器做倒计时。这样做的好处是游戏逻辑和 UI 刷新解耦定时器信号发出后场景只管更新状态由 Qt 事件循环决定何时重绘。m_timerGenerate new QTimer(this); connect(m_timerGenerate, QTimer::timeout, this, MyScene::onGenerateMole); m_timerGenerate-start(800); m_timerCountdown new QTimer(this); connect(m_timerCountdown, QTimer::timeout, this, MyScene::onTimeUpdate); m_timerCountdown-start(1000);逻辑说明onGenerateMole随机在地鼠洞里创建一个图元onTimeUpdate把剩余时间减 1归零时停止两个定时器并发出游戏结束信号。参数说明生成间隔 800ms 是默认值课堂上演示不会太紧张正式运行建议加随机因子比如start(600 qrand() % 500)否则地鼠出现的节奏永远是固定的玩两局就摸清规律了。地鼠存活时间一般取 12001800ms到点没被打中自动消失并扣掉一部分分数这个惩罚机制在源码注释里写的 98 分作业评分点中经常被提到——它能体现出“功能完整性”之外的游戏平衡设计。状态切换用枚举最清晰READY → PLAYING → END。开始按钮只在 READY 状态可用游戏中禁用结束后重新激活。答辩时老师常问“如果游戏没结束前点了关闭按钮怎么办”答案是重写closeEvent提示“游戏正在进行确定退出”这是很多大作业缺失的一个交互细节。void MainWindow::closeEvent(QCloseEvent *event) { if (m_scene-state() PLAYING) { QMessageBox::warning(this, 提示, 游戏正在进行确定要退出吗); // 不做 event-accept()默认取消关闭 return; } event-accept(); }逻辑说明游戏过程中关闭窗口弹提示确认避免玩家误点。参数说明closeEvent里什么都不做时默认行为是取消关闭只有调用event-accept()才会真正退出。这段代码放在mainwindow.cpp里是课程设计答辩时最能主动展示的细节之一。4. 避坑Qt5 打地鼠从编译到运行的 5 个翻车点4.1 图片资源加载失败界面一片空白现象程序能运行窗口正常但背景、地鼠、锤子全部不显示控制台输出QImage: invalid format或Failed to load。原因多半是 qrc 里路径和实际文件路径对不上或者图片格式不支持。源码里同时出现了chui_up.png和chui_up.jpg说明作者处理过格式兼容但如果 qrc 里写的是pic/chui_up.png而磁盘上同名文件其实是 jpg 内容RCC 不会校验内容格式运行时才暴露。解决把 qrc 里所有文件路径逐个核对统一用:/前缀访问图片尽量转成 PNG 格式JPG 在带透明通道的场景里容易出现黑底。我一般还会在代码里加一层保护加载失败时打印完整路径QPixmap pix(:/pic/chui.png); if (pix.isNull()) { qDebug() Failed to load :/pic/chui.png; qDebug() QDir::currentPath(); }这样至少能明确是资源名写错还是文件不存在不用猜。4.2 锤子敲下去地鼠却不消失现象鼠标点击有锤子落下的动画分数也有变化但地鼠永远留在原地。原因碰撞检测用的是item-pos()位置而地鼠图元在场景里可能嵌套了父图元pos()拿到的是相对父亲的坐标不是场景坐标。用相对坐标去和鼠标的scenePos()比较永远对不上。解决统一用item-scenePos()拿场景坐标或者直接调用scene-items(scenePos)让 Qt 自己完成坐标转换QListQGraphicsItem * items scene-items(event-scenePos(), Qt::IntersectsItemShape); for (QGraphicsItem *item : items) { if (item-data(0).toString() mole) { // 命中 break; } }用data(0)做类型标识比type()更灵活代码里不用为每个新图元类型定义一个枚举值。4.3 中文注释乱码或编译报错现象源码里中文注释在 Windows 上显示成乱码或者编译报unexpected character。原因Qt5 源码必须保存成 UTF-8 编码Windows 记事本默认 ANSI 保存会出问题。源码里注释量大一旦编码不对整个文件的中文字符串都会变成非法字符。解决用 Qt Creator 打开工程确认文件编码是 UTF-8。如果已经是乱码文件用 Notepad 批量把编码转成 UTF-8 无 BOM 格式再检查代码里所有QString::fromUtf8(中文)是否被写成了中文后者在 MSVC 编译器下要加QStringLiteral。最省事的办法把源码里所有中文提示文字统一收进一个cfg.h全部用 UTF-8 保存编译时加QTextCodec::setCodecForLocale兜底。4.4 Qt4 写法混入 Qt5编译一堆 deprecated 警告现象QGraphicsScene::setItemIndexMethod在 Qt4 是常规调用Qt5 变成过时 API编译时刷屏deprecated警告甚至有些符号直接没了。原因这套源码在 Qt5 下编写但可能参考了 Qt4 年代的教程混入了老式 API。最常见的有QUrl::fromEncoded、QSound::play(xxx.wav)、qrand()在 Qt5 里还能用但标准做法已经换成QRandomGenerator::global()-bounded()和QSoundEffect。解决我拿到源码后第一步就是清理编译警告快捷键 CtrlShiftW。逐个替换老式写法。qrand()在 Qt5.15 里被标记为 deprecated换成int interval QRandomGenerator::global()-bounded(400, 1200); m_timerGenerate-start(interval);用QRandomGenerator的好处是真随机数qrand()如果不设种子每次启动生成的随机序列完全一样游戏节奏就固定了。4.5 项目能编过但打包到别的电脑上运行报缺 Qt5Core.dll现象把exe拷到同学电脑上双击弹窗提示缺Qt5Core.dll、Qt5Widgets.dll。原因Qt5 程序运行时要找到对应的 Qt 动态库和平台插件。开发机装了 Qt 所以正常裸机没有这些 DLL。解决用windeployqt自动部署。在 Qt 的命令行环境里执行cd /d 你的项目构建目录 windeployqt beatMouse.exe它会自动把需要的 Qt DLL 和 platforms 插件目录复制到 exe 同目录。这个坑在期末大作业验收时特别常见答辩现场有用 U 盘插到教室电脑的能直接双击运行演示印象分会高很多。注意用控制台版 Qt 编译出来的程序如果开着 console 窗口需要把 pro 里加CONFIG console否则双击跑出个黑色终端观感很差。5. 验证与扩展把 98 分作业做成能主动演示的项目5.1 两个手工验证技巧答辩前必做大作业答辩时间一般只有五分钟演示过程必须可控。我习惯准备两个预设状态一是游戏开始瞬间用固定种子生成地鼠位置让地鼠出现在正中间方便演示打中特效二是把倒计时调到 5 秒现场展示结束后分数结算和排行榜弹窗。做法是把生成逻辑抽出来支持外部指定位置void MyScene::debugSpawnMoleAt(int x, int y) { MyItem *mole new MyItem(TYPE_MOLE); mole-setPos(x, y); addItem(mole); }答辩时用快捷键触发debugSpawnMoleAt(200, 150)保证演示不会因随机生成出现“半天地鼠不冒头”的尴尬。5.2 加两个低成本功能连击与音效连击逻辑放在 handler 层不动图元代码void Handler::onHit(MyItem *mole) { if (m_lastHitTime.msecsTo(QTime::currentTime()) 1000) { m_combo; m_lastHitTime QTime::currentTime(); } else { m_combo 1; m_lastHitTime QTime::currentTime(); } m_score 10 * m_combo; }逻辑说明1 秒内连续击中分数递增超过 1 秒重置连击。参数说明10 * m_combo可以改成 10、20、30 的递增序列配合 QSoundEffect 在每次连击时播放不同音调手感会立刻不一样。这个改动只动了 handler展示时明确说“我实现了连击加分系统”比“我做的跟别人一样”更有说服力。音效部分工程里已经有 multimedia 模块依赖#include QSoundEffect QSoundEffect *effect new QSoundEffect(this); effect-setSource(QUrl(qrc:/pic/beat.wav)); effect-setVolume(0.8f); effect-play();逻辑说明QSoundEffect 支持 wav 和部分 ogg 格式体积小适合即时音效。参数说明setVolume范围 0 到 10.8 是常见游戏音量的经验值。注意不要把 mp3 直接塞给 QSoundEffect很多 Qt5 发行版没打包 mp3 解码插件。拿到这套源码之后我最深的一个习惯是改任何一处逻辑先跑一遍debugSpawnMoleAt把地鼠位置固定再手动走三个状态——开始、打中、结束。从那以后我每次做 Qt 课程设计都强制自己先写一个 debug 入口再写正式逻辑。调试接口比功能本身更省时间这条希望帮到你。本文还有配套的精品资源点击获取