简介:一套用C++编写的飞机大战游戏完整源码,适合初学C++或对游戏开发感兴趣的读者学习项目结构、面向对象设计与简单游戏循环。整个压缩包共55个文件,其中13个头文件与13个源码文件构成游戏主体,覆盖飞机控制、子弹发射、敌机生成、得分面板等核心模块;另有13个音频文件负责背景音效,6张图片用于界面材质,3张设计稿保留切图细节,包体仅2.45MB,轻量易上手。项目配置中还包含工程配置、参数配置文件与说明文档,同时将音频、图片、源码分类存放,目录划分清晰,能帮助读者快速定位核心逻辑,并附有初始配置参数方便直接运行。目前已有2036人学习下载,对于想通过实战项目提升C++编码能力、理解碰撞检测与敌人AI的同学来说,是一份值得参考的入门级资料。
1. 拆这套 C++ 飞机大战源码之前,你得先知道它值得你花一个下午
很多人下载“C++语言编写的飞机大战游戏源码.zip”是冲着课程设计来的,结果 zip 一解压,看到几十个 .cpp、.h 和一堆 png 就懵了。其实这套源码的底子是 Qt + C++ 的完整小游戏工程,不是网上那种贴一张控制台字符画就自称飞机大战的玩具。你解压后会看到一个名叫 BeatPlane-master 的工程目录,里面有玩家飞机、敌机编队、子弹发射、碰撞检测、计分、音效和设置面板,主循环和渲染是分开的,素材是 psd 切图切出来的——这意味着你可以直接改配置换参数,不用碰算法也能做出一个“看起来不一样”的版本。
这套题适合两类人:一类是正在做 C++ 课程设计、需要交一个能跑还拿得出手的 Qt 小游戏;另一类是已经写过控制台程序、想看看真实 Qt 游戏工程怎么组织模块的初学者。它给你的不是一堆孤立语法点,而是一套能编译、能玩、能改造的完整代码。前提是你先把它的工程结构看明白,别一上来就改玩法逻辑,否则大概率翻车在环境上。
2. 先拆引擎还是先写玩法:BeatPlane 的模块分层与渲染分离设计
2.1 从源码文件反推架构:模型、渲染、控制器是拆开的
拿到源码第一件事,不要双击 .pro,先把 src 目录下的文件名读一遍。你会发现它的类和你想的“一个 Game 类写完所有逻辑”完全不一样:玩家飞机、敌机、子弹、计分板、时间控制、配置读取全是独立类。下面是源码里能直接对应到文件的核心类清单:
| 类名 | 职责 | 对应文件 |
|---|---|---|
| CMyPlane | 玩家飞机:位置、移动、开火 | CMyPlane.h / CMyPlane.cpp |
| CPlane | 飞机基类,封装通用属性 | CPlane.h / CPlane.cpp |
| CEnemy | 敌机个体:生命值、速度、掉落物 | CEnemy.h / CEnemy.cpp |
| CEnemyController | 敌机生成节奏与编队逻辑 | CEnemyController.h / CEnemyController.cpp |
| CBullet | 子弹对象:方向、速度、是否存活 | CBullet.h / CBullet.cpp |
| CScoreboard | 计分与 UI 数值刷新 | CScoreboard.h / CScoreboard.cpp |
| CTimeDelay | 帧间隔控制,避免游戏速度随帧率漂移 | CTimeDelay.h / CTimeDelay.cpp |
| CRandom | 随机数封装,生成位置和事件 | CRandom.h / CRandom.cpp |
| CConfig | 读取 conf.ini 配置 | CConfig.h / CConfig.cpp |
| QSprite | 精灵类:把图片素材按帧绘制出来 | QSprite.h / QSprite.cpp |
| MainRender | 游戏主渲染循环,驱动一帧一帧跑 | MainRender.h / MainRender.cpp |
这种拆分方式在 Qt 小游戏里是标准做法:逻辑类和渲染类分离,MainRender 就像导演,每帧按顺序调用飞机更新、子弹更新、碰撞检测和绘图。你在 CMyPlane 里改速度、在 CBullet 里改子弹轨迹,都影响不到渲染层,这就是它能被二次开发的底层原因。
我看源码时特别注意 QSprite 这个类,它把“图片”抽象成“精灵”,所有飞机、子弹、爆炸效果都通过它截取贴图帧来绘制。这意味着游戏里的每一个可见对象,都不是随便贴一张 png,而是从一张大图里按坐标把对应区域抠出来的。理解这一点,后面调素材就不会玄学。
2.2 psd 切图定位:图片素材是怎么变成游戏里的飞机、子弹与数字的
源码目录里有一组图片:shoot.png、shoot_background.png、font.png、logo.png,还有 picture1.png 和 picture2.png,以及一个“psd切图定位”的命名标记。在 Photoshop 里打开 psd 源文件,把所有图层分成散图导出,这就是切图;切出来的 png 再被代码用坐标“定位”到一张大图里,游戏运行时按 QRect 截取对应区域绘制。BeatPlane 的素材设计是典型的“雪碧图”方案:多张帧拼在一张 png 里,运行时按行列索引。
怎么确认每个区域的坐标?最笨也最靠谱的办法是写一个枚举表,把每个对象对应的矩形记下来。多数情况下,shoot.png 里会按固定帧宽度排布,类似这种结构:
| 素材区域 | 起始坐标示例 | 尺寸 | 用途 |
|---|---|---|---|
| 玩家飞机 | (0, 0) | 60 x 80 | 玩家主战机 |
| 敌机 A | (0, 84) | 60 x 60 | 普通敌机 |
| 子弹帧 | (0, 148) | 20 x 30 | 玩家子弹 |
| 爆炸帧 x3 | (0, 180) | 60 x 60 | 击毁特效 |
在 QSprite 里,典型做法是按行列索引截取:
// QSprite.cpp 帧截取示意代码,源码中按同类逻辑实现,索引从 0 开始 QRect getFrameRect(int index, int cols, int frameW, int frameH) { int row = index / cols; int col = index % cols; return QRect(col * frameW, row * frameH, frameW, frameH); }这段代码的含义是把一张大图看成一张表格,index 是帧编号,cols 是每行帧数,返回的 QRect 直接交给 QPainter::drawImage 使用。改参数时注意 frameW 和 frameH 必须和 psd 切图时的尺寸一致,否则飞机和子弹会互相“串帧”。我一般会把切图尺寸写进 conf.ini 或者固定成头文件宏,这样后面换素材不用翻代码。
需要提醒的是:psd 切图定位这一步,拿到手的是已经切好的 png 和代码里的坐标常量,psd 原件本身不在资源包里。如果你想自己换一套飞机皮肤,用 Photoshop 的“导出为”把新图层切成同尺寸 png,再覆盖原文件即可。只要尺寸一致,代码不用改;尺寸变了,就得同步改帧常量。这也是很多初学者把素材换成一坨乱码的原因:只换了图片,忘了改宽高。
3. 核心玩法拆解:玩家操控、敌机控制器与子弹碰撞的实现思路
3.1 玩家飞机 CMyPlane:键盘响应与移动边界限制
从源码结构看,CMyPlane 继承自 QSprite,自身持有移动速度和开火间隔两个关键成员。它在 MainRender 里每帧被调用一次更新,键盘事件不是用 Qt 的信号槽,而是由 MainRender 统一捕获。这种做法在 Qt 游戏里很常见:信号槽适合按钮点击,不适合每帧都要响应的高频操作。
想改操控手感,就看这几个参数:移动时每次位移多少像素、开火最小间隔多少毫秒。下面这个类声明是从源码能看到的骨架,成员变量与常见做法保持一致:
// CMyPlane.h 类结构示意,实际成员与源码对应 class CMyPlane : public QSprite { public: void updateFrame(); // 每帧更新位置 void fire(); // 发射子弹 void setSpeed(int s); // 设置移动速度 private: int m_speed; // 每帧移动像素数 int m_fireInterval; // 两次开火间隔(毫秒) CTimeDelay m_fireTimer; // 开火计时器 };其中 m_speed 直接控制键盘按住时飞机移动的快慢,数值范围一般在 5 到 15 之间。m_fireInterval 控制射击频率,源码里 conf.ini 或者初始化函数会给一个默认值,比如 200 毫秒。把 interval 改成 100 会变成机关枪,改成 500 则明显卡顿。调试时建议先固定一个值跑通,再做平衡。
边界限制的逻辑通常在 updateFrame 里:判读飞机 x 坐标是否小于 0 或者大于屏幕宽度减飞机宽度。极限位置不建议用“等于”判断,因为移动步长可能跳过临界值,用“小于等于”和“大于等于”推边界更稳。
3.2 敌机生成 CEnemyController:CRandom 封装与生成节奏
敌机系统是源码里最值得抄的部分。CEnemyController 不直接 new CEnemy,而是维护一个生成计时器,每隔固定时间在屏幕上方随机 x 位置生成一个敌机。随机性交给 CRandom 类,目的是把 std::rand 的初始化细节藏起来,调用方只关心拿到的值在哪个区间。
我看到 CRandom 时特别有好感,因为它解决了 C++ 初学者最容易踩的坑:直接调用 rand() 不设置种子,导致每次运行生成的敌机位置完全一样。源码把它封装后,你只需要 getRandomInt(min, max) 取一个区间值。如果你拿到手的版本里没有封装 mt19937,自己升级时可以用下面这种写法替换:
// CRandom 的 mt19937 封装推荐写法,与源码提供随机功能的定位一致 int getRandomInt(int min, int max) { static std::mt19937 gen(std::random_device{}()); // 静态引擎只初始化一次 std::uniform_int_distribution<int> dist(min, max); return dist(gen); }注意两个细节:一是 gen 必须声明为 static,否则每次调用都重新初始化,生成的序列会重复;二是 max 取的应该是“屏幕宽度减去敌机宽度”,否则敌机会有一半身子在屏幕外面生成。看到这里你应该明白了,CEnemyController 的核心参数是生成间隔,而不是单个敌机逻辑。生成间隔越短,屏幕越挤。
3.3 子弹与碰撞检测:从矩形相交到销毁时机
飞机大战的碰撞检测不需要物理引擎,用 AABB 矩形相交就够了。子弹和敌机各自持有一个 QRect 或者可以转换成 QRect 的位置和尺寸,碰撞检测就是判断两个矩形是否相交。源码里 CScoreboard 的计分逻辑跟碰撞是绑定的:子弹击中敌机后,子弹标为不可用,敌机生命值减一,扣完则触发爆炸帧然后加分。
矩形相交判断在 Qt 里可以直接用 QRect::intersects,但我发现很多课程设计为了让代码“看起来有算法含量”,喜欢手写四边比较。其实两种都可以,后者可控性更强。手写版本如下:
// 手写 AABB 碰撞检测示意,与 QRect::intersects 等价 bool checkCollision(int ax, int ay, int aw, int ah, int bx, int by, int bw, int bh) { // 条件 1:在 x 轴上投影重叠 if (ax > bx + bw || bx > ax + aw) return false; // 条件 2:在 y 轴上投影重叠 if (ay > by + bh || by > ay + ah) return false; return true; }这段代码第一条件判断 A 是否完全在 B 右侧,第二条件判断 A 是否完全在 B 下方。两个条件都不成立,说明在 x 和 y 方向的投影都有交集,碰撞成立。使用时要取敌方“实际命中区域”而不是整个图片尺寸,一般留 3 到 5 像素的余量,否则玩家会抱怨“明明躲开了还是死”。这是一个非常常见的游戏手感坑,后面避坑章节我会再提。
碰撞之后的对象销毁也有讲究:不要直接 delete,而是把对象标记为“不活跃”,再由主循环统一回收。原因是你在遍历子弹列表时删除元素,会导致迭代器失效,这是最常见的崩溃来源。CEnemyController 或者 CBullet 里一定有一个活跃标记,每帧更新时跳过不活跃对象即可。
4. conf.ini 与 Settings.ui:不碰 C++ 代码也能调游戏参数
4.1 conf.ini 里到底能改什么:从窗口尺寸到敌机密度
源码根目录有一个 conf.ini,这是 CConfig 类的数据来源。用记事本打开它,你会发现里面不是乱七八糟的调试信息,而是真正控制游戏行为的参数。常见做法是把窗口宽度、高度、全屏开关、玩家移动速度、射击间隔、敌机生成间隔全部集中在这里,这样改游戏手感完全不用动 C++ 代码。一个典型的配置节选长这样:
[Game] width=480 height=700 fullscreen=false [Play] player_speed=8 fire_interval=200 enemy_interval=800 lives=3这里 [Game] 段的 width 和 height 分辨率直接影响碰撞精度。如果背景图是 480x700,而你强行把 height 改成 900,天空背景会被拉伸,子弹飞行距离变长,但碰撞盒还是原来的尺寸,就会出现“飞机没撞上却算撞上”的视觉误差。改分辨率前先确认 shoot_background.png 的实际像素,保持等比缩放。
[Play] 段里 enemy_interval 是敌机生成间隔,单位毫秒。800 意味着每 0.8 秒出一架敌机,改到 300 就会变成弹幕游戏。我建议你做课程设计时保留这份配置,把它当成“难度预设”,答辩时现场改数值演示效果,比写一堆代码更直观。
4.2 CConfig 读取流程:QSettings 一行代码搞定解析
CConfig.cpp 里的解析逻辑在 Qt 5 环境下通常直接基于 QSettings 的 IniFormat 实现。它负责把 conf.ini 转成游戏内部变量,并在 MainRender 初始化时传递给 CMyPlane、CEnemyController 等对象。下面是 QSettings 读取的典型写法,与源码 CConfig 的读取目标一致:
// CConfig.cpp 读取 conf.ini 的示意写法 CConfig::CConfig(const QString &path) { QSettings settings(path, QSettings::IniFormat); // IniFormat 指定 .ini 解析 m_screenWidth = settings.value("Game/width", 480).toInt(); m_screenHeight = settings.value("Game/height", 700).toInt(); m_playerSpeed = settings.value("Play/player_speed", 8).toInt(); m_fireInterval = settings.value("Play/fire_interval", 200).toInt(); m_enemyInterval = settings.value("Play/enemy_interval", 800).toInt(); }注意 value() 的第二个参数是默认值。如果 conf.ini 里的某一项被误删,游戏会退回到默认值而不是崩溃。这是 QSettings 比手动解析文本安全的地方。你拿到源码后可以试着把 enemy_interval 这项整个删掉,游戏依然能跑,这就是默认值兜底的效果。
CConfig 的调用时机也很关键。它必须在 MainRender 创建玩家和敌机控制器之前完成读取,否则对象初始化时拿不到参数。源码里 main.cpp 的启动顺序一般是:创建 CConfig → 根据配置创建主窗口 → 将配置项传入 MainRender。改代码时不要为了省事把 CConfig 改成全局单例,保持显式传参,后面做设置界面时会更容易映射。
4.3 Settings.ui 与运行时修改:Qt Designer 打开后能改什么
源码根目录还有一个 Settings.ui,这是 Qt Designer 的界面文件,用 Qt Creator 双击就能打开图形编辑界面。它本质上是设置对话框:玩家可以在这里勾选全屏、调整音效开关,界面上的控件和 conf.ini 里的项一一对应。QSettings 写入是可逆的,不但能读,也能写:
// Settings.cpp 保存按钮的示意逻辑,与 Settings.ui 配套 void Settings::saveConfig() { QSettings settings("conf.ini", QSettings::IniFormat); settings.setValue("Game/fullscreen", m_fullscreenCheck->isChecked()); settings.setValue("Play/enemy_interval", m_difficultySlider->value()); }这里要特别提醒:修改 conf.ini 后,必须重启游戏才生效。原因很简单,CConfig 只在启动时读取一次,运行中不会定时重载配置文件。如果你想实现“设置界面改完立即生效”,需要另外传一个指向 CConfig 的指针给 Settings,保存时同步更新内存里的值——但源码默认不这么做,它走的是“重启生效”路线。课程设计答辩演示时建议把设置界面的改动和重启过程一起演示,否则评审看到设置完没反应,会觉得是 bug。
5. 编译运行避坑:Qt 版本、qmake 与中文乱码的连环坑
5.1 环境选择与构建流程:为什么我推荐 Qt 5.15 + MinGW 64 位
这个工程基于 Qt Widgets,.pro 文件是 qmake 格式,所以编译首选 Qt Creator。我一般用 Qt 5.15 LTS 的 MinGW 64 位套件,配对应的 MinGW 编译器。版本选择上有两个坑:一是 Qt 6 对某些老项目的 .pro 语法兼容性有变化,比如部分模块拆分导致 qmake 报错;二是编译器必须和 Qt 套件匹配,否则会出现一堆“未定义的引用”。如果你坚持用 vscode 配置 c/c++ 环境也行,但需要额外写 tasks.json 调用 qmake 和 mingw32-make,比较折腾,先 Qt Creator 跑通再换也不迟。
拿到源码后的构建步骤,简单来说就四步:
mkdir build && cd build # 创建独立的构建目录 qmake ../BeatPlane.pro # 生成 Makefile mingw32-make -j4 # 编译,-j4 表示 4 线程并行 ./BeatPlane.exe # 运行qmake 这一步会把 .pro 里的 SOURCES、HEADERS、RESOURCES 转换成 Makefile。注意必须用 mingw32-make 而不是 make,因为 MinGW 环境的 make 命令可能指向别的工具链。编译过程中如果报错,先看是不是 .pro 里的文件路径和实际目录对不上——解压时如果用了带中文的文件夹路径,比如“飞机大战源码”,qmake 很容易在生成路径时出问题。我反复强调:解压路径全部用英文,这是第一个后悔药。
5.2 四个高频坑:从乱码到实现报错,按现象对号入座
第一个坑是运行黑窗口或编译报错“cannot find -lxxx”。现象是链接阶段找不到某个库,原因多为 Qt 套件版本不匹配,比如用 MSVC 编译器去编译 MinGW 版 Qt 生成的 .pro。解决方法是去“工具 → 构建套件”里确认编译器是 MinGW,而不是 MSVC,两个工具链生成的库不能互通。
第二个坑是游戏窗口标题和 UI 中文乱码。现象是界面上全是“铦铥”之类的字,原因是源码文件用 UTF-8 编码,而 Windows 上老版本 MinGW 默认按本地代码页读取。解决办法有两个:一是把源文件另存为 UTF-8 with BOM;二是在 .pro 文件里加上全局编译选项,强制编译器按 UTF-8 处理字符串。第二个办法更省事:
# BeatPlane.pro 里追加这一行,解决 UTF-8 源码在 Windows 下编译乱码 QMAKE_CXXFLAGS += /utf-8第三个坑是游戏运行时报“Failed to load image”但图片明明在目录里。原因几乎都是工作目录不对。Qt Creator 运行程序时默认工作目录可能是构建目录,而不是源码根目录,而源码加载图片用的是相对路径。解决方法是把图片、conf.ini 复制到构建目录,或者在 Qt Creator 的“运行”设置里把工作目录改成源码目录。第二个方案一劳永逸。
第四个坑是打包发给别人时,对方双击 exe 提示缺少 DLL。原因是 Qt 程序依赖 Qt5Core.dll、Qt5Gui.dll 这些运行时库。解决方法是构建后执行 windeployqt 自动收集依赖:
windeployqt BeatPlane.exe这个命令会扫描 exe 依赖并把需要的 Qt DLL 和插件目录复制到同一文件夹。执行后把整个文件夹压缩发给别人就能运行。另外注意 windeployqt 也要选对套件版本,用 32 位工具链打包出来的程序给 64 位系统跑没问题,反过来就不行。
6. 把它改造成你的课程设计:四个有价值的工程化升级思路
很多课程设计交上去只是“能玩”,但如果想让评分高一点,可以从这个源码往上叠功能。我推荐按性价比排序做四个升级,都是 QSprite 架构下比较容易落地的。
第一件事是把敌机生成间隔改成动态难度曲线。当前 enemy_interval 是固定值,玩到后面也不会变得更难。常见做法是让 CEnemyController 每隔一段时间把间隔乘以一个小于 1 的系数,设一个下限防止难到无解。比如每存活 30 秒,interval *= 0.85,最低 200ms。这个逻辑只改一个成员变量,效果却很明显。
第二件事是升级碰撞精度,把 AABB 矩形替换成圆形碰撞检测。飞机图片很多区域是透明的,矩形碰撞会让人觉得“明明没碰到”。圆形检测只需要半径一个参数,判断两个圆心距离是否小于半径之和即可。注意这里用数学库里的 sqrt,游戏里碰撞对象少,性能不必担心。代码实现也就十几行,但答辩时讲“精确碰撞和可行走区域”比单纯说“用矩形碰撞”有说服力得多。
第三件事是优化打印与调试,建议在 CScoreboard 里加一个帧率显示。CTimeDelay 本身就控制帧间隔,只要把最近 1 秒的帧数算出来,渲染到右上角,就能直观看到碰撞优化前后的性能变化。这不是一个必须功能,但调试时能帮你快速确认 Timer 逻辑没有跑偏。
最后一个值得做的是给 Settings.ui 加“立即生效”逻辑,让设置写入后不用重启游戏。做法是把 CConfig 指针传给 Settings 窗口,saveConfig 时既写配置文件也更新内存中的值。这个改动涉及对象生命周期管理,初期可能会忘记做内存同步,导致设置面板显示的值和实际不一致。但从工程角度看,这比重新启动游戏的用户体验好一个量级。
我把这套源码拆完复现后最大的收获是:游戏代码不难,难的是把素材、配置、渲染循环这些模块理顺。从那以后,我每拿到一套 Qt 小游戏源码,都会先跑通构建、核对 conf.ini 的默认值、确认素材帧对齐,再动逻辑代码。希望你也能用这份源码交出一份经得起追问的课程设计。
本文还有配套的精品资源,点击获取