ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++/Qt飞机大战源码拆解:从工程结构到二次开发实战

C++/Qt飞机大战源码拆解:从工程结构到二次开发实战 简介一套用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.cppCPlane飞机基类封装通用属性CPlane.h / CPlane.cppCEnemy敌机个体生命值、速度、掉落物CEnemy.h / CEnemy.cppCEnemyController敌机生成节奏与编队逻辑CEnemyController.h / CEnemyController.cppCBullet子弹对象方向、速度、是否存活CBullet.h / CBullet.cppCScoreboard计分与 UI 数值刷新CScoreboard.h / CScoreboard.cppCTimeDelay帧间隔控制避免游戏速度随帧率漂移CTimeDelay.h / CTimeDelay.cppCRandom随机数封装生成位置和事件CRandom.h / CRandom.cppCConfig读取 conf.ini 配置CConfig.h / CConfig.cppQSprite精灵类把图片素材按帧绘制出来QSprite.h / QSprite.cppMainRender游戏主渲染循环驱动一帧一帧跑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 敌机生成 CEnemyControllerCRandom 封装与生成节奏敌机系统是源码里最值得抄的部分。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_distributionint 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] width480 height700 fullscreenfalse [Play] player_speed8 fire_interval200 enemy_interval800 lives3这里 [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 的默认值、确认素材帧对齐再动逻辑代码。希望你也能用这份源码交出一份经得起追问的课程设计。本文还有配套的精品资源点击获取
返回列表