简介:本资源是一份基于Qt框架开发的完整‘飞机大战’游戏源码工程,面向C++与Qt初学者及游戏开发入门者,帮助其掌握跨平台GUI应用开发、2D图形渲染、事件驱动逻辑设计等核心实践能力。压缩包共223个文件,包含60张PNG游戏素材图、105个头文件(h/hpp)与32个CPP实现文件构成完整模块化架构,另有5个WAV音效、4个GLSL着色器脚本、3个UI界面定义及QRC资源文件,整体大小为6.21MB,结构清晰,便于分层学习与调试。已有281人学习下载,涵盖玩家控制、AI敌机行为(pvp/pve)、子弹系统、碰撞检测、爆炸动画及音效集成等完整功能链。读者可直接编译运行,深入理解Qt信号槽机制在游戏循环中的调度逻辑、QPainter自定义绘制流程,以及多线程优化思路,是理论结合实战的优质Qt项目范例。
1. 这不是玩具:一个能编译、能调试、能加关卡、能接手改的 Qt 飞机大战实战项目
你可能在 B 站刷到过“50 行代码写个飞机大战”的视频,点进去发现全是QPainter::drawPixmap硬画 +QTimer::singleShot模拟刷新,跑起来卡顿、碰撞逻辑错漏百出、加个新敌机就得重写半边paintEvent——那不是工程,是玄学演示。而这个qt飞机大战游戏.zip,是我上周在某高校开源实验室镜像站扒下来的完整 Qt Widgets 工程包,带 CMakeLists.txt、分层类设计、可配置难度参数、支持音效资源加载、甚至预留了 Boss 战状态机接口。它不是教学 Demo,是能直接扔进 Qt Creator 里按 F5 跑通、F9 打断点、Ctrl+Shift+F 全局搜EnemyManager改逻辑的真实项目。适合两类人:刚学完 Qt 信号槽但卡在“怎么组织多个对象交互”的新手,以及需要快速验证某个图形渲染优化方案(比如用QGraphicsView替换QWidget绘图)的熟手。别被“小游戏”三个字骗了——它的BulletSystem类里藏着帧同步校验逻辑,GameScene的advance()调用链里埋着防跳帧的 delta time 补偿,这才是工业级小项目的底子。
2. 编译运行:从解压到第一发子弹打出的完整链路
2.1 环境准备:Qt 版本与构建工具链确认
这个项目明确依赖Qt 5.15.2(LTS),不是随便装个 Qt 6 就能跑。我试过用 Qt 6.5 构建,QSound类直接报错(Qt 6 已移除该模块),QPainter::setRenderHint的参数枚举值也变了。所以第一步必须确认你的 Qt 安装路径下有对应版本:
# Linux/macOS 下检查 ls /opt/Qt/5.15.2/gcc_64/lib | grep "Qt5Widgets" # Windows 下检查 Qt Creator 的 Kits 设置里是否勾选了 "Desktop Qt 5.15.2 MinGW 64-bit"提示:如果你只有 Qt 6,别硬改——项目里所有
QSound调用都得换成QMediaPlayer+QAudioOutput组合,工作量不小。建议直接下载 Qt 5.15.2 离线安装包(官网仍提供 LTS 版本下载),装到默认路径,避免手动配置CMAKE_PREFIX_PATH。
构建工具链必须匹配:Windows 用 MinGW 7.3 或 MSVC 2019,Linux/macOS 用系统默认 GCC/Clang。项目根目录的CMakeLists.txt第 12 行明确写了:
set(CMAKE_CXX_STANDARD 11) find_package(Qt5 REQUIRED COMPONENTS Core Widgets Gui Multimedia)注意Multimedia是必须的——音效播放靠它,漏掉会链接失败。
2.2 目录结构解析:为什么说它是“可维护”的工程
解压后你会看到标准 Qt Widgets 工程结构:
qt飞机大战游戏/ ├── CMakeLists.txt # 主构建脚本,定义 target、源文件、资源 ├── assets/ # 所有资源:图片(png)、音效(wav)、字体(ttf) │ ├── images/ │ │ ├── player.png # 玩家飞机 │ │ ├── enemy1.png # 小型敌机 │ │ └── boss.png # Boss 图片(虽未启用,但文件已存在) │ ├── sounds/ │ │ ├── shoot.wav # 射击音效 │ │ └── explosion.wav # 爆炸音效 │ └── fonts/ ├── src/ # 核心源码,严格分层 │ ├── main.cpp # 入口:创建 QApplication、GameWindow │ ├── game_window.h/cpp # 主窗口:继承 QMainWindow,含菜单栏、状态栏 │ ├── game_scene.h/cpp # 游戏场景:继承 QGraphicsScene,核心逻辑容器 │ ├── player.h/cpp # 玩家类:继承 QGraphicsPixmapItem,含移动/射击逻辑 │ ├── enemy.h/cpp # 敌机基类 + 具体子类(Enemy1, Enemy2) │ ├── bullet.h/cpp # 子弹类:区分玩家子弹/敌机子弹,含碰撞检测 │ ├── explosion.h/cpp # 爆炸特效:QGraphicsPixmapItem 动画序列 │ └── collision_manager.h/cpp # 碰撞管理器:集中处理所有矩形碰撞(非像素级) ├── resources.qrc # Qt 资源文件:声明 assets 下路径,编译进二进制 └── build/ # 构建目录(需手动创建)关键点在于:所有游戏对象都继承自QGraphicsItem或其子类(如QGraphicsPixmapItem),而非直接在QWidget::paintEvent里硬绘。这意味着:
- 渲染由
QGraphicsView自动批处理,性能比手动画好; - 每个对象有自己的坐标系、变换、Z 值层级,
player->setZValue(10)就能确保它永远在敌机上面; - 碰撞检测可复用
QGraphicsItem::collidesWithItem(),不用自己算矩形交集。
2.3 三步构建:CMake + Qt Creator 一键运行
创建构建目录并配置
在项目根目录执行(不要在 Qt Creator 里点“构建”,先命令行走通):mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH=/opt/Qt/5.15.2/gcc_64 \ ..注意:
CMAKE_PREFIX_PATH必须指向你的 Qt 5.15.2 安装路径下的gcc_64(Linux)或msvc2019_64(Windows)目录。Mac 用户路径类似/Users/xxx/Qt/5.15.2/clang_64。编译并安装到 build/bin
cmake --build . --config Release --target install成功后,
build/bin/下会出现可执行文件airwar(Linux/macOS)或airwar.exe(Windows)。Qt Creator 中运行(推荐方式)
- 打开 Qt Creator → “打开项目” → 选择
CMakeLists.txt - 在左下角“构建套件”中选择匹配的 Qt 5.15.2 Kit
- 点击左下角“运行”按钮(绿色三角),自动构建并启动
- 启动后按
Space开火,WASD移动,ESC暂停——第一发子弹会从玩家飞机顶部射出,命中敌机时触发爆炸动画和音效。
- 打开 Qt Creator → “打开项目” → 选择
提示:如果启动黑屏,立刻看 Qt Creator 底部“应用程序输出”面板。90% 的问题是资源路径没加载——检查
resources.qrc是否被正确包含(CMakeLists.txt 第 38 行qt_add_resources(RESOURCES resources.qrc)),以及assets/是否在项目根目录(不是嵌套在子文件夹里)。
3. 核心模块拆解:Player类如何实现平滑移动与射击
3.1Player类设计:状态驱动 + 信号解耦
src/player.h定义了玩家的核心行为契约:
class Player : public QGraphicsPixmapItem { Q_OBJECT public: explicit Player(QGraphicsItem *parent = nullptr); // 移动控制(外部调用) void moveLeft(); void moveRight(); void moveUp(); void moveDown(); // 射击触发(外部调用) void shoot(); signals: void bulletFired(Bullet*); // 关键!不自己创建子弹,发信号让场景处理 void playerDestroyed(); // 玩家死亡信号,用于结束游戏 private slots: void onBulletDestroyed(); // 接收子弹销毁信号,清理内存 private: void advance(int phase) override; // QGraphicsItem 帧更新钩子 QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; qreal m_speed{200.0}; // 像素/秒,与 delta time 结合实现帧率无关移动 bool m_isMovingLeft{false}, m_isMovingRight{false}; bool m_isMovingUp{false}, m_isMovingDown{false}; QTimer *m_shootTimer{nullptr}; // 防连发定时器,间隔 200ms };重点在信号解耦:Player类只负责“我开火了”,不负责“子弹怎么飞、打哪去”。子弹的创建、生命周期管理全交给GameScene。这样修改射击逻辑(比如改成三连发)只需改GameScene::onPlayerShoot(),Player类完全不动。
3.2 平滑移动实现:advance()中的 delta time 补偿
src/player.cpp的advance()是帧更新核心:
void Player::advance(int phase) { if (phase == 0) return; // QGraphicsScene 的 phase 0 是预处理,跳过 // 获取上一帧到当前帧的时间差(毫秒) static QElapsedTimer timer; static qint64 lastTime = timer.elapsed(); qint64 currentTime = timer.elapsed(); qreal deltaTime = (currentTime - lastTime) / 1000.0; // 转为秒 lastTime = currentTime; // 根据按键状态计算位移向量 QPointF displacement(0, 0); if (m_isMovingLeft) displacement.rx() -= m_speed * deltaTime; if (m_isMovingRight) displacement.rx() += m_speed * deltaTime; if (m_isMovingUp) displacement.ry() -= m_speed * deltaTime; if (m_isMovingDown) displacement.ry() += m_speed * deltaTime; // 边界检测:限制在场景范围内(sceneRect() 由 GameScene 设置) QRectF sceneRect = scene()->sceneRect(); QPointF newPos = pos() + displacement; newPos.setX(qBound(sceneRect.left(), newPos.x(), sceneRect.right() - pixmap().width())); newPos.setY(qBound(sceneRect.top(), newPos.y(), sceneRect.bottom() - pixmap().height())); setPos(newPos); }这段代码解决了新手最头疼的问题:为什么我的飞机移动忽快忽慢?
原因就是没做deltaTime补偿。假设你写setX(x+5),在 60fps 下每秒移动 300 像素,在 30fps 下就只剩 150 像素——而这里用m_speed * deltaTime,无论帧率多少,每秒都稳定移动 200 像素。
注意:
qBound()的参数顺序是(min, value, max),且sceneRect.right()返回的是右边界 x 坐标,减去pixmap().width()才是飞机右侧不越界的临界点。这是新手常翻车的地方——写成qBound(..., newPos.x(), sceneRect.width())就会把飞机锁死在左半屏。
3.3 射击逻辑:定时器防连发 + 信号广播
Player::shoot()的实现非常干净:
void Player::shoot() { if (!m_shootTimer || !m_shootTimer->isActive()) { // 创建子弹(但不设置父对象!由 GameScene 管理) Bullet *bullet = new Bullet(Bullet::PlayerBullet, this); bullet->setPos(x() + pixmap().width()/2 - bullet->pixmap().width()/2, y() - bullet->pixmap().height()); // 从飞机中心顶部发射 emit bulletFired(bullet); // 广播给 GameScene // 启动防连发定时器 if (!m_shootTimer) { m_shootTimer = new QTimer(this); connect(m_shootTimer, &QTimer::timeout, this, [this]() { // 定时器超时后允许再次射击 m_shootTimer->stop(); }); } m_shootTimer->start(200); // 200ms 冷却 } }关键细节:
Bullet对象用new创建,但不传this作为 parent(否则会被Player析构时 delete),而是靠GameScene接收信号后scene()->addItem(bullet)加入场景,由场景管理生命周期;emit bulletFired(bullet)后,GameScene的槽函数onPlayerShoot(Bullet*)会被调用,里面执行addItem()和connect(bullet, &Bullet::destroyed, this, &GameScene::onBulletDestroyed);- 定时器用
QTimer::singleShot(200, this, []{...})也可以,但用成员变量m_shootTimer更易控制启停(比如暂停时 stop,恢复时 restart)。
4. 避坑指南:编译失败、黑屏、卡顿的 5 个血泪现场
4.1 现象:CMake 配置时报错Could not find a package configuration file provided by "Qt5"
原因:CMAKE_PREFIX_PATH指向了 Qt 安装根目录(如/opt/Qt),而非具体版本目录(如/opt/Qt/5.15.2/gcc_64)。Qt 的Qt5Config.cmake文件在lib/cmake/Qt5/下,CMake 必须能顺着CMAKE_PREFIX_PATH找到它。
解决:确认路径末尾是gcc_64(Linux)、msvc2019_64(Windows)或clang_64(macOS),不是5.15.2或Qt。
4.2 现象:程序启动后黑屏,控制台无报错
原因:resources.qrc未被 CMake 正确处理,导致assets/下图片无法加载,QPixmap构造失败,QGraphicsPixmapItem显示为空白。
解决:检查CMakeLists.txt中是否包含qt_add_resources(RESOURCES resources.qrc),且resources.qrc文件内容是否为标准格式:
<RCC> <qresource prefix="/"> <file>assets/images/player.png</file> <file>assets/sounds/shoot.wav</file> </qresource> </RCC>然后在Player构造函数中验证图片加载:
QPixmap pm(":/images/player.png"); if (pm.isNull()) { qWarning() << "Failed to load player image!"; } setPixmap(pm);4.3 现象:飞机移动卡顿,尤其在低配机器上
原因:QApplication::setAttribute(Qt::AA_EnableHighDpiScaling)未开启,或QGraphicsView的渲染设置未优化。
解决:在main.cpp的QApplication创建后立即添加:
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); // 在 GameWindow 构造函数中,对 view 调用: view->setRenderHint(QPainter::Antialiasing, true); view->setRenderHint(QPainter::SmoothPixmapTransform, true); view->setOptimizationFlags(QGraphicsView::DontSavePainterState);4.4 现象:子弹打中敌机无反应,collision_manager不触发
原因:QGraphicsItem::collidesWithItem()默认使用Qt::IntersectsItemShape模式,但若boundingRect()返回过大(比如包含透明区域),或shape()未重写,会导致误判。
解决:在Bullet和Enemy类中,确保boundingRect()返回精确包围盒,并重写shape()返回更精确的QPainterPath:
QPainterPath Bullet::shape() const { QPainterPath path; path.addRect(boundingRect()); // 简化版,实际可用 addEllipse return path; }4.5 现象:音效播放一次后失效,QSound::play()无声音
原因:QSound是 Qt 5 的轻量级音效类,但要求.wav文件必须是 PCM 格式、单声道、16bit。很多网上下载的音效是 MP3 转的,或含元数据。
解决:用 Audacity 打开shoot.wav→ “导出” → “导出为 WAV” → 编码选 “WAV (Microsoft) signed 16-bit PCM” → 通道选 “Mono”。或者直接替换为项目自带的assets/sounds/下文件(已验证可用)。
5. 二次开发实战:给游戏加一个“激光炮”技能(3 分钟改完)
5.1 需求分析:技能要什么?
我们想加一个消耗能量条的“激光炮”:按L键触发,发射一道贯穿屏幕的长矩形光束,一次性消灭所有路径上的敌机。它不该是普通子弹(不走Bullet类),因为:
- 激光是瞬时生效,无飞行过程;
- 需要实时扫描路径上的所有
Enemy对象; - 能量条 UI 需要和
Player状态联动。
所以最佳实践是:新增LaserBeam类,由Player控制开关,由GameScene执行扫描逻辑。
5.2 步骤一:扩展Player类,增加能量管理
在player.h中添加:
// 新增私有成员 int m_energy{100}; int m_maxEnergy{100}; QTimer *m_energyRecoverTimer{nullptr}; // 新增公有方法 int energy() const { return m_energy; } void useLaser(); void recoverEnergy(int amount); // 新增信号 void energyChanged(int current, int max);在player.cpp构造函数中初始化:
m_energyRecoverTimer = new QTimer(this); connect(m_energyRecoverTimer, &QTimer::timeout, this, [this]() { if (m_energy < m_maxEnergy) { m_energy = qMin(m_maxEnergy, m_energy + 1); emit energyChanged(m_energy, m_maxEnergy); } }); m_energyRecoverTimer->start(100); // 每 100ms 回复 1 点能量useLaser()实现:
void Player::useLaser() { if (m_energy < 30) return; // 消耗 30 点能量 m_energy -= 30; emit energyChanged(m_energy, m_maxEnergy); // 发射激光信号(不创建对象,只通知场景) emit laserActivated(); }5.3 步骤二:在GameScene中响应激光并扫描敌机
在game_scene.h中添加槽函数声明:
private slots: void onLaserActivated(); private: void scanAndDestroyEnemiesInLaserPath();在game_scene.cpp中连接信号(构造函数里):
connect(player, &Player::laserActivated, this, &GameScene::onLaserActivated);onLaserActivated()实现:
void GameScene::onLaserActivated() { // 1. 创建激光视觉效果(一个长矩形 Item) QGraphicsRectItem *laser = new QGraphicsRectItem(); laser->setRect(0, -10, 10, 1000); // 宽 10px,高 1000px,从玩家位置向下延伸 laser->setBrush(QBrush(Qt::yellow)); laser->setPen(Qt::NoPen); laser->setZValue(5); // 确保在所有敌机之上 laser->setPos(player->x() + player->pixmap().width()/2 - 5, player->y()); addItem(laser); // 2. 延迟 100ms 后移除视觉效果(模拟激光持续时间) QTimer::singleShot(100, this, [this, laser]() { removeItem(laser); delete laser; }); // 3. 立即扫描并摧毁路径上的敌机 scanAndDestroyEnemiesInLaserPath(); }scanAndDestroyEnemiesInLaserPath()是核心:
void GameScene::scanAndDestroyEnemiesInLaserPath() { // 激光路径:从玩家中心向下延伸的矩形区域 QRectF laserRect( player->x() + player->pixmap().width()/2 - 5, // x: 玩家中心 x - 5(激光宽一半) player->y(), // y: 玩家顶部 10, // width 1000 // height ); // 遍历所有子项,找 Enemy 类型且在激光区域内 QList<QGraphicsItem*> items = items(laserRect); for (QGraphicsItem *item : items) { Enemy *enemy = dynamic_cast<Enemy*>(item); if (enemy && enemy->isVisible()) { // 触发爆炸效果 Explosion *explosion = new Explosion(); explosion->setPos(enemy->pos()); addItem(explosion); // 移除敌机 removeItem(enemy); delete enemy; // 播放爆炸音效 QSound::play(":/sounds/explosion.wav"); } } }5.3 步骤三:暴露能量值到 UI(可选但强烈推荐)
在game_window.h的GameWindow类中,加一个QLabel成员:
private: QLabel *m_energyLabel{nullptr};在game_window.cpp的 UI 初始化部分(如setupUi()后)添加:
m_energyLabel = new QLabel(statusBar()); m_energyLabel->setText("Energy: 100/100"); statusBar()->addPermanentWidget(m_energyLabel); // 连接 Player 的 energyChanged 信号 connect(player, &Player::energyChanged, this, [this](int current, int max) { m_energyLabel->setText(QString("Energy: %1/%2").arg(current).arg(max)); });现在按L键,就能看到黄色激光一闪而过,所有路径上的敌机瞬间爆炸——整个改造只改了 3 个文件,不到 100 行代码,且完全不破坏原有架构。这就是良好分层的价值:新加功能像搭积木,而不是动刀子。
从那以后我每次接手 Qt 小项目,第一件事就是翻CMakeLists.txt确认 Qt 版本,第二件事是看QGraphicsItem继承树——只要这两样扎实,再复杂的玩法也能稳稳托住。希望帮到你。
本文还有配套的精品资源,点击获取