
1. 项目缘起与价值为什么用QT做飞机大战如果你正在学习C和图形界面开发或者对游戏编程感兴趣但又觉得Unity、Unreal这些引擎过于庞大那么用QT来做一个“飞机大战”绝对是个绝佳的练手项目。这不仅仅是因为它经典更因为它能让你在一个相对可控的复杂度内系统地串联起QT框架的多个核心模块。很多人学QT跟着教程做几个对话框、表格就结束了感觉会了但一上手真实项目就无从下手。飞机大战这个项目恰好能填补这个“知道”和“会做”之间的鸿沟。为什么这么说一个完整的飞机大战麻雀虽小五脏俱全。它涉及到图形视图框架QGraphicsView/Scene/Item来管理游戏场景和精灵这是QT中处理2D图形交互的利器远比用QPainter直接画要高效和结构化。你需要处理键盘事件来控制玩家飞机的移动和射击这是理解QT事件系统的绝佳实践。游戏中的碰撞检测、敌机生成逻辑、分数更新、生命值管理会逼着你思考对象生命周期和信号槽通信。更进阶一点你还可以加入音效QSound、粒子特效虽然QT原生支持弱但可以自己模拟、甚至本地数据存储QSettings记录最高分。这个过程里你会反复遇到那些在QT开发中真实存在的问题比如多对象管理时的内存泄漏风险、频繁界面更新的性能优化、跨平台编译的注意事项等。所以这个项目的目的不是做一个商业级的游戏而是通过一个有趣的目标深度掌握QT这套工业级框架的实战用法。当你亲手让飞机动起来子弹飞出去敌机爆炸那种成就感会驱动你啃下那些枯燥的文档。下面我就以一个从零开始的视角带你走一遍核心实现路径并分享那些官方手册里不会写的“坑”和技巧。2. 战场搭建QGraphicsView框架深度解析与初始化游戏的核心是场景。在QT中对于这类包含大量可移动、可交互图形元素的程序强烈推荐使用QGraphicsView框架而不是在QWidget的paintEvent里手动绘制一切。前者提供了现成的场景管理、坐标变换、碰撞检测和高效的渲染机制。2.1 框架三剑客View, Scene, Item你需要理解这三个核心类的关系QGraphicsScene场景一个不可见的容器相当于整个游戏世界的坐标系。它管理着所有QGraphicsItem的生命周期和状态。我们的背景、飞机、子弹、敌机都是添加到Scene里的。QGraphicsView视图一个可见的窗口部件用于显示Scene的内容。你可以把它想象成一个观察场景的“摄像机”。一个View只能绑定一个Scene但一个Scene可以被多个View观察比如分屏显示。QGraphicsItem图元所有出现在场景中的图形对象的基类。我们的飞机、敌机都需要继承自它并重写其paint()函数来定义如何绘制自己以及boundingRect()来定义它的碰撞边界。初始化一个游戏窗口的典型代码如下// mainwindow.h 或 gamewindow.h #include QMainWindow #include QGraphicsView #include QGraphicsScene class GameWindow : public QMainWindow { Q_OBJECT public: GameWindow(QWidget *parent nullptr); private: QGraphicsView *view; QGraphicsScene *scene; // ... 其他成员如玩家飞机指针 }; // mainwindow.cpp GameWindow::GameWindow(QWidget *parent) : QMainWindow(parent) { // 1. 创建场景和视图 scene new QGraphicsScene(this); scene-setSceneRect(0, 0, 800, 600); // 设置场景逻辑大小游戏世界大小 view new QGraphicsView(scene, this); setCentralWidget(view); // 将视图设为主窗口的中心部件 // 2. 视图优化设置 view-setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // 关闭滚动条 view-setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); view-setFixedSize(800, 600); // 固定视图窗口大小 view-setRenderHint(QPainter::Antialiasing); // 抗锯齿让图形边缘更平滑 view-setCacheMode(QGraphicsView::CacheBackground); // 缓存背景提升性能 // 3. 设置背景一种简单方式 scene-setBackgroundBrush(QBrush(Qt::black)); // 纯黑背景 // 4. 创建玩家飞机并添加到场景 // PlayerPlane *player new PlayerPlane(); // scene-addItem(player); // player-setPos(scene-width()/2, scene-height() - 100); // 初始位置 }这里有几个关键点setSceneRect定义了游戏世界的逻辑坐标系。所有Item的坐标都基于这个矩形。它不一定等于视图的像素大小但通常我们设为一致以便于理解。关闭滚动条并固定视图大小是为了让游戏窗口不可缩放保持固定的视觉比例。setCacheMode(QGraphicsView::CacheBackground)在背景不变或变化不频繁时能显著提升渲染性能。如果你的背景是静态图片这个设置非常有效。2.2 自定义图元类玩家飞机的实现现在我们来创建最重要的单位——玩家控制的飞机。我们需要创建一个继承自QGraphicsPixmapItem它是QGraphicsItem的子类方便显示图片的类。// playerplane.h #ifndef PLAYERPLANE_H #define PLAYERPLANE_H #include QGraphicsPixmapItem #include QObject // 为了使用信号槽 #include QKeyEvent #include QTimer class PlayerPlane : public QObject, public QGraphicsPixmapItem { Q_OBJECT // 因为继承了QObject需要Q_OBJECT宏 public: PlayerPlane(QGraphicsItem *parent nullptr); void keyPressEvent(QKeyEvent *event) override; void keyReleaseEvent(QKeyEvent *event) override; public slots: void shoot(); // 发射子弹的槽函数 private: void move(); // 处理持续移动 QTimer *shootTimer; // 用于实现连射的定时器 bool moveLeft, moveRight, moveUp, moveDown; // 移动状态标志 int speed; // 移动速度 }; #endif // PLAYERPLANE_H这里采用了多重继承同时继承QObject和QGraphicsPixmapItem。这是QT中一个常见且强大的模式让图形项具备使用信号槽的能力。实现文件// playerplane.cpp #include playerplane.h #include bullet.h // 假设子弹类为Bullet #include QGraphicsScene #include QDebug PlayerPlane::PlayerPlane(QGraphicsItem *parent) : QGraphicsPixmapItem(parent) { // 1. 加载飞机图片 QPixmap pixmap(:/images/player.png); // 使用资源文件 if(pixmap.isNull()) { qDebug() Failed to load player image!; pixmap QPixmap(50, 50); pixmap.fill(Qt::blue); // 备用图形 } setPixmap(pixmap); // 2. 设置碰撞检测形状重要 // 默认使用boundingRect()作为碰撞形状但矩形不精确。 // 我们可以设置为更精确的图形形状或者对于简单游戏矩形也够用。 // setShapeMode(QGraphicsPixmapItem::BoundingRectShape); // 默认 // 为了性能通常使用BoundingRectShape。如果需要像素级精确碰撞可使用MaskShape但需设置掩码。 // 3. 初始化状态和定时器 moveLeft moveRight moveUp moveDown false; speed 8; shootTimer new QTimer(this); connect(shootTimer, QTimer::timeout, this, PlayerPlane::shoot); // shootTimer-start(200); // 可以在这里启动连射也可以在按键时启动 setFlag(QGraphicsItem::ItemIsFocusable); // 允许接收键盘事件 setFocus(); // 获取焦点这样窗口激活时就能直接接收按键 } void PlayerPlane::keyPressEvent(QKeyEvent *event) { switch(event-key()) { case Qt::Key_Left: moveLeft true; break; case Qt::Key_Right: moveRight true; break; case Qt::Key_Up: moveUp true; break; case Qt::Key_Down: moveDown true; break; case Qt::Key_Space: if(!shootTimer-isActive()) { shoot(); // 立即发射一颗 shootTimer-start(150); // 按下空格开始连射间隔150ms } break; } move(); // 每次按键更新位置 } void PlayerPlane::keyReleaseEvent(QKeyEvent *event) { switch(event-key()) { case Qt::Key_Left: moveLeft false; break; // ... 处理其他键释放 case Qt::Key_Space: shootTimer-stop(); // 松开空格停止连射 break; } } void PlayerPlane::move() { int newX x(); int newY y(); if(moveLeft) newX - speed; if(moveRight) newX speed; if(moveUp) newY - speed; if(moveDown) newY speed; // 边界检查防止飞出场景 QRectF sceneRect scene()-sceneRect(); // 获取场景边界 QRectF planeRect boundingRect().translated(newX, newY); // 计算移动后的飞机矩形 if(planeRect.left() sceneRect.left()) newX sceneRect.left(); if(planeRect.right() sceneRect.right()) newX sceneRect.right() - boundingRect().width(); if(planeRect.top() sceneRect.top()) newY sceneRect.top(); if(planeRect.bottom() sceneRect.bottom()) newY sceneRect.bottom() - boundingRect().height(); setPos(newX, newY); } void PlayerPlane::shoot() { // 创建子弹位置在飞机上方中心 Bullet *bullet new Bullet(Qt::green); // 假设Bullet构造函数接受颜色 bullet-setPos(x() pixmap().width()/2 - bullet-boundingRect().width()/2, y() - 10); scene()-addItem(bullet); // 将子弹添加到当前飞机所在的场景 }关键细节与避坑指南焦点问题图元默认无法接收键盘事件。必须调用setFlag(QGraphicsItem::ItemIsFocusable)并setFocus()。一个常见的坑是当场景中有多个可聚焦项时焦点可能丢失。确保你的游戏主窗口或某个管理类能正确管理焦点。资源文件图片路径:/images/player.png使用了QT的资源系统.qrc文件。这是管理嵌入式资源图片、声音的最佳实践能避免发布时找不到文件的尴尬。务必在.pro文件中正确配置RESOURCES。碰撞形状boundingRect()返回的矩形用于决定图元的绘制区域和默认碰撞区域。如果你的飞机图片不是矩形使用矩形碰撞会不精确。对于飞机大战矩形检测通常可以接受。若追求精确可以重写shape()函数返回一个更精确的QPainterPath但这会消耗更多性能。内存管理子弹在shoot()函数中通过new创建并添加到场景。谁负责删除好消息是当子弹飞出场景边界或与敌机碰撞后我们可以将其从场景中移除scene()-removeItem(bullet)并立即删除delete bullet。更好的做法是让子弹在超出屏幕或碰撞后发射一个信号由场景或一个专门的“游戏管理器”来统一执行移除和删除操作逻辑更清晰。3. 动态实体创建与管理敌机、子弹与游戏循环有了静态场景和玩家接下来就需要让游戏“动”起来这涉及到敌机的自动生成、子弹的飞行、以及碰撞检测。3.1 游戏主循环与定时器驱动在QT中我们通常不使用死循环作为游戏循环而是使用QTimer定时器来驱动。在游戏窗口类如GameWindow中设置一个定时器每隔固定时间如16ms约60FPS触发一次用于更新所有游戏对象的状态。// gamewindow.cpp 构造函数中 gameTimer new QTimer(this); connect(gameTimer, QTimer::timeout, this, GameWindow::updateGame); gameTimer-start(16); // 约60帧/秒 enemySpawnTimer new QTimer(this); connect(enemySpawnTimer, QTimer::timeout, this, GameWindow::spawnEnemy); enemySpawnTimer-start(1000); // 每1秒生成一个敌机可随难度增加而加快updateGame()槽函数是游戏逻辑的核心void GameWindow::updateGame() { // 1. 更新所有子弹位置并检查是否出界 QListQGraphicsItem * allItems scene-items(); for (QGraphicsItem *item : allItems) { if (Bullet *bullet dynamic_castBullet*(item)) { bullet-fly(); // Bullet类自己的飞行方法 if(bullet-isOutOfScene(scene-sceneRect())) { scene-removeItem(bullet); delete bullet; } } else if (EnemyPlane *enemy dynamic_castEnemyPlane*(item)) { enemy-move(); // 敌机移动 if(enemy-isOutOfScene(scene-sceneRect())) { scene-removeItem(enemy); delete enemy; } } } // 2. 碰撞检测 checkCollisions(); // 3. 更新UI分数、生命值 updateScoreDisplay(); }注意在定时器槽函数中遍历场景所有项并进行类型转换和操作在对象很多时可能成为性能瓶颈。更高效的做法是维护单独的链表来管理同类对象如QListBullet* m_bullets但会增加状态同步的复杂度。对于初学者和小型项目遍历场景的方式更直观。3.2 敌机类的实现敌机EnemyPlane的实现与玩家飞机类似但移动逻辑是自动的如匀速下落或更复杂的路径并且通常不需要响应键盘事件。// enemyplane.h class EnemyPlane : public QObject, public QGraphicsPixmapItem { Q_OBJECT public: EnemyPlane(int type 0, QGraphicsItem *parent nullptr); // type可区分不同敌机 void move(); bool isOutOfScene(const QRectF sceneRect); int getScoreValue() const { return scoreValue; } int getDamage() const { return damage; } private: int speedY; int speedX; // 可能有横向移动 int scoreValue; // 击毁得分 int damage; // 撞到玩家造成的伤害 }; // enemyplane.cpp EnemyPlane::EnemyPlane(int type, QGraphicsItem *parent) : QGraphicsPixmapItem(parent) { // 根据类型加载不同图片设置不同属性 if(type 0) { setPixmap(QPixmap(:/images/enemy1.png)); speedY 3; scoreValue 100; damage 1; } else { // ... 其他类型敌机 } } void EnemyPlane::move() { setPos(x() speedX, y() speedY); } bool EnemyPlane::isOutOfScene(const QRectF sceneRect) { // 判断是否完全飞出场景底部或左右边界 return (y() sceneRect.bottom() || x() sceneRect.right() || (x() boundingRect().width()) sceneRect.left()); }3.3 子弹类的实现子弹Bullet相对简单主要属性是飞行方向和速度。// bullet.h class Bullet : public QGraphicsPixmapItem { public: Bullet(const QColor color Qt::yellow, QGraphicsItem *parent nullptr); void fly(); bool isOutOfScene(const QRectF sceneRect); int getDamage() const { return damage; } private: int speedY; // 对于玩家子弹速度向上为负 int damage; }; // bullet.cpp Bullet::Bullet(const QColor color, QGraphicsItem *parent) : QGraphicsPixmapItem(parent) { // 可以用一个简单的矩形或圆形代表子弹 QPixmap pixmap(5, 15); // 宽5像素高15像素 pixmap.fill(Qt::transparent); QPainter painter(pixmap); painter.setBrush(color); painter.setPen(Qt::NoPen); painter.drawEllipse(0, 0, 5, 5); // 头部画个圆 painter.drawRect(2, 5, 1, 10); // 尾部画条线 setPixmap(pixmap); speedY -10; // 玩家子弹向上飞 damage 1; } void Bullet::fly() { setPos(x(), y() speedY); } bool Bullet::isOutOfScene(const QRectF sceneRect) { // 飞出场景顶部或底部 return (y() boundingRect().height() sceneRect.top() || y() sceneRect.bottom()); }3.4 碰撞检测的实现碰撞检测是游戏的核心逻辑。QT提供了QGraphicsScene::collidingItems()和QGraphicsItem::collidesWithItem()等方法。我们可以在GameWindow::checkCollisions()中实现。一种简单直接的实现方式void GameWindow::checkCollisions() { // 获取所有子弹和敌机 QListQGraphicsItem * bullets scene-items(Qt::AscendingOrder); // 可以筛选这里简化为遍历所有 QListQGraphicsItem * enemies scene-items(Qt::AscendingOrder); // 实际中应维护特定列表 // 双层循环检测对于对象不多的情况可行 for(QGraphicsItem *bulletItem : bullets) { Bullet *bullet dynamic_castBullet*(bulletItem); if(!bullet) continue; for(QGraphicsItem *enemyItem : enemies) { EnemyPlane *enemy dynamic_castEnemyPlane*(enemyItem); if(!enemy) continue; // 使用QT内置的碰撞检测 if(bullet-collidesWithItem(enemy)) { // 处理碰撞敌机减血或销毁子弹销毁增加分数 handleBulletEnemyCollision(bullet, enemy); break; // 一颗子弹只能击中一个敌机 } } } // 检测玩家与敌机的碰撞 if(player) { QListQGraphicsItem * collidingEnemies player-collidingItems(); for(QGraphicsItem *item : collidingEnemies) { if(EnemyPlane *enemy dynamic_castEnemyPlane*(item)) { handlePlayerEnemyCollision(player, enemy); break; } } } } void GameWindow::handleBulletEnemyCollision(Bullet *bullet, EnemyPlane *enemy) { // 1. 从场景移除子弹并删除 scene-removeItem(bullet); delete bullet; // 2. 敌机处理例如减血血量为0则销毁 // enemy-takeDamage(bullet-getDamage()); // if(enemy-isDestroyed()) { scene-removeItem(enemy); delete enemy; score enemy-getScoreValue(); // 增加分数 // } // 3. 可以在这里触发爆炸动画或音效 // createExplosion(enemy-pos()); } void GameWindow::handlePlayerEnemyCollision(PlayerPlane *player, EnemyPlane *enemy) { // 1. 玩家减血 playerHealth - enemy-getDamage(); if(playerHealth 0) { // 游戏结束逻辑 gameOver(); } // 2. 敌机销毁 scene-removeItem(enemy); delete enemy; // 3. 可能添加玩家无敌时间或闪烁效果 }性能优化提示上述双层循环在对象数量N较多时复杂度是O(N²)会严重影响性能。优化方案包括空间划分将场景划分为网格只检测相邻网格内的对象是否碰撞。QT的QGraphicsScene本身有索引机制QGraphicsScene::itemIndexMethod对于大量静态项设置setItemIndexMethod(QGraphicsScene::NoIndex)或BspTreeIndex可能影响性能需要根据实际情况测试。维护对象列表分别维护子弹、敌机的链表减少不必要的动态类型转换和遍历。简化碰撞形状确保boundingRect()尽可能紧贴图形并使用简单的碰撞检测模式。降低检测频率不一定每帧都进行全量碰撞检测可以每2-3帧检测一次。对于练手项目在对象数量少于100时直接使用上述方法问题不大重点是先让逻辑跑通。4. 打磨与扩展UI、状态管理与项目构建基础功能完成后我们需要一个游戏界面来显示分数、生命值并管理游戏状态开始、暂停、结束。4.1 游戏状态管理与UI集成在GameWindow类中添加游戏状态变量和UI控件。// gamewindow.h class GameWindow : public QMainWindow { Q_OBJECT public: enum GameState { Ready, Playing, Paused, GameOver }; // ... 其他成员 private slots: void startGame(); void pauseGame(); void gameOver(); void updateGame(); // 游戏主循环 void spawnEnemy(); private: GameState state; int score; int playerHealth; int playerMaxHealth; QLabel *scoreLabel; QLabel *healthLabel; QPushButton *startButton; QPushButton *pauseButton; // ... 其他UI和游戏对象指针 };在构造函数中初始化UI// 在setupUi或构造函数中假设使用代码布局而非Designer QWidget *central new QWidget; setCentralWidget(central); QVBoxLayout *mainLayout new QVBoxLayout(central); // 游戏视图 view new QGraphicsView(scene); mainLayout-addWidget(view); // 信息面板 QHBoxLayout *infoLayout new QHBoxLayout; scoreLabel new QLabel(得分: 0); healthLabel new QLabel(生命: 5/5); startButton new QPushButton(开始); pauseButton new QPushButton(暂停); pauseButton-setEnabled(false); infoLayout-addWidget(scoreLabel); infoLayout-addWidget(healthLabel); infoLayout-addStretch(); infoLayout-addWidget(startButton); infoLayout-addWidget(pauseButton); mainLayout-addLayout(infoLayout); connect(startButton, QPushButton::clicked, this, GameWindow::startGame); connect(pauseButton, QPushButton::clicked, this, GameWindow::pauseGame);状态切换逻辑void GameWindow::startGame() { if(state Ready || state GameOver) { // 重置游戏 clearScene(); // 清空所有敌机、子弹 score 0; playerHealth playerMaxHealth; updateUI(); // 创建玩家飞机并定位 player new PlayerPlane(); scene-addItem(player); player-setPos(...); player-setFocus(); // 重新获取焦点 // 启动定时器 gameTimer-start(16); enemySpawnTimer-start(1000); state Playing; startButton-setText(重新开始); pauseButton-setEnabled(true); view-setFocus(); // 将焦点还给视图确保键盘事件能传递到player } else if (state Paused) { gameTimer-start(); enemySpawnTimer-start(); state Playing; pauseButton-setText(暂停); view-setFocus(); } } void GameWindow::pauseGame() { if(state Playing) { gameTimer-stop(); enemySpawnTimer-stop(); state Paused; pauseButton-setText(继续); } else if (state Paused) { startGame(); // 调用startGame中继续的逻辑 } } void GameWindow::gameOver() { gameTimer-stop(); enemySpawnTimer-stop(); state GameOver; pauseButton-setEnabled(false); // 显示GameOver文字或对话框 QGraphicsTextItem *gameOverText scene-addText(游戏结束\n最终得分: QString::number(score)); gameOverText-setDefaultTextColor(Qt::red); gameOverText-setFont(QFont(Arial, 30, QFont::Bold)); gameOverText-setPos(scene-width()/2 - gameOverText-boundingRect().width()/2, scene-height()/2 - gameOverText-boundingRect().height()/2); }4.2 资源管理与项目配置.pro文件一个健壮的QT项目离不开正确的.pro文件配置。特别是使用资源文件时。# 飞机大战项目 .pro 文件示例 QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets CONFIG c11 # 如果你使用了声音可能需要 multimedia # QT multimedia SOURCES \ main.cpp \ gamewindow.cpp \ playerplane.cpp \ bullet.cpp \ enemyplane.cpp HEADERS \ gamewindow.h \ playerplane.h \ bullet.h \ enemyplane.h # 资源文件将images目录下的所有图片编译进程序 RESOURCES \ resources.qrc # 发布版本优化设置 RELEASE: DEFINES QT_NO_DEBUG_OUTPUT RELEASE: DEFINES QT_NO_WARNING_OUTPUT # 设置应用程序图标可选 RC_ICONS app.icoresources.qrc文件内容示例XML格式RCC qresource prefix/images fileimages/player.png/file fileimages/enemy1.png/file fileimages/enemy2.png/file fileimages/bullet.png/file fileimages/background.jpg/file /qresource qresource prefix/sounds filesounds/shoot.wav/file filesounds/explosion.wav/file /qresource /RCC4.3 常见编译与运行问题排查在开发过程中你几乎一定会遇到各种编译和运行时问题。这里集中列举几个高频坑点“error: unknown module(s) in QT: xlsx”这个问题在相关热搜词里排名很高。这表示你在.pro文件中写了QT xlsx但你的QT安装中没有包含QtXlsx模块。解决方案方案A推荐如果你不需要处理Excel文件直接从.pro文件中移除QT xlsx。方案B你需要自行编译并安装QtXlsx模块。这是一个第三方模块不随官方安装包提供。可以去GitHub搜索qtxlsx按照其README进行编译。发布软件时找不到依赖的DLL在Windows上使用MSVC编译发布后直接运行生成的.exe会报错缺少Qt5Core.dll等。你需要使用QT自带的windeployqt工具。# 在QT命令行如Qt 5.15.2 MinGW 64-bit中进入你的.exe所在目录 windeployqt your_game.exe这个命令会自动将所需的所有QT库、插件拷贝到exe同级目录。如果还缺少VC运行时需要手动安装或分发vcredist。程序崩溃Qt崩溃最常见的原因是野指针和多线程GUI操作。野指针确保所有QObject或QGraphicsItem派生对象都有明确的父对象在构造函数中指定或者被妥善管理。删除一个已被删除的对象或访问一个已失效的指针会导致崩溃。使用QPointer用于QObject可以部分缓解这个问题。跨线程操作GUI绝对不能在非主线程比如在一个自己创建的QThread里直接创建或修改任何QWidget、QGraphicsItem。必须使用信号槽并且连接类型为Qt::QueuedConnection或Qt::BlockingQueuedConnection。在飞机大战这种简单项目中建议所有逻辑都在主线程的定时器槽函数中处理避免多线程。键盘事件不响应如前所述确保图元设置了setFlag(QGraphicsItem::ItemIsFocusable)并setFocus()。另外检查是否有其他部件如按钮抢占了焦点。可以在keyPressEvent事件处理函数开头加qDebug() Key pressed: event-key();来调试事件是否被接收到。画面卡顿如果敌机和子弹很多时感觉卡首先检查updateGame()中的逻辑是否过于复杂如O(N²)的碰撞检测。其次可以尝试减少不必要的绘图更新对于变化不频繁的UI如分数标签不要每帧都更新。使用QGraphicsItem::ItemDoesntPropagateOpacityToChildren等标志优化。确保图片尺寸适中过大的图片会占用大量内存和显存。在QGraphicsView上设置setViewportUpdateMode(QGraphicsView::MinimalViewportUpdate)或SmartViewportUpdate减少重绘区域。把这个项目从头到尾实现一遍你对QT的理解会远超只看书和教程。接下来你可以尝试添加更多功能比如不同类型的敌机Boss、武器升级系统、关卡设计、本地高分榜用QSettings实现、粒子爆炸效果用多个旋转缩放的图形项模拟甚至加入简单的网络对战功能。每一个扩展点都是深入学习QT一个新模块的机会。动手去做遇到问题就查文档、搜社区这才是掌握一个框架最扎实的路径。