简介:基于QT的陨石撞击飞机游戏设计与实现,是一份C++期末大作业的完整源码及文档说明,面向计算机相关专业学生和需要项目实战练习的初学者,可直接用于课程大作业或毕业设计参考。压缩包共79个文件,约34.93MB,包括8个C++源文件、8个头文件,以及47张PNG图片、5张JPG背景图、ICO图标、WAV音效等资源,覆盖了游戏界面显示、核心逻辑、素材资源和配置工程等模块,目录结构清晰。项目在导师指导下完成并通过评审,得分98分,所有源码均在本地编译并调试过,确保可运行;随附文档说明了需求分析、设计思路和实现细节,便于读者复现和二次开发。目前已有58人学习下载,适合希望掌握QT界面开发、C++游戏逻辑编写以及完整项目流程的学习者参考使用。
1. 为什么期末大作业选QT做陨石撞击飞机游戏:一门课能做完,还能把答辩讲清楚
期末C++大作业,不少同学做控制台图书管理、成绩排序这种黑框程序。不是不能做,而是答辩时只能念菜单,讲不出技术含量。基于QT的陨石撞击飞机游戏设计与实现属于图形界面加游戏逻辑的典型课设:玩法直观,但类设计、事件循环、碰撞检测、定时器动画全都要落真实代码,工作量一到两周,源码和文档说明也容易凑出完整交付物。接下来直接拆可复现方案:从对象设计、QT运行机制到C++源码怎么写,再到期末文档怎么组织、常见坑在哪。适合正在做QT课设、C++期末大作业,想避开控制台项目拿高分的人。不需要游戏引擎基础,会用QGraphics系列类就能动手。
2. 先拆对象再写代码:陨石飞机需要的类设计与QT的2D游戏运行机制
用QT做2D小游戏,最忌讳一上来就在MainWindow里堆代码。这个项目规模不大,但对象关系清晰能省大量调试时间。先想清楚“谁在动”“谁负责判碰撞”“谁管流程”,再动手写类。
2.1 游戏对象清单:飞机、陨石、子弹、计分与碰撞该归谁管
按职责划分,常见做法是:
| 类名 | 继承自 | 职责 | 关键成员 |
|---|---|---|---|
| GameScene | QGraphicsScene | 管理游戏对象、驱动帧循环、碰撞判定、计分 | frameTimer_、spawnTimer_、plane_、meteorites_、score_ |
| Plane | QGraphicsPixmapItem | 玩家飞机,跟随键盘移动 | 移动步长、碰撞实例 |
| Meteorite | QGraphicsPixmapItem | 陨石,持续下落、自转,与飞机和子弹产生碰撞 | speed_、angle_ |
| Bullet | QGraphicsPixmapItem | 子弹,向上飞行 | speed_ |
| MainWindow | QMainWindow | 装配Scene与View,转发键盘事件 | scene_、view_ |
这里有一个容易被期末答辩追问的点:为什么飞机、陨石都继承QGraphicsPixmapItem而不是自己用QWidget?因为QGraphicsScene管理大量图元时内部有空间索引,大量陨石移动比QWidget逐个setGeometry高效得多,而且自带collidingItems()碰撞查询。QGraphicsItem是轻量对象,只能在场景里用,做2D游戏刚好。如果没找到图片素材,先用QGraphicsRectItem画一个矩形占位,后面再换QPixmap,接口不变。
2.2 场景与视图:QGraphicsScene、QGraphicsView、QGraphicsItem三件套的关系
三件套的关系可以这样理解:QGraphicsItem是演员,QGraphicsScene是舞台,QGraphicsView是摄像机。你操作的是item在scene里的坐标;view负责把舞台画面渲染到屏幕上。碰撞、命中测试都在scene的坐标系里做,和屏幕像素无关。这一点在后续碰撞检测时很关键——别拿view坐标去算重叠。
初始化代码写在MainWindow构造函数里:
#include "gamescene.h" #include <QGraphicsView> GameScene *scene = new GameScene(this); QGraphicsView *view = new QGraphicsView(scene, this); scene->setSceneRect(0, 0, 800, 600); // 逻辑世界范围,单位是scene坐标 view->setFixedSize(820, 640); // 视口略大于sceneRect,留出外边距 view->setRenderHint(QPainter::Antialiasing); view->setBackgroundBrush(QBrush(QColor(20, 28, 48)));setSceneRect(0, 0, 800, 600)定义游戏世界的边界,后续陨石生成坐标、越界判断都以它为准;setFixedSize把窗口定死,避免用户拉伸窗口导致scene坐标和视口错位。背景刷成深色是为了让飞机和陨石更醒目。如果习惯把窗口尺寸写成变量,建议把800和600提成常量,否则生成陨石和越界判断里到处写魔法数,答辩被问参数时容易答不上来。
2.3 游戏主循环:QTimer不是QThread,别用sleep做延时
QT是事件驱动框架,没有像Unity那样的引擎主循环。常见做法是用QTimer每隔固定毫秒触发一次“刷新逻辑”。注意:QTimer的回调在界面线程执行,回调里不能做耗时操作,也不要调用QThread::msleep,否则整个窗口会卡死。这个项目用不到多线程。
frameTimer_ = new QTimer(this); frameTimer_->setInterval(16); // 每16ms刷新一次,约62FPS connect(frameTimer_, &QTimer::timeout, this, &GameScene::OnFrameUpdate); frameTimer_->start(); spawnTimer_ = new QTimer(this); spawnTimer_->setInterval(700); // 每700ms生成一颗陨石 connect(spawnTimer_, &QTimer::timeout, this, &GameScene::SpawnMeteorite); spawnTimer_->start();interval的单位是毫秒,16ms对应约62帧每秒,对人眼和这个游戏量级足够。生成陨石的700ms是节奏参数:低于400ms手残玩家反应不过来,高于1000ms游戏挑战性不足。这两个timer通常放在GameScene构造函数里,不需要手动调用delete,QObject父子机制会处理。
这里也顺带解释一个常被问的问题:为什么不直接在MainWindow里用while循环setPos?因为while循环不把控制权交还事件循环,窗口根本不会重绘,QTimer的timeout信号也不会派发。底层是QObject的事件循环驱动,不是游戏引擎那种固定step。
3. 用C++把游戏写出来:从工程骨架到碰撞计分的可复现代码
开始写代码前,先说清一个原则:这段代码不是让你直接Ctrl+C交差的,而是让你看懂“生成、移动、碰撞、清理”四个环节后自己写出完整版。每个类只干一件事,避免MainWindow膨胀成几百行。
3.1 工程骨架:用Qt Creator建QWidgets项目,整理源码文件
打开Qt Creator,新建项目时选“Qt Widgets Application”,基类选QMainWindow,构建套件建议用与系统匹配的MinGW 64-bit或MSVC。不勾选“创建界面文件(.ui)”,游戏画面全部用代码布局,比Qt Designer界面设计拖控件更可控——Qt Designer适合表单,不适合频繁增删游戏对象的场景。
建好后把默认生成的MainWindow文件保留,其余按模块拆分。推荐文件组织:
| 文件 | 职责 |
|---|---|
| main.cpp | 程序入口 |
| mainwindow.h/cpp | 主窗口、装配scene和view、键盘事件转发 |
| gamescene.h/cpp | 场景逻辑:定时器、陨石生成、碰撞、计分 |
| plane.h/cpp | 玩家飞机 |
| meteorite.h/cpp | 陨石 |
| bullet.h/cpp | 子弹 |
.pro文件按需加模块:
QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets CONFIG += c++11 TARGET = MeteorStrike TEMPLATE = app SOURCES += \ main.cpp \ mainwindow.cpp \ gamescene.cpp \ plane.cpp \ meteorite.cpp \ bullet.cpp HEADERS += \ mainwindow.h \ gamescene.h \ plane.h \ meteorite.h \ bullet.h RESOURCES += res.qrcCONFIG += c++11 是为了用C++11特性;资源文件res.qrc用来管理飞机、陨石、子弹图片。如果暂时没有图片资源,可以不加RESOURCES行,先用纯色矩形代替,后面答辩前补上素材即可。
3.2 陨石类:C++随机数、自转与越界回收
陨石是这个游戏里最频繁生成的对象,构造函数要解决三个问题:随机横向坐标、从屏幕外进入、每帧向下移动并自转。C++随机数这里直接用Qt封装的QRandomGenerator,比手写rand()安全得多,也不会出现“每次运行陨石出生点一样”的经典翻车。
// meteorite.h #pragma once #include <QGraphicsPixmapItem> class Meteorite : public QGraphicsPixmapItem { public: Meteorite(const QPixmap &pixmap, qreal speed, QGraphicsItem *parent = nullptr); bool Advance(); // 推进一帧;false表示已越界,外部负责回收 private: qreal speed_; // 垂直下落速度,单位:scene坐标/帧 int angle_; // 当前自转角度 };// meteorite.cpp #include "meteorite.h" #include <QRandomGenerator> Meteorite::Meteorite(const QPixmap &pixmap, qreal speed, QGraphicsItem *parent) : QGraphicsPixmapItem(pixmap, parent), speed_(speed), angle_(0) { int worldWidth = 800; // 与sceneRect保持一致 qreal x = QRandomGenerator::global()->bounded(worldWidth); setPos(x, -pixmap.height()); // 从屏幕上方进入 setTransformOriginPoint(boundingRect().center()); } bool Meteorite::Advance() { setPos(pos().x(), pos().y() + speed_); angle_ = (angle_ + 5) % 360; setRotation(angle_); return pos().y() <= 600 + 50; // 超出场景下边界50像素后判定越界 }提示:bounded(worldWidth)返回[0, worldWidth)区间的整数,左闭右开,所以陨石x坐标不会等于800,避免一开始就卡在边界外。
angle每次加5度,取模360度防止int溢出。注意QGraphicsItem默认旋转是绕原点(0,0),图片会把左上角当轴心,所以要先setTransformOriginPoint(boundingRect().center())把旋转中心移到图元中心,否则旋转起来像是在绕一角甩。速度speed_的取值建议2~5,越大游戏越难;它由生成逻辑在外部传入,方便后面做难度递增。
3.3 玩家飞机与键盘控制:事件过滤器避免焦点玄学
键盘输入是整个QT小游戏最容易翻车的环节,现象通常是:keyPressEvent写了,运行没反应。与其研究焦点策略,不如在MainWindow里装事件过滤器,把按键统一接管。创建view之后,补三部曲:
view->installEventFilter(this); view->setFocusPolicy(Qt::StrongFocus); view->setFocus();然后重写事件过滤器:
// mainwindow.cpp bool MainWindow::eventFilter(QObject *obj, QEvent *event) { if (obj == view_ && event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); int key = keyEvent->key(); // 方向键记录为“被按下”,空格键直接发射子弹 if (key == Qt::Key_Space) { scene_->SpawnBullet(); return true; } scene_->SetKeyPressed(key, true); return true; } if (obj == view_ && event->type() == QEvent::KeyRelease) { QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); scene_->SetKeyPressed(keyEvent->key(), false); return true; } return QMainWindow::eventFilter(obj, event); }这里没有在按键事件里直接移动飞机,而是把“按键状态”存进GameScene的按下的键集合,再在帧刷新里统一移动。原因是系统键盘的AutoRepeat初次延迟和重复频率因机器而异,如果直接在keyPressEvent里setPos,按住方向键时飞机会先停一下再一格一格跳,手感很糟。按键状态方案让位移速度和帧率解耦:
// gamescene.h #include <QSet> class GameScene : public QGraphicsScene { // ... public: void SetKeyPressed(int key, bool pressed); // ... private: QSet<int> pressedKeys_; // 当前按住的键,用Qt::Key_取值 }; // gamescene.cpp void GameScene::OnFrameUpdate() { qreal dx = 0, dy = 0; if (pressedKeys_.contains(Qt::Key_Left)) dx -= 8; if (pressedKeys_.contains(Qt::Key_Right)) dx += 8; if (pressedKeys_.contains(Qt::Key_Up)) dy -= 8; if (pressedKeys_.contains(Qt::Key_Down)) dy += 8; if (plane_) plane_->moveBy(dx, dy); // ... 然后处理陨石移动与碰撞 }单方向移动步长8像素/帧,四方向同时按下时位移向量会是(±8, ±8),对角线速度约11.3,会略快,但小游戏里多数人不会纠结。想更严谨可以按位移向量归一化,把对角线步长改成5或6。飞机需要在GameScene构造函数里初始化位置,比如放在场景底部中央:plane_->setPos(400 - plane_->boundingRect().width() / 2, 500)。
3.4 碰撞检测与计分:用collidingItems而不是手写矩形相交
陨石、子弹、飞机都是用图片表示的图元,手动算“两矩形相交”很容易被图片透明区域和旋转角度坑到。常见做法是直接用QGraphicsItem::collidingItems(),它基于shape()做相交检测,旋转和坐标变换都包含在内。在GameScene里统一处理碰撞,避免陨石反向依赖飞机和子弹类型。
void GameScene::OnFrameUpdate() { // 1. 飞机移动 // ... 已在上面实现 // 2. 陨石推进、判定越界与碰撞 QList<Meteorite *> deadMeteors; for (Meteorite *m : meteorites_) { if (!m->Advance()) { deadMeteors.append(m); continue; } QList<QGraphicsItem *> hits = m->collidingItems(); for (QGraphicsItem *item : hits) { if (dynamic_cast<Bullet *>(item)) { deadMeteors.append(m); removeItem(item); delete item; AddScore(10); break; } if (dynamic_cast<Plane *>(item)) { GameOver(); return; } } } // 3. 统一回收越界/被击碎的陨石 for (Meteorite *m : deadMeteors) { meteorites_.removeAll(m); removeItem(m); delete m; } }这里刻意没有直接在碰撞循环里delete陨石,因为当前正在遍历meteorites_,边遍历边删会崩。先把待回收对象收集到deadMeteors,循环结束后统一处理,这是STL容器遍历删除的一贯做法。子弹被删除时它不在meteorites_里,直接removeItem和delete没有问题。AddScore只是让score_加10并emit一个ScoreChanged信号,MainWindow把分数显示到窗口标题栏或一个QLabel上。GameOver里停掉两个timer,再emit GameFinished(score_)。碰撞判断顺序“先子弹、后飞机”是刻意的:同一帧里陨石可能同时和飞机、子弹相交,优先判子弹,玩家会先打掉陨石而不是立刻死亡。
碰撞检测还有一个细节:默认QGraphicsPixmapItem的shape()会按图片是否有透明通道决定,素材不同行为完全不同。如果感觉“没碰到却扣血”,显式调用setShapeMode(QGraphicsPixmapItem::BoundingRectShape)让判定严谨到矩形边界;反之素材边缘留白大,改用默认的MaskShape。
4. 期末作业的“文档说明”怎么写:从需求说明到测试记录的组织思路
期末大作业的文档说明,老师真正在意的是三件事:程序解决什么问题、用什么结构实现、验证过哪些边界。文档不要写成代码逐行注释,按下面四块组织即可。
4.1 需求说明:把玩法翻译成可验收的功能需求
需求说明里最实用的是功能需求表和验收标准。写“飞机可以移动”没有验收意义,要写到“按下方向键后飞机能左移/右移,位移量与帧率无关”。下面表格可以直接套进文档:
| 编号 | 功能需求 | 验收标准 | 优先级 |
|---|---|---|---|
| FR-01 | 初始化游戏画面 | 启动后显示800x600场景,飞机位于底部中央 | P0 |
| FR-02 | 键盘控制飞机 | 按住方向键飞机连续四向移动,不超出场景边界 | P0 |
| FR-03 | 陨石生成与下落 | 每700ms生成一颗陨石,从顶部落下并自转 | P0 |
| FR-04 | 子弹射击 | 按空格键发射一枚子弹,向上飞行 | P1 |
| FR-05 | 碰撞判定 | 子弹击中陨石加10分;飞机被陨石撞击后游戏结束 | P0 |
| FR-06 | 分数显示 | 游戏过程中分数实时更新,结束后显示总分 | P2 |
再加一段非功能需求:运行环境写明“Windows 10 64位,Qt 5.15.2,MinGW 64-bit编译器”;性能要求写明“低配笔记本上保持约60FPS,不出现持续卡顿”;约束写明“不依赖第三方游戏引擎,仅使用Qt Widgets和QGraphics框架”。很多同学的文档缺这块,答辩被问“换台电脑能不能跑”时答不出来。
4.2 设计文档:类图、流程表与关键参数说明
设计文档不需要贴源码,把类的继承关系画清楚即可。不能贴图片就画表格,老师也认:
| 类 | 父类 | 关键接口 | 说明 |
|---|---|---|---|
| GameScene | QGraphicsScene | OnFrameUpdate(), SpawnMeteorite(), GameOver() | 主循环、碰撞、计分 |
| Plane | QGraphicsPixmapItem | moveBy(dx, dy) | 玩家图元 |
| Meteorite | QGraphicsPixmapItem | Advance() | 自转与下落 |
| Bullet | QGraphicsPixmapItem | Advance() | 上升,越界回收 |
然后写两条关键流程。第一条“主循环流程”:QTimer每16ms触发OnFrameUpdate,先读pressedKeys_移动飞机,再遍历陨石推进位置并查询碰撞,最后统一回收越界对象。第二条“一次碰撞处理流程”:陨石collidingItems()命中子弹,加分并标记回收;命中飞机,停掉timer并弹出GameOver。流程用编号列表写清楚,别画超出自己能力的状态机图。
关键参数表值得单列:场景尺寸800x600、帧间隔16ms、陨石生成间隔700ms、陨石下落速度2-5、飞机移动步长8像素/帧、旋转增量5度/帧。每个参数写一行设定理由,比如“陨石生成间隔700ms是因为低于400ms时玩家反应不过来”,答辩时老师问“为什么是700”就能拿出依据,而不是回答“我随便调的”。
4.3 测试记录与README:让另一个同学能按文档跑通
测试记录部分做一个简单的用例表,不要只写“测试通过”:
| 用例编号 | 操作步骤 | 预期结果 | 实际结果 |
|---|---|---|---|
| TC-01 | 按住右方向键5秒 | 飞机向右连续移动,不超出右边界 | 通过 |
| TC-02 | 按住上方向键5秒 | 飞机向上移动,不超出上边界 | 通过 |
| TC-03 | 连续击碎10颗陨石 | 分数每次增加10,共增加100分 | 通过 |
| TC-04 | 让飞机撞上一颗陨石 | 游戏结束,停止所有定时器 | 通过 |
| TC-05 | 陨石移出底部后 | 被回收,场景中不存在该对象 | 通过 |
README是另一份容易被忽略的文档。给个能直接改的模板:
# 陨石撞击飞机游戏(MeteorStrike) ## 运行环境 - Qt 5.15.2 或更高版本 - 编译器:MinGW 64-bit / MSVC - 操作系统:Windows 10 / Ubuntu 20.04 ## 操作说明 - 方向键:控制飞机移动 - 空格键:发射子弹 ## 目录结构 - gamescene/:场景逻辑 - plane/:玩家飞机 - meteorite/:陨石 - bullet/:子弹 ## 如何运行 1. 用Qt Creator打开.pro文件 2. 选择已配置好的Kit,构建并运行“用Qt Creator打开.pro文件”是最稳妥的运行方式,不要写过多的qmake/make手动命令,很多同学的环境配置藏坑,写多了反而把简单事复杂化。文档交付时连同源码打包,别忘了放3~5张运行截图,标注“游戏进行中分数累积”“GameOver界面”等。光有文字没截图,评阅老师不好快速判断程序形态。
5. 避坑清单:QT陨石飞机从能编译到顺畅运行的5个常见问题
这个章节收集的是这个题目里最容易让新人卡住的真实问题。如果照着前面代码写还出问题,按下面清单逐一排查。
5.1 源码里写的中文变乱码,甚至编译崩溃
现象:在mainwindow.cpp里写setWindowTitle("游戏结束"),运行后标题栏显示乱码,有些编译器配置下直接编译失败或运行时崩溃。
原因:MSVC编译器默认按本地代码页GBK解释源码字符串字面量,而Qt Creator默认用UTF-8保存文件,两边不一致,中文字符串就变成了一堆乱码字节。
解决:不要依赖源码里的中文。最省心的做法是在MSVC工具链下给.pro加一行QMAKE_CXXFLAGS += /utf-8,再在Qt Creator的文件编码设置里统一UTF-8。更稳妥的做法是用QString::fromUtf8("游戏结束")包一层,或者把界面文案都写成英文,少一个坑。
5.2 陨石每次运行生成的初始位置都一样
现象:游戏每次启动,陨石出生的横向坐标几乎是固定顺序,甚至第一颗总在同一个位置出现。
原因:用了rand()或旧的qrand(),且没有设置随机种子。C++11之前的标准随机数未经播种时默认序列固定,Qt 5的qrand同样要qsrand手动播种,忘了就等同于每次播放同一盘录影带。
解决:别用rand/qrand,直接用QRandomGenerator::global()->bounded(min, max)。这个全局实例自带随机源和种子,每次进程启动会得到不同序列,API和调用成本都友好。调试时想让某一局按固定陨石分布重现,可以改用QRandomGenerator rng(固定种子)复现,这是另一个话题了。
5.3 keyPressEvent写了但按键没反应
现象:飞机完全不受方向键控制,但窗口标题栏还在正常响应。
原因:键盘事件被发给当前有焦点的widget。QGraphicsView默认没有焦点策略,或焦点被其它控件抢走,场景自然接不到按键。
解决:用上一章的事件过滤器方案:view_->installEventFilter(this),再给view设view_->setFocusPolicy(Qt::StrongFocus),窗口显示后调用view_->setFocus()。事件过滤器先把KeyPress/KeyRelease拦下来,再转给GameScene的按键状态集合,不再依赖焦点玄学。
5.4 程序启动报“fatal: cannot mix incompatible qt library”或找不到linuxfb平台插件
现象:代码在别人电脑上能跑,换一台机器启动瞬间崩溃,控制台报错类似fatal: cannot mix incompatible qt library (version ex50601) with this library,或在无桌面Linux板子上报qt.qpa.plugin: could not find the qt platform plugin "linuxfb"。
原因:前者多数是PATH里混入了多个版本的QT运行库,程序加载到旧版本dll;后者是运行环境没有桌面,QT找不到对应的platform plugin,或插件目录不在限定路径内。
解决:前者在Qt Creator中检查构建套件和“运行环境”变量,别把不同版本的QT bin目录同时塞进PATH,老项目还要确认.pro里没有残留的旧版本依赖。后者在无桌面设备上显式设置QT_QPA_PLATFORM=linuxfb环境变量,并确认该平台插件已经编译进QT安装目录的plugins/platforms下。这类问题本质是运行库与插件不匹配,按“一套QT走天下”就不会遇到。
5.5 程序关闭时崩溃:陨石或子弹被删了两次
现象:正常玩没问题,一按右上角关闭就报“双精度释放”或“访问违规”,调试器定位在delete某个QGraphicsItem。
原因:QGraphicsScene销毁时会自动释放所有仍挂着的item;如果代码已经手动delete过某个对象,析构时场景又删一次,就翻车。常见写法是removeItem(m); delete m;已经做完整移除,但某些分支里移除后忘了把指针从容器清掉,列表析构时又delete,二次释放。
解决:统一管理策略——要么所有item都由容器持有,场景销毁前置空容器并暂停所有逻辑;要么每个item创建时不指定parent,删除时只用removeItem(item); delete item;两步走,不要混用delete和场景自动清理。期末项目规模小,让GameScene在析构时先停timer,再对meteorites_里的成员逐一removeItem和delete,容器自身什么都不做,最省心。
6. 验证工作量与进阶手感:用QElapsedTimer量化帧耗时,再谈扩展示例
6.1 用QElapsedTimer验证帧耗时,别只靠“看起来不卡”
很多人验收项目只看“还能跑”,但答辩老师可能问“性能怎么样”。帧耗时是客观指标,在OnFrameUpdate末尾用QElapsedTimer做一轮测量:
#include <QElapsedTimer> void GameScene::OnFrameUpdate() { QElapsedTimer timer; timer.start(); // ... 原有移动、碰撞逻辑 qint64 cost = timer.nsecsElapsed() / 1000000; // 换算为毫秒 if (cost > 30) qWarning() << "frame cost(ms):" << cost; }30ms对应约33FPS,如果帧耗时持续超过这个值,优先检查陨石链表长度和残留的透明图元。这类性能验证不需要第三方工具,Qt自带类就够了,控制台输出截图也能写进测试记录。若发现低配机器卡顿,把陨石数量上限设为20,超出后不再生成;新增陨石统一走SpawnMeteorite而不是散兵式new,后面好做对象池优化。
做这个课设时,我自己最后悔的是把大量时间花在找陨石PNG素材上,素材换了三套,游戏逻辑却没变动。先用纯色矩形把玩法调通,再去Qt Designer里拼界面、换图片,这个顺序能省出两个晚上写文档。答辩通过后回头补一个“难度递增”功能,只是把spawnTimer间隔从700ms逐步降到400ms,代码量很小,却能让项目从“能跑”变成“有可玩性”。先把碰撞、计分和性能验证做扎实,少走弯路,希望帮到你。
本文还有配套的精品资源,点击获取