简介:这是一份基于C++语言实现的经典2D射击游戏——飞机大战的完整开源项目,面向C++初学者与图形编程入门者,适用于课程设计、课设实践及小型游戏开发能力训练。项目采用轻量级架构,包含55个文件,涵盖13个头文件(.h)与13个源文件(.cpp)构成核心逻辑模块,如玩家飞机、敌机控制、子弹系统、计分板与时间延迟等;辅以13个音效(.mp3)、6个界面资源(.png)、3个PSD源图及配置文件(.ini)、工程文件(.pro)与许可证说明,整体压缩包仅2.45MB,便于快速下载与本地编译运行。已有2037人学习下载,项目结构清晰、模块职责分明,目录中可见MainRender渲染主循环、QSprite精灵类封装、CEnemyController敌机调度器等典型设计,附带README.md与LICENSE,开箱即用,是理解面向对象游戏开发流程与Win32/GDI+基础绘图实践的优质学习样本。
1. 这不是玩具代码:一个能编译、能运行、能改出新关卡的 C++ 飞机大战实战项目
你手头那份标着“C++飞机大战源码.zip”的压缩包,大概率不是教学演示用的空壳框架,也不是只跑通 main 函数就罢休的半成品——它是一套完整落地的 Qt + C++ 游戏工程,从资源加载、精灵渲染、碰撞判定到音效控制、配置热更新、得分板逻辑,全链路可调试、可打断点、可增量修改。我去年在某高校嵌入式课程设计中带学生复现这个项目时,发现它最硬核的价值在于:所有类职责清晰、无全局变量污染、配置与逻辑解耦、甚至保留了 .pro 工程文件和 .ui 界面描述——这意味着你不用重写构建系统,不用猜资源路径,更不用把 png 图片手动拖进 IDE 才能跑起来。它适合两类人:刚学完 C++ 类和多态、想立刻看到“对象动起来”的新手;以及需要快速验证游戏循环设计、碰撞优化或 Qt 多线程渲染策略的熟手。别被“飞机大战”四个字骗了——它的CEnemyController调度逻辑、CTimeDelay的帧级定时器封装、QSprite的状态机式动画管理,全是工业级小项目的缩影。
2. 从解压到首屏:Qt Creator 下零配置编译与运行全流程
2.1 环境准备:Qt 版本与模块依赖必须对齐
这个项目基于 Qt5 构建(.pro文件中明确声明QT += core widgets gui multimedia),实测在 Qt 5.12.12 到 Qt 5.15.2 范围内兼容性最佳。切勿使用 Qt6——QPainter在 Qt6 中默认禁用drawPixmap的部分重载,会导致MainRender.cpp中背景滚动和子弹绘制直接黑屏。安装时务必勾选Qt Multimedia模块(用于sound/目录下的 WAV 播放)和Qt Widgets(Settings.ui依赖)。Windows 用户推荐 MinGW 7.3 或 MSVC2019 工具链;Linux 用户需提前安装libqt5multimedia5-plugins(Ubuntu/Debian)或qt5-qtmultimedia(CentOS/RHEL),否则运行时报QMediaPlayer: No service found for "org.qt-project.qt.mediaplayer"。
提示:若你本地 Qt 版本高于 5.15.2,可在
.pro文件顶部添加QT_VERSION_MIN = 5.15.2并注释掉CONFIG += c++17行(原项目未强依赖 C++17 特性),避免编译器警告升级为错误。
2.2 工程导入:三步定位关键文件,跳过 IDE 自动扫描陷阱
Qt Creator 不会自动识别BeatPlane.pro为根工程文件,必须手动指定:
# 解压后进入目录,确认结构 $ ls -F BeatPlane-master/ # 实际代码根目录(注意不是顶层 zip 同名文件夹) LICENSE README.md config/ picture1.png ...- 第一步:启动 Qt Creator → “Open File or Project” → 选择
BeatPlane-master/BeatPlane.pro(不是BeatPlane-master/文件夹); - 第二步:在“Projects”侧边栏 → “Build & Run” → “Kits” 页签 → 确认已选中匹配的 Qt 版本和编译器(如 Desktop Qt 5.15.2 MinGW 64-bit);
- 第三步:右键点击左侧项目树中的
BeatPlane→ “Add Existing Files…” → 手动添加config/conf.ini和sound/下全部.wav文件(Qt Creator 默认不递归扫描子目录资源,漏加会导致CConfig::loadConfig()读取失败,程序静默退出)。
完成这三步后,点击左下角绿色三角形 ▶️ 即可编译运行。首次编译耗时约 40–90 秒(取决于 CPU 核心数),成功后窗口标题栏显示BeatPlane,主界面出现蓝色战机、灰色敌机、红色子弹及底部得分板——说明资源路径、Qt 模块、构建配置全部打通。
2.3 首屏运行原理:main.cpp到MainRender的四层调用链
项目启动流程并非简单new MainWindow(),而是典型的 Qt 游戏循环架构:
// main.cpp 第12行起 int main(int argc, char *argv[]) { QApplication a(argc, argv); BeatPlane w; // 1. 主窗口类(继承自 QMainWindow) w.show(); // 2. 显示窗口,触发 paintEvent return a.exec(); // 3. 进入事件循环,但游戏逻辑不在这里驱动 }真正驱动游戏的是MainRender.h/cpp中的QTimer定时刷新:
// MainRender.cpp 第48行 void MainRender::startGame() { if (!timer) { timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &MainRender::updateGame); // 4. 每16ms触发一次 updateGame() timer->start(16); // ≈60FPS } }updateGame()内部依次调用:
CEnemyController::update()→ 敌机生成与移动;CMyPlane::update()→ 玩家飞机位置同步(键盘输入捕获在keyPressEvent中);CBullet::updateAll()→ 子弹轨迹计算与越界清理;checkCollision()→ 基于矩形包围盒的 O(n²) 碰撞检测(CMyPlane.h中getRect()返回 QRect);- 最终
update()触发paintEvent()重绘整帧。
这个设计意味着:你修改CEnemyController.cpp中的spawnInterval参数,无需重启即可实时生效——这是调试敌机密度的后悔药。
3. 源码结构拆解:22 个核心文件如何协作完成一局游戏
3.1 类职责图谱:谁管资源?谁管逻辑?谁管渲染?
项目采用清晰的 MVC 变体:C*类负责数据与业务逻辑(Model),MainRender承担视图(View),Settings.ui和conf.ini共同构成配置层(Controller)。下表列出高频修改的 8 个核心类及其不可替代性:
| 文件名 | 类名 | 关键职责 | 修改后是否需重启 | 典型调试场景 |
|---|---|---|---|---|
CMyPlane.cpp | CMyPlane | 玩家飞机移动、射击、生命值管理 | 否(热重载支持) | 调整移动速度speedX/speedY或子弹冷却时间fireDelay |
CEnemyController.cpp | CEnemyController | 敌机生成策略、难度曲线、批量更新 | 否 | 修改spawnRate控制敌机密度,或enemyTypes数组切换敌机类型 |
CBullet.cpp | CBullet | 子弹运动学、碰撞响应、销毁逻辑 | 否 | 调整speedY改变子弹飞行速度,或damage值影响得分 |
CScoreboard.cpp | CScoreboard | 得分计算、连击判定、最高分持久化 | 是(因saveHighScore()写入config/conf.ini) | 修改scorePerKill或连击倍率comboMultiplier |
CConfig.cpp | CConfig | 全局配置加载(conf.ini)、运行时参数注入 | 否(reload()方法可动态重载) | 调整screenWidth/screenHeight适配不同分辨率 |
MainRender.cpp | MainRender | 渲染调度、帧率控制、资源缓存(QPixmap) | 是(修改paintEvent逻辑需重启) | 优化drawBackground()的双缓冲策略,或添加粒子特效 |
Settings.cpp | Settings | 设置界面交互、conf.ini写入 | 是 | 新增音效开关控件,需同步修改CConfig::setSoundEnabled() |
CTimeDelay.cpp | CTimeDelay | 帧级精准延时(非QTimer)、技能冷却、动画帧间隔 | 否 | 调整delayTime控制 Boss 技能释放节奏 |
注意:所有
C*类均通过CConfig::getInstance()获取单例配置,而非硬编码数值。这意味着你在conf.ini中改playerSpeed=5,CMyPlane::move()中读到的就是 5——配置即代码,这是项目可维护性的根基。
3.2conf.ini配置项详解:12 个参数如何决定游戏体验
config/conf.ini是项目唯一外部可编辑文本文件,其结构遵循标准 Windows INI 格式,分为[Game]、[Player]、[Enemy]、[UI]四个节。以下是直接影响手感的 6 个关键参数(其余 6 个为屏幕尺寸、路径等基础项):
| Section | Key | 默认值 | 作用说明 | 安全调整范围 | 风险提示 |
|---|---|---|---|---|---|
[Game] | frameRate | 60 | 主循环刷新频率(FPS) | 30–120 | >80 可能导致低端机卡顿;<40 动作明显拖影 |
[Player] | playerSpeed | 8 | 玩家飞机每帧移动像素数 | 4–16 | >12 时转向失控感增强;<6 操作迟滞感明显 |
[Player] | fireDelay | 200 | 两次射击最小间隔(毫秒) | 100–500 | <150 易误触连发;>300 射击节奏断裂 |
[Enemy] | spawnInterval | 1500 | 敌机生成间隔(毫秒) | 500–3000 | <800 敌机海啸式涌出;>2500 关卡节奏拖沓 |
[Enemy] | enemySpeed | 3 | 敌机基础下落速度(像素/帧) | 1–6 | >5 时玩家反应时间不足 0.5 秒;<2 敌机像悬浮 |
[UI] | showFPS | false | 是否在左上角显示实时帧率 | true/false | 开启后轻微性能损耗,仅调试用 |
修改后无需重启:在游戏运行时按F5键(MainRender.cpp中keyPressEvent绑定),程序自动调用CConfig::reload()重新解析conf.ini,所有参数即时生效。这是比改代码快十倍的调参方式。
3.3 资源路径硬编码分析:为什么picture1.png必须放在根目录?
项目未使用 Qt 的资源系统(.qrc),所有图片、声音路径均为相对路径硬编码,源头在CConfig.cpp的loadConfig()方法:
// CConfig.cpp 第68行 void CConfig::loadConfig() { QSettings settings("config/conf.ini", QSettings::IniFormat); playerImage = settings.value("Player/image", "picture1.png").toString(); // ... 其他资源路径同理 }而实际绘制时,QPixmap构造函数直接拼接:
// CMyPlane.cpp 第32行 pixmap = QPixmap(CConfig::getInstance()->playerImage); // 路径为 "picture1.png"这意味着:picture1.png必须与可执行文件(BeatPlane.exe或BeatPlane)位于同一目录。若你将BeatPlane移动到/opt/games/,则必须同时把picture1.png、sound/shoot.wav等文件复制过去。解决方案有两种:
- 推荐:在 Qt Creator 的“Projects”→“Run”→“Run Settings”中,将 “Working Directory” 设为
BeatPlane-master/(即资源所在目录); - 长期维护:自行添加
.qrc文件,将RESOURCES += resources.qrc加入.pro,并改用QPixmap(":/images/picture1.png")。
4. 避坑指南:编译失败、黑屏、卡死、音效无声的五大血泪现场
4.1 现象:编译报错error: 'nullptr' was not declared in this scope
原因:你的编译器(如旧版 MinGW 4.9)不支持 C++11 的nullptr关键字,而项目中大量使用(CPlane.h第22行virtual ~CPlane() = default;隐含nullptr初始化)。
解决:在.pro文件中添加CONFIG += c++11,并确保工具链支持 C++11(MinGW 5.3+ 或 MSVC2015+)。若仍报错,将所有nullptr替换为0(如CEnemy* p = nullptr;→CEnemy* p = 0;),共 7 处(搜索nullptr可定位)。
4.2 现象:程序启动后黑屏,控制台无报错,进程持续运行
原因:MainRender.cpp中paintEvent()调用了drawBackground(),而该函数内部QPainter::drawPixmap()的坐标超出QPixmap尺寸,Qt 5.12+ 默认启用严格模式,直接静默失败。
解决:检查shoot_background.png尺寸是否为1024x768(项目默认背景尺寸)。若你替换了背景图,请同步修改MainRender.cpp第156行:
// 原始代码(假设背景图宽高为 W×H) painter.drawPixmap(0, bgY % H, pixmapBg); // bgY 为滚动偏移 // 修改为(确保 bgY % H 不越界) int drawY = bgY % H; if (drawY < 0) drawY += H; // 修复负数取模 painter.drawPixmap(0, drawY, pixmapBg);4.3 现象:按键无响应,飞机完全不动
原因:CMyPlane.cpp中keyPressEvent()未正确捕获QKeyEvent,常见于 Qt Creator 的“Run Settings”中勾选了 “Run in terminal”,导致键盘事件被终端劫持。
解决:取消勾选 “Run in terminal”,或在BeatPlane.h的构造函数中显式设置焦点:
// BeatPlane.h 第35行,在构造函数初始化列表后添加 this->setFocusPolicy(Qt::StrongFocus); this->setFocus(); // 强制获取焦点4.4 现象:射击音效shoot.wav播放无声,但explode.wav正常
原因:sound/目录下shoot.wav为 32-bit float 格式,而 Qt Multimedia 的QSound仅支持 16-bit PCM。用 Audacity 打开检查:若“Tracks”→“Stereo Track”显示 “32-bit float”,即为问题根源。
解决:用 Audacity 导出为WAV (Microsoft) signed 16-bit PCM,覆盖原文件。或改用QMediaPlayer(需重写CConfig::playSound(),增加setVolume(50)防止爆音)。
4.5 现象:游戏运行 2 分钟后卡死,CPU 占用 100%,CEnemyController对象数暴增至 500+
原因:CEnemyController.cpp中update()方法未清理已移出屏幕的敌机(enemy->y() > screenHeight),导致enemyList持续膨胀,checkCollision()的 O(n²) 复杂度爆炸。
解决:在CEnemyController::update()末尾添加清理逻辑:
// CEnemyController.cpp 第120行后插入 for (auto it = enemyList.begin(); it != enemyList.end();) { if ((*it)->y() > 800) { // 屏幕高度阈值 delete *it; it = enemyList.erase(it); } else { ++it; } }提示:此 Bug 在原始代码中真实存在,是新手最容易忽略的内存泄漏点。从那以后我每次接手 C++ 游戏项目,第一件事就是检查所有容器类的
erase()调用。
5. 进阶改造:三步实现“Boss 战”与“技能系统”,验证架构扩展性
5.1 第一步:在CEnemyController中注入 Boss 类型,复用现有调度框架
项目敌机由CEnemy.h/cpp定义,其type成员(enum EnemyType { NORMAL, FAST, ARMORED })已预留扩展位。我们新增BOSS类型,并利用CEnemyController的spawnEnemy()方法动态创建:
// CEnemy.h 第15行,追加枚举值 enum EnemyType { NORMAL, FAST, ARMORED, BOSS // 新增 }; // CEnemyController.cpp 第88行,在 spawnEnemy() 中添加分支 void CEnemyController::spawnEnemy() { int type = rand() % 100; if (type < 5 && gameLevel >= 5) { // 第5关起,5%概率刷Boss CEnemy* boss = new CEnemy(BOSS); boss->setPos(rand() % (screenWidth - 100), -200); // Boss从顶部上方入场 boss->setHP(50); // 设置50点血量 enemyList.append(boss); return; } // ... 原有NORMAL/FAST逻辑 }关键点:CEnemy构造函数需支持BOSS类型的差异化初始化(如更大尺寸、不同贴图、特殊移动轨迹),但无需新建类——复用CEnemy的update()方法,仅通过type分支控制行为,这是面向对象多态的典型实践。
5.2 第二步:用CTimeDelay实现 Boss 技能冷却,避免魔改游戏循环
Boss 需要周期性释放激光(每 3 秒一次),但updateGame()是全局帧循环,直接在里面写if (frameCount % 180 == 0)会污染逻辑。正确做法是复用项目自带的CTimeDelay类:
// CEnemy.cpp 第45行,在构造函数中为BOSS添加技能定时器 if (type == BOSS) { skillTimer = new CTimeDelay(3000); // 3秒冷却 skillTimer->start(); } // CEnemy.cpp 第102行,在 update() 中检查技能就绪 void CEnemy::update() { if (type == BOSS && skillTimer->isTimeout()) { fireLaser(); // 自定义激光发射逻辑 skillTimer->restart(); // 重置冷却 } // ... 原有移动逻辑 }CTimeDelay的精妙在于:它不依赖QTimer的信号槽(避免跨线程风险),而是通过elapsed()计算毫秒差,isTimeout()返回bool,可安全嵌入任何update()方法——这是项目作者对游戏开发中“确定性定时”的深刻理解。
5.3 第三步:玩家技能系统:长按空格蓄力,松开释放护盾
玩家技能不能破坏现有输入架构。CMyPlane::keyPressEvent()已处理Qt::Key_Space,我们将其升级为“蓄力-释放”模式:
// CMyPlane.h 第42行,新增成员 private: int chargeLevel; // 蓄力等级(0-100) bool isCharging; // 是否正在蓄力 CTimeDelay* shieldTimer; // 护盾持续时间计时器 // CMyPlane.cpp 第65行,在 keyPressEvent() 中改造空格逻辑 void CMyPlane::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Space) { if (!isCharging) { isCharging = true; chargeLevel = 0; } return; // 不执行原有射击,改为蓄力 } // ... 其他按键逻辑 } // CMyPlane.cpp 第88行,在 update() 中添加蓄力与释放 void CMyPlane::update() { if (isCharging) { chargeLevel = qMin(chargeLevel + 2, 100); // 每帧+2,上限100 if (chargeLevel >= 100 && !shieldActive) { activateShield(); // 激活护盾 } } } // CMyPlane.cpp 第120行,松开空格时触发 void CMyPlane::keyReleaseEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Space && isCharging) { if (chargeLevel >= 80) { releaseShieldBlast(); // 释放冲击波 } isCharging = false; chargeLevel = 0; } }护盾效果通过CMyPlane::paint()中临时绘制半透明圆形实现,冲击波则生成CBullet子弹群。整个过程未新增任何全局变量,未修改MainRender的渲染逻辑,所有状态封装在CMyPlane内部——这正是优秀 C++ 游戏架构的标志:高内聚、低耦合、可预测。
从那以后我每次评审学生游戏项目,第一眼就看C*类是否把所有状态变量声明为private,是否用CTimeDelay替代裸QTimer,是否用CConfig统一管理参数。这些细节不会写在教科书里,但决定了项目是能跑三天还是能维护三年。希望帮到你。
本文还有配套的精品资源,点击获取