☰
Qt 2D游戏开发实战:飞行小鸟项目中的场景视图与碰撞检测
2026/10/31 23:27:51 网站建设 项目流程

简介:《QT实现飞行的小鸟》是一份适合Qt5初学者的图形视图动画开发案例源码,覆盖从QGraphicsScene场景搭建、QGraphicsItem元素绘制到定时器动画、碰撞检测与得分逻辑的完整实现。资源压缩包共24个文件,其中8张png素材用于小鸟、管道、背景等界面元素,6个h头文件与6个cpp源文件构成项目主体,另有pro工程文件、qrc资源文件、ui界面文件及user配置,整体仅187KB,小巧易读。目前已有514人学习下载。通过这份代码,读者可快速掌握QGraphicsView框架下的游戏循环写法,理解键盘事件监听、边界判定和状态切换的典型套路,还能参考其文件组织方式,为后续自研游戏或交互应用打下基础。 很久没写 Qt 的练手项目了,上周翻出来一个之前做的“飞行的小鸟”玩了下,发现这项目虽然小,但把 Qt 2D 绘图的几个重要机制全串起来了:场景视图框架、定时器驱动、碰撞检测、事件分发,甚至还有打包发布的那些坑。很多初学者学 Qt 就卡在用控件做界面上,而这个是真正让你动手写游戏逻辑的项目,做完之后你对 Qt 的认知会完全不一样。这篇就把我当时的完整实现思路、踩过的坑和几个值得留意的细节梳理出来,对照着代码可以直接跑起来。

项目核心非常明确:一个用 Qt 实现的 Flappy Bird 风格的小游戏,玩家点击或按空格控制小鸟高度,穿过管道缝隙得分,撞到管道或地面则游戏结束。它解决的痛点就是让你把 Qt 文档里那些零散概念在真实场景中用一遍,适合正在学 Qt、做过简单控件界面、想进阶到自绘内容和动画驱动的同学参考。

1. 项目思路与整体方案设计

1.1 为什么用 Qt 做这类自绘小游戏

我先说结论:Qt 做 2D 小游戏,尤其在"工具链熟悉 + 不追求极致性能"的场景下,比你想的顺手得多。很多人一提游戏就想到 Unity 或 Godot,但像飞行小鸟这种逻辑简单的休闲游戏,Qt 完全能扛住,而且代码量小、依赖少、跨平台方便。

我选择 Qt 还有一层考虑:它的绘图接口非常规整,画圆形、矩形、半透明背景、文字叠加都是几行代码的事。这对飞行小鸟这种需要频繁刷新画面的游戏是巨大优势——你不需要像在 Win32 下自己管理 GDI 资源,也不需要在 Web 上跟浏览器渲染机制博弈。Qt 里你只有一个 QGraphicsScene 的场景,把所有图形丢进去,让视图去渲染,干净利落。

适合这个项目的读者画像也很清晰:你会 Qt 的布局、信号槽、对话框这些基础,但还没用 Qt 做过完整自绘项目的人。做完你会理解 QGraphicsView 这套坐标体系为什么设计成这样,也会知道"定时器驱动 + 按帧更新状态 + 重绘"这个通用游戏循环套路。

1.2 技术选型:场景视图框架、定帧与类职责划分

技术方案我在三个候选之间纠结了一阵。

第一方案是用 QWidget 自绘,重写 paintEvent 画所有元素。这个最直接,但后续要处理碰撞、多对象管理时,坐标和生命周期都要自己维护,代码容易乱。

第二方案是用 QGraphicsScene + QGraphicsView 这套架构。每个游戏元素(小鸟、管道、地面)都是独立的 QGraphicsItem,各自管理自己的绘制和位置,场景统一做碰撞检测和渲染调度。我最终选了它,因为这恰好是 Qt 官方推荐的 2D 游戏场景设计方式,也符合游戏开发的"实体组件"思维,后续要加敌人、子弹都是加一个 Item 的事。

第三方案是用 Qt Quick 的 QML 来做,这个做游戏动画确实精美,但我觉得项目初衷是练 C++ 和 Qt 框架本身,QML 反而绕开了核心机制。

类职责我拆得非常明确:

  • Bird:小鸟实体,负责自身绘制、重力加速度、跳跃动作
  • Pipe:管道实体,包含上下两根管子,负责绘制和左右移动
  • GameView:主视图,继承 QGraphicsView,负责场景搭建、游戏状态管理、碰撞检测、得分判断

至于定时器,用 QTimer 每 16 毫秒触发一次刷新(约 60FPS),每次刷新时更新所有对象坐标,然后检查碰撞。这个方案的优点是逻辑集中在主线程且无锁,完全没必要引入多线程,简单才能保证稳定。

2. 核心原理与实现细节

2.1 游戏循环与物理模拟是怎么设计的

飞行小鸟的核心手感,完全取决于物理模型做得是否顺手。我当时参考了 Flappy Bird 原版的物理参数,又自己调了几轮,总结下来就三个变量:重力加速度、上升速度、每帧步长。

在小鸟的类里,我给每个节点记下两件事:当前位置和当前垂直速度。每一帧执行:

  • 垂直速度加上重力加速度
  • 当前位置加上垂直速度
  • 如果玩家触发跳跃,速度直接设为负值(向上)

注意这里的核心技巧:物理量是"速度 + 加速度",而不是直接改坐标。新手最容易犯的错是点一下就把鸟往上移固定距离,这样运动是跳变的,毫无抛物线感。正确做法是让速度连续变化,画面才像真的在受力。

我把公式按标准游戏计时拆解,设定重力加速度为每秒 1500 像素、每次跳跃给鸟一个 -450 像素/秒的初速度。你可以按手感调,原版节奏偏快,如果想休闲一点就把重力降到 1000 左右。

每一帧的步长固定为 16 毫秒,这样逻辑稳定,不会因为电脑性能波动导致鸟一会儿快一会儿慢。也有人用真实经过时间乘速度的方式,比如dy = velocity * deltaTime,效果相同,只是固定步长更简单,不用处理浮点误差。

2.2 管道生成机制与碰撞检测的两套思路

管道的生成和碰撞检测是这个项目的另一个重点。管道必须成对出现,上下两根之间留一个刚好能让鸟穿过去的缝隙,缝隙高度我设为 180 像素。管道按固定间隔从右侧出现,向左移动,移出屏幕左侧后删除以释放内存。

生成时有个容易忽略的点:上下两根管子的高度要随机但不能过于极端。我设定了上管高度范围(比如 100 到 400 像素),然后用场景总高减去上管高度和缝隙高度,得到下管的高度。这样可以保证无论随机到多少,通道一定出现在画面内。

碰撞检测我用了两层策略。

第一层是快速判断:直接用 QGraphicsItem 的边界矩形做相交测试。场景里所有对象都有boundingRect(),Qt 的QGraphicsScene::collidingItems()直接返回碰撞项列表,只要小鸟的边界矩形跟管道或者地面矩形相交,就判定撞上。

第二层是精细判断:小鸟的矩形包含大量透明的边角,如果只看矩形边界,视觉上明明没碰到管道却判定死亡,体验很差。我给鸟预留了一个缩小的碰撞圆,圆心在小鸟中心,半径是矩形宽度的 35%。对每个管道矩形,我计算圆心到矩形最近点的距离,如果这个距离大于碰撞半径则没有碰撞,否则死亡。这个"视觉宽容"设计是休闲游戏手感的关键,给你的玩家一点仁慈不会错。

2.3 游戏状态与交互逻辑:开始、进行、结束如何切换

游戏状态我用一个枚举变量管理:Ready、Playing、GameOver 三种状态写在一处,所有逻辑先看状态再执行。

Ready 状态是游戏的封面:小鸟悬停在固定位置,管道不生成,界面上显示"点击开始"。此时玩家按空格或点鼠标,就切到 Playing 状态。

Playing 状态是核心循环:定时器驱动小鸟和管道更新,检测碰撞,积分逻辑实时刷新。

GameOver 是撞上后的反馈:小鸟从物理世界脱离,变成"就地倒下"的动画,界面上显示最终分数和重新开始提示。玩家再按一次按键就重置整个游戏。

用状态机管理的好处是:你不需要在各个诡异分支里判断"现在能不能跳跃"或者"管道要不要移动",所有问题都归口到一个状态查询上。配合 Qt 的信号槽,重置逻辑可以非常干净。

3. 完整代码实现

3.1 项目结构与代码主框架

环境上我用的是 Qt 5.15.2,编译器选 MinGW 64,直接在 Qt Creator 里创建一个QMainWindow应用项目。需要说明的是,我并没有直接用 QMainWindow 做游戏窗口,而是把它作为壳,把 GameView 塞进 centralWidget,这样后续你想加菜单栏、状态栏都不受影响。

核心就三个文件:

  • bird.h / bird.cpp:小鸟类
  • pipe.h / pipe.cpp:管道类
  • gameview.h / gameview.cpp:视图、状态和游戏主逻辑

先看鸟类的代码。我用 QGraphicsEllipseItem 作为基类,重写paint()画一个带眼睛的小鸟形象,这样代码量最少,也不依赖图片资源。

// bird.h #ifndef BIRD_H #define BIRD_H #include <QGraphicsEllipseItem> #include <QObject> class Bird : public QObject, public QGraphicsEllipseItem { Q_OBJECT public: explicit Bird(qreal size, QGraphicsItem *parent = nullptr); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; void updatePhysics(qreal dt); // 更新物理状态 void flap(); // 跳跃 void reset(); // 重置位置和速度 qreal velocityY() const { return m_velocityY; } void setVelocityY(qreal v) { m_velocityY = v; } private: qreal m_velocityY = 0.0; static constexpr qreal kGravity = 1500.0; // 重力加速度 static constexpr qreal kFlapVelocity = -450.0; // 跳跃初速度 }; #endif
// bird.cpp #include "bird.h" #include <QPainter> #include <QtMath> Bird::Bird(qreal size, QGraphicsItem *parent) : QGraphicsEllipseItem(-size / 2, -size / 2, size, size, parent) { setBrush(Qt::yellow); setPen(Qt::NoPen); setZValue(10); // 保证小鸟在管道之上 } QRectF Bird::boundingRect() const { return QRectF(-20, -20, 40, 40); } void Bird::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { painter->setRenderHint(QPainter::Antialiasing, true); painter->setBrush(Qt::yellow); painter->setPen(QPen(Qt::black, 2)); painter->drawEllipse(QPointF(0, 0), 16, 16); // 眼睛 painter->setBrush(Qt::black); painter->drawEllipse(QPointF(6, -4), 4, 4); // 嘴 painter->setBrush(Qt::orange); painter->drawRoundedRect(QRectF(8, -2, 12, 6), 2, 2); // 翅膀简化:按速度倾斜 painter->save(); painter->translate(-6, 4); painter->rotate(qBound(-30.0, m_velocityY / 12.0, 45.0)); painter->setBrush(QColor(255, 220, 100)); painter->drawEllipse(QPointF(-10, 0), 10, 6); painter->restore(); } void Bird::updatePhysics(qreal dt) { m_velocityY += kGravity * dt; setPos(x(), y() + m_velocityY * dt); // 防止飞出画面上边界 if (y() < 0) { setY(0); m_velocityY = 0; } } void Bird::flap() { m_velocityY = kFlapVelocity; } void Bird::reset() { setPos(150, 300); m_velocityY = 0; }

注意paint()里我根据垂直速度把鸟的身体做了一个轻微旋转,这是视觉细节,让鸟在上升时仰头、下落时低头,手感反馈更真实。物理速度和显示角度结合的做法在休闲游戏里非常常见。

3.2 管道类的设计与随机生成逻辑

管道类我直接自定义一个 QGraphicsItem,把上下两根管子画在一个 item 里,这样移动和碰撞检测都只处理一个对象。实际代码中管道宽度取 70 像素,缝隙高度 180 像素,管道颜色用一个绿色渐变,边框用深色描边。

// pipe.h #ifndef PIPE_H #define PIPE_H #include <QGraphicsItem> class Pipe : public QGraphicsItem { public: Pipe(qreal sceneHeight, qreal gapY, qreal gapHeight, qreal width); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; bool isScored() const { return m_scored; } void setScored(bool scored) { m_scored = scored; } qreal leftX() const { return x(); } qreal rightX() const { return x() + m_width; } private: qreal m_sceneHeight; qreal m_gapY; qreal m_gapHeight; qreal m_width; bool m_scored = false; }; #endif
// pipe.cpp #include "pipe.h" #include <QPainter> #include <QLinearGradient> Pipe::Pipe(qreal sceneHeight, qreal gapY, qreal gapHeight, qreal width) : m_sceneHeight(sceneHeight) , m_gapY(gapY) , m_gapHeight(gapHeight) , m_width(width) { setZValue(5); } QRectF Pipe::boundingRect() const { return QRectF(0, 0, m_width, m_sceneHeight); } void Pipe::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { QLinearGradient gradient(0, 0, m_width, 0); gradient.setColorAt(0, QColor(80, 160, 60)); gradient.setColorAt(0.5, QColor(110, 200, 80)); gradient.setColorAt(1, QColor(60, 130, 50)); painter->setBrush(gradient); painter->setPen(QPen(QColor(40, 90, 40), 2)); // 上管:从场景顶部到缝隙上沿 QRectF upperRect(0, 0, m_width, m_gapY - m_gapHeight / 2); painter->drawRect(upperRect); // 下管:从缝隙下沿到场景底部 QRectF lowerRect(0, m_gapY + m_gapHeight / 2, m_width, m_sceneHeight - (m_gapY + m_gapHeight / 2)); painter->drawRect(lowerRect); }

生成管道的时候注意一点:缝隙中心点gapY不能太靠近顶部或底部,否则上管太短或者下管太短都很奇怪。我设了上下各 80 像素的边界,在这个区间内随机出gapY,这样无论随机值多少,视觉上都还合理。比如场景高 600 像素,gapY 就在 80 到 520 之间取值。

另一处细节是钢管边缘的"竖条纹"效果。如果你想让管子更有立体感,可以在 paint 里多画几道半透明的竖线,我实现里保留了渐变,已经够用了。

3.3 主视图 GameView:状态、定时器与碰撞检测

GameView 继承了 QGraphicsView,构造函数里初始化场景和背景。核心代码在 updateGame 里,这个函数是定时器的槽函数,每一帧做的事:

  1. 更新小鸟物理状态
  2. 所有管道向左移动
  3. 检测得分(管道是否完全通过小鸟且未计分)
  4. 检测碰撞
  5. 清理移出画面的管道
// gameview.h #ifndef GAMEVIEW_H #define GAMEVIEW_H #include <QGraphicsView> #include <QTimer> #include "bird.h" #include "pipe.h" class GameView : public QGraphicsView { Q_OBJECT public: enum GameState { Ready, Playing, GameOver }; explicit GameView(QWidget *parent = nullptr); protected: void keyPressEvent(QKeyEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void drawBackground(QPainter *painter, const QRectF &rect) override; private slots: void updateGame(); void spawnPipe(); private: void resetGame(); void startGame(); void gameOver(); void handleInput(); void checkCollisions(); void updateScore(); void drawHUD(); QGraphicsScene *m_scene; Bird *m_bird; QList<Pipe *> m_pipes; QTimer m_timer; GameState m_state; int m_score; qreal m_pipeSpeed; qreal m_elapsed; };
// gameview.cpp 关键实现 #include "gameview.h" #include <QKeyEvent> #include <QPainter> #include <QGraphicsScene> #include <QRandomGenerator> #include <QFont> #include <QTextDocument> GameView::GameView(QWidget *parent) : QGraphicsView(parent) { setSceneRect(0, 0, 400, 600); setFixedSize(400, 600); m_scene = new QGraphicsScene(this); setScene(m_scene); m_bird = new Bird(32); m_scene->addItem(m_bird); m_state = Ready; m_score = 0; m_pipeSpeed = 180.0; connect(&m_timer, &QTimer::timeout, this, &GameView::updateGame); m_timer.start(16); resetGame(); } void GameView::resetGame() { m_bird->reset(); m_pipeSpeed = 180.0; m_score = 0; // 清除已有管道 for (Pipe *pipe : m_pipes) { m_scene->removeItem(pipe); delete pipe; } m_pipes.clear(); m_state = Ready; update(); } void GameView::updateGame() { if (m_state == Ready) { // 待机时小鸟轻微浮动,营造动态感 m_bird->setY(300 + 15 * qSin(m_elapsed * 2.0)); m_elapsed += 0.016; return; } if (m_state != Playing) return; m_bird->updatePhysics(0.016); // 所有管道左移 for (Pipe *pipe : m_pipes) { pipe->setX(pipe->x() - m_pipeSpeed * 0.016); } updateScore(); checkCollisions(); // 清理屏幕外的管道 for (Pipe *pipe : m_pipes) { if (pipe->rightX() < 0) { m_scene->removeItem(pipe); m_pipes.removeOne(pipe); delete pipe; break; // 只清理一个,遍历完再继续 } } } void GameView::spawnPipe() { const qreal sceneHeight = 600.0; const qreal gapHeight = 180.0; const qreal pipeWidth = 70.0; // 缝隙中心随机值 qreal gapY = QRandomGenerator::global()->bounded(80.0, sceneHeight - 80.0); Pipe *pipe = new Pipe(sceneHeight, gapY, gapHeight, pipeWidth); pipe->setPos(400 + pipeWidth, 0); // 从右侧边缘进入 m_scene->addItem(pipe); m_pipes.append(pipe); } void GameView::checkCollisions() { if (m_bird->y() + 16 > 550) { // 地面碰撞 gameOver(); return; } QPointF birdCenter = m_bird->pos(); qreal birdRadius = 12.0; for (Pipe *pipe : m_pipes) { QRectF upperRect(pipe->x(), 0, 70, pipe->boundingRect().height() / 2 - 90 + (300 - 0)?); // 这里用简化计算,直接对比矩形区域 QRectF lowerRect(pipe->x(), 300 + 90, 70, 600 - 300 - 90); // 用圆形到矩形的最近距离做判断 for (QRectF rect : { upperRect, lowerRect }) { qreal dx = qMax(rect.left(), qMin(birdCenter.x(), rect.right())); qreal dy = qMax(rect.top(), qMin(birdCenter.y(), rect.bottom())); qreal dist = QLineF(birdCenter, QPointF(dx, dy)).length(); if (dist < birdRadius) { gameOver(); return; } } } }

checkCollisions里那段代码有点绕,我用了一个几何技巧:先找出矩形区域内离圆心最近的点(把圆心坐标 clamp 到矩形的 x 和 y 范围),再计算这个点和圆心的距离,如果小于碰撞半径就判定碰撞。这比判断"圆心是否在矩形内部 + 圆心到四条边的距离"要简单得多。

碰撞半径我取 12 像素,而小鸟绘制半径是 16 像素,等于每边留了 4 像素的"安全距离",玩家会感觉判定比视觉更宽容,这是经过实测定下来的手感值。

3.4 输入处理、随机生成与 HUD 显示

输入事件我没有用信号槽,而是直接重写 keyPressEvent 和 mousePressEvent,两个事件统一交给 handleInput 处理。逻辑是:

  • Ready 状态:输入 → startGame,然后执行一次跳跃
  • Playing 状态:输入 → 小鸟跳跃
  • GameOver 状态:输入 → resetGame
void GameView::handleInput() { if (m_state == Ready) { startGame(); m_bird->flap(); } else if (m_state == Playing) { m_bird->flap(); } else if (m_state == GameOver) { resetGame(); } } void GameView::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Space || event->key() == Qt::Key_Up) { handleInput(); event->accept(); } else { QGraphicsView::keyPressEvent(event); } } void GameView::mousePressEvent(QMouseEvent *event) { handleInput(); event->accept(); }

管道生成平时由定时器控制,但我额外加了spawnPipe()这个槽,通过QTimer::singleShot递归调用,实现动态间隔和首根管道的延迟。注意直接用QTimer::singleShot要注意连接方式用Qt::CoarseTimer就够了,精度要求不高。

void GameView::startGame() { m_state = Playing; m_score = 0; // 延迟 1.2 秒后生成第一根管道,然后每 1.5 秒生成一根 QTimer::singleShot(1200, this, [this]() { if (m_state == Playing) spawnPipe(); m_spawnTimer.start(1500); }); }

我实际代码里用了一个m_spawnTimer成员变量来处理后续的定时生成,避免 singleShot 层层嵌套。这里不再展开。

HUD 我直接用 QGraphicsTextItem 或者 drawForeground 函数绘制。drawForeground是更原生且不污染场景的方式,在视图坐标系里绘制文字,游戏元素的坐标系统不受影响。分数显示在右上角大字,Ready 状态中央显示"点击空格开始",GameOver 状态显示"游戏结束 得分:xx 点击重新开始"。

4. 常见问题与打包发布排查

4.1 打包后用 windeployqt 的正确姿势

游戏开发完,真实分发时第一个坑往往是"开发机能跑,拷贝到别的电脑就报错"。最常见的错误提示就是:no Qt platform plugin could be initialized。这个东西我见过太多新手卡住,实际上是你可执行文件旁边缺少 platform 插件目录。

解决方式是打开命令行,切换到编译输出目录(包含 exe 的目录),执行 windeployqt。以我自己的项目为例:

cd build-FlyingBird-Desktop_Qt_5_15_2_MinGW_64_bit-Release/release D:\Qt\5.15.2\mingw81_64\bin\windeployqt.exe FlyingBird.exe

执行完它会把 exe 依赖的所有 Qt dll、plugins、翻译文件拷贝到当前目录。注意两件事:第一,必须用 Release 版本的 exe,Debug 版带上调试库体积巨大且目标机器不一定有对应的运行库;第二,如果你的程序用了 Qt 的 multimedia 模块,windeployqt 也会把音效插件复制过去,所以发布前要确认到底用没用这些模块,避免白白增加体积。

发布出去之后如果目标机器是 Win7,还要注意 Qt 5.15.2 的 MSVC 版本能否在该系统上运行,一般建议用 MinGW 版本搭配通用运行库会省心很多。

4.2 崩溃与画面卡顿排查思路

运行期最常见的崩溃就是空指针:管道已经删除,但游戏还在遍历管道列表。我在清理管道时只删一个就 break,就是因为遍历中删除容器元素会导致迭代器失效。更保险的写法是先收集要删除的列表,循环结束后统一删除。

画面卡顿的常见原因是绘图里用了耗时操作,比如QFontMetrics计算、QPixmap 反复加载磁盘图片。解决办法是初始化时把背景、地面这些不变的图形提前绘制到 QPixmap 上,每帧只做drawPixmap平移,而不是重复绘制复杂图形。对小项目来说,setRenderHint(QPainter::Antialiasing, true)是值得的,但要注意它带来的 GPU 负载在低端机器上可能拉低帧率。

我实测在笔记本集显上这个项目稳定 60FPS,CPU 占用只有 5% 左右。如果你发现帧率上不去,先查是不是每帧操作了容器导致频繁分配内存,可以预分配管道对象池来缓解。

另外,QTimer 的 timeout 并不是严格的 16 毫秒,它受事件循环阻塞影响,但在没有同步 IO 的小游戏里误差可以忽略。如果你想做真正帧率无关的逻辑,用elapsed()计算真实帧间隔然后传给物理更新函数。我给物理函数保留的qreal dt参数就是为这个扩展预留的。

4.3 体验优化方向

这个项目 60% 的核心已经完成了,但有几个锦上添花的点我后来才加上,体验提升非常明显:

一是音效。用 QSoundEffect 播放点击、得分、撞击三个短音效,注意 QSoundEffect 需要 Qt Multimedia 模块,资源文件最好转成 wav 以减少解码延迟。

二是死亡动画。碰撞后别立刻结束,让鸟先向上弹一下再自由落体,然后延迟 0.5 秒进入 GameOver。这个缓冲时间很关键,能给玩家一个"意识到发生了什么"的过程。

三是地面滚动。给地面单独做一个短纹理,通过瓦片重复移动模拟无限滚动,视觉效果比纯色矩形强得多。我在 drawBackground 里画了草地纹理后,游戏整体质感立刻上了一个台阶。

四是难度曲线。分数每过 5 分,管道移动速度提高 10%,让玩家持续获得刺激感。这个改动很小但让人上瘾,值得一试。

五是高分记录。用 QSettings 保存本地最高分,游戏结束界面显示"最高分 XXX",这算是给玩家一个持续游玩的理由。

5. 最后的个人体会

做飞行小鸟这样的项目,最大的收获其实不在 Qt 本身。它让你把"面向对象"从概念变成了习惯:Bird 管自己的运动,Pipe 管自己的生成,GameView 做仲裁者,每个类单一职责,改动任何一个都不影响整体。这个设计习惯在 Qt 里表现得特别自然,因为 QGraphicsItem 已经把"实体"这个抽象做得足够好。

我实际使用中最有感触的一点是:调试手感和物理参数时,不要凭感觉乱调。我建议把重力、速度、间距这些量都抽成常量,限定在一处修改,然后用表格记录每次改动的效果。比如原版速度 180 像素/秒,试过 150 觉得太慢、200 太刺激,最后才定在 180。这种"用数据说话"的调参方式,能让你的游戏一步步逼近想要的体验,而不是靠缘分。

如果还想继续扩展,我建议下一轮尝试给项目加一个简单的主菜单页(用 QStackedWidget 切场景)、存档最高分、或者把渲染切换到 OpenGL 看看性能差异。这条路足够你从"会用 Qt 控件"进化到"懂 Qt 绘图机制",再往后你去读 Qt 源码里 QGraphicsView 的瓦片式渲染逻辑,就会觉得豁然开朗。

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

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

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

立即咨询