简介:一份面向计算机专业学生的 C++ 期末大作业/毕业设计参考项目,基于 Qt 框架实现陨石撞击飞机小游戏,覆盖界面渲染、玩家控制、陨石生成、碰撞检测、计分与生命值、难度递增等完整流程。压缩包共 79 个文件、34.93MB,主体为 8 个 C++ 源文件和 8 个头文件,配合 47 张 PNG 界面元素/场景图、5 张 JPG 背景、WAV 音效、ICO 图标,以及 .pro 工程文件、qrc/rcc 资源配置、ts 翻译文件和 README 说明,目录结构清晰,便于直接打开工程对照学习。项目源码均在本地编译调试通过,并附带文档说明,适合课程设计、期末作业或实战练习;已有 59 人学习/下载。资源从需求分析、游戏设计到具体实现均有完整呈现,可参考其资源分包、多关卡地图、玩家飞机与道具设计等模块化写法,对想提升 Qt/C++ 项目能力的学习者有较高参考价值。
1. 从期末大作业到完整游戏:QT陨石飞机项目到底在做什么
期末课程设计里,C++ 大作业的经典题目非常多,而基于 QT 的陨石撞击飞机游戏,几乎年年都能在计算机、软件、物联网专业里见到。它不需要 3D 引擎,不需要复杂算法,只要一个窗口、两个定时器和一组碰撞判断,就能把 C++ 课程里的类、容器、事件、绘图串成一个能现场演示的完整作品。这个项目真正考察的不是“算法多难”,而是你对 QT 事件循环、绘图刷新和对象生命周期有没有直观理解。适合第一次接触 QT 的课程设计初学者,也适合想快速把 GUI 项目跑起来的期末赶工人。
玩法一句话就能说清:键盘控制飞机左右上下移动,陨石不断从屏幕顶部下落,碰到即结束,存活时间越长分数越高。要把成绩做成“看一眼就是完整作品”,重点在三个地方——程序能不能稳定跑完演示、帧率顺不顺畅、状态切换时会不会闪退或崩溃。下面从选型到答辩,把这条路线完整过一遍。
2. 环境搭建与项目骨架:Qt 5.15.2/C++17 下跑通最小版本
2.1 选型:为什么我建议 Qt 5.15.2 + MSVC 2019 64 位
第一个决策点就是 Qt 版本。网上搜“qt 下载”“qt creator”能翻到 Qt 5.12、5.15、6.x 一堆版本。我一般建议期末项目锁在 Qt 5.15.2,原因很实际:它是 LTS 长期支持版本,网上搜“qt 5.15.2 配置”“msvc2019_64”能找到大量历史教程,你踩过的坑大概率已经有人贴过解决办法。Qt 6 虽然新,但模块划分和部分 API 有调整,评委老师如果用的是 Qt 5 环境,你自己升到 6 反而容易在演示时翻车。
编译器方面,最稳妥的是跟随 Qt 安装包里的 MSVC 2019 64 位组件,并把 Qt Creator 的构建套件(Kit)切到这一套。MSVC 和 MinGW 的差别在于,前者编译出的程序换到另一台 Windows 机器上,需要装有对应版本的 Visual C++ Redistributable 运行库;后者是 MinGW 运行时。期末答辩大概率要换电脑演示,统一用实验室或老师推荐的那套会省掉很多“为什么我这跑不起来”的尴尬。
注意:选 MSVC 套件时,Creator 里显示的编译器名称要和 Qt 库目录里的
msvc2019_64对应。装的是 2019 就用 2019,装 2022 却选 2019 的库,编出来的程序一样能跑,但链接时偶尔会跳警告,不建议逃课式组合。
2.2 pro 文件:四行配置决定你今晚几点睡
.pro文件是 qmake 的工程描述。我帮人看项目时,第一件事就是打开.pro,百分之六七十的编译报错都能在这定位到原因。一个陨石飞机项目的最小.pro长这样:
QT += core gui widgets CONFIG += c++17 TARGET = MeteorGame TEMPLATE = app SOURCES += main.cpp gamewindow.cpp HEADERS += gamewindow.h逻辑说明:第一行的widgets是 QWidget、QPainter、QRectF 这些类所在的模块。core和gui是 Qt 自带依赖,实际使用中QT += widgets会自动带上它们,写全是为了让人看清引了哪几个模块。CONFIG += c++17让编译器按 C++17 标准编译,QList、共享指针、结构化绑定这些特性都能用。TARGET是生成的 exe 名字,TEMPLATE = app表示这是一个可执行程序。
参数说明:如果你之后加了资源文件,比如图标、图片、音效,还要补RESOURCES += res.qrc。如果图片用到了QPixmap但.pro忘了写widgets,编译时不会立刻报错,等到链接阶段才会以“无法解析的外部符号”形式出现,新人很容易被这个误导。所以项目创建后第一件事,是确认.pro里这几行都没少。
2.3 最小骨架:让一个矩形先动起来
不要一上来就写飞机、陨石、分数,先把“窗口能显示、定时器能驱动、键盘能响应”这条最小链路跑通。我用一个矩形代替飞机,方向键控制它移动,这样能用最少代码验证 QT 的核心机制。
#include <QApplication> #include <QWidget> #include <QTimer> #include <QKeyEvent> #include <QPainter> class TestWindow : public QWidget { public: TestWindow() { setFixedSize(800, 600); setFocusPolicy(Qt::StrongFocus); QTimer* timer = new QTimer(this); connect(timer, &QTimer::timeout, this, [=] { if (leftPressed) posX -= 4; if (rightPressed) posX += 4; update(); }); timer->start(16); } protected: void keyPressEvent(QKeyEvent* e) override { if (e->key() == Qt::Key_Left) leftPressed = true; if (e->key() == Qt::Key_Right) rightPressed = true; } void keyReleaseEvent(QKeyEvent* e) override { if (e->key() == Qt::Key_Left) leftPressed = false; if (e->key() == Qt::Key_Right) rightPressed = false; } private: void paintEvent(QPaintEvent*) override { QPainter painter(this); painter.fillRect(posX, 500, 40, 30, Qt::blue); } bool leftPressed = false; bool rightPressed = false; int posX = 400; }; int main(int argc, char* argv[]) { QApplication app(argc, argv); TestWindow w; w.show(); return app.exec(); }逻辑说明:setFocusPolicy(Qt::StrongFocus)让窗口能收到键盘事件,很多初学者漏掉这句,按键怎么按都没反应。定时器每 16 毫秒触发一次 lambda,lambda 里根据按键状态更新posX,再调用update()请求重绘。paintEvent里只做一件事——把当前状态的画面画出来,不改变任何游戏数据。
参数说明:16毫秒约等于 60 FPS。这里用的是 bool 记录按键状态,而不是在 keyPressEvent 里直接移动,原因是按住方向键时系统会先触发一次 keyPress,再按固定频率触发 keyRelease 和 keyPress 的自动重复消息,如果在事件里移动,移动速度会忽快忽慢,而且无法同时响应多个按键。用状态记录是游戏键盘输入的常见做法,后续加 WASD 也只是多两个 bool 的事。
3. 核心玩法实现:陨石生成、飞机移动与碰撞检测
3.1 对象建模:Plane 类管移动,Meteor 类管坠落
游戏逻辑稍微复杂后,再全写在窗口类里就会变成“上帝类”。一个窗口类几百行、又管键盘又管绘制又管碰撞,期末答辩老师问“这个功能在哪个类”,你只能尴尬地指向一个大函数。常见的做法是给飞机和陨石各建一个普通 C++ 类,它们不继承 QWidget,只保存数据并提供一个绘制方法。
// plane.h #ifndef PLANE_H #define PLANE_H #include <QPointF> #include <QRectF> #include <QPainter> class Plane { public: void setPosition(qreal x, qreal y) { pos = QPointF(x, y); } void moveBy(qreal dx, qreal dy) { pos += QPointF(dx, dy); } // 返回飞机所在的碰撞矩形 QRectF rect() const { return QRectF(pos.x() - 20, pos.y() - 15, 40, 30); } void paint(QPainter* painter) const { painter->save(); painter->setBrush(Qt::cyan); painter->setPen(Qt::NoPen); painter->drawRect(rect()); painter->restore(); } private: QPointF pos; }; #endif // PLANE_H// meteor.h #ifndef METEOR_H #define METEOR_H #include <QRectF> #include <QPainter> #include <QRandomGenerator> class Meteor { public: static Meteor randomInside(int screenWidth) { Meteor m; m.pos.setX(QRandomGenerator::global()->bounded(screenWidth)); m.pos.setY(-m.r * 2); m.speed = QRandomGenerator::global()->bounded(2, 6); return m; } void fall() { pos.ry() += speed; } QRectF rect() const { return QRectF(pos.x() - r, pos.y() - r, 2 * r, 2 * r); } bool isOutOfScreen(int screenHeight) const { return pos.y() - r > screenHeight; } void paint(QPainter* painter) const { painter->save(); painter->setBrush(Qt::darkGray); painter->setPen(Qt::NoPen); painter->drawEllipse(rect()); painter->restore(); } private: QPointF pos; qreal r = 18; qreal speed = 3; }; #endif // METEOR_H逻辑说明:两个类都不依赖窗口,Plane通过moveBy改变坐标,Meteor通过fall让 y 坐标每帧增加。rect()是碰撞和绘制共用的数据源,飞机用 40x30 的矩形近似,陨石用正方形外框圈出圆,QRectF::intersects判断的是这两个框是否相交。
参数说明:randomInside用QRandomGenerator::global()->bounded生成随机数,第一个参数是陨石 x 范围,第二个是下落速度范围 2~6。把生成函数设计成静态工厂而不是构造函数里随机,是为了让代码更明确——你在窗口里调用Meteor::randomInside(width())时,一眼就知道这是在创建一颗随机陨石。qreal在大多数平台上是 double,不需要纠结。
3.2 游戏循环:QTimer 的 16 毫秒与 paintEvent 的配合
游戏循环的核心槽函数在窗口类里,每一帧做的事只有四件:读按键、更新飞机位置、更新所有陨石位置、检查碰撞。检查完再调用update(),由 QT 在合适时机触发paintEvent。
void GameWindow::updateFrame() { if (!running) return; // 1. 读取当前按下的方向键,每帧移动 4 像素 if (pressedKeys.contains(Qt::Key_Left) || pressedKeys.contains(Qt::Key_A)) plane.moveBy(-4, 0); if (pressedKeys.contains(Qt::Key_Right) || pressedKeys.contains(Qt::Key_D)) plane.moveBy(4, 0); if (pressedKeys.contains(Qt::Key_Up) || pressedKeys.contains(Qt::Key_W)) plane.moveBy(0, -4); if (pressedKeys.contains(Qt::Key_Down) || pressedKeys.contains(Qt::Key_S)) plane.moveBy(0, 4); // 2. 所有陨石向下落 for (Meteor& m : meteors) { m.fall(); } // 3. 自后向前遍历:出界则移除,碰撞则结束游戏 for (int i = meteors.size() - 1; i >= 0; --i) { if (meteors[i].isOutOfScreen(height())) { meteors.removeAt(i); continue; } if (plane.rect().intersects(meteors[i].rect())) { running = false; frameTimer->stop(); spawnTimer->stop(); update(); return; } } update(); }逻辑说明:pressedKeys是QSet<int>,记录当前所有处于按下状态的键名。这样设计的好处是同时按“右”和“上”时飞机能斜着走,而不是互斥。陨石遍历时从size() - 1向前删,避免删除中间元素导致后面的下标错位——这是 QList 遍历删除最容易踩的坑。碰撞命中后停掉两个定时器,界面停留在最后一帧,文字提示等状态管理放下一章做。
参数说明:移动量 4 像素对应 16ms 帧间隔,理论上每秒 250 像素。如果你把定时器改成 8ms 或陨石速度调大,移动量和碰撞体积也要联动调整。这里更工程的做法是基于物理时间计算位移,我在第 5 章的卡顿问题里会细讲。
3.3 碰撞检测:矩形相交与圆心距离怎么选
前面代码里用的是矩形相交,这是最简单也最稳定的方式:QRectF::intersects是 Qt 内置方法,运算开销小。但陨石是圆,飞机的矩形四角在视觉上并没有物体,用外接圆做检测在某些角度下判定更符合直觉:
bool hitByCircle(const QPointF& center1, qreal r1, const QPointF& center2, qreal r2) { qreal dx = center1.x() - center2.x(); qreal dy = center1.y() - center2.y(); qreal minDist = r1 + r2; return dx * dx + dy * dy <= minDist * minDist; }逻辑说明:判断两圆是否相交,只需比较圆心距离和半径和。代码里用平方和比较,省掉了一次sqrt开方运算,碰撞检测每帧要做几十次,这个优化虽然渺小但写法更干净。圆的圆心可以从rect().center()拿到,半径就是rect().width() / 2。
参数说明:矩形检测适合“飞机”这种大体接近矩形的物体,圆形检测适合陨石。期末项目完全可以把两个方法都写出来,飞机用矩形、陨石用圆,然后组合判断:先判断飞机矩形是否包含陨石圆心,再判断圆心到矩形四条边的最短距离是否小于半径。这个组合法在答辩时能多讲一分钟,属于不会扣分还能加分的部分。不要做的是每帧创建一堆 QRectF 临时对象然后用指针管理,让代码绕得看不懂。
4. 画面表现与游戏状态:从“能跑”到“像个游戏”
4.1 QPainter 绘图:抗锯齿、贴图和视觉层次
paintEvent是整个游戏唯一允许出现 QPainter 绘图的地方。很多人把游戏逻辑写在里面,然后发现程序忽快忽慢——因为重绘频率不固定,逻辑被重绘次数绑架了。绘图函数里只做三件事:按状态画背景、画所有游戏对象、画 UI 文字。
void GameWindow::paintEvent(QPaintEvent*) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); // 画背景:上半部分深蓝渐变,模拟星空 QLinearGradient bg(0, 0, 0, height()); bg.setColorAt(0, QColor(10, 10, 30)); bg.setColorAt(1, QColor(30, 30, 60)); painter.fillRect(rect(), bg); // 画飞机(贴图版) painter.setRenderHint(QPainter::SmoothPixmapTransform); painter.drawPixmap( QPointF(plane.rect().left(), plane.rect().top()), planePixmap); // 画所有陨石 for (const Meteor& m : meteors) { m.paint(&painter); } // 画分数 painter.setPen(Qt::white); painter.setFont(QFont("Microsoft YaHei", 14)); painter.drawText(20, 40, QString("分数: %1").arg(score)); }逻辑说明:setRenderHint(QPainter::Antialiasing)让圆形和斜线边缘平滑,否则陨石会呈锯齿状。QLinearGradient用两点颜色做一个星空渐变,比纯色背景更有“游戏感”。drawPixmap之前要把图片加载成成员变量planePixmap,不要在绘图函数里重复QPixmap::load,磁盘 IO 会让帧率断崖式下跌。
参数说明:飞机贴图尺寸最好预缩放到 40x30 左右,和碰撞矩形保持一致,否则会出现“图片被陨石碰到了但判定没触发”或反之的违和感。drawText的坐标是文字左上角,QPainter 的坐标原点在屏幕左上角、y 轴向下,很多习惯数学坐标系的人会把文字画出屏幕外,这也是常见的低级坑。
4.2 状态机:开始、暂停、结束三种界面怎么切换
游戏逻辑逐渐丰富了,最迟在这里必须引入状态。常见做法是用一个枚举变量GameState保存当前状态,主循环每帧先看状态再决策,只有Running状态才更新逻辑。
enum class GameState { Ready, Running, Paused, GameOver }; void GameWindow::updateFrame() { if (state != GameState::Running) return; // ...移动、下落、碰撞检测逻辑... if (hit) { state = GameState::GameOver; frameTimer->stop(); spawnTimer->stop(); update(); } } void GameWindow::keyPressEvent(QKeyEvent* e) { if (e->key() == Qt::Key_Space) { if (state == GameState::Running) { state = GameState::Paused; frameTimer->stop(); } else if (state == GameState::Paused) { state = GameState::Running; frameTimer->start(); } else if (state == GameState::GameOver) { resetGame(); } update(); return; } pressedKeys.insert(e->key()); } void GameWindow::paintEvent(QPaintEvent*) { // ...绘图... if (state == GameState::Ready) { painter.drawText(center, "按空格开始"); } else if (state == GameState::Paused) { painter.drawText(center, "游戏暂停,按空格继续"); } else if (state == GameState::GameOver) { painter.drawText(center, QString("游戏结束,得分 %1,按空格重开").arg(score)); } }逻辑说明:状态枚举放在GameWindow的头部或者单独的头文件里。Ready时定时器不启动,画面只画静态的初始场景;Running时主循环全速运行;Paused停掉定时器但保留画面,避免重绘数据不一致;GameOver后空格键调用resetGame()清空陨石、归零分数,重新进入Ready。这样设计后,每个函数入口都先看状态,状态越多越不会互相干扰。
提示:
QTimer::stop()不会销毁定时器,它只是不再触发 timeout。可以反复 start 和 stop,比每次 new 一个 QTimer 安全得多。
4.3 手感细节:键位映射、陨石密度和音效
陨石生成逻辑我在第 3 章提到用spawnTimer定时生成,间隔 800ms。实际玩起来你会发现 800ms 太均匀,几分钟后节奏就着摸透了。常见调整是让生成间隔随时间递减,比如每 30 秒把间隔从 800 降到 500,难度曲线就出来了:
spawnTimer->setInterval(qMax(400, 800 - playSeconds * 10));参数说明:qMax保证间隔最短不会超过 400ms,否则满屏陨石直接没法玩。下落速度也可以从 2~6 均匀分布调整为随游戏时间推进整体上调,这两处参数配合,评委现场试玩时会明显感到“有难度设计”。
音效用QSoundEffect播放 wav 文件,生命周期要作为成员变量,局部变量一结束声音马上就没了。音效是典型的加分项,但还是那句——先保证碰撞和状态稳定,最后一天有空再顺手加。画面再好看,卡顿或崩溃都是前功尽弃。
5. 期末大作业避坑指南:编译报错与逻辑翻车现场
5.1 编译期报错:dependent '......\qt\5.15.2\msvc2019_64\include\qtwidgets'
现象:项目一开始还正常,某次加了图片资源后,Qt Creator 编译输出栏突然冒出一行红色报错,类似:-1: error: dependent '..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwidgets' does not exist.,后面还跟着一堆找不到头文件的提示。
原因:这个报错翻译成人话是“Qt 找不到 widgets 这个头文件目录”。最常见的触发条件有三个:.pro里少了QT += widgets;项目目录路径太长,Windows 下超过 260 字符路径上限;换过 Qt 安装位置但打开的是旧的.pro.user文件,构建套件还指向旧路径。
解决:第一步,打开.pro确认QT += core gui widgets这一行存在。第二步,项目菜单里选“清理项目”,删掉 build 目录,重新 qmake 并构建。第三步,如果还是报错,把整个项目目录剪切到盘符根目录,比如D:\MeteorGame,路径从 200 多字符缩到 30 个字符内。多数依赖路径问题走到这一步就解决了。
# 命令行方式重建项目(比在 Creator 里把 build 文件夹右键删除更彻底) cd /d D:\MeteorGame D:\Qt\5.15.2\msvc2019_64\bin\qmake.exe MeteorGame.pro nmake clean nmake参数说明:上面假设你的 Qt 装在D:\Qt,编译器用的是 MSVC 对应的 nmake。用命令行构建的好处是能看到完整错误流,不会像 Creator 里只截一行。如果你用的是 MinGW 套件,最后一行要换成mingw32-make,别照抄。
5.2 中文乱码:MSVC 把源码当作本地编码读
现象:窗口标题、按钮文字、注释在别人电脑上打开显示成???或乱码,但在你自己机器上一切正常。
原因:Qt Creator 默认把源文件存成 UTF-8 编码;MSVC 编译时,如果源文件没有 BOM 头,会按系统本地代码页 GBK 去读,中文字符就被解释成别的字符了。
解决:.pro文件里加一行编译器选项,告诉 MSVC 把源文件按 UTF-8 处理:
msvc { QMAKE_CXXFLAGS += /utf-8 }逻辑说明:/utf-8会让 MSVC 同时把源文件读取和执行字符集设为 UTF-8,是中文 Qt 项目最省心的解法。如果你不想改.pro,备选方案是每次新建文件后先用 Qt Creator 右下角的编码选择器把文件转成 UTF-8 with BOM,这样 MSVC 也能识别。但 BOM 在部分 Git 和评审工具里会产生不可见字符,统一用/utf-8更干净。
5.3 碰撞漏检:陨石穿过飞机的黑匣子
现象:陨石下落速度调到 8 以上,肉眼清晰看到它从飞机身上“穿”了过去,但碰撞判定就是没触发。
原因:现在是一帧检测一次碰撞。假设飞机只有 30 像素高,陨石一帧下落 10 像素,飞机顶部到陨石底部的相对位移如果一帧内超过了飞机高度,上一帧和下一帧的采样点都会错过交集,表现为穿模。
解决:给快速物体做“扫掠矩形”检测。把陨石上一帧位置和当前帧位置连成一个更大的矩形,再用这个大矩形和飞机判断相交:
QRectF prevRect = meteor.rect(); // 上一帧的位置 meteor.fall(); // 移动 QRectF curRect = meteor.rect(); // 当前帧位置 QRectF swept = prevRect.united(curRect); // 扫掠矩形 if (swept.intersects(plane.rect())) { // 判定碰撞 }逻辑说明:united返回同时包含两个矩形的最小矩形,物理意义上等价于陨石在这两帧之间划过的路径范围。这个矩形比原来的大得多,所以能抓住高速穿过的漏检。
参数说明:扫掠矩形只对“移动的物体”有意义,飞机移动时也可以对飞机做同样的处理。代价是碰撞判定会略微偏向保守,比如陨石擦着边沿快速掠过时会提前几毫秒触发碰撞。游戏里这是可接受的误差,而且会让玩家觉得“判定很严”,不会骂穿模。这是黑匣子式玄学问题,尽早用扫掠矩形能少掉不少头发。
5.4 定时器与卡顿:QTimer 不是银弹
现象:游戏刚打开很流畅,拖动窗口、最小化再恢复后,飞机移动明显变快或画面卡顿,陨石位置像是“瞬移”。
原因:QTimer的 timeout 事件进入事件循环后,如果主线程被窗口拖动等操作阻塞,timeout 会被延迟触发。看起来表现为:一帧耗时变成 200ms,但游戏逻辑仍然按“每帧移动 4 像素”来算,恢复后时间被吞掉,物体位置比应该的位置偏小(看起来变慢);或者事件堆满后连续触发好几帧,看起来像瞬移。
解决:不要假设每帧耗时都是 16ms,用QElapsedTimer计算真实时间差,让位移和速度挂到时间上:
QElapsedTimer deltaTimer; deltaTimer.start(); // 在 updateFrame 中: qint64 elapsed = deltaTimer.elapsed(); qreal dt = (elapsed - lastFrameTime) / 1000.0; // 秒 lastFrameTime = elapsed; plane.moveBy(planeSpeed * dt * direction, 0);参数说明:planeSpeed单位现在是“像素/秒”,比如 240;dt单位是秒,一帧 16ms 时约 0.016。把dt乘进移动量里,程序慢就少移,快就多移,物理时间保持一致。期末报告里写“使用基于帧时间差的运动模型”,这行字比你在答辩现场说一百句“我优化过”都有说服力。不过要注意dt单帧上限,比如超过 0.05 秒就当作 0.05,否则从后台切回来那一下会一次性把几百像素的距离移完,玩家直接懵。
5.5 内存与崩溃:new 出来的对象没人回收
现象:游戏运行几分钟后内存持续上涨,关闭窗口时偶尔报段错误,或者 Qt 输出 “QObject: Do not delete grandchildren”。原因多半出在new上。
常见翻车现场是这样:每次生成陨石就Meteor* m = new Meteor(); meteors.append(m);,游戏结束时只clear()列表——但这是 QList 清空元素,不会 delete 指针,堆上的 Meteor 对象一个都没释放。这还只是泄漏,如果某个地方提前delete m后列表里还留着悬空指针,碰撞检测一访问就直接崩溃。
解决:普通 C++ 对象直接用QList<Meteor>存值拷贝,别用指针堆分配。如果陨石需要多态或有复杂的构造开销,用QList<std::shared_ptr<Meteor>>。所有 QObject 子类在构造函数里都要传入 parent,例如QTimer* timer = new QTimer(this),这样父窗口析构时它们会自动释放。
// 遍历删除时,自后向前,避免下标错位 for (int i = meteors.size() - 1; i >= 0; --i) { if (meteors[i].isOutOfScreen(height())) { meteors.removeAt(i); } }逻辑说明:这条规则一句话总结——谁创建谁负责,捞到篮子里的不一定都是菜。期末项目一般循环几千帧、生成上千颗陨石,不用智能指针的话,就算不崩溃,内存曲线也是一个让人尴尬的上扬直线。运行完看任务管理器内存还留在高位,答辩时被问到,你总不能说是陨石太重。
6. 让代码拿高分的进阶技巧:模块划分、文档写作与答辩准备
6.1 把游戏逻辑和界面绘制拆开:MVC 最小落地
如果你还有半天时间,把GameWindow里的陨石列表、分数、碰撞判定全部搬到一个名叫GameModel的普通 C++ 类里。GameWindow只做三件事:收集键盘输入、驱动model.updateFrame(dt)、在 paintEvent 里按model的数据绘制。这个 MVC 思路能让代码结构立刻从“大作业”变成“小项目”。答辩时老师问“你这个架构怎么设计的”,你可以指着GameModel说“核心逻辑不依赖 Qt Widgets,换到命令行或者服务器上也能跑”——这句话在期末答辩里就是一句话的“架构亮点”。
6.2 文档说明:需求分析、类图、测试三件套
源码和文档说明放在同一个目录,通常是一片几百字的“课程设计报告”,结构遵循学校模板就行。但有几处建议别省:需求分析里写玩法规则和功能列表,总体设计画一张类关系图,详细设计里把碰撞检测、定时器驱动这两个函数贴出来讲。截图要真实,放“运行中的游戏画面”和“碰撞生效瞬间”各一张,不要用网图。文档里代码和实际源码不一致,是答辩时被扣分最常见的原因。
6.3 答辩追问:三个必背问题和参考答法
第一个:为什么用 QTimer 而不用线程?答:游戏逻辑简单,主循环用定时器驱动足够,线程还要考虑跨线程访问界面,复杂度不值当。第二个:碰撞检测矩形和圆形哪个更准?答:圆周用圆心距离更符合视觉,矩形运算开销小,工程上按物体形状混用。第三个:游戏对象为什么不用 new?答:容器直接存储对象能自动管理内存,避免手动 delete 遗漏,也避免指针悬空。
期末项目里我见过最多的遗憾,不是算法不会写,而是窗口类里堆了所有逻辑,一演示就卡,一关闭就崩。你花两小时把状态机和对象生命周期理清楚,剩下的时候就会非常稳。这套路子我带人走了一遍又一遍,最后说一句:把文档截图留够,把代码目录整理干净,愿你的期末答辩不再翻车,希望帮到你。
本文还有配套的精品资源,点击获取