☰
C++飞机大战源码解析:Qt框架下的游戏开发实战
2026/10/11 20:32:08 网站建设 项目流程

简介:这是一份用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 版本是 Qt6,QT += multimedia的模块名和 API 与 Qt5 有差异,音效部分可能需要改成QSoundEffect或QAudioOutput。源码里如果用的是QSound,Qt6 下会直接编译报错。

2.2 核心类之间的协作关系

把src目录下的文件按职责分一下,能看出作者的设计思路:

类名职责关键依赖
CPlane飞机基类,定义坐标、速度、血量、绘制接口QSprite
CMyPlane玩家飞机,继承CPlane,处理键盘输入和射击CBullet
CEnemy敌机类,继承CPlane,带下落逻辑QSprite
CEnemyController敌机生成控制器,管理生成频率和波次CEnemy、CRandom
CBullet子弹类,负责飞行和碰撞检测QSprite
CScoreboard分数板,记录和显示得分无
QSprite精灵封装,统一管理图片加载和绘制QPixmap
CConfig读取conf.ini配置QSettings
CTimeDelay帧率控制或延时工具QElapsedTimer
MainRender主渲染循环,驱动所有对象更新和重绘以上全部
Settings设置界面逻辑Settings.ui

这个划分不算复杂,但足够清晰。CPlane作为基类抽出了公共属性,CMyPlane和CEnemy各自扩展。CEnemyController单独管生成逻辑,而不是把随机生成塞进MainRender,这一点对后续调难度很有帮助。QSprite把QPixmap的加载和绘制包了一层,虽然功能简单,但避免了每个类都写一遍drawPixmap。

2.3 配置文件 conf.ini 的读取方式

conf.ini是外部配置,通常长这样:

[game] enemy_speed=3 bullet_speed=8 spawn_interval=800 player_life=3 [window] width=480 height=640

CConfig类一般用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/2019,Linux 下用系统自带的 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,或者用QList<QSharedPointer<CEnemy>>自动管理。

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()会改变容器大小,正序遍历会跳过元素。子弹同理。如果嫌手动管理麻烦,可以把容器换成QList<QSharedPointer<CEnemy>>,但要注意循环引用问题。

5.4 现象:音效不播放,控制台提示 “QSound: No such file or directory”

原因:sound目录没有复制到 exe 同目录,或者音效文件格式不被 Qt 支持。解决:确认sound目录和 exe 在同一级,检查.wav或.mp3文件是否完整。Qt5 的QSound对 mp3 支持有限,建议用 wav 格式。如果用的是 Qt6,QSound已废弃,需要改用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++ 飞机大战源码跑起来、改下去。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询