简介这是一份面向Qt与C初学者及课程设计学习者的2D射击游戏完整项目源码基于Qt框架与QGraphicsView图形视图模块实现飞机、子弹、敌机的动态渲染涵盖键盘事件响应、QTimer定时动画、碰撞检测、音效播放与资源管理等核心知识点适合作为游戏开发入门练手或期末项目参考。压缩包共1017个文件以png、jpg图片素材和mp3、ogg音频资源为主另含37个cpp源文件、36个头文件、8个ui界面文件及qrc资源脚本整体约42.88MB工程结构与素材组织较为完整。目前已有1025人学习下载。项目附带实践报告记录了设计思路、实现过程与问题解决方式读者可据此理清游戏主循环、对象封装与碰撞判定逻辑并对照源码理解Qt信号槽与场景重绘机制快速搭建可运行版本并在此基础上扩展玩法。1. 从一份 7z 压缩包说起Qt 飞机大战能拿来干什么下载下来是一个 7z 包解压之后不是一堆散落的 cpp 文件而是一个能直接用 Qt Creator 打开的完整工程——这件事本身就决定了它的用法。很多人搜「Qt 飞机大战 c」是想找一个能跑起来的图形界面小游戏练手但网上大量代码片段只有半截逻辑缺资源、缺工程文件、缺信号槽连接拷进项目里编译都过不去。这份资源的定位很明确一个基于 Qt 的 2D 竖版射击游戏完整工程C 实现包含玩家飞机控制、敌机生成、子弹碰撞、分数统计这一整套闭环适合刚学完 Qt 基础控件、想看看「一个真实的小游戏工程长什么样」的开发者也适合要交课程设计、需要一份可运行骨架的人。它不追求商业级手感胜在结构完整、依赖干净拿 Qt Creator 打开就能编译改起来不用先花两天读懂别人的抽象层。2. 工程结构与技术选型为什么是 QGraphicsScene 而不是纯 QWidget 手绘2.1 目录骨架与文件职责解压后的工程目录通常长这样不同版本命名略有出入但分层逻辑一致PlaneWar/ ├── PlaneWar.pro # qmake 工程文件声明模块与源文件 ├── main.cpp # 程序入口创建 QApplication 与主窗口 ├── mainwindow.h/.cpp # 主窗口承载游戏视图与菜单/分数栏 ├── gamescene.h/.cpp # 游戏场景负责定时器、生成、碰撞、计分 ├── player.h/.cpp # 玩家飞机图元 ├── enemy.h/.cpp # 敌机图元 ├── bullet.h/.cpp # 子弹图元 ├── resources.qrc # 图片、音效等资源索引 └── images/ # 飞机、子弹、背景图.pro文件是整个工程的入口契约决定了能不能编译通过。典型内容如下QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET PlaneWar TEMPLATE app SOURCES main.cpp \ mainwindow.cpp \ gamescene.cpp \ player.cpp \ enemy.cpp \ bullet.cpp HEADERS mainwindow.h \ gamescene.h \ player.h \ enemy.h \ bullet.h RESOURCES resources.qrcQT widgets是必须的少了它QGraphicsView相关头文件直接找不到RESOURCES resources.qrc把图片打进可执行文件避免运行时因为相对路径找不到图而白屏。我一般会在这里顺手加一行CONFIG c11老版本 Qt 默认标准偏低图元类里用override会报警告。2.2 图元体系QGraphicsItem 派生出的三类对象这个项目没有用QWidget手动paintEvent画所有东西而是走 Qt 的 Graphics View 框架QGraphicsScene管逻辑对象QGraphicsView管显示与坐标变换每个游戏对象继承QGraphicsItem或更省事的QGraphicsPixmapItem。这样做的直接好处是碰撞检测、坐标管理、对象增删都由框架兜底不用自己维护一堆矩形数组。玩家类的核心声明大致是class Player : public QGraphicsPixmapItem { public: Player(QGraphicsItem *parent nullptr); void keyPressEvent(QKeyEvent *event) override; // 处理左右移动 void advance(int phase) override; // 每帧推进 int hp() const { return m_hp; } void hit() { --m_hp; } private: int m_hp 3; qreal m_speed 6.0; };QGraphicsPixmapItem已经帮你处理了贴图和包围盒boundingRect()默认就是图片尺寸碰撞检测直接可用。advance(int phase)是 Graphics View 的帧推进钩子场景调用advance()时会遍历所有图元phase 0做逻辑、phase 1做重绘这个约定新手容易忽略导致移动逻辑写了两遍。2.3 场景驱动一个 QTimer 撑起整个游戏循环游戏循环没有用独立线程而是QTimer定时触发advance()这是 Qt 小游戏最常见的做法简单且不会踩跨线程操作 UI 的坑GameScene::GameScene(QObject *parent) : QGraphicsScene(parent) { setSceneRect(0, 0, 480, 700); m_player new Player; addItem(m_player); m_player-setPos(width() / 2 - 30, height() - 100); m_timer new QTimer(this); connect(m_timer, QTimer::timeout, this, GameScene::gameLoop); m_timer-start(16); // 约 60 FPS } void GameScene::gameLoop() { advance(); // 驱动所有图元 advance() spawnEnemy(); // 按概率生成敌机 checkCollisions(); // 子弹与敌机、敌机与玩家 cleanupOutOfBounds(); // 移出屏幕的对象 deleteLater }start(16)的 16 毫秒对应约 60 帧实际帧率受机器负载影响所以移动速度要按「每帧位移」而不是「每秒位移」来写否则换台机器手感就变了。cleanupOutOfBounds()里删除图元必须用removeItem()加delete或者deleteLater()直接delete正在被场景遍历的对象会崩这是血泪经验。3. 核心模块拆解从按键到碰撞的完整链路3.1 玩家控制焦点、按键与边界钳制玩家移动依赖键盘事件但QGraphicsItem默认不接收焦点必须显式设置否则按方向键毫无反应Player::Player(QGraphicsItem *parent) : QGraphicsPixmapItem(parent) { setPixmap(QPixmap(:/images/player.png)); setFlag(QGraphicsItem::ItemIsFocusable, true); setFocus(); } void Player::keyPressEvent(QKeyEvent *event) { qreal nx x(); switch (event-key()) { case Qt::Key_Left: nx - m_speed; break; case Qt::Key_Right: nx m_speed; break; default: QGraphicsPixmapItem::keyPressEvent(event); return; } // 边界钳制防止飞出场景 qreal maxX scene()-sceneRect().width() - pixmap().width(); setPos(qBound(0.0, nx, maxX), y()); }setFlag(ItemIsFocusable, true)和setFocus()两行缺一不可qBound做上下界钳制比手写if干净。注意scene()在对象刚构造、还没addItem时是空指针所以边界计算放在按键回调里而不是构造函数里这个顺序坑不少人踩过。3.2 敌机与子弹对象池思路的简化版敌机生成用随机数控制出现位置和频率子弹由玩家定时发射。为了不让对象无限增长生成逻辑里加一个上限判断void GameScene::spawnEnemy() { if (m_enemies.size() 12) return; // 屏幕内敌机上限 if (QRandomGenerator::global()-bounded(100) 3) return; // 约 3% 概率 auto *enemy new Enemy; int x QRandomGenerator::global()-bounded( static_castint(width() - enemy-pixmap().width())); enemy-setPos(x, -enemy-pixmap().height()); addItem(enemy); m_enemies.append(enemy); }QRandomGenerator是 Qt 5.10 之后推荐的随机数接口比qrand()更安全也更方便。m_enemies这个列表不是必须的但保留它能快速做数量上限和批量清理比每次遍历items()再qgraphicsitem_cast省事。子弹同理玩家按空格或定时器触发fire()子弹图元向上移动出界即删。3.3 碰撞检测collidingItems 与类型判定碰撞没有自己写矩形相交直接用框架接口void GameScene::checkCollisions() { for (auto *bullet : m_bullets) { const auto hits bullet-collidingItems(); for (auto *item : hits) { if (auto *enemy qgraphicsitem_castEnemy *(item)) { removeItem(bullet); removeItem(enemy); m_bullets.removeOne(bullet); m_enemies.removeOne(enemy); delete bullet; delete enemy; m_score 10; emit scoreChanged(m_score); break; // 一颗子弹只打一个目标 } } } }collidingItems()返回与当前图元包围盒相交的所有图元qgraphicsitem_cast做安全向下转型比dynamic_cast快且是 Qt 推荐写法。这里有个细节删除图元后必须同步从m_bullets、m_enemies里移除否则下一帧遍历到野指针直接崩这是这类项目最高频的崩溃原因。break保证一颗子弹只结算一次避免一弹多杀导致分数异常。3.4 分数与状态信号槽把逻辑层和界面层隔开分数变化不直接操作界面控件而是发信号主窗口连槽更新标签// gamescene.h signals: void scoreChanged(int score); void gameOver(int finalScore); // mainwindow.cpp connect(m_scene, GameScene::scoreChanged, this, [this](int s){ m_scoreLabel-setText(QString(分数: %1).arg(s)); }); connect(m_scene, GameScene::gameOver, this, MainWindow::showGameOver);这样场景类不依赖任何具体控件后面想把分数显示换成自定义绘制、或者加个排行榜只改主窗口一侧即可。游戏结束判定放在玩家血量归零或敌机撞到玩家时停掉定时器、发gameOver界面弹对话框逻辑干净。4. 编译运行与资源打包把工程真正跑起来4.1 环境准备与打开方式需要 Qt 5.x 或 Qt 6.x 加对应编译器MinGW 或 MSVC 都行装 Qt Creator 时勾选 Widgets 模块即可。打开方式两种直接双击.pro让 Qt Creator 接管或者命令行qmake PlaneWar.pro makeWindows 下mingw32-make。构建套件Kit选错是最常见的「编译不过」原因——用 MSVC 的 Kit 打开 MinGW 工程报的错往往和代码无关。4.2 资源路径与 qrc 的坑图片必须通过:/images/xxx.png这种资源路径引用而不是images/xxx.png相对路径。前者打进二进制后者依赖工作目录换个启动位置就白屏。resources.qrc里每个文件都要列全新增图片后记得在 Qt Creator 里右键 qrc 重新添加否则编译时资源不存在但也不报错运行时才显示空白。4.3 发布时的依赖收集直接拷 exe 到别的机器大概率打不开缺 Qt 运行库。Windows 下用windeployqt PlaneWar.exe自动收集 dllLinux 下用ldd查依赖。这一步不做交付给别人的「能跑」就变成「在我这能跑」。5. 避坑与排查这类 Qt 小游戏最容易翻车的几处5.1 按键没反应现象程序能跑飞机不动。原因多半是图元没设焦点或者主窗口把键盘事件截走了。解决确认ItemIsFocusable已设、setFocus()已调必要时在QGraphicsView上setFocusPolicy(Qt::StrongFocus)。5.2 运行一段时间后崩溃现象玩十几秒闪退。原因删除图元后列表里残留野指针下一帧遍历访问已释放内存。解决删除时同步removeOne或统一用deleteLater()并延迟清理列表。5.3 图片显示为空白现象窗口有、飞机没有。原因qrc 没包含图片或路径写成了相对路径。解决检查resources.qrc条目代码里统一用:/前缀重新构建。5.4 换机器后速度变快或变慢现象同一份代码在不同电脑上手感差异大。原因移动量按帧写死帧率受定时器精度和负载影响。解决用QElapsedTimer记录帧间隔位移乘以实际 delta或至少把定时器间隔和速度参数集中成常量方便调。5.5 敌机生成后不消失现象敌机飞出屏幕还在内存里累积。原因只做了生成没做越界清理。解决在gameLoop里遍历列表y() sceneRect().height()的对象统一移除删除。6. 二次开发把它改成你自己的射击游戏骨架拿到这份工程后最值钱的不是「飞机大战」这个题材而是它那套「场景 定时器 图元 信号槽」的骨架。我一般会先做三件事来验证改造方向可行第一把玩家、敌机、子弹的贴图换成自己的素材确认资源链路通第二把m_speed、生成概率、敌机上限抽到一个config.h里集中管理后面调难度不用满工程找魔法数字第三给Enemy加一个type字段区分普通敌机和小 Boss碰撞结算时按类型给不同分数和血量。再往深走可以按下面的方向扩展每个方向都对应工程里一个明确的改动点改造方向改动位置关键点多种敌机与 Bossenemy.h/.cpp、spawnEnemy()加类型枚举与血量字段生成逻辑按概率分流道具与增益新增powerup.h/.cpp复用子弹的碰撞检测链路命中后改玩家属性音效QSoundEffect qrc发射、爆炸各一个音效对象注意别在循环里反复 new关卡与难度曲线GameScene增加计时/计数按存活时间或分数调整生成概率与速度存档最高分QSettings键值存本地启动时读回几行代码的事验证改造是否成功别靠肉眼盯着玩。我的习惯是加一个调试开关在gameLoop里打印当前敌机数、子弹数、分数跑一分钟看数字是否稳定收敛——如果敌机数只增不减说明清理逻辑漏了如果分数跳变异常说明碰撞结算重复触发。这个土办法比断点调试快得多。最后说个我自己的教训早期改这类工程我总想先把架构重构成「更优雅」的样子结果改到一半编译不过、原版也回不去。后来我每次动手前都先git init提交一版能跑的原始状态再按「一次只改一个模块、改完立刻跑一遍」的节奏推进。这个习惯让我少丢过很多次后悔药。希望这份拆解能帮你把压缩包里的工程真正跑起来、改下去。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
VoiceStudio:开源语音工作流操作系统与Electron底层实践 1. VoiceStudio不是“又一个语音克隆工具”,而是开源语音工作流的底层操作系统你搜“VoiceStudio”时,大概率会看到一堆零散的GitHub star、Electron打包截图、AGPL-3.0许可证弹窗,还有人问“怎么在鸿蒙上跑”。但没人告诉你:它根… · 2026/11/2 17:49:42
ODM模式解析:从概念到商业实践 1. ODM模式深度解析:从概念到商业实践在消费电子、家电、医疗器械等行业,我们经常听到"ODM"这个术语。作为从业15年的产品开发专家,我见证了大量品牌商通过ODM模式快速推出市场爆款。今天我们就来彻底拆解这种"交钥匙"式… · 2026/11/2 17:49:42
GPT-Image API 实战:图片编辑蒙版与 Alpha 通道避坑指南 最近我把 GPT-Image API 接进一个图片批处理工具里,需求本身不复杂:给商品图换背景、去掉照片角落的水印、局部改个颜色。原以为一个编辑接口几分钟调通,结果在图片蒙版(mask)和 Alpha 通道上反反复折腾了两天… · 2026/11/2 17:48:41
xmake使用总结 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 18:36:28
C++享元结构文档样式应用 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 18:36:28
QML调用JS /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 18:36:28
基于MAX14431与PIC18F86K90的工业SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 18:35:27
ESP32+VESC的BLDC牵引控制单元设计与实现 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 18:35:27
PLFM_RADAR:开源相控阵雷达全栈实践指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 18:35:27
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18
雷达信号调制技术解析:从LFM到伪线性调频原理与应用 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 1:01:15
基于MAX14431与TM4C123的工业SPI隔离电路设计全攻略 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 1:01:15
PLFM_RADAR:相位线性调频雷达信号处理框架 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 1:01:15
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18