
简介基于QT与C开发的Flappy Bird游戏完整源码主要面向毕业设计、课程设计以及个人项目练手适合有一定C基础并希望入门桌面游戏开发的读者。项目源码已通过严格测试可直接运行参考也可在此基础上扩展功能。资源包共27个文件体积约3.38MB包含5个C源文件、4个头文件、8张PNG图片素材、2个MP3音频和1个WAV音效另外还有项目配置文件、资源文件列表、许可证与说明文档目录结构清晰方便快速定位与二次开发。目前已有187人学习/下载。通过阅读源码可掌握QT窗口搭建、定时器动画、键盘事件处理、管道与碰撞检测逻辑等核心知识点也能学习widget、pipeItem、roadItem等类的职责划分了解qmake工程配置、QSS资源管理及调试技巧。无论是课程答辩还是拓展练习这套源码都能提供清晰完整的参考适合作为QT游戏开发入门与课程设计的高质量选择。1. 用QTC复刻flappyBird一份能跑通完整玩法的项目源码到底值不值得读我的结论放在最前基于QTC开发的flappyBird游戏源码是现阶段毕业设计和课程设计里投入产出比最高的项目之一。它不是那种动辄几千行的重型框架也不像只摆一个按钮的“假项目”而是把游戏循环、物理模拟、绘制渲染、键盘交互和状态管理全部串起来通读一遍就能建立起对QT客户端开发整体脉络的认识。这套方案在Windows和Linux桌面都能编译通过用来应付答辩演示、代码讲解都足够扎实。适合的人群也很明确刚学完C语法、想在QT上动手做第一个完整项目的学生以及准备转向客户端开发、需要一个能写进简历的作品的从业者。下文从环境搭到发布一条线讲完全部基于我实际跑通的写法展开。2. 先搭好QTC的编译环境选对版本和构建套件后面才不翻车2.1 用Qt 5.15.2还是Qt 6课程设计场景下的选型思路很多同学一上来就下载最新版Qt 6.6甚至更高版本结果创建工程后连QRegExp和QTextCodec的位置都找不到代码抄过来全是红色波浪线。这里给出我的选择做flappyBird这类2D小游戏Qt 5.15.2是更稳的版本。原因有三点第一Qt 5.15.2是Qt 5系列的长期支持版本LTS网上教程和博客给的示例代码绝大多数基于这个版本遇到编译错误时更容易检索到现成答案第二它默认支持C11/14/17写游戏逻辑完全够用第三Qt 6虽然性能更好但把部分模块位置做了调整旧工程直接迁移会多出一堆不必要的工作。安装时注意到一个高频坑勾选组件时MSVC 2019 64-bit和MinGW 8.1.0 64-bit对应的是两套不同的编译器二选一即可。用Visual Studio开发就选MSVC套件习惯Qt Creator自带编译器就选MinGW。最容易翻车的场景是装了MSVC版本的库却在构建套件Kit里选MinGWQt Creator会直接提示编译器与Qt库不匹配。网上搜“qt 5.15.2下载安装”能找到大量镜像站安装包体积不小下载时注意选对系统位数。安装完成后打开Qt Creator在“工具→选项→Kits”里确认编译器、Qt版本、CMake三项状态正常没有黄色感叹号再开始建工程。这一步多花十分钟后面能省一整天。另外提一个玄学经验如果电脑上同时装了多个Visual Studio版本Kits列表里可能自动注册多个MSVC编译器务必手动指定与你安装的Qt库对应的那一个不然编译时冒出的“cannot mix incompatible qt library”会让人一头雾水。2.2 工程文件与最小代码骨架从.pro到第一个窗口flappyBird这类项目我用的是QWidget方案不引入QML原因有两个课程设计答辩时老师大概率会问“这个窗口是怎么画出来的”QWidget的paintEvent更容易讲清楚另一个原因是QWidget方案对初学者来说依赖更少不会因为QML引擎和C交互问题卡住。先看工程文件。在Qt Creator里新建“Qt Widgets Application”后手动改一下.pro文件把不需要的模块注释掉QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET FlappyBird TEMPLATE app SOURCES \ main.cpp \ gamewidget.cpp HEADERS \ gamewidget.h这段配置的核心是QT core gui和后面的widgets条件判断。在Qt 5及以上版本QWidget相关类必须显式加上widgets模块否则#include QWidget会报“No such file or directory”。TARGET指定生成的exe文件名称SOURCES和HEADERS列出工程里的源文件和头文件后续新增文件记得在这里补上。然后是入口文件main.cpp负责创建应用对象和显示主窗口#include QApplication #include gamewidget.h int main(int argc, char *argv[]) { QApplication a(argc, argv); GameWidget w; w.setWindowTitle(FlappyBird - Qt C); w.resize(420, 640); // 窗口宽高和素材尺寸保持比例 w.show(); return a.exec(); }QApplication是QT GUI程序的必需对象它管理事件循环和全局资源。a.exec()启动事件循环后程序才会响应键盘、定时器和重绘事件。这里我把窗口固定为420×640宽高比接近2:3和经典flappyBird的画面比例一致后续绘制背景和小鸟时不需要做复杂的拉伸适配。接下来是头文件gamewidget.h声明游戏需要用到的主要成员#ifndef GAMEWIDGET_H #define GAMEWIDGET_H #include QWidget #include QTimer class GameWidget : public QWidget { Q_OBJECT public: explicit GameWidget(QWidget *parent nullptr); protected: void paintEvent(QPaintEvent *event) override; // 所有绘制集中在这里 void keyPressEvent(QKeyEvent *event) override; // 响应空格键 private: void updateLogic(); // 每帧刷新游戏逻辑 void resetGame(); // 重置全部状态 QTimer m_timer; // 游戏主循环定时器 int m_birdY; // 小鸟Y坐标相对于窗口 int m_birdVy; // 小鸟垂直速度像素/帧 int m_score; // 得分 bool m_gameOver; // 游戏是否结束 }; #endif // GAMEWIDGET_H注意Q_OBJECT宏只要类里定义了信号或槽就必须在类声明中加入这个宏否则connect相关功能无法正常工作。因为我在构造函数里用Lambda表达式连接定时器所以Q_OBJECT不能省。m_birdVy记录的是每帧约16毫秒小鸟在垂直方向移动的像素数不是每秒的物理速度后面物理计算都以帧为单位避免在不同刷新率下表现不一致。最后是最简版本的gamewidget.cpp先让窗口能响应空格并让小鸟做抛物线运动#include gamewidget.h #include QPainter #include QKeyEvent GameWidget::GameWidget(QWidget *parent) : QWidget(parent), m_birdY(300), m_birdVy(0), m_score(0), m_gameOver(false) { setFocusPolicy(Qt::StrongFocus); // 让窗口能接收键盘事件 m_timer.start(16); // 约60FPS刷新 connect(m_timer, QTimer::timeout, this, [this]() { updateLogic(); update(); // 触发paintEvent重绘 }); } void GameWidget::updateLogic() { if (m_gameOver) return; m_birdVy 1; // 重力加速度每帧速度1像素 m_birdY m_birdVy; // 新位置 旧位置 当前速度 if (m_birdY 0) m_birdY 0; // 防止飞出窗口顶部 } void GameWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setBrush(QColor(135, 206, 235)); painter.drawRect(rect()); // 画天空背景 painter.setBrush(QColor(255, 215, 0)); painter.drawEllipse(180, m_birdY, 40, 30); // 画小鸟先用椭圆代替 } void GameWidget::keyPressEvent(QKeyEvent *event) { if (event-key() Qt::Key_Space) { m_birdVy -10; // 按空格速度瞬间变为负值小鸟向上跳 } } void GameWidget::resetGame() { m_birdY 300; m_birdVy 0; m_score 0; m_gameOver false; }这段代码是后续所有功能的基础。m_timer.start(16)里的16毫秒是定时器周期对应约60FPS太短会增加CPU负担太长会感到掉帧。重力加速度取每帧1像素的设计逻辑是初速度为0时经过n帧后下落距离是123...n像素这个加速度下小鸟的坠落曲线不会太生硬。主循环里updateLogic()负责改数据update()负责让QT重绘两者分离的好处是逻辑和渲染互不干扰改物理参数时不需要动绘图代码。2.3 编译与运行三个必须看懂的构建输出跑通这段骨架后编译输出窗口会有三类信息需要你认识。第一类是“ui_xxx.h: No such file or directory”这通常是因为你的.pro文件里没有加QT widgets或者构建目录和源码目录不一致清理项目后重新qmake即可。第二类是“undefined reference to vtable for GameWidget”这个十有八九是头文件里有Q_OBJECT宏但编译器没有重新执行moc在Qt Creator里“清除→重新构建”就能解决。第三类是程序能编译通过但窗口一闪而过绝大多数是main.cpp里写成了QApplication a;而没调用a.exec()事件循环没起来窗口自然秒退。3. 核心玩法实现小鸟物理、管道生成与碰撞判定的具体写法3.1 小鸟的坐标与速度公式每帧16ms怎么算上一章的骨架版本里重力加速度是每帧加1像素跳跃速度是-10。这个数值能跑但手感偏硬——“跳一下”之后小鸟会立刻进入快速下坠玩起来像在操作一块石头。我在实际调参时把物理模型改成更接近经典flappyBird的写法重力加速度0.4像素/帧²跳跃瞬间速度设为-8像素/帧同时在updateLogic里加入最大下落速度限制防止小鸟在管道间下落时速度累积到一帧几十像素导致碰撞检测直接穿透管道。void GameWidget::updateLogic() { if (m_gameOver) return; // 重力加速度每帧速度增加0.4像素/帧 m_birdVy 0.4; // 限制最大下落速度避免一帧移动太多像素导致穿透 if (m_birdVy 10) m_birdVy 10; // 更新位置新位置 旧位置 当前速度 m_birdY static_castint(m_birdVy); // 如果小鸟飞出窗口顶部直接顶住 if (m_birdY 0) m_birdY 0; // 如果小鸟飞出窗口底部视为死亡 if (m_birdY m_birdHeight height()) { m_gameOver true; } }这里的参数选择有几个讲究。0.4这个重力值对应的是“每帧速度增加0.4像素/帧”和上一章的1相比跳起来之后滞空时间更长玩家有更充足的反应时间调整位置难度曲线更平滑。10是速度上限防止在连续几帧不按键后速度累积到20以上。假设速度是20一帧16毫秒内小鸟移动20像素而管道厚度只有40像素如果碰撞检测只检查当前帧的位置就可能出现“上一帧还在管道左边下一帧已经在管道右边”的穿透现象。限制最大速度本质上是给碰撞检测创造安全的工作区间这是做2D平台游戏时一个容易被忽略的关键细节。跳跃速度-8和重力0.4的配合下小鸟从起跳到落回原位大约需要20帧也就是0.33秒左右这个节奏我对比过多个同类作品属于不松不紧的区间。3.2 管道生成与移动用固定间隔生成、按时间回收管道是flappyBird玩法里真正的“敌人”。管道需要满足几个条件成对出现上方管道和下方管道水平方向持续向左移动垂直方向留出一个缺口让小鸟通过。我的实现是用一个QVector存放管道数据结构每个管道对象记录横坐标、缺口中心Y坐标、是否已经计过分这样的结构比用两个数组分别管理上管道和下管道更清晰。struct Pipe { int x; // 管道左边界横坐标 int gapCenterY; // 上下管道之间缺口的中心Y坐标 int gapHeight; // 缺口高度默认160像素 bool scored; // 是否已经计过得分 }; // 在GameWidget中增加成员 QVectorPipe m_pipes; int m_pipeInterval; // 相邻管道的水平间距默认260像素 int m_pipeSpeed; // 管道左移速度默认2像素/帧管道生成不能每帧都创建新管道而是按间隔时间触发。我的做法是维护一个m_pipeTick计数变量每帧加1当计数达到m_pipeInterval / m_pipeSpeed时生成一根新管道并重置计数器。新管道的高度用C随机数生成让每次游戏的地形都不一样void GameWidget::spawnPipe() { Pipe p; p.x width(); // 从窗口右边缘进入 // 用随机数生成缺口中心位置范围控制在窗口高度的30%~70% int minY static_castint(height() * 0.3); int maxY static_castint(height() * 0.7); p.gapCenterY minY rand() % (maxY - minY 1); p.gapHeight 160; p.scored false; m_pipes.append(p); }rand() % (maxY - minY 1)确保缺口中心不会太靠近窗口顶部或底部否则上下管道会出现“一根极长、一根极短”的失衡局面。gapHeight取160像素和窗口高度640相比留给小鸟的竖向空间大约有160像素而小鸟本身只有40×30像素这样玩家不需要做极限微调也能通过。如果你想调整难度把gapHeight改小到130或者把m_pipeSpeed从2改到3难度立刻上一个台阶。每个管道生成后每帧里遍历m_pipes将所有管道的x坐标减去m_pipeSpeed同时检查管道是否已经完全离开左边界移出窗口则从容器里移除防止内存占用无限增长for (int i m_pipes.size() - 1; i 0; --i) { m_pipes[i].x - m_pipeSpeed; if (m_pipes[i].x m_pipeWidth 0) { m_pipes.removeAt(i); // 管道完全移出窗口回收 } }这里从后往前遍历是因为removeAt会改变数组下标正序遍历时删除元素会导致索引错乱这是用QVector做动态删除时最容易踩的坑。3.3 碰撞检测写成函数矩形相交判断与游戏状态切换碰撞判定考虑起来很简单小鸟和管道都是矩形用QRect的intersects方法判断相交即可。但具体实现时要注意管道的上管道和下管道是两个矩形分别覆盖“从窗口顶部到缺口上边缘”和“从缺口下边缘到窗口底部”两个区域。另外碰撞不只发生在管道上碰到窗口上下边界同样要判负。bool GameWidget::checkCollision() { // 检测窗口上下边界 if (m_birdY m_birdHeight height() || m_birdY 0) { return true; } // 小鸟的碰撞矩形稍微缩小一圈优化手感 QRect birdRect(180, m_birdY, 36, 26); for (const Pipe p : m_pipes) { // 上管道从窗口顶部到缺口上边缘 QRect topRect(p.x, 0, m_pipeWidth, p.gapCenterY - p.gapHeight / 2); // 下管道从缺口下边缘到窗口底部 QRect bottomRect(p.x, p.gapCenterY p.gapHeight / 2, m_pipeWidth, height()); if (birdRect.intersects(topRect) || birdRect.intersects(bottomRect)) { return true; } } return false; }一个容易被忽视的细节是碰撞矩形比贴图略小一圈。我把小鸟的碰撞矩形从实际的40×30缩小到36×26四周各缩了2像素左右。这个做法的原因是玩家视觉判断和实际判定之间存在“视觉宽容度”如果碰撞边界和贴图严丝合缝玩家会频繁觉得“明明没碰到却死了”。这种“给玩家一点宽容”的碰撞优化在商业游戏里很常见放在课程设计中是很好的答辩亮点你可以直接给老师解释缩小碰撞框是为了减少视觉误差带来的挫败感同时保持物理模拟的真实性。碰撞判定在updateLogic()末尾调用一旦返回true就把m_gameOver置为true同时停止所有管道和小鸟的位置更新。游戏结束的界面不单独做一个窗口而是在paintEvent里叠加半透明遮罩和“按R重新开始”的文字让整个游戏在一个窗口内完成状态切换代码量和答辩讲解成本都更低。4. 绘制、得分与结束态把游戏从「能玩」做到「能展示」4.1 paintEvent绘制与定时器驱动游戏所有的视觉输出都集中在paintEvent里。这个函数会被update()自动触发也会在窗口被遮挡后重新暴露时触发。绘制顺序很有讲究先画背景再画管道再画小鸟最后画分数和结束遮罩后画的覆盖先画的这个顺序一旦颠倒就会出现小鸟被管道盖住的错觉。void GameWidget::paintEvent(QPaintEvent *) { QPainter painter(this); // 1. 背景淡蓝色天空 painter.fillRect(rect(), QColor(135, 206, 235)); // 2. 管道绿色矩形边缘加白边更好看 painter.setPen(QPen(QColor(255, 255, 255), 2)); painter.setBrush(QColor(87, 157, 60)); for (const Pipe p : m_pipes) { int gapTop p.gapCenterY - p.gapHeight / 2; int gapBottom p.gapCenterY p.gapHeight / 2; painter.drawRect(p.x, 0, m_pipeWidth, gapTop); painter.drawRect(p.x, gapBottom, m_pipeWidth, height() - gapBottom); } // 3. 小鸟先用一个椭圆占位后期替换为贴图 painter.setBrush(QColor(255, 215, 0)); painter.drawEllipse(180, m_birdY, 40, 30); // 4. 游戏结束遮罩 if (m_gameOver) { painter.fillRect(rect(), QColor(0, 0, 0, 100)); painter.setPen(Qt::white); painter.drawText(rect(), Qt::AlignCenter, Game Over\nPress R to Restart); } }QPainter的使用有几个习惯性注意点fillRect用于铺大面积纯色背景比drawRect加setBrush更高效给管道描边用的setPen笔宽是2像素白色描边是为了让管道轮廓在蓝天背景下更清晰这是纯视觉优化不影响碰撞区域画结束遮罩时用的QColor(0, 0, 0, 100)第四位是透明度参数100是半透明级别0为完全透明255为不透明这个值也可以调成80让背景隐约可见的同时保持文字的辨识度。绘制这块还有个QT绘图相关的性能点不要在一帧里频繁创建QPen和QBrush对象。我见过不少源码把QPen创建放在for循环内部一帧画10根管道就创建10次画笔虽然这个小规模项目看不出性能问题但属于不好的习惯。正确的做法是在paintEvent开头或类成员中创建一次循环复用。4.2 得分判定与音效触发得分逻辑最简单直接的做法是小鸟每通过一根管道加1分。代码实现上只需要在每帧遍历管道时检查管道是否已经越过小鸟的X坐标、且scored标记为false满足条件就加分并立刻将scored置为true保证同一根管道不会重复计分void GameWidget::updateScore() { for (Pipe p : m_pipes) { // 管道右边界已经越过小鸟X坐标说明这管道已经通过了 if (!p.scored p.x m_pipeWidth 180) { p.scored true; m_score; // 这里可以触发一个音效 } } }判断条件是p.x m_pipeWidth 180其中180是设定的小鸟固定X坐标。用“管道右边界越过小鸟中心点”作为通过条件比用“管道左边界越过”更精确因为玩家在小鸟通过管道一半时才应该得分不会出现明明头部还没到、分数已经加上的错觉。分数显示直接绘制在窗口顶部中央painter.setPen(Qt::white); QFont font painter.font(); font.setPointSize(32); font.setBold(true); painter.setFont(font); painter.drawText(width() / 2 - 30, 70, QString::number(m_score));悬浮的分数层应该在绘制遮罩之前或之后按需选择。我的方案是正常游戏时分数画在最上层游戏结束后分数留在遮罩上方方便玩家看到最终得分。这个顺序在代码里表现为paintEvent中先画遮罩、再画分数这样分数永远是白色高亮不会被半透明黑幕盖住变暗。4.3 游戏结束与重新开始流程游戏结束后的处理重点是暂停所有动态逻辑但不关停定时器。因为定时器一旦stop()程序还需要一个事件来监听“按R重新开始”的键盘输入如果事件循环停了键盘事件根本进不来。所以正确做法是保留定时器运行在updateLogic()开头通过m_gameOver标记直接返回让所有物理计算静止。void GameWidget::keyPressEvent(QKeyEvent *event) { if (event-key() Qt::Key_Space !m_gameOver) { m_birdVy -8; } else if (event-key() Qt::Key_R m_gameOver) { resetGame(); } }resetGame里除了把小鸟坐标、速度、分数、游戏结束标记复位还必须清空所有管道。我见过一个翻车案例作者忘了清管道重新开始后屏幕上还残留上一局的上百根管道新游戏的第一帧就碰撞判定失败。清空操作只需要一行m_pipes.clear()但如果你还用了计时器计数字段生成新管道记得把计数变量一起复位否则第一根新管道会“迟到”很久。初始化状态尽量集中到一个函数里。构造GameWidget时直接调用resetGame()能避免“成员变量有默认值但启动后状态不对”的边界问题。这样写还有一个好处答辩时老师问“游戏重新开始逻辑怎么实现”你只需要回答“一个函数复位所有状态”思路一目了然。5. 踩坑记录QT版本不匹配、平台插件缺失、画面闪烁的三个真实场景5.1 编译报错cannot mix incompatible qt library (version ex50601) with this library现象项目在Qt Creator里一切正常但直接用命令行编译时报出类似fatal: cannot mix incompatible qt library (version ex50601) with this library的错误后面跟着一串MOC相关的路径。原因这个错误几乎都是因为编译器和Qt库的构建版本不一致导致的。最常见的情况是MinGW版本的Qt库配了MSVC的编译器或者反过来。比如你安装的是mingw81_64目录下的Qt库却在Kits设置里选择了“Desktop Qt 5.15.2 MSVC2019 64bit”两个工具链的标准库实现不同链接阶段就会对不上。解决重新检查“工具→选项→Kits”确保编译器类型和Qt库后缀完全匹配。若你用的是MinGW库编译器一栏应该带有mingw字样若用MSVC库编译器要选择Visual Studio自带的cl.exe。如果Kits里配置正确仍然报错清理构建目录build-XXX文件夹删除后重新qmake再构建。这个错误我遇过不下五次其中一半是换了电脑后路径失效另一半是同时装了两个版本的Qt构建套件名长得太像导致选错。5.2 运行时找不到平台插件could not find the qt platform plugin linuxfb现象程序在Windows上编译通过拷到Linux开发板或桌面环境执行时退出并提示qt.qpa.plugin: could not find the qt platform plugin linuxfb in后面是一段安装路径。原因QT程序的界面展示依赖平台插件Platform Plugin和系统交互Windows用的插件是windowsLinux桌面环境用的是xcb而linuxfb是嵌入式开发板用的帧缓冲插件。程序找不到对应插件说明Qt的platforms目录没有正确加载或者环境变量QT_QPA_PLATFORM_PLATFORM_PATH指向了错误位置。解决在Linux桌面上运行时显式指定平台为xcb命令行执行./FlappyBird -platform xcb或者设置环境变量export QT_QPA_PLATFORMxcb后再运行。如果提示xcb也找不到多半是libqt5gui5和libxcb-*依赖没装全执行sudo apt install libxcb-xinerama0 libxkbcommon-x11-0这类系统库补全即可。做课程设计的同学如果打算把程序在实验楼的Linux服务器上演示务必提前在纯命令行环境测试许多学校服务器只有ssh没有图形界面程序根本跑不起来提前准备截图或录屏是个好习惯。5.3 画面闪烁严重重绘时背景一明一暗现象游戏运行时画面持续闪烁尤其在小鸟移动和管道移动的时候整个窗口像是黑白帧在交替跳变看几秒就眼睛酸。原因paintEvent里如果每次都先fillRect整个窗口再画图形系统可能会在“清屏”和“重画”之间露出空档产生闪烁。QT的QWidget在默认情况下有双缓冲机制但如果你设置了Qt::WA_OpaquePaintEvent或Qt::WA_NoSystemBackground这类属性双缓冲会被绕过清屏和绘图就可能不同步。解决一个稳妥做法是关闭QWidget的自动背景填充避免系统在paintEvent前先帮你刷一遍背景色setAttribute(Qt::WA_OpaquePaintEvent); setAttribute(Qt::WA_NoSystemBackground);同时确保paintEvent里的第一个操作就是fillRect(rect(), 背景色)把整块画布一次性涂满再重复绘制过程。如果还闪可以把定时器类型改为精确模式m_timer.setTimerType(Qt::PreciseTimer)防止系统在节能模式下将16ms的定时器合并成20ms或30ms触发间接导致画面跳变。5.4 小鸟下落速度越来越快的怪象现象不开跳跃、只靠重力情况下小鸟下坠速度看起来不是“先慢后快”的平滑加速而是“慢→突然极快”的跳变甚至有时画面感觉像被快进。原因这是典型的定时器精度问题。默认情况下QTimer使用CoarseTimer系统为了省电会合并短周期定时器16ms可能被拖到30ms甚至更久。当定时器触发间隔不稳定时每一帧的物理计算依然累加固定的重力值但实际“每帧”的时长变长了视觉效果就是速度一会儿正常、一会儿猛增。解决在start之前加一行m_timer.setTimerType(Qt::PreciseTimer); m_timer.start(16);PreciseTimer会尽量保证16ms的触发周期但要注意这会略微增加CPU占用对几十毫瓦的嵌入式设备可能不友好。如果你的程序是跑在开发板上的建议改用QElapsedTimer记录真实经过的时间在updateLogic里按实际毫秒数换算每帧的位移量才能彻底摆脱定时器间隔波动的影响。6. 进阶与发布把游戏改成「能拿出手」的课程设计作品6.1 不用手动清零用QSettings保存最高分课程设计答辩时最高分记录是个低成本高亮点的功能。QT里保存简单的键值对不需要开数据库用QSettings写注册表或配置文件即可#include QSettings // 读取历史最高分 QSettings settings(MyCompany, FlappyBird); int bestScore settings.value(bestScore, 0).toInt(); // 游戏结束时更新最高分 if (m_score bestScore) { settings.setValue(bestScore, m_score); settings.sync(); // 强制写盘防止程序崩溃丢数据 }QSettings的构造函数参数是组织名和应用名Windows下默认写入注册表Linux下写入配置文件。答辩现场演示时可以把最高分画在窗口左上角评委第一眼就能看到“这个项目有数据持久化”。6.2 把程序发给没有装Qt的人windeployqt一行命令如果程序只在装了Qt的机器上跑那还停留在“开发完成”阶段能拷贝到另一台没有Qt环境的机器正常运行才是“交付完成”。Windows上最省事的做法是用Qt自带的windeployqt工具windeployqt FlappyBird.exe把编译生成的exe拷贝到一个空白目录打开命令行工具Qt自带的“Qt 5.15.2 (MinGW 8.1.0)”快捷方式切换目录后执行这行命令工具会自动把你需要的Qt运行库如Qt5Core.dll、Qt5Gui.dll、platforms目录下的qwindows.dll拷贝到exe旁边。验证方法很简单把整个目录压缩拷贝到一台没装Qt的Windows机器上双击exe看能否正常运行。需要注意两点一是发布目录里放的是release版本exe不是debug版本否则会提示找不到调试运行库二是拷贝目标机器运行如果报缺libgcc_s_seh-1.dll或libwinpthread-1.dll把这些MinGW运行时库一并拷入即可。6.3 一张表看清源码职责讲稿组织与答辩思路答辩讲代码时最忌讳从头到尾逐行念评委不关心每一行写什么只关心“你知不知道自己在写什么”。我建议把所有源码文件按职责拆成一张表每个文件三句话讲完。下面是flappyBird项目最基本的文件划分你可以根据自己的源码结构调整文件职责答辩要点main.cpp创建应用与主窗口介绍QApplication和事件循环gamewidget.h类声明与成员变量说明每个成员变量的作用gamewidget.cpp游戏主循环、物理、碰撞、绘制重点讲定时器驱动和碰撞检测res/图片资源目录说明贴图通过QPixmap加载答辩时先花一分钟讲整体架构再挑三个点展开定时器如何驱动游戏循环、重力加速度怎么调参、碰撞检测为什么用小一圈的矩形。这三个点能证明你真的理解了游戏开发的三个核心问题时间驱动、物理模拟、交互判定。另外有个实用建议把重力加速度、管道速度、缺口高度这些参数全部定义为类的成员变量放在文件顶部集中管理答辩时老师问“想调整难度怎么改”你直接指着那一排初始化列表说“改这里就行”。从代码审查角度看魔法数字越少代码越显得专业。这个项目做完后再回头看那些动不动就几百行的“完整引擎”其实都是纸老虎。一个能跑通全流程的flappyBird本质上就是“一个定时器 三个结构体 一个碰撞判断函数”。把这份源码吃透后续做更复杂的QT项目时你会发现当年踩过的编译器不匹配、平台插件缺失、定时器不精确这些坑现在都能提前避开。希望这篇笔记能帮你少走一段弯路把省下来的时间花在真正值得打磨的游戏手感上。本文还有配套的精品资源点击获取