尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Qt飞机大战实战:QPainter逐帧绘图与QTimer游戏循环
简介基于 C 语言与 Qt 应用框架的飞机大战小游戏项目代码均经过实际运行测试功能完整可直接用作课程设计、毕业设计或新手进阶的练习素材。面向计算机、人工智能、通信工程、自动化、电子信息等专业的在校学生、老师及企业开发者既能帮助理解 Qt 图形界面、事件循环、对象树与信号槽机制也能在此基础上扩展屏幕滚动、敌机智能、得分系统等功能。压缩包内共 58 个文件整体大小 33.2MB其中包含 7 个源码文件与 7 个头文件另有 31 张游戏素材图片、5 张背景图片、2 个音效文件以及 QRC 资源索引文件资源按模块存放定位和替换都很方便。项目还附有 README 说明和 Qt 工程文件英雄机、敌机、子弹、地图等关键模块独立成文件代码结构清晰便于对照学习资源打包、信号连接、碰撞检测等常见开发技巧。目前已有 349 人下载学习。1. 一个没有控件的Qt程序逐帧绘制撑起的飞机大战压缩包里找不到一个.ui文件也看不到QPushButton、QLabel这类常规控件。整个“飞机大战”从背景、敌机到子弹全都是QPainter在每一帧把贴图画到QWidget上再由QTimer按固定间隔反复触发。对用惯控件拖拽的人来说这个思路第一眼会有点别扭但恰恰是 c 小游戏里 Qt 绘图最典型的骨架。它解决的是“会用控件但不会做游戏循环”的断层贴图怎么管理、碰撞怎么判定、地图怎么滚起来、爆炸动画怎么不卡帧。适合做过两三个 Qt 窗口、想试游戏逻辑的在校学生也适合拿来当课程设计或立项演示。代码量不大结构也干净改起来不费劲值得完整拆一遍。2. 工程骨架与资源编译从AirplaneWar.pro到plane.rcc在 Qt 官网下载 Qt 5.15 LTS 或 Qt 6.x安装时勾选 MinGW 或 MSVC 套件双击AirplaneWar.pro就能被 Qt Creator 读取。这套源码用 qmake 管理工程文件把源文件、资源、平台配置都串在一起先读工程再读代码能少踩不少环境层面的坑。2.1 pro 文件如何约束一个 Qt Widgets 项目AirplaneWar.pro是整套工程的起点它决定了编译哪些源码、链接哪些 Qt 模块、生成什么名字的可执行文件。典型的项目文件内容如下QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET AirplaneWar TEMPLATE app CONFIG c11 SOURCES main.cpp mainscene.cpp map.cpp \ heroplane.cpp enemyplane.cpp \ bullet.cpp bomb.cpp HEADERS config.h mainscene.h map.h \ heroplane.h enemyplane.h \ bullet.h bomb.h RESOURCES res.qrc RC_ICONS app.ico第一行的core gui是最基础的模块greaterThan(QT_MAJOR_VERSION, 4): QT widgets是 Qt5 兼容 Qt4 的写法在 Qt 5.15 与 Qt 6 下都成立凡是直接用QWidget子类的程序都必须保留这一行。CONFIG c11把编译器切到 C11 标准代码里大量用到的auto、范围 for 和 lambda 都依赖它。TARGET指定生成的 exe 文件名RC_ICONS app.ico在 Windows 下直接把图标嵌进 exe发布后不需要额外携带 ico 文件。配置项作用本项目的值TARGET输出的可执行文件名AirplaneWarTEMPLATE工程模板app 表示生成可执行程序appCONFIG c11启用 C11 标准auto、范围 for、lambdaRC_ICONSWindows 下设置 exe 图标app.icoRESOURCES把 qrc 资源编译进程序res.qrc有一点容易忽略项目里放了bg.wav和bomb.wav如果实际工程用了QSoundEffect或QMediaPlayerpro 文件里通常还要加QT multimedia。如果编译时提示找不到QSoundEffect头文件先检查这一项而不是去翻代码。AirplaneWar.pro.user是 Qt Creator 生成的个人配置记录了打开时用哪个构建套件不同机器会有差异真正需要提交的是.pro和源码文件。提示缺失 multimedia 模块时Qt 头文件搜索阶段就会失败。先把 pro 改对再谈代码。2.2 资源系统res.qrc 与 plane.rcc图片和音频通过res.qrc统一管理。qrc 文件本质是 XML把项目目录下的文件路径登记进去编译时 Qt 会把它们全部转换成 C 数组最终内嵌进 exe。这个项目的贴图文件很多光是子弹就有bullet_1.png到bullet_14.png敌机有img-plane_1.png到img-plane_7.png背景有img_bg_level_1.jpg到img_bg_level_5.jpg在 qrc 里逐个登记后代码里用:/路径/文件名访问。注意 qrc 里的路径前缀要一致比如前缀设为/引用就写:/hero.png如果代码里写的是:/res/hero.pngqrc 里也要保持同样的相对结构。qrc resource fileres/hero.png/file fileres/bg.wav/file /resource /qrc这段 xml 对应的是把hero.png和bg.wav放进 qrc 的最简写法编译后代码通过:/res/hero.png加载。这个项目还带了plane.rcc它是rcc工具编译出来的二进制资源容器用QResource::registerResource在运行时注册。qrc 是构建期编译进 exercc 是运行时从外部文件加载常见做法是以 qrc 为主rcc 留着做资源热更新或调试用。2.3 入口main.cpp 与 mainscene.cpp 的启动顺序main.cpp非常短只负责创建应用、创建主场景、显示窗口、进入事件循环#include mainscene.h #include QApplication int main(int argc, char *argv[]) { QApplication a(argc, argv); MainScene w; // 主场景QWidget 子类 w.setWindowTitle(Airplane War); w.show(); return a.exec(); }这里容易忽略的是setWindowTitle和show的顺序先设置标题再显示避免窗口闪现默认标题。MainScene直接继承QWidget而不是QMainWindow原因在于游戏不需要菜单栏、工具栏和状态栏QWidget的整个客户区都可以交给QPainter少了一个中间层坐标计算也少一层窗口框架偏移。真正重要的是游戏循环不在main里启动而在MainScene构造完成后、窗口显示之后由QTimer驱动事件循环起来后定时器回调才会稳定触发。3. 游戏对象设计敌机、子弹、炸弹与 QTimer 驱动的主循环看heroplane.h、enemyplane.h、bullet.h、bomb.h会发现它们的结构高度一致每一个类都维护自己的坐标、贴图、速度和一个更新函数谁该做什么由自己决定。这就是 c 小游戏最基础的对象拆分方式也是后续加新机型、新子弹时最省事的设计。3.1 让每个对象“自己会动”坐标、贴图与碰撞盒以敌机为例头文件里通常长这样class EnemyPlane : public QObject { Q_OBJECT public: EnemyPlane(); void updatePosition(); // 每帧更新一次坐标 QRect rect() const; // 给碰撞检测用的矩形 QPixmap img; // 当前贴图 int x, y; // 敌机左上角坐标 int speed; // 向下移动速度 int hp; // 生命值 bool isFree; // false 表示可复用/可删除 };这个类的设计核心是updatePosition()每一帧由主场景统一调用函数内部只做y speed;这样的事。rect()返回当前坐标下的QRect碰撞检测完全基于它不直接碰贴图像素。isFree是一个复用标记被打中的敌机不立即销毁而是等统一回收。类职责关键成员HeroPlane玩家控制的飞机x、y、hp、bulletIntervalEnemyPlane自动下落的敌机speed、hp、isFreeBullet子弹的移动与回收isFree、speed、rect()Bomb爆炸动画与伤害范围动画帧索引、范围3.2 QTimer 驱动主循环而不是 while sleep游戏主循环在MainScene里常见做法是维护一个成员QTimer间隔设为 15ms 或 20mstimeout 触发一次更新函数m_timer.setInterval(15); // 约 66 FPS connect(m_timer, QTimer::timeout, this, MainScene::updateGame); m_timer.start();updateGame内部做的事按顺序是更新地图坐标、读取键盘状态移动英雄机、按概率生成敌机、更新所有子弹和敌机坐标、做碰撞检测、统一回收死亡对象、最后调用update()触发重绘。键盘控制不要写在keyPressEvent里直接动坐标因为系统键盘事件有按下延迟按住方向键会一顿一顿。正确做法是维护四个布尔状态void MainScene::keyPressEvent(QKeyEvent *e) { if (e-key() Qt::Key_Left) m_keyLeft true; if (e-key() Qt::Key_Right) m_keyRight true; if (e-key() Qt::Key_Space) m_keySpace true; } void MainScene::keyReleaseEvent(QKeyEvent *e) { if (e-key() Qt::Key_Left) m_keyLeft false; if (e-key() Qt::Key_Right) m_keyRight false; if (e-key() Qt::Key_Space) m_keySpace false; }然后在updateGame里每帧检查布尔值按下期间每帧都移动松开立即停止。左右边界一个是0一个是width() - hero.img.width()防止飞机半个身子飞出窗口。空格键同理用布尔状态加上发射间隔就能实现按住连发不需要每按一次都点一下。注意connect 用的槽函数返回值必须是 void。如果写成int updateGame()编译期不会立刻报错但信号槽连接会失败游戏一直不刷新。这是“qt 槽函数 返回值”最常见的坑。用QTimer而不是while(true) sleep的好处是事件循环不被阻塞键盘事件、窗口拖拽、音频回调都能正常执行。想要调节难度改setInterval的数值就够了。对 CPU 占用敏感的机器把 15ms 调整为 20ms帧率虽然下降但游戏逻辑照常跑。3.3 对象生命周期删除、复用与迭代器失效游戏里子弹、敌机是高频创建销毁的对象直接new/delete会产生大量碎片。常见做法是维护一个QList或QVector对象死亡时置isFree true每隔几帧统一回收一次for (int i m_enemies.size() - 1; i 0; --i) { if (m_enemies[i].isFree) m_enemies.removeAt(i); }这里必须从尾部向前遍历。removeAt(i)会把后面的元素整体前移如果从前往后删下标会错乱漏删或越界都有可能。先标记再回收的好处是碰撞检测遍历时不会因为删除操作导致迭代器失效同一批对象可以被多个系统安全访问。4. 滚动地图与碰撞检测让游戏开始“像样”地图滚动是飞机大战里最抢眼的 Qt 绘图效果也是map.cpp的全部职责。看img_bg_level_1.jpg到img_bg_level_5.jpg命名方式说明背景是按关卡拆了多张图使用时根据关卡索引加载对应文件即可不必做拼接大图。4.1 双图循环拼出无尽背景Map类内部放两张QPixmap和两个纵坐标一个简化的更新逻辑如下void Map::updatePosition() { m_y2 m_speed; // 第二张向下滚动 if (m_y2 m_height) // 第二张完全滚出屏幕底部 m_y2 0; // 回到顶部重新开始 m_y1 m_y2 - m_height; // 第一张永远接在第二张上方 } void Map::draw(QPainter p) { p.drawPixmap(0, m_y2, m_pixBg2); p.drawPixmap(0, m_y1, m_pixBg1); }初始时m_y2 0m_y1 -m_height第一张图正好悬在屏幕上方一帧的偏移处。每帧m_y2增加 speedm_y1同步跟随画面上始终有一张图从底部进入、另一张从上端补位衔接处没有视觉断口。speed 通常放在config.h里统一配置换关时改背景图、提速都只用改一处。绘制顺序也值得注意。paintEvent里先画背景再画爆炸特效、敌机、英雄机、子弹最后画血条和分数。前后遮挡关系一旦颠倒比如英雄机被背景盖住游戏画面就会“消失”这种问题排查起来比逻辑错误更隐蔽。4.2 QRect::intersects 做碰撞检测碰撞检测用的是QRect::intersects也就是两个矩形区域是否有交集。整个游戏最核心的判断就三对英雄机与敌机、子弹与敌机、炸弹与敌机。子弹打敌机的逻辑是这样for (Bullet b : m_bullets) { if (b.isFree) continue; for (EnemyPlane e : m_enemies) { if (e.rect().intersects(b.rect())) { b.isFree true; // 子弹消失 e.hp--; if (e.hp 0) e.isFree true; // 敌机标记为待回收 break; } } }这个循环只做标记不做删除配合上一章的回收机制遍历非常安全。QRect的判定速度足够快敌方单位几十个的规模下完全不用做四叉树之类的优化。碰撞对触发条件本项目通常的处理英雄机 vs 敌机两个 rect 相交英雄血量减少敌机标记销毁子弹 vs 敌机两个 rect 相交子弹消失敌机扣血炸弹 vs 敌机爆炸范围与敌机 rect 相交范围内敌机扣血敌机 vs 屏幕底部y 坐标超过窗口高度标记销毁视规则扣生命有一个细节值得注意QRect是按整块矩形算的飞机贴图往往有透明边角直接拿整张图做碰撞会显得“还没碰到就掉了”。想更精细要么把rect()返回值向内缩几像素要么手动指定热点区域比如把 x 和 y 各加 4宽高各减 8。4.3 音效与绘制性能的边界bg.wav做背景音乐、bomb.wav做爆炸音效在 Qt 里比较轻量的做法是QSoundEffect音频文件小、循环播放方便QSoundEffect bgMusic; bgMusic.setSource(QUrl(qrc:/bg.wav)); bgMusic.setLoopCount(QSoundEffect::Infinite); bgMusic.setVolume(0.3f); bgMusic.play();如果bg.wav实际是较长的音乐QSoundEffect在部分平台会性能吃紧那时再换QMediaPlayerQMediaPlaylist也不迟。本项目音频资源很小QSoundEffect足够。绘制性能方面最影响体验的是每帧都去加载贴图。QPixmap(:/img-plane_1.png)这种写法一旦出现在paintEvent里文件 IO 会直接拖垮帧率。正确做法是对象构造时把QPixmap存成成员变量绘制只做drawPixmap。此外在paintEvent里创建QPainter后建议开启setRenderHint(QPainter::SmoothPixmapTransform)缩放贴图时边缘不会出现明显的锯齿。5. 发布、打包与血条绘制从开发机到别人能跑开发机上程序能跑只是第一步把 exe 发给别人还能跑才是 qt 发布软件的完整流程。这一章先解决打包再补两个能直接用进游戏的技巧。5.1 Release 构建与 windeployqt 一键起飞在 Qt Creator 左侧选择 Release 模式构建完成后在构建目录里找到AirplaneWar.exe。打开命令行进入 exe 所在目录执行windeployqt AirplaneWar.exewindeployqt会把 Qt 的动态库、platforms插件目录、styles、imageformats等依赖一次性复制到 exe 旁边。这里最常见的报错是运行 exe 时提示qt.qpa.platform: could not load the Qt platform plugin windows这就是plugins/platforms/qwindows.dll没有随程序一起部署。windeployqt默认会处理关键是别手动删掉plugins目录。源码里的图片和音频如果只存在于 qrc不用额外拷贝如果是运行时从文件系统读取就要把res目录整体放到 exe 同级。用QFileInfo可以快速自检部署结果QFileInfo check(QCoreApplication::applicationDirPath() /plugins/platforms/qwindows.dll); qDebug() (check.exists() ? ok : missing);MSVC 套件编译出来的 exe 在干净机器上可能还缺 VC 运行库打包时把对应版本的 Visual C Redistributable 一并带上或提示用户安装即可。5.2 自己画血条一个可复用的 QPainter 进度条组件飞机大战里英雄机的血量很少用QProgressBar控件因为控件的边框样式和游戏画面不搭。常见做法是直接用QPainter画这也就是“qt 自定义进度条”在游戏里的落地方式void paintProgressBar(QPainter p, int x, int y, int w, int h, qreal ratio, const QColor bg, const QColor fg) { ratio qBoundqreal(0.0, ratio, 1.0); p.fillRect(x, y, w, h, bg); p.fillRect(x, y, qRound(w * ratio), h, fg); p.drawRect(x, y, w, h); }调用时传入英雄机当前血量和最大血量的比值比如paintProgressBar(p, 10, 10, 200, 14, hero.hp * 1.0 / hero.maxHp, QColor(60, 60, 60), QColor(255, 80, 80))。ratio先被qBound限制在 0 到 1 之间即使hp扣成负数也不会把进度条画到区域外面。灰色底、红色前景、最外层描边三段式绘制足够应付大多数游戏 UI。如果嫌纯色单调把前景换成渐变填充或叠加一张纹理 PNG 就是完整血条皮肤。5.3 国际化前置先用 tr() 包字符串这个项目暂时只有中文界面但setWindowTitle(Airplane War)这类字符串建议从一开始就用tr()包起来。后续做 qt 国际化时在工程根目录执行lupdate AirplaneWar.pro会扫描全部源码把tr()包住的字符串导出成 ts 文件用 Qt Linguist 翻译后用lrelease生成 qm最后在main.cpp里加载QTranslator即可切换语言。包一层tr()的成本几乎为零却能让代码在需要出多语言版本时不用回头改源码。本文还有配套的精品资源点击获取
RELATED

相关推荐

从DIABLO源码看VC++与DirectX 2D RPG开发架构

从DIABLO源码看VC++与DirectX 2D RPG开发架构

简介:一套基于Visual C与DirectX开发的仿Diablo暗黑破坏神RPG游戏完整源代码,适合有一定C基础、希望进阶学习DirectX游戏编程与RPG架构的开发者。项目包含游戏引擎、角色行为、敌人AI、战斗系统、物品管理、界面绘制及输入处理等模块,代码结构…

📅 2026/9/15 3:49:07
Python实现盒计数维:从图像到分形维数的完整指南

Python实现盒计数维:从图像到分形维数的完整指南

简介:针对分形维数计算需求,这份压缩包提供了一套基于Box Counting方法的MATLAB实现,适合分形几何、图像分析与复杂系统研究方向的初学者及科研人员参考。压缩包仅2KB,共包含3个m文件,分别覆盖核心计数算法、Sierpinsk…

📅 2026/9/15 3:49:07
SpringBoot优雅停机,别再kill-9了

SpringBoot优雅停机,别再kill-9了

线上发布时,你有没有用过 kill -9 强行终止应用?进程瞬间消失,部署脚本跑得飞快,看起来一切正常。但用户那边可能正在提交订单、正在支付回调、正在上传文件——这些请求在毫秒之间被腰斩,数据写了一半,消息…

📅 2026/9/15 3:49:07
MORE NEWS

更多资讯

📰

Vite打包Vue项目Nginx部署避坑指南:从路径配置到刷新404

我想先说一个比较反直觉的结论:Vite打包默认出来的文件,如果你直接丢到Nginx的root目录里,大概率能跑,但只要你的项目里用了路由懒加载、图片资源引用了绝对路径、或者你想把前端项目挂到某个子路径下面,就会立刻翻车。…

📰

视觉提示词注入攻击:原理、实现与防御方案

1. 项目概述:视觉提示词注入攻击的本质与危害视觉提示词注入攻击(Visual Prompt Injection)是近年来AI安全领域涌现的新型攻击手段,它通过精心构造的视觉输入干扰多模态大模型的生成过程。与传统文本提示词注入不同,这…

📰

通信数据备份系统设计与实战经验分享

1. 通信数据备份与保障的核心价值在数字化时代,通信数据已成为企业和个人的核心资产。我经历过多次因数据丢失导致的业务中断,深刻体会到一套可靠的备份系统有多重要。通信数据备份不仅仅是简单的文件拷贝,而是需要综合考虑数据类型、存储介质…

📰

前端实习二面通关秘籍:原理、场景与工程化实战

1. 接到面试通知后,先把这个公司研究透前端实习二面通常不是纯背书局。一面已经筛过一轮基础,二面会明显往“原理、设计、综合能力”方向倾斜。MiniMax作为一家做多模态大模型方向的公司,前端在里面承担的更多是工具链、Web端交互、可视化产品…

📰

Cave项目从零搭建:UE4 nDisplay多屏集群配置实战指南

接手第一个Cave项目时,我差点被nDisplay这一堆术语劝退。Cluster、Node、Viewport、ProjectionPolicy,每个词单独看都懂,拼在一起就变成了天书。当时我用的就是UE4,项目要从零搭一个三面CAVE展示空间,网上资料零零散散…

📰

Qt飞机大战实战:QPainter逐帧绘图与QTimer游戏循环

简介:基于 C 语言与 Qt 应用框架的飞机大战小游戏项目,代码均经过实际运行测试,功能完整,可直接用作课程设计、毕业设计或新手进阶的练习素材。面向计算机、人工智能、通信工程、自动化、电子信息等专业的在校学生、老师及企业开发…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬