news 2026/9/15 3:48:54

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt飞机大战实战:QPainter逐帧绘图与QTimer游戏循环

简介:基于 C++ 语言与 Qt 应用框架的飞机大战小游戏项目,代码均经过实际运行测试,功能完整,可直接用作课程设计、毕业设计或新手进阶的练习素材。面向计算机、人工智能、通信工程、自动化、电子信息等专业的在校学生、老师及企业开发者,既能帮助理解 Qt 图形界面、事件循环、对象树与信号槽机制,也能在此基础上扩展屏幕滚动、敌机智能、得分系统等功能。压缩包内共 58 个文件,整体大小 33.2MB,其中包含 7 个源码文件与 7 个头文件,另有 31 张游戏素材图片、5 张背景图片、2 个音效文件,以及 QRC 资源索引文件,资源按模块存放,定位和替换都很方便。项目还附有 README 说明和 Qt 工程文件,英雄机、敌机、子弹、地图等关键模块独立成文件,代码结构清晰,便于对照学习资源打包、信号连接、碰撞检测等常见开发技巧。目前已有 349 人下载学习。

1. 一个没有控件的Qt程序:逐帧绘制撑起的飞机大战

压缩包里找不到一个.ui文件,也看不到QPushButtonQLabel这类常规控件。整个“飞机大战”从背景、敌机到子弹,全都是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 += c++11 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 += c++11把编译器切到 C++11 标准,代码里大量用到的auto、范围 for 和 lambda 都依赖它。TARGET指定生成的 exe 文件名,RC_ICONS = app.ico在 Windows 下直接把图标嵌进 exe,发布后不需要额外携带 ico 文件。

配置项作用本项目的值
TARGET输出的可执行文件名AirplaneWar
TEMPLATE工程模板,app 表示生成可执行程序app
CONFIG += c++11启用 C++11 标准auto、范围 for、lambda
RC_ICONSWindows 下设置 exe 图标app.ico
RESOURCES把 qrc 资源编译进程序res.qrc

有一点容易忽略:项目里放了bg.wavbomb.wav,如果实际工程用了QSoundEffectQMediaPlayer,pro 文件里通常还要加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.pngbullet_14.png,敌机有img-plane_1.pngimg-plane_7.png,背景有img_bg_level_1.jpgimg_bg_level_5.jpg,在 qrc 里逐个登记后,代码里用":/路径/文件名"访问。注意 qrc 里的路径前缀要一致,比如前缀设为/,引用就写":/hero.png";如果代码里写的是":/res/hero.png",qrc 里也要保持同样的相对结构。

<qrc> <resource> <file>res/hero.png</file> <file>res/bg.wav</file> </resource> </qrc>

这段 xml 对应的是把hero.pngbg.wav放进 qrc 的最简写法,编译后代码通过":/res/hero.png"加载。这个项目还带了plane.rcc,它是rcc工具编译出来的二进制资源容器,用QResource::registerResource在运行时注册。qrc 是构建期编译进 exe,rcc 是运行时从外部文件加载,常见做法是以 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(); }

这里容易忽略的是setWindowTitleshow的顺序:先设置标题再显示,避免窗口闪现默认标题。MainScene直接继承QWidget而不是QMainWindow,原因在于游戏不需要菜单栏、工具栏和状态栏,QWidget的整个客户区都可以交给QPainter,少了一个中间层,坐标计算也少一层窗口框架偏移。真正重要的是游戏循环不在main里启动,而在MainScene构造完成后、窗口显示之后由QTimer驱动,事件循环起来后定时器回调才会稳定触发。

3. 游戏对象设计:敌机、子弹、炸弹与 QTimer 驱动的主循环

heroplane.henemyplane.hbullet.hbomb.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、bulletInterval
EnemyPlane自动下落的敌机speed、hp、isFree
Bullet子弹的移动与回收isFree、speed、rect()
Bomb爆炸动画与伤害范围动画帧索引、范围

3.2 QTimer 驱动主循环,而不是 while + sleep

游戏主循环在MainScene里,常见做法是维护一个成员QTimer,间隔设为 15ms 或 20ms,timeout 触发一次更新函数:

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会产生大量碎片。常见做法是维护一个QListQVector,对象死亡时置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.jpgimg_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增加 speed,m_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在部分平台会性能吃紧,那时再换QMediaPlayer+QMediaPlaylist也不迟。本项目音频资源很小,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.exe

windeployqt会把 Qt 的动态库、platforms插件目录、stylesimageformats等依赖一次性复制到 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 = qBound<qreal>(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()的成本几乎为零,却能让代码在需要出多语言版本时不用回头改源码。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 3:48:06

React组件传参全攻略:从Props到路由传参的实战避坑指南

2. 组件传参的整体设计与思路拆解2.1 为什么组件传参是 React 开发绕不开的坎React 的核心思想就是组件化。一个页面&#xff0c;不是一坨写死的 HTML&#xff0c;而是拆成若干个独立的组件&#xff0c;每个组件只管自己的那一块 UI 和逻辑。组件之间要协作、要共享数据&#x…

作者头像 李华
网站建设 2026/9/15 3:48:04

做网站都有什么功能?老手避坑指南

做网站都有什么功能?老手避坑指南 网站被黑挂马,后台突然多出个陌生管理员,打开首页全是博彩广告,这种时候你是不是懵了?别慌,这不是玄学,是架构没搭好。 很多新手以为“做网站”就是买个域名加个模板,结果上线三个月就被拖库。今天这篇 避坑指南…

作者头像 李华
网站建设 2026/9/15 3:45:26

Doris 全景解析:实时数仓架构、特性与选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 3:45:21

区块链混合共识机制:原理、实现与优化实践

1. 混合共识机制的本质与行业痛点区块链世界正面临一个根本性悖论&#xff1a;PoW&#xff08;工作量证明&#xff09;消耗巨大能源却提供顶级安全性&#xff0c;PoS&#xff08;权益证明&#xff09;节能高效但中心化风险如影随形。2021年以太坊Gas费飙升至200gwei时&#xff…

作者头像 李华
网站建设 2026/9/15 3:44:16

LSTM+CNN混合模型实现蛋白质二级结构预测

简介&#xff1a;本资源是一套基于Python实现的蛋白质二级结构预测完整项目代码&#xff0c;面向生物信息学初学者、计算机专业本科生及毕业设计/课程设计学生&#xff0c;解决从零入门蛋白质结构预测建模与实践落地的核心需求。压缩包共35个文件&#xff0c;含5个核心Python脚…

作者头像 李华
网站建设 2026/9/15 3:44:10

WSL2局域网访问Web服务:两步法搞定端口转发与防火墙配置

最近有朋友问了我一个特别典型的问题&#xff1a;他在 Windows 上用 WSL2 起了个 Web 服务&#xff0c;自己电脑上打开http://localhost:8080一切正常&#xff0c;但换到手机或者办公室另一台电脑&#xff0c;输入他那台 Windows 主机的局域网 IP&#xff0c;就是死活连不上。这…

作者头像 李华