☰
Qt FlappyBird课程设计实战:从环境搭建到碰撞检测与性能优化
2026/10/10 21:16:08 网站建设 项目流程

简介:这份资源是面向 C++ 课程设计学习者与 Qt 入门开发者的 FlappyBird 小游戏完整源码,适合需要完成课设、想通过实战理解 Qt 图形界面与游戏循环机制的学生参考。项目实现点击屏幕让小鸟向上飞行、避开障碍物的经典玩法,涵盖窗口绘制、碰撞检测与事件响应等核心逻辑。压缩包共 80 个文件,约 263KB,其中 12 个 cpp 与 11 个 h 文件承载主要业务代码,5 个 ui 文件描述界面布局,47 个 png 图片提供小鸟、管道与背景等素材,另有 pro 工程文件、qrc 资源文件与 README 说明,目录按 Sources、Headers、Forms、Resources 等模块划分,结构清晰便于按功能阅读。已有 103 人学习下载,可作为课程设计选题的直接参考,帮助读者快速理清 Qt 项目组织方式、信号槽与定时器驱动动画的实现思路,并在此基础上做二次修改与功能扩展。

1. 从一份 FlappyBird 源码说起:Qt 课程设计到底该做成什么样

很多同学拿到「C++ 课程设计基于 Qt 制作的经典小游戏 FlappyBird 源代码」这个题目时,第一反应是去搜一份能跑的代码,改改贴上去交差。但真正做过答辩的人都知道,老师翻两页就会问:碰撞检测怎么写的、定时器周期多少、为什么用 QGraphicsView 而不是直接 QWidget 重绘。答不上来,源码再完整也白搭。这份课程设计的价值不在于「有个能飞的小鸟」,而在于它把 C++ 面向对象、Qt 事件循环、图形渲染、资源管理这几件事压缩进一个几百行的项目里,是一个能讲清楚、能改得动、能扩展的最小闭环。适合刚学完 C++ 语法、想找一个不依赖数据库和网络的纯桌面项目练手的同学,也适合已经会写 Qt 界面但没系统做过游戏循环的人。下面我按「先跑通、再拆解、后避坑」的顺序,把这份 FlappyBird 从环境到代码讲透。

2. 环境与工程骨架:让第一只鸟飞起来需要几步

2.1 Qt 版本与编译器怎么选才不翻车

课程设计最常见的翻车点不是代码写错,而是环境没配好。Qt 目前主流是 Qt 5.15 和 Qt 6.x 两条线,FlappyBird 这种 2D 小游戏两者都能做,但 API 有差异。如果你学校机房装的是 Qt 5.15 + MinGW,就统一用这套,别自己升到 Qt 6,否则QMediaPlayer、QRegExp这些类的头文件路径和用法会让你改到怀疑人生。编译器方面,Windows 下 MinGW 和 MSVC 都行,但 MSVC 需要额外装 Visual C++ 运行库,也就是热搜里常出现的 microsoft visual c++ 2015-2022 redistributable (x64),装完才能保证生成的 exe 在别人机器上不报缺 dll。我一般建议:课程设计用 Qt 5.15.2 + MinGW 8.1 64-bit,安装包在 Qt 官方在线安装器里勾选即可,不需要单独配 vscode 或 Visual Studio。

工程文件用.pro管理,这是 qmake 体系,比 CMake 更适合教学场景,因为依赖少、语法直观。一个典型的 FlappyBird 工程结构如下:

FlappyBird/ ├── FlappyBird.pro ├── main.cpp ├── mainwindow.h / .cpp / .ui ├── bird.h / .cpp ├── pipe.h / .cpp ├── gamescene.h / .cpp └── resources/ ├── bird.png ├── pipe.png ├── background.png └── resources.qrc

.pro文件里必须写QT += core gui widgets,如果用到多媒体音效再加multimedia。资源文件通过.qrc注册,路径写相对路径,避免绝对路径导致换机器就找不到图。

2.2 用 QGraphicsScene 搭游戏主循环的最小代码

FlappyBird 的核心是一个持续刷新的游戏循环。Qt 里不要用while(1)死循环,那会阻塞事件循环导致界面卡死。正确做法是用QTimer驱动,每隔固定毫秒触发一次update()或场景推进。下面是一个最小可运行的场景类骨架:

// gamescene.h #ifndef GAMESCENE_H #define GAMESCENE_H #include <QGraphicsScene> #include <QTimer> #include <QGraphicsPixmapItem> class GameScene : public QGraphicsScene { Q_OBJECT public: explicit GameScene(QObject *parent = nullptr); void startGame(); void stopGame(); private slots: void onTick(); // 每帧逻辑 void spawnPipe(); // 定时生成管道 private: QTimer *frameTimer; // 帧定时器,约 16ms QTimer *pipeTimer; // 管道生成定时器,约 1500ms QGraphicsPixmapItem *birdItem; qreal birdVelocity; // 小鸟垂直速度 bool gameRunning; }; #endif
// gamescene.cpp #include "gamescene.h" #include <QGraphicsView> #include <QKeyEvent> #include <QRandomGenerator> GameScene::GameScene(QObject *parent) : QGraphicsScene(parent), birdVelocity(0), gameRunning(false) { setSceneRect(0, 0, 400, 600); // 背景 QGraphicsPixmapItem *bg = addPixmap(QPixmap(":/resources/background.png")); bg->setZValue(-1); // 小鸟 birdItem = addPixmap(QPixmap(":/resources/bird.png")); birdItem->setPos(100, 300); birdItem->setZValue(1); frameTimer = new QTimer(this); frameTimer->setInterval(16); // 约 60 FPS connect(frameTimer, &QTimer::timeout, this, &GameScene::onTick); pipeTimer = new QTimer(this); pipeTimer->setInterval(1500); connect(pipeTimer, &QTimer::timeout, this, &GameScene::spawnPipe); } void GameScene::startGame() { gameRunning = true; birdVelocity = 0; frameTimer->start(); pipeTimer->start(); } void GameScene::stopGame() { gameRunning = false; frameTimer->stop(); pipeTimer->stop(); } void GameScene::onTick() { if (!gameRunning) return; // 重力加速度,单位:像素/帧 birdVelocity += 0.5; qreal newY = birdItem->y() + birdVelocity; // 边界检测 if (newY < 0) { newY = 0; birdVelocity = 0; } if (newY > 560) { stopGame(); return; } birdItem->setPos(birdItem->x(), newY); }

这段代码里几个参数必须说清楚:frameTimer的 16ms 对应约 60 帧每秒,这是游戏流畅度的底线,低于 30 帧小鸟会明显卡顿;birdVelocity += 0.5是重力加速度,数值越大下落越快,一般 0.4 到 0.8 之间手感较好;pipeTimer的 1500ms 决定管道密度,太密难度过高,太疏没有挑战。setSceneRect固定场景尺寸,配合QGraphicsView的setFixedSize可以防止窗口拉伸导致坐标错乱。

2.3 键盘事件与小鸟起跳的绑定方式

FlappyBird 的操作只有一个:点击或按空格让小鸟向上跳。在QGraphicsScene里重写keyPressEvent是最直接的方式:

void GameScene::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Space && gameRunning) { birdVelocity = -8; // 向上初速度 } QGraphicsScene::keyPressEvent(event); }

birdVelocity = -8是起跳瞬间的向上速度,负号表示 Y 轴向上。这个值要和重力加速度配合调:重力 0.5、起跳 -8,小鸟大约每 16 帧回到最高点,手感比较接近原版。如果起跳值太小(比如 -5),小鸟会显得很沉;太大(比如 -12)则一跳撞天花板。注意QGraphicsScene默认不接收键盘事件,需要在QGraphicsView上设置setFocusPolicy(Qt::StrongFocus)并确保场景有焦点。

3. 碰撞检测与管道生成:游戏可玩性的两个核心

3.1 用 QGraphicsItem 的碰撞函数做矩形检测

FlappyBird 的碰撞逻辑很简单:小鸟碰到管道或地面就结束。Qt 的QGraphicsItem自带collidingItems()函数,返回与当前项相交的所有项。但直接用它有个坑:图片的透明区域也会被算进包围盒,导致「看起来没碰到却死了」。解决办法是给小鸟和管道设置自定义的shape(),或者用boundingRect()缩小检测范围。

// 在 onTick 中每帧检测 QList<QGraphicsItem *> items = birdItem->collidingItems(); for (QGraphicsItem *item : items) { if (item->type() == Pipe::Type) { stopGame(); return; } }

如果不想自定义shape(),可以在Pipe类里重写boundingRect(),把实际管道区域缩小几个像素:

QRectF Pipe::boundingRect() const { // 原始图片宽 60,高 400,左右各缩 5 像素 return QRectF(5, 0, 50, 400); }

这样透明边缘不会误判。注意collidingItems()每帧调用有性能开销,管道数量多时建议只检测最近的两根,或者用简单的坐标区间判断代替。

3.2 管道随机高度与间隙的参数化设计

管道生成的核心是上下两根管子之间的间隙位置随机,但间隙大小固定。常见做法是:在场景高度范围内随机一个中心 Y 值,然后上下管道分别定位。

void GameScene::spawnPipe() { if (!gameRunning) return; const qreal gapHeight = 150; // 上下管道间隙 const qreal pipeWidth = 60; const qreal sceneHeight = 600; // 随机间隙中心,保证上下都留出至少 50 像素 qreal minCenter = gapHeight / 2 + 50; qreal maxCenter = sceneHeight - gapHeight / 2 - 50; qreal centerY = minCenter + QRandomGenerator::global()->bounded( static_cast<int>(maxCenter - minCenter)); // 上管道:从顶部到 centerY - gapHeight/2 Pipe *topPipe = new Pipe(true); topPipe->setPos(400, centerY - gapHeight / 2 - 400); addItem(topPipe); // 下管道:从 centerY + gapHeight/2 到底部 Pipe *bottomPipe = new Pipe(false); bottomPipe->setPos(400, centerY + gapHeight / 2); addItem(bottomPipe); }

gapHeight = 150是难度关键参数,小于 120 会非常难,大于 200 则太简单。minCenter和maxCenter的 50 像素边距防止管道贴边导致无法通过。管道移动可以在onTick里统一左移:

// 在 onTick 中遍历所有 Pipe 并左移 for (QGraphicsItem *item : items()) { if (item->type() == Pipe::Type) { item->setX(item->x() - 3); // 每帧左移 3 像素 if (item->x() < -60) { removeItem(item); delete item; } } }

每帧左移 3 像素配合 16ms 帧间隔,管道速度约 187 像素/秒,和原版节奏接近。移出屏幕后必须removeItem并delete,否则内存持续增长,跑几分钟就会卡顿——这是课程设计答辩时最容易被问到的内存管理点。

3.3 分数统计与游戏状态切换的实现

分数用管道通过小鸟的 X 坐标来计数。给每个管道加一个bool scored标记,当管道 X 小于小鸟 X 且未计分时,分数加一。

// 在 Pipe 类中 bool scored = false; // 在 onTick 中 if (!pipe->scored && pipe->x() + 60 < birdItem->x()) { pipe->scored = true; score++; scoreText->setPlainText(QString::number(score)); }

游戏状态用枚举管理:Ready、Running、GameOver。Ready状态显示提示文字,按空格进入Running;GameOver停止所有定时器,显示重开按钮。状态切换时记得重置小鸟位置、速度、分数和所有管道,否则重开后管道还在原地,小鸟一出来就撞。

4. 避坑与排查:课程设计答辩前必须过的五道坎

4.1 现象:编译通过但运行闪退,报「缺少 Qt5Core.dll」

原因:直接双击 build 目录下的 exe,系统找不到 Qt 的运行库。解决:在 Qt Creator 里运行没问题,但独立运行需要把 Qt 的 bin 目录加入 PATH,或者用windeployqt工具打包。命令行执行windeployqt FlappyBird.exe,它会自动把需要的 dll 拷到同目录。如果换到没装 Qt 的机器,还要确保 Visual C++ 运行库已安装,就是前面提到的 microsoft visual c++ 2015-2022 redistributable (x64)。

4.2 现象:小鸟移动一顿一顿的,帧率明显低于 60

原因:QTimer的精度受系统调度影响,默认精度是毫秒级,但实际触发间隔可能波动。另外如果在onTick里做了耗时操作(比如加载图片、大量碰撞检测),也会拖慢帧率。解决:把图片加载移到构造函数里,onTick只做坐标更新和碰撞检测;frameTimer->setTimerType(Qt::PreciseTimer)可以提高精度。如果场景项很多,考虑用QGraphicsView的setViewportUpdateMode(QGraphicsView::MinimalViewportUpdate)减少重绘区域。

4.3 现象:管道图片显示为黑块或透明

原因:.qrc资源文件没有正确编译,或者图片路径写错。解决:检查.pro里是否有RESOURCES += resources.qrc,路径用:/resources/xxx.png而不是resources/xxx.png。改完.qrc后必须重新执行 qmake(Qt Creator 里「构建」→「执行 qmake」),否则资源不会更新。另外 PNG 图片如果带 Alpha 通道,QGraphicsPixmapItem默认支持透明,不需要额外设置。

4.4 现象:按空格没反应,小鸟不动

原因:QGraphicsScene没有获得键盘焦点。解决:在mainwindow.cpp里对QGraphicsView调用setFocusPolicy(Qt::StrongFocus),并在startGame后调用view->setFocus()。如果用了QGraphicsView的setInteractive(true),还要确保场景没有被子项拦截事件。可以在keyPressEvent里加qDebug() << "key pressed"确认事件是否到达。

4.5 现象:游戏结束后重新开始,管道重叠或分数不清零

原因:stopGame只停了定时器,没有清理场景中的管道项和重置变量。解决:写一个resetGame()函数,遍历所有Pipe类型的项并删除,重置score = 0、birdVelocity = 0、birdItem->setPos(100, 300),然后调用startGame()。注意删除项时要用removeItem+delete,只delete不removeItem会导致场景里残留野指针,下次items()遍历时崩溃。

5. 从能跑到能讲:三个让课程设计加分的技术细节

5.1 用 QPropertyAnimation 做小鸟翅膀扇动效果

原版 FlappyBird 的小鸟翅膀是动态的,用两张图交替显示即可。不需要引入复杂的动画框架,用QTimer每 100ms 切换一次QPixmap就行:

// 在 Bird 类中 QTimer *wingTimer = new QTimer(this); wingTimer->setInterval(100); connect(wingTimer, &QTimer::timeout, [this]() { static bool frame = false; frame = !frame; setPixmap(QPixmap(frame ? ":/resources/bird_up.png" : ":/resources/bird_down.png")); }); wingTimer->start();

这个细节在答辩时很讨巧,代码量不到 10 行,但演示效果明显。注意setPixmap会改变项的包围盒,如果碰撞检测依赖boundingRect(),要确保两张图尺寸一致,否则会出现「翅膀一扇就撞管道」的玄学问题。

5.2 用 QSettings 保存最高分

课程设计如果能加一个本地最高分记录,会显得完整很多。QSettings是 Qt 自带的跨平台配置存储,Windows 下写注册表,Linux 下写 ini 文件,不需要额外依赖。

// 游戏结束时 QSettings settings("MyCompany", "FlappyBird"); int highScore = settings.value("highScore", 0).toInt(); if (score > highScore) { settings.setValue("highScore", score); highScore = score; } // 显示最高分 highScoreText->setPlainText("Best: " + QString::number(highScore));

QSettings的构造函数两个参数分别是组织名和应用名,随便填但要保持一致。读取时第二个参数是默认值,第一次运行返回 0。这个功能在答辩时能引出「数据持久化」的话题,比单纯说「我做了个游戏」有深度。

5.3 用 QGraphicsView 的缩放做适配不同分辨率

如果答辩演示的投影仪分辨率和你开发机不同,窗口可能显示不全。在main.cpp里根据屏幕大小自动缩放:

QApplication a(argc, argv); MainWindow w; QGraphicsView *view = w.findChild<QGraphicsView *>(); if (view) { QRect screenRect = QApplication::primaryScreen()->availableGeometry(); qreal scale = qMin(screenRect.width() / 400.0, screenRect.height() / 600.0); view->scale(scale * 0.8, scale * 0.8); // 留 20% 边距 } w.show(); return a.exec();

scale函数会同时缩放场景坐标和渲染,碰撞检测不受影响,因为逻辑坐标没变。乘 0.8 是为了留出窗口边框和任务栏的空间。这个技巧在换机器演示时能救急,避免「老师电脑上窗口超出屏幕」的尴尬。

最后说一个我自己的习惯:每次改完代码,先跑三遍——第一遍正常玩到死,第二遍故意撞管道看结束逻辑,第三遍重开看状态重置。这三遍能覆盖 90% 的课程设计 bug。FlappyBird 代码量不大,但每一行都值得你亲手敲一遍,而不是复制粘贴。希望帮到你。

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

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

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

立即咨询