news 2026/10/21 2:49:18

Qt飞机大战C++源码解析:从游戏循环到碰撞检测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt飞机大战C++源码解析:从游戏循环到碰撞检测

简介:这是一份基于Qt与C++开发的飞机大战2D射击游戏项目,面向有C++基础、希望系统学习Qt游戏编程的初学者和课程设计者。压缩包共包含1017个文件,总大小约42.88MB,以cpp、h源文件及pro工程为主干,附有大量png、jpg、bmp图片素材和mp3、ogg音效音乐,还有qrc资源清单、ui界面文件、dll运行库与exe可执行程序,并单独收录了一份doc实践报告。项目基于QGraphicsView/QGraphicsScene构建游戏场景,通过QTimer驱动动画节奏,利用键盘事件控制飞机移动与射击,并实现碰撞检测、多类型敌机生成及分数计分逻辑,游戏素材以QResource统一管理。学习者可以直接编译运行体验完整对战流程,也可以对照源码和报告拆解对象封装、信号槽协作与渲染更新机制,为后续扩展Boss战、道具系统等提供基础。目前已有1024人学习下载,是一份实用且完整的Qt游戏开发入门参考。

1. 从解压一个.7z开始:这个Qt飞机大战能带你看懂游戏循环的每一环

如果你刚把C++语法学完,正愁没有小项目练手,这份“基于Qt的飞机大战C++”的压缩包是个不错的入口。它不是那种几百个文件堆叠起来的完整商业游戏,而是一个把QGraphicsScene、QTimer、碰撞检测、资源加载全部串起来的迷你工程。解开之后你会发现,玩家飞机、敌机、子弹、爆炸动画各自独立成类,主窗口只管显示分数和状态,游戏逻辑由控制器统一调度。我花了一个晚上把它跑通后又改了几轮,觉得它刚好适合两类人:一类是刚学完C++想用Qt写出能玩的东西的初学者,另一类是课设或演示需要快速交一个互动Demo的同学。你不需要先读几百页文档,从“开始游戏”到“Game Over”这条链路,读代码就能完整走一遍。

2. 架构与技术选型:谁在管飞机、子弹和敌机

在动手跑之前,先花十分钟把工程的结构看清,后面改起来才会踏实。这个项目采用Qt的QGraphicsView框架,也就是“场景—视图—项目”模型。场景(QGraphicsScene)是所有游戏对象的容器,视图(QGraphicsView)负责渲染场景并接收鼠标键盘事件,玩家飞机、敌机、子弹则是QGraphicsItem的子类,被放进场景里各自管理自己的位置和绘制。游戏循环由QTimer驱动,没有单独的线程,所有逻辑都在主线程里排队执行。这个量级的小游戏并不需要多线程,把定时器当作心跳,每个tick里刷新位置、检测碰撞、清理对象,整体结构非常清晰。

2.1 技术栈:为什么选QGraphicsView而不是QWidget重绘

技术选型上,项目主体是Qt,按源码配置推测它基于Qt 5.15或6.2编写,这两个版本在QGraphicsView API上差异不大。渲染层没有选择在QWidget里自己画,而是直接用QGraphicsScene。区别在哪?如果自己写paintEvent,你要维护一个对象数组,每帧遍历全部对象再绘制背景、飞机、子弹,同时自己处理碰撞检测;而QGraphicsScene提供了addItem、removeItem、collidingItems这些现成接口,配合QGraphicsItem的重写机制,框架帮你把“对象管理”和“碰撞查询”这两件事做了。对于飞机大战这种对象数量几十上百的小游戏,性能完全没问题。

代码里用到的C++特性基本是C++11/14,比如智能指针、Lambda表达式、auto类型推导。游戏中有些地方用shared_ptr管理对象生命周期,避免人手delete漏掉;有些地方仍使用裸指针,比如QGraphicsItem,因为Qt场景自己持有对象所有权。这个混合写法在Qt项目里很常见,读的时候不用纠结,只要记住一点:场景中的QGraphicsItem由场景负责删除,普通C++对象由控制器管理。

技术点项目中的常见选型用在这里的理由
图形框架QGraphicsScene + QGraphicsView自带Item管理和碰撞查询接口
语言标准C++11 / C++14智能指针、Lambda、类型推导
游戏驱动QTimer固定间隔触发帧刷新,无额外线程
素材PNG图片 + WAV音效Qt直接支持,替换方便
构建.pro qmake + CMakeLists兼容IDE与命令行两种工作流

我一般会把CMakeLists和.pro文件都留一份。很多Qt示例默认只给.pro,但在Linux服务器或CI环境里没有Qt Creator时,CMake更方便。你拿到的是.7z压缩包,解压后先看根目录下有没有这两个文件,有哪个就用哪个,后面第四章会详细讲。

2.2 目录结构与类职责:碰撞检测代码到底藏在哪个文件

这份工程的目录结构大致是这样的:

AircraftWar/ ├─ AircraftWar.pro ├─ CMakeLists.txt ├─ main.cpp ├─ mainwindow.h / mainwindow.cpp ├─ gamecontroller.h / gamecontroller.cpp ├─ gamescene.h / gamescene.cpp ├─ playerplane.h / playerplane.cpp ├─ enemy.h / enemy.cpp ├─ bullet.h / bullet.cpp ├─ explosion.h / explosion.cpp └─ resources/ ├─ images/ └─ sounds/

main.cpp只做了三件事:设置应用属性、创建MainWindow、进入事件循环。MainWindow是窗口壳,包含开始按钮、分数标签、结束提示,不写具体游戏逻辑。GameController是“大脑”,持有QTimer和GameScene指针,负责启动/停止游戏、生成敌机、检测碰撞、更新分数。GameScene继承QGraphicsScene,相当于“世界容器”,负责背景绘制、对象统一增删。PlayerPlane、Enemy、Bullet、Explosion是具体实体,每个类只处理自己的绘制和移动。

第一次看Qt项目的人容易满世界找“主循环”,以为是while(1)那种。其实Qt程序的运行循环藏在app.exec()里,游戏循环则藏在QTimer的timeout信号里。整个调用关系可以理解为:你点击“开始游戏”按钮,MainWindow调用GameController::startGame;startGame让GameScene初始化场景并创建玩家飞机,同时启动一个间隔30毫秒的QTimer;此后每隔30毫秒触发一次GameController::onFrameUpdate,这就是一帧。找碰撞检测代码不需要从main一路翻,直接去GameController里找checkCollisions函数就行,这是这个项目最值得先读的一个函数。

3. 核心代码拆解:一个子弹从发射到命中的完整链路

这一章按代码执行的顺序来讲,而不是按类表顺序。我们把一局游戏拆成几个连续动作:定时器驱动帧更新,玩家按空格发射子弹,子弹向上飞并在场景里移动,场景检测到碰撞后计分并生成爆炸。读下来你会发现,所谓“游戏逻辑”其实就是一层层具名函数,最终在QTimer的驱动下不断重复。

3.1 游戏循环:QTimer驱动的onFrameUpdate在做什么

先看开始游戏的入口:

// GameController.cpp void GameController::startGame() { m_isGameOver = false; m_score = 0; m_spawnCounter = 0; m_frameCount = 0; m_scene->clear(); m_scene->initGame(); // 玩家飞机初始位置放在底部中央 m_player = new PlayerPlane(); m_scene->addItem(m_player); m_player->setPos( m_scene->sceneRect().center().x() - m_player->boundingRect().width() / 2, m_scene->sceneRect().bottom() - 100 ); m_timer = new QTimer(this); connect(m_timer, &QTimer::timeout, this, &GameController::onFrameUpdate); m_timer->start(30); }

这段代码里,QTimer的interval是30毫秒,意味着游戏循环大约每秒33帧。飞机大战不是赛车模拟器,不需要每帧都做物理运算,30毫秒足够流畅。玩家飞机坐标用了sceneRect的中心和底部做参考:center().x()取场景水平中心点,再减去玩家飞机自身宽度的一半,这样飞机才能居中而不是偏到右边;bottom() - 100让飞机离底边100像素,留出操作空间。

接着是每一帧都在执行的onFrameUpdate:

void GameController::onFrameUpdate() { m_frameCount++; // 让所有Item移动到下一帧位置 m_scene->advance(); // 检查子弹与敌机的碰撞 checkCollisions(); // 删除标记为dead的对象 removeDeadObjects(); // 累计生成敌机的间隔 m_spawnCounter++; if (m_spawnCounter >= spawnInterval()) { spawnEnemy(); m_spawnCounter = 0; } emit refreshScore(m_score); }

onFrameUpdate里有个容易被忽略的细节:m_scene->advance()会调用场景内每个QGraphicsItem的advance(int phase),phase等于0时表示准备阶段,phase等于1时才实际移动。我们习惯在phase == 1里做moveBy,这样所有Item先准备再统一移动,避免某个对象先移动后影响另一个对象尚未执行的坐标计算。checkCollisions放在advance之后,因为移动完后场景中的位置才是最新状态,此时做碰撞检测才能拿到准确结果。removeDeadObjects放在碰撞之后,因为碰撞会产生新的死亡对象,集中清理比边碰撞边delete更安全。

3.2 子弹、敌机的移动,以及碰撞检测的实现

子弹本身是一个QGraphicsItem子类,在advance里每帧向上移动。玩家飞机通过keyPressEvent监听空格,然后创建Bullet:

// PlayerPlane.cpp void PlayerPlane::keyPressEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Space) { shoot(); } } void PlayerPlane::shoot() { Bullet* bullet = new Bullet(); bullet->setPos( x() + boundingRect().width() / 2 - bullet->boundingRect().width() / 2, y() - 10 ); scene()->addItem(bullet); }

这里的坐标换算很关键。QGraphicsItem的x()和y()返回的是Item左上角在场景中的位置,玩家飞机的boundingRect是以左上角为原点计算的。如果直接把子弹放在飞机左上角,子弹看起来会从左翅膀冒出来。所以子弹的x坐标等于飞机的中心点:x() + 飞机宽度/2,再减去子弹宽度的一半,让子弹头部对准机身中心。y方向用y() - 10,让子弹从机头上方一点点出现。

子弹移动的代码:

// Bullet.cpp void Bullet::advance(int phase) { if (phase == 0) return; moveBy(0, -m_speed); if (y() < -boundingRect().height()) { setDead(true); // 飞出屏幕就标记,等待统一清理 } }

moveBy(0, -m_speed)表示x不动,y方向向上移动,所以速度是负方向。m_speed是子弹的速度属性,可以在构造时传入不同值,方便以后做道具子弹或加强子弹。如果子弹y坐标小于负的高度,说明已经飞出屏幕顶部,标记为dead,等待onFrameUpdate里的removeDeadObjects统一清除,避免越界节点一直堆积。

碰撞检测是核心:

// GameController.cpp void GameController::checkCollisions() { const QList<QGraphicsItem*> allItems = m_scene->items(); for (QGraphicsItem* item : allItems) { Bullet* bullet = qgraphicsitem_cast<Bullet*>(item); if (!bullet) continue; QList<QGraphicsItem*> hitItems = m_scene->collidingItems(bullet); for (QGraphicsItem* hit : hitItems) { Enemy* enemy = qgraphicsitem_cast<Enemy*>(hit); if (enemy && !enemy->isDead()) { enemy->setDead(true); m_score += enemy->scoreValue(); spawnExplosion(enemy->pos()); bullet->setDead(true); } } } }

qgraphicsitem_cast相当于动态类型转换,它需要QGraphicsItem子类在头文件里声明元对象类型,如果不方便声明,也可以用type()先判断再强转。collidingItems默认使用item的shape()做重叠判断,而不是boundingRect。shape()返回QPainterPath,可以精确到图像轮廓;如果你不重写shape(),Qt就默认使用boundingRect,这会导致子弹还没碰到飞机上的像素,矩形框已经重叠,敌机提前爆炸。所以这个项目里Enemy类最好重写shape(),比如用椭圆近似机身轮廓,碰撞手感会好很多。

3.3 敌机生成:随机坐标与难度参数的搭配

敌机生成函数这样写:

void GameController::spawnEnemy() { Enemy* enemy = new Enemy(chooseEnemyType()); qreal enemyWidth = enemy->boundingRect().width(); qreal sceneWidth = m_scene->sceneRect().width(); // 随机生成一个 [0, sceneWidth - enemyWidth] 范围内的x坐标 qreal x = QRandomGenerator::global()->bounded( qMax(0.0, sceneWidth - enemyWidth) ); enemy->setPos(x, -enemyWidth); m_scene->addItem(enemy); }

x范围这里有个细节:如果只在[0, sceneWidth]里随机生成,敌机可能会有一半身子露在场景外面。场景宽度减去敌机自身宽度,再把结果传给QRandomGenerator::bounded,生成的范围是[0, sceneWidth - enemyWidth],这样敌机完全进入屏幕后才不会越界。如果场景宽度小于敌机宽度,qMax会返回0,虽然这种情况下敌机可能直接占满全屏,至少不会崩溃。

敌机生成频率和难度挂钩,常见做法是单独写一个函数:

int GameController::spawnInterval() const { int seconds = m_frameCount * 30 / 1000; // 前30秒每800ms一个,之后逐渐加快到400ms,最低300ms int interval = 800 - qMin(400, seconds * 12); return qMax(300, interval); }

m_frameCount每帧加1,乘以30毫秒再除以1000,就是游戏已经运行了多少秒。这个函数作用是:前期生成间隔大,新手能适应;后期间隔变小,敌机密度增加。你可以直接改这里的三个数字:初始间隔800、递减速率12、下限300,就能调整整局游戏的难度曲线。如果想要更平滑的加权,可以提取出一个difficultyFactor函数,第五章会讲。

4. 编译运行与避坑:解压后最容易翻车的六个细节

这一章不谈架构,只讲怎么把源码跑起来,以及我踩过的那些坑。飞机大战源码本身不复杂,但Qt环境配置和资源路径问题经常让新手卡住,甚至怀疑代码有问题。

4.1 从.7z到可执行文件:Qt Creator与命令行的两种跑法

拿到压缩包后先解压,然后看根目录。如果里面有.pro文件,直接用Qt Creator打开,选择对应的Kit,点击Run就能编译运行。如果里面有CMakeLists.txt,那更适合命令行或者CLion。我一般会这样做:

mkdir build && cd build cmake -DCMAKE_PREFIX_PATH=/opt/Qt/5.15.2/gcc_64 .. cmake --build . --config Release ./AircraftWar

CMAKE_PREFIX_PATH要指向你安装Qt的库目录,路径以实际安装位置为准。如果是在Windows上,编译完生成的exe可能在build目录的子目录下,找不到exe时用cmake --install或直接搜索.exe。运行exe如果提示找不到Qt5Core.dll,说明动态链接库路径没配置好,最简单的方法是把Qt的bin目录加到PATH,然后重新打开命令行启动程序;也可以用Qt自带的windeployqt工具把依赖DLL复制到exe所在目录。

如果你只有.pro文件,Qt Creator最省事。但要注意:打开.pro文件时,如果Qt Creator提示“No suitable kits”,是因为没有安装对应的编译器套件,或者系统里有多个Qt版本,工具链与Qt库不匹配。解决办法是在Qt Creator的“Tools -> Options -> Kits”里选一个能用的套件,比如MinGW或MSVC,并确保该套件下的Qt版本路径正确。命令行下可以输入qmake -v看看当前qmake来自哪里,如果指向的不是你用的那个Qt版本,构建就会失败。

4.2 避坑清单:现象、原因、解决

我跑这类源码时,问题基本集中在五个地方,下面是我反复遇到的真实情况。

现象一:窗口能打开,但飞机、子弹、背景全不显示,只有一片空白场景。原因是代码里的图片路径是相对路径,比如“resources/images/plane.png”,而程序的工作目录不是工程根目录,尤其是从Qt Creator里F5运行时,工作目录常常是build目录。解决办法是把素材全部收进.qrc资源文件里。新建resources.qrc,在文件中注册图片和音效,代码里改用QPixmap(":/images/plane.png")这样的资源路径。这样无论你从哪里启动程序,Qt都能通过资源系统找到文件。

现象二:游戏运行几秒后突然崩溃,报错里出现“delete in event dispatch”或野指针。原因是有人直接在碰撞检测或advance里delete了QGraphicsItem,但Qt的场景事件队列里还引用着这个对象。解决办法是禁止在帧循环中直接delete,统一使用“标记死亡+集中清理”模式。每个可销毁对象提供setDead和isDead方法,在碰撞检测里只设置标记,然后onFrameUpdate最后统一removeItem并delete。

现象三:子弹还没碰到敌机,敌机就爆炸了。原因是碰撞检测默认使用boundingRect,而图片素材有透明边缘,矩形框先重叠了。解决办法是重写Enemy的shape()函数,返回QPainterPath来定义实际碰撞轮廓。比如给敌机定义椭圆近似:

QPainterPath Enemy::shape() const { QPainterPath path; path.addEllipse(boundingRect()); return path; }

这样碰撞区域更接近机身,子弹擦边穿过时不会误伤。

现象四:高分屏下画面和文字发糊。原因是Qt 5.x默认不开启高DPI缩放,而你的Windows显示缩放是125%或150%。解决办法是在main函数最前面加上QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);,注意必须在创建QApplication之前调用。如果用的是Qt 6,这个特性已经默认开启,再设置会打出告警,所以最好加个版本判断。

现象五:关闭窗口后进程不结束,任务管理器里还留着进程,或者编译时提示“cannot find -lQt5Widgets”。前者是因为QTimer还在运行,或者MainWindow的closeEvent里没有调用stopGame。后者是因为qmake使用的Qt路径不对,可能混装了多个版本。解决前者的办法是确保在MainWindow的closeEvent里执行stopGame,停掉定时器并执行scene->clear;解决后者则要重新配置套件,或者命令行时用完整的qmake路径。

5. 二次开发:给飞机大战加一条会呼吸的难度曲线

如果你已经把基础代码跑通,下一步可以试着从“难度控制”切入改造。现在的spawnInterval是一个线性递减的公式,但不够“呼吸感”,我们希望游戏难度能在波动中上升,而不是死板地越变越密。这个改造不需要动界面,只需要在GameController里加一个难度因子函数,再让生成间距和敌机类型都依赖它。

改法如下:先定义一个difficultyFactor,它把游戏时间映射到0.3到1.5之间:

double GameController::difficultyFactor() const { int seconds = m_frameCount * m_timer->interval() / 1000; double factor = 0.3 + seconds * 0.004; return qMin(1.5, factor); }

然后让生成间隔跟随因子变化:

int GameController::spawnInterval() const { double factor = difficultyFactor(); // 按因子从1000ms平滑降到350ms return qMax(350, int(1000 - 650 * (factor - 0.3) / 1.2)); }

这只是一个起点。你还可以在chooseEnemyType里加入概率判断,让难度因子大于一定值时开始出现带血量的精英怪,或者每隔60秒生成一个Boss。如果想把分数、生命、等级统一管理,我建议把GameController里的零散成员变量收敛成一个GameState结构体,这样后续加道具、加关卡都不会让函数头变得很长。

改完怎么验证?运行游戏,观察前30秒敌机是否稀疏,1分钟后密度是否明显上升,再把间隔下限调到200ms试一盘,感受操作是否还来得及反应。如果无法顺利编译,先看报错是不是又踩了第四章里的资源路径或内存释放的坑。我第一次改这段代码时,直接把sleep塞进onFrameUpdate里想模拟延迟,结果整个界面卡得像幻灯片。从那以后我每次改动都强制走一遍:先跑通原版、再增量修改、最后回到定时器和信号槽里排异步逻辑,绝不阻塞事件循环。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 2:49:16

Foxmail Server v2.0 公测版Windows部署全指南

简介&#xff1a;本资源为Foxmail Server For Windows v2.0公测版安装包&#xff0c;面向中小型企业IT管理员、邮件系统初学者及国产化邮件服务实践者&#xff0c;提供轻量级、全中文、开箱即用的自建邮件服务器解决方案。资源包共2个文件&#xff08;1个安装程序exe、1个说明h…

作者头像 李华
网站建设 2026/10/21 2:49:15

Python歌词文本分析实战:中文分词、词频统计、情感分析与词云可视化

最近在整理歌曲数据时&#xff0c;发现不少朋友对“歌名热度”“歌词高频词”“情感倾向”这类分析很感兴趣。正好《思念漫过海岸线》这首歌在网络上讨论不断&#xff0c;相关话题也比较热&#xff0c;我便以它为例&#xff0c;完整跑通了一套“从零搭建歌曲歌词文本分析与可视…

作者头像 李华
网站建设 2026/10/21 2:48:32

目前短视频账号互动率下跌严重------预计每天总播放量>5000

预计可能会从每日2500互动减少到1000互动&#xff0c;最不利的情况可能会下跌到500互动/天。所以怕个屁啊&#xff0c;那不是还有500各客户吗&#xff1f;只要文案搞得好&#xff0c;每天转化几个我都愿意。--------一个好消息是&#xff1a;抖音粉丝数量>2000,那么每天视频…

作者头像 李华
网站建设 2026/10/21 2:48:12

征途服务端源码全解析:从C++架构到游戏二次开发实战

简介&#xff1a;这份压缩包是经典网络游戏《征途》的完整开发源码集&#xff0c;包含服务端、客户端与配套数据库&#xff0c;面向对大型网游架构感兴趣的C/C开发者、游戏客户端与服务端工程师。服务端源码可研究网络通信、任务调度与负载均衡&#xff1b;客户端源码覆盖渲染、…

作者头像 李华
网站建设 2026/10/21 2:47:26

原生HTML5多图上传预览:EXIF修正与Canvas渲染实战

简介&#xff1a;本资源是一份基于HTML5原生API实现多图上传与实时预览的完整前端源码包&#xff0c;面向Web前端初学者及希望深入理解File API与拖放交互的开发者&#xff0c;解决传统表单上传无法本地预览、依赖后端中转的痛点。压缩包共18个文件&#xff0c;包含5个核心JS脚…

作者头像 李华
网站建设 2026/10/21 2:46:39

Win7 64位系统Realtek PCIe GBE Family Controller驱动安装指南

简介&#xff1a;面向瑞昱公司Realtek PCIe GBE Family Controller千兆网卡的Windows 7 64位驱动包&#xff0c;主要解决系统无法识别网卡、网络掉线、速度跑不满等问题。适用人群包括需要重装Win7系统的老机器用户、企业IT运维人员以及经常帮人装机的技术爱好者。压缩包内共含…

作者头像 李华