
简介这是一份基于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 列表生成 Makefilemingw32-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 qBoundqreal(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最后发现是管道间距随机范围上下限写反了窄管道频繁出现导致难度暴增。这类小游戏的价值从来不在实现难度而在物理参数和体验细节的闭环希望这份排查思路帮到你。本文还有配套的精品资源点击获取