资讯详情

资讯详情

Qt飞机大战实战:QGraphicsScene与碰撞检测

简介这份Qt飞机大战游戏工程面向具备C基础、希望通过完整项目理解Qt 2D游戏开发的学习者也适合作为课程设计、毕业设计或Qt练手项目的参考素材。工程围绕玩家操控、敌机生成、AI对战、子弹发射与碰撞判定等玩法组织代码呈现QPainter绘图、信号与槽事件响应、键盘输入处理、帧动画更新、音效播放以及多线程任务拆分等实现思路。压缩包共223个文件以105个头文件、32个cpp源文件、60个png图像和5个wav音效为主另含ui界面、qrc资源、glsl着色器、pro工程配置及运行所需库文件整体约6.21MB。目前已有281人学习适合按模块阅读源码、梳理类结构与资源加载流程并借鉴其渲染、事件处理和工程组织方式完善自己的Qt小游戏项目对准备Qt课程作业、项目复盘或小型游戏原型开发也有参考价值。1. 从一份能直接编译的 qt飞机大战游戏说起它到底适合拿来练什么解压一份 qt飞机大战游戏.zip最先看到的不是散落的图片而是.pro工程、qrc资源和一个能直接编译的 Qt Widgets 场景。对想练手事件循环、QGraphicsView和碰撞检测的人来说这种完整度比看十篇零散教程更实在。它没有把逻辑塞进一个几千行的main.cpp而是把玩家、敌机、子弹拆成独立实体再用场景类统一调度刚好覆盖了 Qt 桌面小游戏最核心的那条链路。这份资源适合两类人一类是刚学完 Qt 控件、想找一个能跑能改的完整小项目练手另一类是想给现有工具加个小游戏模块需要参考QGraphicsScene对象管理和定时器刷新的写法。如果你只想要一个换皮就能上架的成品它可能不够但如果你想搞清楚“一个飞机大战从按键到碰撞到底经过哪些类”它比空泛的框架demo更有用。下面按工程骨架、核心链路、运行步骤、避坑和二次开发顺序拆。2. 工程骨架拆解qmake 项目、场景层与实体类是怎么分层的2.1 从 .pro 和 main.cpp 看入口谁创建了窗口谁驱动了游戏循环Qt 小游戏最容易翻车的地方不是玩法而是入口文件里把窗口、场景、定时器全混在一起。这个项目的入口通常很薄main.cpp只负责创建QApplication、场景和视图把“游戏世界”交给GameScene把“显示窗口”交给QGraphicsView。先看工程文件它决定了你拿到手能不能一次编译通过。# PlaneGame.pro QT core gui widgets # 如果项目里有射击音效或背景音乐还需要下面这行 QT multimedia CONFIG c17 TEMPLATE app TARGET PlaneGame SOURCES \ main.cpp \ game/GameScene.cpp \ game/Player.cpp \ game/Enemy.cpp \ game/Bullet.cpp HEADERS \ game/GameScene.h \ game/Player.h \ game/Enemy.h \ game/Bullet.h RESOURCES resources/resources.qrc这段.pro里最关键的是QT widgets和RESOURCES。widgets提供QApplication、QGraphicsView这些桌面窗口能力multimedia不是所有飞机大战都带但只要代码里出现QSoundEffect或QMediaPlayer漏掉它就会在链接阶段报未定义引用。CONFIG c17是常见写法项目里如果用了QRandomGenerator、qBound这些现代接口编译器标准不能太低。SOURCES和HEADERS必须把新增的.cpp/.h都列进去Qt 的 qmake 不会自动扫描目录这是新手最常踩的坑之一。入口文件一般长这样// main.cpp #include QApplication #include game/GameScene.h #include QGraphicsView int main(int argc, char *argv[]) { QApplication app(argc, argv); GameScene scene; scene.setSceneRect(0, 0, 480, 800); QGraphicsView view(scene); view.setFixedSize(480, 800); view.setWindowTitle(QStringLiteral(Qt 飞机大战)); view.setRenderHint(QPainter::Antialiasing, true); view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); view.show(); return app.exec(); }这里scene.setSceneRect(0, 0, 480, 800)定义了游戏世界的逻辑坐标视图大小也按 480×800 固定竖屏比例刚好适合飞机大战。view.setScene(scene)其实由构造函数完成QGraphicsView view(scene)已经把场景挂上去了。关闭滚动条是必须的否则玩家按方向键移动时视图可能跟着滚动看起来像“飞机飞出屏幕”。setRenderHint只影响绘制质量不影响碰撞检测别把它当成性能开关。真正让游戏动起来的是GameScene内部的QTimer入口文件只负责把窗口立起来。2.2 目录分层场景、实体、资源三块怎么摆这类项目常见的目录结构不会太复杂但分层清楚与否直接决定你后面改起来顺不顺手。一个比较合理的摆法是把“场景调度”“可移动实体”“静态资源”分开PlaneGame/ ├── PlaneGame.pro ├── main.cpp ├── game/ │ ├── GameScene.h │ ├── GameScene.cpp │ ├── Player.h │ ├── Player.cpp │ ├── Enemy.h │ ├── Enemy.cpp │ ├── Bullet.h │ └── Bullet.cpp ├── resources/ │ ├── images/ │ │ ├── player.png │ │ ├── enemy.png │ │ ├── bullet.png │ │ └── explosion.png │ ├── sounds/ │ │ └── shoot.wav │ └── resources.qrc └── README.mdgame/放所有游戏逻辑类resources/放图片、音效和 qrc 清单根目录只留工程文件和入口。实际拿到的压缩包可能把所有类平铺在根目录这不影响运行但后面加 Boss、加道具时会越来越乱。resources.qrc是 Qt 的资源清单它把磁盘文件映射成:/images/player.png这样的虚拟路径编译时打进可执行文件。这样发布时不用额外拷图片目录但也意味着你替换图片后必须重新构建不能只改磁盘上的 png。我一般会先检查.pro里的SOURCES/HEADERS是否和实际文件一一对应再检查resources.qrc里有没有漏掉新加的资源。只要这两处对得上项目基本能编译。目录里如果出现.ui文件说明它可能混用了 Designer 窗体飞机大战这种全自绘场景通常不需要.ui有也不影响但别把场景逻辑写到窗体类里。2.3 场景坐标系与 item 管理为什么用 QGraphicsScene 而不是纯 QWidget 自绘飞机大战的核心不是“画飞机”而是“管理一堆会移动、会碰撞、会销毁的对象”。用QGraphicsScene QGraphicsItem的好处是每个飞机、子弹都是独立对象场景负责坐标变换、碰撞查询和绘制调度。纯QWidget自绘也能做但你要自己维护对象列表、自己算包围盒、自己处理层叠顺序代码量会翻倍。场景里通常有三类 itemPlayer、Enemy、Bullet它们都继承QGraphicsPixmapItem各自带speed()、damage()之类的属性。GameScene持有QListBullet* m_bullets和QListEnemy* m_enemies不是为了绘制而是为了每帧遍历更新位置和做回收。addItem()把对象挂到场景后场景才负责绘制只new不addItem对象存在但看不见这是黑屏问题里最常见的一种。坐标系方面场景左上角是(0,0)x 向右、y 向下。玩家初始位置一般放在底部中间x 场景宽/2 - 玩家宽/2y 场景高 - 玩家高 - 80。敌机从y -敌机高出生向下移动子弹从玩家上方出生向上移动。所有pos()都是 item 左上角在父坐标系里的位置sceneBoundingRect()才是场景坐标下的包围盒。做碰撞和出界判断时优先用sceneBoundingRect()不要拿boundingRect()直接和场景宽高比否则 item 一多、一嵌套就会算错。3. 核心链路玩家移动、子弹发射、敌机生成与碰撞检测怎么串起来3.1 按键状态到玩家位移一帧里先读输入再更新坐标玩家移动最稳的写法不是在每个keyPressEvent里直接moveBy而是把按键状态存下来在定时器 tick 里统一更新。这样按住两个方向键时不会因为事件顺序导致抖动也方便后面把“按帧移动”改成“按时间移动”。GameScene里维护一个QSetint m_keys按下插入、松开移除tick 时根据集合计算方向。// GameScene.h 片段 class GameScene : public QGraphicsScene { Q_OBJECT public: explicit GameScene(QObject *parent nullptr); protected: void keyPressEvent(QKeyEvent *event) override; void keyReleaseEvent(QKeyEvent *event) override; private slots: void tick(); private: void updatePlayer(qreal dt); void spawnBullet(); void spawnEnemy(); void checkCollisions(); QSetint m_keys; Player *m_player nullptr; QListBullet * m_bullets; QListEnemy * m_enemies; QTimer m_tickTimer; QTimer m_fireTimer; QTimer m_enemyTimer; QElapsedTimer m_clock; qint64 m_lastMs 0; bool m_gameOver false; };// GameScene.cpp 片段 void GameScene::keyPressEvent(QKeyEvent *event) { if (event-isAutoRepeat()) { return; } m_keys.insert(event-key()); if (event-key() Qt::Key_Space) { spawnBullet(); } QGraphicsScene::keyPressEvent(event); } void GameScene::keyReleaseEvent(QKeyEvent *event) { if (event-isAutoRepeat()) { return; } m_keys.remove(event-key()); QGraphicsScene::keyReleaseEvent(event); } void GameScene::updatePlayer(qreal dt) { if (!m_player || m_gameOver) { return; } int dx 0; int dy 0; if (m_keys.contains(Qt::Key_A) || m_keys.contains(Qt::Key_Left)) --dx; if (m_keys.contains(Qt::Key_D) || m_keys.contains(Qt::Key_Right)) dx; if (m_keys.contains(Qt::Key_W) || m_keys.contains(Qt::Key_Up)) --dy; if (m_keys.contains(Qt::Key_S) || m_keys.contains(Qt::Key_Down)) dy; if (dx 0 dy 0) { return; } // 斜向移动归一化避免同时按两个方向时速度变成 1.414 倍 const qreal len std::sqrt(qreal(dx * dx dy * dy)); const qreal step m_player-speed() * dt; m_player-moveBy(dx / len * step, dy / len * step); // 把玩家夹在场景内防止飞出边界 const QRectF itemRect m_player-boundingRect(); QPointF p m_player-pos(); p.setX(qBound(0.0, p.x(), sceneRect().width() - itemRect.width())); p.setY(qBound(0.0, p.y(), sceneRect().height() - itemRect.height())); m_player-setPos(p); }keyPressEvent里先判断isAutoRepeat()是为了过滤系统长按产生的重复按键事件。不过滤也能跑但长按方向键时事件会密集触发配合定时器容易出现移动速度不一致。m_keys用QSetint存键值松开时移除这样同时按A和D也不会互相覆盖。updatePlayer里dt是秒m_player-speed()建议定义成“像素/秒”比如 280这样 16ms 一帧移动约 4.5 像素看起来比较顺。如果项目里是按帧写死moveBy(0, -5)也能玩但换高刷屏或机器卡顿时速度会变后面最好改成时间驱动。qBound(min, value, max)的三个参数分别是下界、当前值、上界这里用来把玩家坐标夹在场景内。注意max要减去 item 自身的宽高因为pos()是左上角不是中心点。很多“飞机能飞出屏幕一半”的问题就是只夹了0到sceneRect().width()忘了减 item 尺寸。3.2 子弹与敌机的生命周期生成、移动、出界回收子弹和敌机是典型的“短生命周期对象”生成、移动、出界、销毁四个阶段必须闭环。只new不删玩几分钟内存和对象数都会涨只removeItem不delete对象还在内存里场景不画了但指针仍有效后面容易野指针。先看子弹生成和更新。// GameScene.cpp 片段 void GameScene::spawnBullet() { if (!m_player || m_gameOver) { return; } auto *bullet new Bullet(); const QRectF playerRect m_player-sceneBoundingRect(); // 子弹从玩家中心上方出生 bullet-setPos(playerRect.center().x() - bullet-boundingRect().width() / 2.0, playerRect.top() - bullet-boundingRect().height()); addItem(bullet); m_bullets.append(bullet); } void GameScene::updateBullets(qreal dt) { for (int i m_bullets.size() - 1; i 0; --i) { Bullet *bullet m_bullets.at(i); bullet-moveBy(0, -bullet-speed() * dt); // 完全飞出场景顶部就回收 if (bullet-sceneBoundingRect().bottom() 0) { removeItem(bullet); m_bullets.removeAt(i); delete bullet; } } }spawnBullet里用sceneBoundingRect()拿玩家在场景中的实际包围盒再用center().x()算水平居中。子弹的speed()建议设成 700 到 900 像素/秒太小会和敌机“黏”在一起太大又像瞬移。updateBullets必须反向遍历因为删除元素会让后面的索引前移正向遍历再removeAt(i)会跳过下一个元素表现就是“有的子弹飞出屏幕不消失”。removeItem只把 item 从场景摘掉delete才释放内存这两步要连着写。敌机生成同理但多了一个随机 x 坐标void GameScene::spawnEnemy() { if (m_gameOver) { return; } auto *enemy new Enemy(); const qreal maxX sceneRect().width() - enemy-boundingRect().width(); const int x QRandomGenerator::global()-bounded(int(maxX)); enemy-setPos(x, -enemy-boundingRect().height()); addItem(enemy); m_enemies.append(enemy); } void GameScene::updateEnemies(qreal dt) { for (int i m_enemies.size() - 1; i 0; --i) { Enemy *enemy m_enemies.at(i); enemy-moveBy(0, enemy-speed() * dt); if (enemy-sceneBoundingRect().top() sceneRect().height()) { removeItem(enemy); m_enemies.removeAt(i); delete enemy; } } }QRandomGenerator::global()-bounded(int(maxX))生成0到maxX-1的随机整数保证敌机不会从右边界外面出生。如果maxX小于等于 0说明敌机图片比场景还宽这时候要么缩放图片要么直接返回否则bounded(0)是非法参数。敌机速度可以在Enemy构造里随机 80 到 180 像素/秒也可以按分数动态提高但别在 tick 里每帧随机否则敌机会忽快忽慢看起来像卡顿。3.3 碰撞检测与分数结算collidingItems 的用法和删除顺序碰撞检测用QGraphicsItem::collidingItems()最省事它返回和当前 item 相交的所有 item。但要注意默认QGraphicsPixmapItem的 shape 是矩形包围盒透明区域也算碰撞所以“擦边没碰到就爆炸”很常见。要精确一点可以在实体构造里设置setShapeMode(QGraphicsPixmapItem::MaskShape)让 Qt 按图片的 alpha 通道判断形状。先看子弹和敌机的碰撞。void GameScene::checkCollisions() { if (m_gameOver || !m_player) { return; } // 子弹与敌机 for (int i m_bullets.size() - 1; i 0; --i) { Bullet *bullet m_bullets.at(i); const QListQGraphicsItem * hits bullet-collidingItems(); Enemy *hitEnemy nullptr; for (QGraphicsItem *item : hits) { hitEnemy dynamic_castEnemy *(item); if (hitEnemy) { break; } } if (!hitEnemy) { continue; } // 先处理敌机再处理子弹避免删除顺序错乱 m_enemies.removeAll(hitEnemy); removeItem(hitEnemy); delete hitEnemy; removeItem(bullet); m_bullets.removeAt(i); delete bullet; } // 玩家与敌机 const QListQGraphicsItem * playerHits m_player-collidingItems(); for (QGraphicsItem *item : playerHits) { if (dynamic_castEnemy *(item)) { m_gameOver true; m_tickTimer.stop(); m_fireTimer.stop(); m_enemyTimer.stop(); break; } } }这段代码里最容易出错的是删除顺序。collidingItems()返回的列表可能包含多个敌机但一颗子弹只应该打掉一个所以找到第一个Enemy就break。删除时先把敌机从m_enemies列表移除再removeItem再delete子弹同理。如果先delete敌机再removeAllQList里会留下悬空指针下一次遍历直接崩溃。dynamic_cast需要类有虚函数QGraphicsItem本身有虚析构所以没问题但如果你用qgraphicsitem_cast记得在自定义类里重写type()。玩家碰撞检测放在子弹之后是因为子弹可能在同一帧先打掉敌机避免玩家“被已经死掉的敌机撞死”。这个顺序在飞机大战里很关键尤其是敌机密集的时候。m_gameOver置位后停掉三个定时器防止游戏结束后还在生成子弹和敌机。如果项目里有爆炸动画最好在delete前先addItem一个爆炸 item并给它设一个短生命周期的QTimer::singleShot否则爆炸图会跟着敌机一起被删掉。3.4 用 QElapsedTimer 把按帧移动改成按时间移动很多飞机大战 demo 直接moveBy(0, -5)在 60Hz 屏幕上看起来没问题但换到 144Hz 或机器卡顿时速度就变了。更稳的做法是记录上一帧时间用dt缩放移动量。QElapsedTimer适合做这种单调计时不受系统时间调整影响。void GameScene::tick() { const qint64 now m_clock.elapsed(); qreal dt (now - m_lastMs) / 1000.0; m_lastMs now; // 卡顿或断点调试后 dt 可能很大限制在 50ms 以内防止对象瞬移 dt qMin(dt, 0.05); updatePlayer(dt); updateBullets(dt); updateEnemies(dt); checkCollisions(); }m_clock.start()要在GameScene构造里调用m_lastMs初始设为 0。dt单位是秒qMin(dt, 0.05)是防止调试断点后恢复时一帧走了好几秒子弹直接飞出屏幕、敌机跳过碰撞。这个上限不要设得太小否则低帧率机器上游戏会变成慢动作50ms 对应 20fps已经是很卡的情况了。定时器周期可以设 16ms但QTimer不保证精确 16ms实际帧间隔会受事件循环影响所以按dt缩放比写死每帧位移更可靠。4. 跑起来与换素材Kit 选择、qrc 资源、帧率与打包的落地步骤4.1 首次构建打开 .pro、选 Kit、qmake 与运行拿到压缩包后先解压到纯英文路径中文路径在 qmake 和资源编译阶段容易出玄学问题。用 Qt Creator 打开根目录的.pro文件不要直接打开main.cpp否则 Creator 不会加载工程配置。打开后左侧项目栏会提示选择 Kit选你本机安装的桌面 Qt 套件确保它带widgets模块。然后执行“构建 → 执行 qmake”再点运行。命令行构建也可以# 在项目根目录执行PlaneGame.pro 替换成你实际的工程文件名 qmake PlaneGame.pro make -j4 # Windows MinGW 环境用 mingw32-makeMSVC 环境用 nmake 或 jom ./PlaneGameqmake负责根据.pro生成 Makefilemake编译链接。如果报Project ERROR: Unknown module(s) in QT: multimedia说明当前 Kit 没装多媒体模块要么在 Qt 安装器里补上要么先把音效相关代码和QT multimedia去掉再跑。构建目录建议和源码目录分开Qt Creator 默认的 shadow build 就挺好清理时直接删构建目录不会误删源码。4.2 资源替换图片尺寸、透明 PNG 与 qrc 路径换素材是二次开发里最快出效果的一步但资源路径和尺寸很容易翻车。resources.qrc的写法通常是这样RCC qresource prefix/ fileimages/player.png/file fileimages/enemy.png/file fileimages/bullet.png/file filesounds/shoot.wav/file /qresource /RCCprefix/表示资源根路径是/代码里用:/images/player.png访问。file里的路径是相对于resources.qrc所在目录的不是相对于项目根目录。如果你把 qrc 放在resources/下图片也放在resources/images/下就写images/player.png如果写resources/images/player.png编译时可能找不到文件。替换图片时保持文件名不变最省事尺寸可以变但要在实体构造里重新scaled否则碰撞盒和视觉大小会对不上。Player::Player(QGraphicsItem *parent) : QGraphicsPixmapItem(parent) { QPixmap pix(QStringLiteral(:/images/player.png)); if (pix.isNull()) { qWarning() player.png 加载失败检查 qrc 路径; } // 保持宽高比缩放到 60x60 的包围盒内 setPixmap(pix.scaled(60, 60, Qt::KeepAspectRatio, Qt::SmoothTransformation)); setShapeMode(QGraphicsPixmapItem::MaskShape); }scaled的第三个参数Qt::KeepAspectRatio保证图片不变形第四个参数Qt::SmoothTransformation让缩放更平滑。如果原图透明边很大MaskShape会让碰撞区域跟着 alpha 走视觉上更准但透明边太大会导致“明明碰到却不算”这时候要么裁图要么自定义shape()返回一个略小的矩形。pix.isNull()的判断很值钱资源路径错的时候它会在应用程序输出里留一行警告比黑屏干瞪眼强。4.3 帧率与调试输出16ms 定时器不是万能药QTimer设 16ms 只是“至少 16ms 后触发”实际帧率受事件循环、绘制量、机器负载影响。飞机大战对象不多时 16ms 够用但如果你加了大量爆炸动画、粒子效果或者每帧对所有子弹调用collidingItems()帧率会掉。调试时可以在tick()里打印帧间隔和对象数void GameScene::tick() { // ... 计算 dt static int frameCount 0; if (frameCount % 60 0) { qDebug() dt(ms): dt * 1000.0 bullets: m_bullets.size() enemies: m_enemies.size(); } // ... 更新逻辑 }每 60 帧打印一次避免输出刷屏拖慢程序。如果dt经常大于 33ms说明已经低于 30fps先看对象数是不是只增不减再看collidingItems()的调用次数。子弹和敌机各 100 个时每颗子弹都查一次碰撞就是 1 万次相交判断虽然 Qt 有空间索引但对象管理不当仍会卡。优化方向不是盲目提高定时器精度而是减少无效对象、及时回收、必要时用对象池。4.4 发布windeployqt 与插件目录检查开发机能跑不代表拷到别的机器能跑。Windows 下用windeployqt把依赖的 Qt DLL、平台插件、图片格式插件拷到可执行文件旁边# 在 release 构建目录执行PlaneGame.exe 替换成实际可执行文件名 windeployqt --release --no-translations PlaneGame.exe执行后目录里会出现Qt5Core.dll或Qt6Core.dll、platforms/qwindows.dll、imageformats/等。如果用了音效还要检查multimedia/插件目录和对应的多媒体 DLL 是否被拷进来windeployqt不一定总能识别所有动态加载的插件。发布前把整个 release 目录拷到一台没装 Qt 的机器上双击运行能起来、有图、有声音才算过。缺qwindows.dll的典型现象是双击没反应或弹“could not find or load the Qt platform plugin windows”不是代码问题是插件没带。5. 避坑排查编译报错、黑屏、卡顿与发布失败的 5 个现场5.1 编译期模块没加、qrc 没更新现象代码里用了QSoundEffect编译报QSoundEffect: No such file or directory或者链接阶段报undefined reference to QSoundEffect::...。原因.pro里只写了QT core gui widgets漏了multimedia。Qt 的模块不是头文件包含进来就能用必须在工程文件里声明qmake 才会把对应的 include 路径和库加进去。解决在.pro里补QT multimedia回到 Qt Creator 执行“构建 → 执行 qmake”再重新构建。如果当前 Kit 根本没装多媒体模块要么补装要么把音效代码暂时去掉。另一个常见现象是 qrc 里加了新图片代码里:/images/xxx.png却加载失败原因通常是改了.qrc后没有重新 qmake或者file路径写错。先执行 qmake再用QFile::exists(:/images/xxx.png)打印确认比盯着代码猜快。5.2 运行期黑屏、资源加载失败现象窗口出来了但场景全黑飞机、子弹一个都看不见控制台也没有明显报错。原因常见有三种。第一item 只new了没addItem()对象存在但不属于场景第二sceneRect和视图尺寸不一致item 坐标落在可视区外第三图片路径错QPixmap为空QGraphicsPixmapItem不绘制任何东西。第三种最隐蔽因为空 pixmap 不会让程序崩溃。解决先在GameScene构造末尾打印items().size()确认对象数量再在实体构造里打印pix.isNull()最后检查view.setScene(scene)和scene.setSceneRect(...)是否在show()之前设置。如果是 qrc 路径问题注意:/images/player.png前面的冒号和斜杠不能少file里的相对路径要和 qrc 文件位置对得上。黑屏时不要急着重写绘制逻辑先把“对象有没有加进场景、图片有没有加载成功”这两件事查清楚。5.3 性能子弹和敌机越玩越多现象刚启动很流畅玩一两分钟后越来越卡qDebug里bullets和enemies数量只涨不跌。原因对象出界后只removeItem()没有delete场景不画了但内存没释放或者出界判断写错子弹bottom() 0写成了top() 0永远不满足还有一种是指针列表只append不removeAt遍历时仍处理已经移除场景的对象。解决所有回收路径统一成“从列表移除 →removeItem→delete”三步反向遍历删除。出界判断用sceneBoundingRect()别用boundingRect()和场景坐标直接比。子弹和敌机数量上来后collidingItems()的调用次数也会涨必要时先做一次粗略的矩形相交判断再调精确碰撞。对象池是后期优化手段先把生命周期闭环再谈池化。5.4 判定碰撞时灵时不灵现象明明看到子弹擦着敌机边缘过去敌机却爆炸了或者飞机头已经碰到敌机却没触发游戏结束。原因QGraphicsPixmapItem默认的shape()是矩形包围盒图片透明区域也算碰撞所以视觉上没碰到、逻辑上已经相交。反过来如果设置了MaskShape但素材透明边裁得不干净或者缩放后 alpha 边缘变化也会出现“看起来碰到却不判”。解决先明确你要的是“手感优先”还是“视觉精确”。手感优先就用略小的矩形 hitbox比如在Player里重写shape()返回中心缩小 20% 的QPainterPath视觉精确就用setShapeMode(QGraphicsPixmapItem::MaskShape)并保证素材透明背景干净。调试时可以把 hitbox 画出来在paint()里加一笔drawRect(boundingRect())临时看一眼包围盒和图片是否对齐确认后再去掉。5.5 发布换机器没声音、缺 DLL现象开发机上一切正常拷到另一台电脑双击要么提示缺 DLL要么游戏能跑但没声音。原因可执行文件依赖的 Qt DLL 和插件没一起拷。Windows 下缺platforms/qwindows.dll会直接起不来缺multimedia/插件或多媒体 DLL 时程序能跑但音效静音。windeployqt能解决大部分依赖但它不会替你检查所有业务资源qrc 里的图片和音效是编进 exe 的不需要额外拷但外部配置文件、存档目录如果存在要单独处理。解决在 release 目录执行windeployqt然后把整个目录拷到干净机器测试。没声音时先看应用程序输出有没有“multimedia plugin not found”之类的警告再检查plugins/multimedia是否存在。发布目录不要只拷 exe也不要把开发机的 Qt 安装目录整个拖过去用windeployqt生成最小依赖集最稳。6. 二次开发从换皮到加 Boss 战改动点与验证方法二次开发最容易上头的是一上来就加 Boss、加道具、加排行榜结果改到一半发现子弹回收没闭环、资源路径全是硬编码最后连原版都跑不起来。我的习惯是先做“不动逻辑的改造”把玩家速度、子弹间隔、敌机生成间隔抽到GameConfig.h或一个 JSON 里确认改参数能立刻看到效果再动类结构。下面这张表是我一般会按顺序推进的改造清单改造点主要改动验证方式换皮替换resources/images下同名图片更新 qrc删除构建目录重跑 qmake确认新图尺寸和碰撞盒一致加 BossEnemy增加hp、damageGameScene增加 Boss 生成计时固定 30 秒生成打印每次子弹伤害和剩余血量加道具新增PowerUp类玩家碰撞后改子弹间隔强制掉落率 100%确认拾取后m_fireTimer周期变化对象池新增BulletPool回收时 reset 而不是 delete连续射击 10 分钟观察对象数和内存是否稳定加 Boss 的最小闭环不需要改渲染层给Enemy加一个int m_hp 1和void takeDamage(int dmg)Bullet本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →