资讯详情

C++飞机大战源码解析:Qt框架下的游戏开发实战

📅 2026/10/11 20:32:10 | 华诺云谱 👁 阅读
C++飞机大战源码解析:Qt框架下的游戏开发实战
简介这是一份用C编写的飞机大战游戏完整源码面向正在学习C、希望借助小游戏项目巩固面向对象编程与图形渲染的初学者和进阶者。压缩包共55个文件约2.45MB包含13个cpp与13个h源文件、13个mp3音效、6个png图片及psd切图、ui界面文件、pro工程配置和readme说明覆盖游戏逻辑、资源与工程配置各环节。源码按模块拆分清晰涵盖玩家飞机、敌机控制器、子弹、随机数、计分板、延时控制、主渲染与配置读取等类并配有Settings界面与音效资源便于理解游戏循环、碰撞检测与状态管理。目前已有2036人学习下载适合作为课程设计、练手项目或二次开发的基础帮助读者快速掌握C小游戏的工程组织与实现思路。1. 从一份 C 飞机大战源码说起它到底能跑出什么很多人第一次接触 C 项目实战都是从「飞机大战」这类小游戏开始的。这份BeatPlane-master源码包用纯 C 配合 Qt 框架实现了一个完整的竖版射击游戏包含玩家飞机、敌机生成、子弹碰撞、分数统计、音效播放和配置读取等模块。它不是那种只有几十行的玩具 demo而是拆成了CPlane、CEnemy、CBullet、CEnemyController、CScoreboard、QSprite等十几个类文件外加Settings.ui图形化设置界面和conf.ini外部配置文件。如果你正在找一份能编译、能改、能拿来当课程设计或简历项目的 C 小游戏源码这份资源值得花时间拆一遍。它适合已经学过 C 基础语法、想通过一个真实项目理解「类怎么协作、资源怎么管理、游戏循环怎么跑」的开发者也适合需要快速交作业但不想用网上烂大街控制台版本的学生。2. 拆开 BeatPlane 的工程结构Qt 项目文件与模块划分2.1 从 .pro 文件看构建依赖拿到源码包后第一件事不是急着编译而是先看BeatPlane.pro。这个文件决定了 Qt 怎么组织编译单元、链接哪些库。打开后你会看到类似下面的结构QT core gui QT multimedia greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET BeatPlane TEMPLATE app SOURCES main.cpp \ CPlane.cpp \ CMyPlane.cpp \ CEnemy.cpp \ CEnemyController.cpp \ CBullet.cpp \ CScoreboard.cpp \ QSprite.cpp \ CConfig.cpp \ CTimeDelay.cpp \ MainRender.cpp \ Settings.cpp HEADERS CPlane.h \ CMyPlane.h \ CEnemy.h \ CEnemyController.h \ CBullet.h \ CScoreboard.h \ QSprite.h \ CConfig.h \ CTimeDelay.h \ MainRender.h \ Settings.h FORMS Settings.ui RESOURCES resource.qrc这里有几个关键点。QT multimedia说明游戏用到了音效播放通常对应QSound或QMediaPlayer。FORMS Settings.ui表示有一个可视化设计的设置界面Qt Designer 生成的.ui文件会在编译时被uic工具转成 C 头文件。RESOURCES resource.qrc则是把图片和声音打包进可执行文件的资源系统这样发布时不用额外带一堆散图。注意如果你的 Qt 版本是 Qt6QT multimedia的模块名和 API 与 Qt5 有差异音效部分可能需要改成QSoundEffect或QAudioOutput。源码里如果用的是QSoundQt6 下会直接编译报错。2.2 核心类之间的协作关系把src目录下的文件按职责分一下能看出作者的设计思路类名职责关键依赖CPlane飞机基类定义坐标、速度、血量、绘制接口QSpriteCMyPlane玩家飞机继承CPlane处理键盘输入和射击CBulletCEnemy敌机类继承CPlane带下落逻辑QSpriteCEnemyController敌机生成控制器管理生成频率和波次CEnemy、CRandomCBullet子弹类负责飞行和碰撞检测QSpriteCScoreboard分数板记录和显示得分无QSprite精灵封装统一管理图片加载和绘制QPixmapCConfig读取conf.ini配置QSettingsCTimeDelay帧率控制或延时工具QElapsedTimerMainRender主渲染循环驱动所有对象更新和重绘以上全部Settings设置界面逻辑Settings.ui这个划分不算复杂但足够清晰。CPlane作为基类抽出了公共属性CMyPlane和CEnemy各自扩展。CEnemyController单独管生成逻辑而不是把随机生成塞进MainRender这一点对后续调难度很有帮助。QSprite把QPixmap的加载和绘制包了一层虽然功能简单但避免了每个类都写一遍drawPixmap。2.3 配置文件 conf.ini 的读取方式conf.ini是外部配置通常长这样[game] enemy_speed3 bullet_speed8 spawn_interval800 player_life3 [window] width480 height640CConfig类一般用QSettings来读// CConfig.cpp #include CConfig.h #include QSettings #include QCoreApplication CConfig::CConfig() { QString path QCoreApplication::applicationDirPath() /conf.ini; QSettings settings(path, QSettings::IniFormat); enemySpeed settings.value(game/enemy_speed, 3).toInt(); bulletSpeed settings.value(game/bullet_speed, 8).toInt(); spawnInterval settings.value(game/spawn_interval, 800).toInt(); playerLife settings.value(game/player_life, 3).toInt(); windowWidth settings.value(window/width, 480).toInt(); windowHeight settings.value(window/height, 640).toInt(); }QSettings的IniFormat模式会直接解析 ini 文件value()的第二个参数是默认值防止配置文件缺失时程序崩溃。applicationDirPath()保证读取的是可执行文件同目录下的conf.ini而不是当前工作目录。这个细节很多新手会踩坑在 Qt Creator 里运行和直接双击 exe 运行工作目录可能不同用applicationDirPath()最稳妥。参数说明enemy_speed控制敌机每帧下移像素数数值越大难度越高spawn_interval是敌机生成间隔毫秒数越小越密集player_life是玩家初始生命值。改这些值不需要重新编译直接编辑conf.ini即可。3. 编译与运行从 Qt Creator 到独立可执行文件3.1 环境准备与 Qt 版本选择这份源码用的是 Qt Widgets 加 Qt Multimedia没有用 QML。编译前需要确认三件事Qt 版本、编译器、多媒体模块。Qt 5.12 到 Qt 5.15 是最稳的区间Qt 6 需要改音效相关代码。编译器在 Windows 上推荐 MinGW 7.3 或 MSVC 2017/2019Linux 下用系统自带的 g 即可。安装 Qt 时在组件选择界面勾选Qt 5.15.2下的MinGW 8.1 64-bit和Qt Multimedia。如果漏了 Multimedia编译时会报Unknown module(s) in QT: multimedia。这个错误很常见补装模块后重新打开 Qt Creator 就能解决。3.2 用 qmake 命令行构建除了在 Qt Creator 里点绿色三角也可以用命令行构建这样更容易排查错误# 进入源码目录 cd BeatPlane-master # 生成 Makefile qmake BeatPlane.pro # 编译-j4 表示四线程并行 make -j4 # Windows MinGW 下用 mingw32-make mingw32-make -j4如果qmake不在 PATH 里需要用 Qt 安装目录下的完整路径比如C:\Qt\5.15.2\mingw81_64\bin\qmake.exe。编译成功后可执行文件在release或debug子目录下。把conf.ini、picture1.png、picture2.png、shoot.png、shoot_background.png、logo.png、font.png和sound目录复制到 exe 同目录否则运行时会缺图缺音效。提示如果用了resource.qrc把资源编译进 exe就不需要手动复制图片和声音。但conf.ini通常不放进 qrc因为它是给用户改的放进去反而读不到外部修改。3.3 资源路径与工作目录的坑Qt 程序里读图片有两种方式绝对路径、相对路径、资源路径:/。这份源码里QSprite加载图片时如果用相对路径比如QPixmap(picture1.png)那么程序运行时的工作目录必须是图片所在目录。在 Qt Creator 里默认工作目录是构建目录不是源码目录所以经常出现「源码里明明有图运行却显示空白」的情况。解决办法有两个一是在 Qt Creator 的 Projects → Run → Working directory 里改成源码目录二是把图片路径改成QCoreApplication::applicationDirPath() /picture1.png。第二种更可靠发布时只要把图片和 exe 放一起就行。// QSprite.cpp 中加载图片的稳妥写法 #include QCoreApplication #include QPixmap void QSprite::load(const QString fileName) { QString fullPath QCoreApplication::applicationDirPath() / fileName; m_pixmap.load(fullPath); if (m_pixmap.isNull()) { qWarning() 图片加载失败: fullPath; } }applicationDirPath()返回 exe 所在目录qWarning()在图片缺失时输出警告方便定位问题。如果图片放在 qrc 里路径要写成:/images/picture1.png冒号开头是 Qt 资源系统的约定。4. 核心玩法实现碰撞检测、敌机生成与分数统计4.1 碰撞检测的矩形判断飞机大战的碰撞检测不需要物理引擎用矩形相交就够了。QRect的intersects()方法能直接判断两个矩形是否重叠// MainRender.cpp 中碰撞检测片段 bool MainRender::checkCollision(const QRect a, const QRect b) { return a.intersects(b); } void MainRender::updateGame() { // 子弹与敌机碰撞 for (int i 0; i bullets.size(); i) { for (int j 0; j enemies.size(); j) { if (checkCollision(bullets[i]-rect(), enemies[j]-rect())) { enemies[j]-setDead(true); bullets[i]-setDead(true); scoreboard-addScore(10); } } } // 玩家与敌机碰撞 for (int j 0; j enemies.size(); j) { if (checkCollision(myPlane-rect(), enemies[j]-rect())) { myPlane-reduceLife(); enemies[j]-setDead(true); } } }rect()返回每个对象的包围盒通常用QRect(x, y, width, height)构造。子弹和敌机碰撞后双方标记为死亡分数加 10。玩家和敌机碰撞后玩家扣血敌机消失。这里没有做像素级精确碰撞因为飞机大战的判定不需要那么细矩形相交足够用而且性能更好。参数说明addScore(10)里的 10 是每击毁一架敌机的得分可以在CScoreboard里改成不同敌机不同分值。reduceLife()默认扣 1 点生命player_life初始值从conf.ini读取。4.2 敌机生成控制器与随机数CEnemyController负责按时间间隔生成敌机位置随机。CRandom类封装了随机数生成避免每次都用rand()// CEnemyController.cpp #include CEnemyController.h #include CRandom.h #include CEnemy.h CEnemyController::CEnemyController(int interval, int windowWidth) : m_interval(interval), m_windowWidth(windowWidth) { m_timer new QTimer(this); connect(m_timer, QTimer::timeout, this, CEnemyController::spawnEnemy); m_timer-start(m_interval); } void CEnemyController::spawnEnemy() { int x CRandom::getInstance().nextInt(0, m_windowWidth - 50); CEnemy *enemy new CEnemy(x, -50); emit enemyCreated(enemy); }QTimer每m_interval毫秒触发一次spawnEnemy()CRandom::nextInt(0, m_windowWidth - 50)生成敌机横坐标减 50 是防止敌机超出右边界。emit enemyCreated(enemy)把新敌机交给MainRender管理。CRandom用单例模式内部可以用QRandomGenerator替代rand()因为rand()的随机质量差而且不是线程安全的。注意new CEnemy之后一定要在合适时机delete否则每生成一架敌机就泄漏一块内存。常见做法是在MainRender的更新循环里遍历死亡对象并delete或者用QListQSharedPointerCEnemy自动管理。4.3 分数板与游戏状态切换CScoreboard负责记录分数和显示。分数显示一般用QPainter::drawText在paintEvent里画出来// CScoreboard.cpp #include CScoreboard.h #include QPainter void CScoreboard::draw(QPainter *painter) { painter-setPen(Qt::white); painter-setFont(QFont(Arial, 16)); painter-drawText(10, 30, QString(Score: %1).arg(m_score)); painter-drawText(10, 55, QString(Life: %1).arg(m_life)); } void CScoreboard::addScore(int delta) { m_score delta; } void CScoreboard::reset() { m_score 0; m_life 3; }draw()接收一个QPainter指针在MainRender::paintEvent里统一调用。addScore()累加分数reset()在游戏重新开始时清零。生命值显示也从这里走方便统一管理。如果要做最高分记录可以在reset()前把m_score写入QSettings下次启动时读出来。5. 避坑与排查编译报错、资源丢失、内存泄漏5.1 现象编译报错 “Unknown module(s) in QT: multimedia”原因Qt 安装时没有勾选 Qt Multimedia 模块或者.pro里写了QT multimedia但当前 Qt 版本不包含该模块。解决打开 Qt 维护工具补装对应版本的 Multimedia 模块重新运行 qmake 并重新构建。如果用的是 Linux 发行版自带的 Qt需要安装qtmultimedia5-dev之类的包。5.2 现象程序运行后窗口一片黑图片不显示原因图片路径不对或者工作目录不是源码目录。QPixmap加载失败时不会崩溃只是画不出东西。解决在QSprite::load()里加qWarning()输出完整路径确认程序实际找的是哪个目录。然后把图片路径改成applicationDirPath()拼接或者把图片放进 qrc 资源文件。5.3 现象玩几分钟后程序越来越卡内存占用持续上升原因敌机和子弹用new创建后没有delete死亡对象只是标记了dead但没从容器里移除。解决在每帧更新末尾加一段清理逻辑// 清理死亡对象 for (int i enemies.size() - 1; i 0; --i) { if (enemies[i]-isDead()) { delete enemies[i]; enemies.removeAt(i); } }倒序遍历是因为removeAt()会改变容器大小正序遍历会跳过元素。子弹同理。如果嫌手动管理麻烦可以把容器换成QListQSharedPointerCEnemy但要注意循环引用问题。5.4 现象音效不播放控制台提示 “QSound: No such file or directory”原因sound目录没有复制到 exe 同目录或者音效文件格式不被 Qt 支持。解决确认sound目录和 exe 在同一级检查.wav或.mp3文件是否完整。Qt5 的QSound对 mp3 支持有限建议用 wav 格式。如果用的是 Qt6QSound已废弃需要改用QSoundEffect。5.5 现象修改 conf.ini 后游戏参数没变化原因CConfig在程序启动时只读一次运行中改 ini 不会生效。解决这是正常行为改完 ini 需要重启游戏。如果想让配置热更新可以在Settings界面里加一个「保存并应用」按钮重新调用CConfig的读取逻辑并更新运行参数。6. 进阶改造把 BeatPlane 改成你自己的版本6.1 增加敌机类型与掉落道具原版敌机只有一种想加难度可以按分数阶段生成不同敌机。在CEnemyController::spawnEnemy()里根据当前分数决定敌机类型void CEnemyController::spawnEnemy() { int x CRandom::getInstance().nextInt(0, m_windowWidth - 50); CEnemy *enemy nullptr; if (m_score 500 CRandom::getInstance().nextInt(0, 100) 30) { enemy new CEnemy(x, -50, CEnemy::Fast); } else { enemy new CEnemy(x, -50, CEnemy::Normal); } emit enemyCreated(enemy); }CEnemy::Fast类型的敌机速度更快、血量更厚但击毁后得分更高。掉落道具可以在敌机死亡时随机生成一个CItem对象玩家碰到后加生命或升级子弹。这个改造不需要动MainRender的主循环只要在碰撞检测里加一条「玩家与道具」的判断即可。6.2 用 QSettings 保存最高分最高分记录是提升可玩性的小功能用QSettings几行就能实现// 游戏结束时保存最高分 QSettings settings(BeatPlane, Game); int highScore settings.value(highScore, 0).toInt(); if (m_score highScore) { settings.setValue(highScore, m_score); } // 启动时读取 int highScore settings.value(highScore, 0).toInt();QSettings在 Windows 下会写入注册表Linux 下写入~/.config不需要手动管理文件路径。读取时给默认值 0防止首次运行返回无效值。6.3 帧率控制与 CTimeDelay 的用法CTimeDelay通常用QElapsedTimer实现固定帧率// CTimeDelay.cpp #include CTimeDelay.h #include QElapsedTimer #include QThread CTimeDelay::CTimeDelay(int fps) : m_frameTime(1000 / fps) { m_timer.start(); } void CTimeDelay::wait() { qint64 elapsed m_timer.elapsed(); if (elapsed m_frameTime) { QThread::msleep(m_frameTime - elapsed); } m_timer.restart(); }m_frameTime是每帧毫秒数60 FPS 对应 16 毫秒。wait()在每帧末尾调用如果本帧耗时不足就补睡保证游戏速度在不同机器上一致。这个逻辑比QTimer定时刷新更稳因为QTimer的精度受系统调度影响高负载时可能跳帧。6.4 从 Qt Widgets 迁移到 QML 的边界如果想把界面改成更现代的 QML核心游戏逻辑可以保留但渲染层要重写。QSprite的QPixmap绘制要换成QQuickPaintedItem或QQuickItem加QPainter。碰撞检测和对象管理可以不动因为它们是纯 C 逻辑。迁移成本主要在渲染和输入事件上Qt Widgets 的keyPressEvent要换成 QML 的Keys.onPressed。如果只是交课程设计Qt Widgets 版本已经够用不必为了「看起来新」强行上 QML。6.5 打包发布时要注意的依赖Windows 下用windeployqt自动收集依赖windeployqt --release BeatPlane.exe这个命令会把 Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll、Qt5Multimedia.dll 以及 platforms 插件复制到 exe 同目录。如果漏了platforms/qwindows.dll程序启动会报「This application failed to start because no Qt platform plugin could be initialized」。Linux 下可以用linuxdeployqt或手动打包 AppImage。发布前记得把conf.ini、图片和声音一起带上除非已经放进 qrc。我自己的习惯是每次改完代码先跑一遍qmake make确认没有警告再进 Qt Creator 调试。资源路径和内存泄漏这两个坑我踩过不止一次后来养成了在QSprite::load()里加日志、在每帧末尾加清理循环的习惯。希望这份拆解能帮你少走点弯路顺利把这份 C 飞机大战源码跑起来、改下去。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑