ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Qt的C++陨石撞击飞机游戏设计与实现:从类设计到碰撞检测

基于Qt的C++陨石撞击飞机游戏设计与实现:从类设计到碰撞检测 简介基于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 游戏对象清单飞机、陨石、子弹、计分与碰撞该归谁管按职责划分常见做法是类名继承自职责关键成员GameSceneQGraphicsScene管理游戏对象、驱动帧循环、碰撞判定、计分frameTimer_、spawnTimer_、plane_、meteorites_、score_PlaneQGraphicsPixmapItem玩家飞机跟随键盘移动移动步长、碰撞实例MeteoriteQGraphicsPixmapItem陨石持续下落、自转与飞机和子弹产生碰撞speed_、angle_BulletQGraphicsPixmapItem子弹向上飞行speed_MainWindowQMainWindow装配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构造函数里不需要手动调用deleteQObject父子机制会处理。这里也顺带解释一个常被问的问题为什么不直接在MainWindow里用while循环setPos因为while循环不把控制权交还事件循环窗口根本不会重绘QTimer的timeout信号也不会派发。底层是QObject的事件循环驱动不是游戏引擎那种固定step。3. 用C把游戏写出来从工程骨架到碰撞计分的可复现代码开始写代码前先说清一个原则这段代码不是让你直接CtrlC交差的而是让你看懂“生成、移动、碰撞、清理”四个环节后自己写出完整版。每个类只干一件事避免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 c11 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 c11 是为了用C11特性资源文件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_castQKeyEvent *(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_castQKeyEvent *(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: QSetint 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. 陨石推进、判定越界与碰撞 QListMeteorite * deadMeteors; for (Meteorite *m : meteorites_) { if (!m-Advance()) { deadMeteors.append(m); continue; } QListQGraphicsItem * hits m-collidingItems(); for (QGraphicsItem *item : hits) { if (dynamic_castBullet *(item)) { deadMeteors.append(m); removeItem(item); delete item; AddScore(10); break; } if (dynamic_castPlane *(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场景飞机位于底部中央P0FR-02键盘控制飞机按住方向键飞机连续四向移动不超出场景边界P0FR-03陨石生成与下落每700ms生成一颗陨石从顶部落下并自转P0FR-04子弹射击按空格键发射一枚子弹向上飞行P1FR-05碰撞判定子弹击中陨石加10分飞机被陨石撞击后游戏结束P0FR-06分数显示游戏过程中分数实时更新结束后显示总分P2再加一段非功能需求运行环境写明“Windows 10 64位Qt 5.15.2MinGW 64-bit编译器”性能要求写明“低配笔记本上保持约60FPS不出现持续卡顿”约束写明“不依赖第三方游戏引擎仅使用Qt Widgets和QGraphics框架”。很多同学的文档缺这块答辩被问“换台电脑能不能跑”时答不出来。4.2 设计文档类图、流程表与关键参数说明设计文档不需要贴源码把类的继承关系画清楚即可。不能贴图片就画表格老师也认类父类关键接口说明GameSceneQGraphicsSceneOnFrameUpdate(), SpawnMeteorite(), GameOver()主循环、碰撞、计分PlaneQGraphicsPixmapItemmoveBy(dx, dy)玩家图元MeteoriteQGraphicsPixmapItemAdvance()自转与下落BulletQGraphicsPixmapItemAdvance()上升越界回收然后写两条关键流程。第一条“主循环流程”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()且没有设置随机种子。C11之前的标准随机数未经播种时默认序列固定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_PLATFORMlinuxfb环境变量并确认该平台插件已经编译进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代码量很小却能让项目从“能跑”变成“有可玩性”。先把碰撞、计分和性能验证做扎实少走弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表