简介:这是一份基于Qt框架的Flappy Bird小游戏的完整源代码压缩包,面向对Qt游戏开发感兴趣的初学者和有一定C++基础的开发者。资源共包含57个文件,其中28张PNG图片用于角色、背景与界面元素,8个C++源文件与7个头文件构成项目主体逻辑,5个MP3音频负责游戏音效,另有Qt工程文件(.pro、.qrc)、说明文档等,压缩包总计约490KB。该项目覆盖了QGraphicsView/QGraphicsScene场景管理、信号槽事件处理、QTimer动画驱动、鸟的升降物理模拟、障碍物生成与碰撞检测等关键知识点,并通过资源系统统一管理图片和音频,方便复用与修改。代码结构清晰,附带读我说明,适合直接编译运行学习。目前已有189人学习下载,是快速理解Qt2D游戏开发流程的实用参考。
1. 拿到这个 Qt FlappyBird 工程包,先别急着双击 exe
很多初学者下载 QtFlappyBird-master.zip 之后,第一反应是找里面的 .exe 双击运行,结果要么没反应,要么弹个缺 DLL 的框。这个 Qt 制作的 Flappy Bird 项目,本质是一份 Qt Widgets 或 Qt Quick 的源码工程,不是你一解压就能玩的成品游戏,你需要先把它放进 Qt Creator 编译一遍。你看到的名字里带着 “master.zip”,说明是 GitHub 常见的主分支压缩包形式,源码完整度通常不错,适合拿来学习 QTimer 事件驱动、碰撞检测和绘图这套 Qt 桌面小游戏的基础链路。
这篇笔记我会按自己惯用的排查顺序来讲:怎么把这份源码在本地跑起来、工程里最值得抄的逻辑段落在哪、参数调到什么值手感才像 Flappy Bird,以及我踩过几次的编译和运行坑。对象是刚把 Qt 环境装好、想在真实工程里练手 Qt 的开发者,也包括想拿这个小游戏当面试作品但不想从零开始画鸟的人。下面所有操作都以 Qt 5.15.2 MSVC2019 64 位 + Qt Creator 4.x 为基准,工程若是 Qt 6 或 MinGW 套件,差异我会单独标出。
2. 把 zip 变成本机能编译的工程:从解压到第一个可运行窗口
2.1 先花两分钟判断这套源码是 qmake 还是 CMake 工程
解压 QtFlappyBird-master.zip 之后,不要急着打开 Qt Creator。我一般先看根目录文件列表:如果根目录里有 .pro 文件,这是 qmake 工程,直接用 Qt Creator 打开 .pro 即可;如果根目录里有 CMakeLists.txt,那是 CMake 工程,需要先配置 CMake 工具链。两种工程在 Qt Creator 里的打开方式不同,选错入口会出现“无法为项目解析套件”这类提示。
$ unzip QtFlappyBird-master.zip $ ls QtFlappyBird-master/ bird.png pipe.png background.png main.cpp widget.cpp widget.h QtFlappyBird.pro拿到的工程若是如上结构,主逻辑通常在 widget.cpp 里,资源文件是散落的图片而非 qrc 资源包。这种散图方案对 Qt 游戏来说更直白,因为你可以直接看到管道和背景图的原图尺寸,后面调碰撞判定盒时没黑匣子可猜,这也是我愿意拿它当教学样本的原因。
2.2 Qt Creator 或命令行:两种方式把工程编译出第一个窗口
用 Qt Creator 打开 QtFlappyBird.pro 后,要确认右下角的构建套件选中的是“Qt 5.15.2 MSVC2019 64bit”,而不是 MinGW 或 Android 套件。如果套件选错,最常见结果是编译能过、运行时报平台插件错误,或者干脆报错说找不到 qtwidget 相关头文件。套件确认完毕,直接点绿色运行按钮,正常会弹出游戏窗口,此时还不需要任何玩法操作,因为代码一般默认从游戏开始状态进入。
不带界面操作习惯的人也可以走命令行,前提是环境变量里能直接找到 qmake。这条命令链通常更贴近 CI 或服务器编译场景,在本地也完全可用:
$ cd QtFlappyBird-master $ qmake QtFlappyBird.pro $ mingw32-make # 若你用的是 MSVC 套件,这一步换成 nmake $ ./release/QtFlappyBird.exe这里 qmake 的作用是读取 .pro 文件里的 QT += core gui 配置和 SOURCES/HEADERS 列表,生成 Makefile;mingw32-make 或 nmake 再按依赖关系逐文件编译。值得说的是,qmake 步骤最容易出问题的是路径带空格,比如你把 zip 解压到了 D:\my games\QtFlappyBird 这种目录里,qmake 生成的 Makefile 在某些老版本下会解析出错,表现为“无法打开包含文件”或各种诡异依赖错误,我把 zip 一律解压到 D:\projects 这种无空格路径后才消停。
2.3 首跑黑窗口或闪退时,先查这三个文件
第一次编译通过但运行出问题,我一般按顺序排查 main.cpp、widget.cpp 的构造函数和资源文件路径。main.cpp 里通常是标准 QApplication 加显示主窗口,问题不大;widget.cpp 的构造函数如果写了 scene->addPixmap 之类操作,而图片路径用的是相对路径而非 qrc 资源路径,那你的“工作目录”必须能定位到图片。Qt Creator 里默认工作目录是构建目录而不是源码目录,这是新手最容易翻车的第一个点。见下方常见失败与修复:
// 错误写法:依赖当前工作目录 QPixmap bird("bird.png"); // 修复写法:把图片声明进 QtFlappyBird.qrc,再用资源路径 QPixmap bird(":/images/bird.png");图片加载失败通常不会直接崩,而是显示空白背景或者小鸟区域透明,很多人以为是代码逻辑问题,其实只是图片没加载进来。判断方法是在构造函数里临时加一行 qDebug() << bird.isNull(),输出 true 就说明路径问题实锤了。当然,如果这套源码本来就用了 qrc 方案,这一步不会出幺蛾子,你只需要确认 .pro 文件里 RESOURCES += 那一行没被注释掉。
3. 游戏主循环拆解:QTimer 驱动、碰撞检测与绘制这三块必须吃透
3.1 QTimer 不是用来计时的,是驱动整个游戏状态更新的心脏
Flappy Bird 这类游戏不需要每帧实时输入响应,它本质是一个离散状态机:每 16 毫秒或 20 毫秒“心跳”一次,所有物体位置、碰撞状态、得分判断都在这个心跳里更新。常见做法是在 widget.h 里声明一个 QTimer *timer 和三个状态坐标,然后按如下方式初始化:
// widget.h 关键成员 QTimer *m_timer; // 游戏心跳定时器 int m_birdY; // 小鸟纵坐标,屏幕左上角为原点,y 越大越靠下 int m_pipeX; // 管道横坐标,从右向左移动 int m_speed; // 每帧管道左移的像素数 bool m_birdUp; // 当前是否处于“抬升”状态 // widget.cpp 构造函数 m_timer = new QTimer(this); m_timer->setInterval(16); // 约 60 FPS connect(m_timer, SIGNAL(timeout()), this, SLOT(updateGame())); m_timer->start();这里 setInterval(16) 不是绝对的,Qt 定时器在 Windows 上默认精度在毫秒级,实际触发间隔可能有正负几毫秒的抖动。做 Flappy Bird 这种简单游戏可以接受,但如果你追求稳定手感,后面第 6 章我再给固定时间步长的升级方案。这里需要解释清楚的是 connect 绑定到 updateGame() 这个槽函数,之后所有逻辑都往这个槽里放,而不是在 paintEvent 里写游戏状态推进,这是很多自学者写 Qt 游戏最常犯的错误——把状态推进写进 paintEvent 会导致绘制频率和逻辑频率耦合,窗口尺寸变化、拖动时游戏速度会突变,非常玄学。
3.2 碰撞检测的三种写法和一种推荐写法
Flappy Bird 的碰撞判定通常有三种做法:矩形相交检测、圆形与矩形检测、像素级检测。像素级最精确但性能消耗大且代码复杂,实际工程里没人对 2D 休闲游戏这么做;圆形与矩形检测适合小鸟是圆图的场景;最省事、也是大多数 Qt 教程默认方案的是 QRect 矩形相交检测。实现如下:
bool Widget::checkCollision() { // 小鸟碰撞盒:特意缩一圈,给玩家一点容错 QRect birdRect(m_birdX + 10, m_birdY + 10, m_birdWidth - 20, m_birdHeight - 20); // 上下管道各占一个矩形 QRect pipeRectUp(m_pipeX, 0, m_pipeWidth, m_pipeGapY); QRect pipeRectDown(m_pipeX, m_pipeGapY + m_pipeGapHeight, m_pipeWidth, m_groundY - (m_pipeGapY + m_pipeGapHeight)); return birdRect.intersects(pipeRectUp) || birdRect.intersects(pipeRectDown) || m_birdY <= 0 || m_birdY >= m_groundY; }用 QRect::intersects 能直接拿到两个矩形是否重叠的布尔值,代码最直观。但注意我给小鸟碰撞盒做了向内收缩:左右各收缩 10 像素、上下各收缩 10 像素,这个参数直接影响游戏的“公平感”。如果直接用原图完整尺寸做碰撞,玩家会觉得明明没碰到管道却判死了,因为 PNG 图片通常有透明边,而 QRect 是按图片像素边界算的,这是 Flappy Bird 常见口碑翻车点之一。
撞击地面和撞管道分开处理也很有必要:撞管道直接结束,撞地面我通常会做成小鸟原地落地、不再弹跳,这样手感更接近原版。判别点 m_groundY 一般取窗口高度减去地面贴图高度,写死数值在换背景图后就失灵了,我习惯在 resizeEvent 里动态取。
3.3 绘制双缓冲与 update() 的正确调用姿势
Qt Widgets 绘图默认是擦掉重画的,不给窗口手动设置双缓冲时,高速运动物体(管道移动速度稍快时)会有闪烁。常见做法是在构造函数里加 setAttribute(Qt::WA_OpaquePaintEvent) 和 setAutoFillBackground(false),这两个设置配合起来让 Qt 跳过背景填充,减少闪烁。而触发重画用的是 update(),不是 repaint():
void Widget::updateGame() { if (m_gameState == GameState::PLAYING) { m_pipeX -= m_speed; m_birdY += m_gravity; // 每帧下落,受重力加速度影响 if (m_birdUp) m_birdY -= m_jumpPower; if (checkCollision()) { m_gameState = GameState::OVER; m_timer->stop(); } // 得分判定:管道通过小鸟左缘 if (m_pipeX + m_pipeWidth < m_birdX && !m_scoreGiven) { m_score++; m_scoreGiven = true; } } update(); // 只做“请求重绘”,马上返回,不会阻塞逻辑 }这里把 update() 放在函数末尾是正确的,因为它只是给事件循环发一个绘制请求,不会立即触发 paintEvent,逻辑循环不会被阻塞。有些初学者会在这个函数里直接调用 paintEvent,这不是 API 设计里的用法,会让绘制在非 GUI 线程或错误时机执行,轻则白屏,重则崩溃。
3.4 按键控制与事件处理:空格抬升的两种实现路径
按键监听的实现路径有两套:重写 keyPressEvent,或使用 QShortcut 信号绑定。Flappy Bird 这种低复杂度游戏,重写 keyPressEvent 更直接:
void Widget::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Space) { if (m_gameState == GameState::READY) { m_gameState = GameState::PLAYING; } else if (m_gameState == GameState::PLAYING) { m_birdY -= m_jumpPower; // 瞬时抬升,而不是持续上升 m_birdUp = true; } else { restartGame(); // 游戏结束后按键重新开始 } } }这里面有个手感关键点:按一次空格只抬升固定像素,而不是按住持续上升。原版 Flappy Bird 的物理模型就是这种“拍翅膀”式的冲量,你给 m_jumpPower 设成 45 到 60 像素比较合适,设得太大玩家没法微调,设得太小三连击也拉不起高度。另外,m_birdUp 这个标记位别忘记在下一帧的 updateGame() 里重置,否则小鸟会一直上升或一直不下降,常见做法是在使用 m_birdUp 判断后立刻置 false。
4. 让手感像 Flappy Bird:重力、管道间距、速度这套参数才是核心资产
4.1 参数表与手感量级参考
下面这批参数不是我凭空拍的,而是按 800x600 窗口、16ms 帧间隔做基准调出来的区间值。不同窗口高度下需要等比缩放,但量级关系值得记住:重力加速度不宜超过每帧 1.2 像素,否则下降速度曲线太陡,玩家没有反应时间:
| 参数 | 作用对象 | 推荐区间 | 说明 |
|---|---|---|---|
| m_gravity | 小鸟 | 0.5 ~ 1.2 像素/帧² | 决定下降加速度;调太大游戏像自由落体,没有“飘”的感觉 |
| m_jumpPower | 小鸟 | 45 ~ 60 像素/次 | 单次点击抬升高度;与重力配合达成手感平衡 |
| m_speed | 管道 | 3 ~ 5 像素/帧 | 管道水平左移速度;超过 6 时玩家视觉负担明显增大 |
| m_pipeGapHeight | 管道对 | 150 ~ 220 像素 | 上下管道间距;低于 140 即难度偏高,高于 240 则失去挑战性 |
| m_pipeInterval | 管道生成 | 360 ~ 480 像素 | 相邻两对管道的水平间距;间距太小会让人觉得“无路可走” |
这组参数我提供的不是死值,而是区间。原因是每个人显示器窗口比例不同,宽屏下 170 像素的管道间隙看起来会比方屏下更窄,尤其在你的窗口没有锁定宽高比时。判断参数是否合适的办法很简单:自己连打三轮,如果三局平均都过不了 5 个管道,就把管道间距调大 20 像素,重力调小 0.1,不要一步调太多,每次只改一个变量你才知道是哪个参数造成了手感变化。
4.2 管道生成的随机性与得分标记
管道高度的随机生成是个容易写错的细节:如果你每一对管道的 gap 高度一样,只是上下平移,玩家形成肌肉记忆后游戏会变得太机械。常见实现是在新管道生成时让 gap 中心点在安全区间内随机波动:
void Widget::generatePipe() { // gap 中心点范围:上部留出 120px,下部留出 100px 给地面 int minCenter = 120 + m_pipeGapHeight / 2; int maxCenter = m_groundY - 100 - m_pipeGapHeight / 2; int centerY = minCenter + qrand() % (maxCenter - minCenter); m_pipeGapY = centerY - m_pipeGapHeight / 2; m_scoreGiven = false; // 重置本管道得分标记 m_pipeX = m_windowWidth; // 从窗口右缘进入 }qrand() 使用前最好 qsrand(QTime(0,0,0).secsTo(QTime::currentTime())) 初始化随机种子,否则每次启动游戏的管道序列完全一致——这本来是玄学,但真有人在调试时因为“这次能过下次不能过”而怀疑代码不稳定,排查半天发现是随机种子没置。得分标记 m_scoreGiven 必须随新管道重置,否则一对管道只会给你加一次分,逻辑上没问题但你需要在玩家穿过后再次生成管道时把它复原,遗漏会导致分数只涨一次。
4.3 窗口缩放时游戏不僵死的处理
游戏运行时用户拖拽改变窗口大小,是所有 Qt 小游戏最容易暴露问题的场景。Flappy Bird 原版是固定竖屏布局,桌面上拉伸窗口后,地面坐标 m_groundY 需要重算,管道间隙比例也需要同步变化。常见做法是重写 resizeEvent,将地面位置、管道上下边界、小鸟出生位置全部基于当前窗口尺寸重新计算:
void Widget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); m_groundY = height() - m_groundHeight; m_birdX = width() / 3; // 小鸟始终固定在距左缘 1/3 处 if (m_pipeX < width()) { m_pipeX = width(); // 窗口变宽时避免管道“瞬移”到画面中间 } }处理窗口宽高比巨变时有个实用策略:限制最小窗口尺寸为 480x600,在构造函数里 setMinimumSize(480, 600),这样玩家调整窗口时不会把游戏压成一条缝。另外 resizeEvent 在构造阶段就可能触发,所以 m_groundHeight 和 m_birdX 这些成员变量一定要在构造函数里先初始化,否则会在事件回调里读到未初始化值,运行期表现为随机闪退,这是标准的 C++ 未定义行为坑。
5. Qt 工程实战避坑:编译、运行、崩溃的 5 条血泪排查记录
5.1 编译报错 “:-1: error: dependent '........\qt\5.15.2\msvc2019_64\include\qtwidgets\qtwidgetsdepends.h' does not exist.”
现象:用 Qt Creator 打开 .pro 后,编译一开始就报 dependent 文件不存在,整个工程根本无法产出 Makefile。原因:这是 qmake 生成的 Makefile 里记录的依赖路径失效了,常见于你的 Qt 库路径和 .pro.user 文件里缓存的路径不一致。比如你之前用 Qt 5.12 打开过这个工程,后来换了 5.15.2,或者把 Qt 安装目录移动过位置。解决:找到工程目录下的 .pro.user 文件并删除,关闭 Qt Creator 重新打开 .pro 文件,让它重新识别当前已安装的 Qt 版本。也可以在“工具-选项-Kits”里确认当前 Kit 的 qmake 路径是否正确,注意 Qt 5.15.2 的 MSVC 和 MinGW 两套 kit 不能混用。
提示:删除 .pro.user 属于“后悔药”,它不影响源码,只丢掉 Qt Creator 记住的构建配置,下次打开全部重来一遍,这不会花超过一分钟。
5.2 中文乱码问题:MSVC 下控制台和窗口标题全是问号
现象:main.cpp 里写的 setWindowTitle("Flappy Bird 中文名"),编译出来窗口标题显示为问号或乱码。原因:MSVC 编译器默认按 ACP 编码读取源文件,如果你用 UTF-8 无 BOM 保存源码,MSVC 会按 GBK 理解字符串字面量的字节序列,从根上读错了。解决:最省事的方法是统一用 QStringLiteral 或 u8 前缀,更彻底的方案是把所有源文件转存为 UTF-8 with BOM 格式。顺手还能在 .pro 文件里加一行 msvc: QMAKE_CXXFLAGS += /utf-8,让 MSVC 按 UTF-8 解析源码,这一行在你使用 Qt 5.12 之后的版本时几乎必加。
5.3 程序编译成功但运行后画面一片空白,或者图片区域是透明方块
现象:窗口正常弹出,背景色存在但没有小鸟和管道图形。原因:图片资源加载失败。最常见的是散图方案里用了相对路径,而 Qt Creator 构建时的工作目录是 build-xxx 目录,不是源码目录,相对路径找不到图。解决:把图片资源移入 .qrc 文件,用 :/images/bird.png 这种资源路径替代相对路径;如果你坚持用相对路径,就把图片复制一份到构建目录下,但这在 release 发布阶段同样会出问题,所以不是长久之计。排查可以用 QFile::exists(":/images/bird.png") 快速判断资源是否注册成功。
5.4 点击空格没反应,或按键响应延迟明显
现象:游戏运行中,快速连续按键只有第一次生效,后续按键没反应。原因:keyPressEvent 被窗口焦点抢走,或者你在 keyPressEvent 里没调用 event->accept(),事件冒泡到底层被丢弃。另外,如果窗口当前聚焦的是某个 QLineEdit 或按钮,按键事件根本不会到主窗口。解决:在构造函数里 setFocusPolicy(Qt::StrongFocus),确保窗口能接收键盘事件;同时在 keyPressEvent 里对已处理事件调用 event->accept()。还有一种情况是 QTimer 间隔设到 40ms 以上,按键虽然立刻被记录但逻辑刷新太慢,体感上像延迟,把定时器回调最小间隔压到 16ms 即可。
5.5 游戏结束标志后程序崩溃,不是闪退就是调用栈指向奇怪位置
现象:game over 之后按空格重启游戏,有时直接闪退,有时窗口卡死。原因:大概率是 restartGame() 里清理了 QTimer 或场景对象,却没有重新初始化所有成员变量;更隐蔽的是第二次游戏开始时 m_scoreGiven 还停留在 true,导致永远不会加分。解决:把 restartGame() 写成“把构造函数里所有初始化全部执行一遍”,而不是只复位几个坐标。我在里面会显式调用一次 m_timer->start(),并注意 new 出来的指针不重复 delete。如果崩溃调用栈指向 QGraphicsScene 或 QGraphicsItem,那说明这套分支用了 QGraphicsView 架构,需要排查所有 item 是否在场景中重复添加。
6. 进阶一步:固定时间步长、发布打包与验证手感的数据抓手
固定时间步长是让 Qt 定时器驱动的游戏在低帧率机器上不变得更难的关键手段。前面第 3 章里我用了 setInterval(16) 的简单方案,但 Windows 上 Qt 定时器并非严格实时,后台负载高时帧率会掉到 50 FPS 甚至更低,此时每帧移动 3 像素的管道会变得肉眼可见的卡顿和加速,游戏难度剧增。常见的处理是用真实经过的毫秒数折算位移量:
void Widget::updateGame() { qint64 now = QDateTime::currentMSecsSinceEpoch(); qreal delta = qBound<qreal>(0.0, (now - m_lastTime) / 16.0, 4.0); m_lastTime = now; m_pipeX -= m_speed * delta; // 帧率低时移动距离变大,实际速度恒定 m_birdY += m_gravity * delta * delta; // ... 其余逻辑不变 }delta 乘以了速度值,16ms 是基线时间单位,所以帧率 60 时 delta 约等于 1,帧率掉到 30 时 delta 约等于 2,管道位移补偿到同样的实际速度。注意 m_birdY 使用了 delta 的平方,这是因为重力是加速度量纲,位移和时间的平方成正比,不做平方补偿会出现低帧率时小鸟下落速度看起来加快或变慢的怪现象。这套处理做完后,手感验证方法是把窗口拖动到画面复杂的屏幕区域,同时打开任务管理器观察帧率,再和一个固定满帧的机器对比同分数下的通关难度,两边的操作节奏应趋于一致。
发布打包这个环节同样不可跳过,因为 Qt 程序在没装 Qt 的机器上运行会缺一堆 DLL。我一般习惯先切换到 release 编译,然后在构建目录里执行官方工具 windeployqt:
$ cd build-QtFlappyBird-Desktop_Qt_5_15_2_MSVC2019_64bit-Release/release $ windeployqt QtFlappyBird.exewindeployqt 会自动把 platforms、styles、imageformats 等运行依赖复制到 exe 同级目录,这时整个文件夹就能压缩分发给其他 Windows 机器。它不会帮你处理音效文件之类的自定义资源,所以 assets 目录要手动包含。遇到杀软提示缺失 msvcp140.dll 时,说明目标机器缺 VC++ 运行库,把 vc_redist.x64.exe 一并放进压缩包是常见做法。
验证工程是否值得继续投入,我最后会用分数曲线来判断。修改代码让每得一分把分数写进 QFile 追加一行“得分-时间戳”,跑十轮后拉出平均分和最高分,平均分过低说明管道间距或重力参数不合适,平均分高但单局方差极大说明随机管道生成区间过宽,两种诊断方向完全不同。我自己在调试 Flappy Bird 手感时被这种分数曲线救过一次,当时觉得重力调得没问题,但十轮平均分只有 3,最后发现是管道间距随机范围上下限写反了,窄管道频繁出现导致难度暴增。这类小游戏的价值从来不在实现难度,而在物理参数和体验细节的闭环,希望这份排查思路帮到你。
本文还有配套的精品资源,点击获取