news 2026/9/24 21:53:50

QT+C++复刻FlappyBird:从环境搭建到碰撞检测的完整实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
QT+C++复刻FlappyBird:从环境搭建到碰撞检测的完整实战指南

简介:基于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. 用QT+C++复刻flappyBird:一份能跑通完整玩法的项目源码到底值不值得读

我的结论放在最前:基于QT+C++开发的flappyBird游戏源码,是现阶段毕业设计和课程设计里投入产出比最高的项目之一。它不是那种动辄几千行的重型框架,也不像只摆一个按钮的“假项目”,而是把游戏循环、物理模拟、绘制渲染、键盘交互和状态管理全部串起来,通读一遍就能建立起对QT客户端开发整体脉络的认识。这套方案在Windows和Linux桌面都能编译通过,用来应付答辩演示、代码讲解都足够扎实。适合的人群也很明确:刚学完C++语法、想在QT上动手做第一个完整项目的学生,以及准备转向客户端开发、需要一个能写进简历的作品的从业者。下文从环境搭到发布一条线讲完,全部基于我实际跑通的写法展开。

2. 先搭好QT+C++的编译环境:选对版本和构建套件,后面才不翻车

2.1 用Qt 5.15.2还是Qt 6:课程设计场景下的选型思路

很多同学一上来就下载最新版Qt 6.6甚至更高版本,结果创建工程后连QRegExpQTextCodec的位置都找不到,代码抄过来全是红色波浪线。这里给出我的选择:做flappyBird这类2D小游戏,Qt 5.15.2是更稳的版本。原因有三点:第一,Qt 5.15.2是Qt 5系列的长期支持版本(LTS),网上教程和博客给的示例代码绝大多数基于这个版本,遇到编译错误时更容易检索到现成答案;第二,它默认支持C++11/14/17,写游戏逻辑完全够用;第三,Qt 6虽然性能更好,但把部分模块位置做了调整,旧工程直接迁移会多出一堆不必要的工作。

安装时注意到一个高频坑:勾选组件时,MSVC 2019 64-bit和MinGW 8.1.0 64-bit对应的是两套不同的编译器,二选一即可。用Visual Studio开发就选MSVC套件,习惯Qt Creator自带编译器就选MinGW。最容易翻车的场景是装了MSVC版本的库,却在构建套件(Kit)里选MinGW,Qt 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文件名称,SOURCESHEADERS列出工程里的源文件和头文件,后续新增文件记得在这里补上。

然后是入口文件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帧后下落距离是1+2+3+...+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_cast<int>(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中增加成员 QVector<Pipe> 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_cast<int>(height() * 0.3); int maxY = static_cast<int>(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 碰撞检测写成函数:矩形相交判断与游戏状态切换

碰撞判定考虑起来很简单:小鸟和管道都是矩形,用QRectintersects方法判断相交即可。但具体实现时要注意:管道的上管道和下管道是两个矩形,分别覆盖“从窗口顶部到缺口上边缘”和“从缺口下边缘到窗口底部”两个区域。另外碰撞不只发生在管道上,碰到窗口上下边界同样要判负。

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用于铺大面积纯色背景,比drawRectsetBrush更高效;给管道描边用的setPen笔宽是2像素,白色描边是为了让管道轮廓在蓝天背景下更清晰,这是纯视觉优化,不影响碰撞区域;画结束遮罩时用的QColor(0, 0, 0, 100)第四位是透明度参数,100是半透明级别,0为完全透明,255为不透明,这个值也可以调成80,让背景隐约可见的同时保持文字的辨识度。

绘制这块还有个QT绘图相关的性能点:不要在一帧里频繁创建QPenQBrush对象。我见过不少源码把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用的插件是windows,Linux桌面环境用的是xcb,而linuxfb是嵌入式开发板用的帧缓冲插件。程序找不到对应插件,说明Qt的platforms目录没有正确加载,或者环境变量QT_QPA_PLATFORM_PLATFORM_PATH指向了错误位置。

解决:在Linux桌面上运行时,显式指定平台为xcb,命令行执行./FlappyBird -platform xcb,或者设置环境变量export QT_QPA_PLATFORM=xcb后再运行。如果提示xcb也找不到,多半是libqt5gui5libxcb-*依赖没装全,执行sudo apt install libxcb-xinerama0 libxkbcommon-x11-0这类系统库补全即可。做课程设计的同学如果打算把程序在实验楼的Linux服务器上演示,务必提前在纯命令行环境测试,许多学校服务器只有ssh没有图形界面,程序根本跑不起来,提前准备截图或录屏是个好习惯。

5.3 画面闪烁严重:重绘时背景一明一暗

现象:游戏运行时画面持续闪烁,尤其在小鸟移动和管道移动的时候,整个窗口像是黑白帧在交替跳变,看几秒就眼睛酸。

原因:paintEvent里如果每次都先fillRect整个窗口再画图形,系统可能会在“清屏”和“重画”之间露出空档,产生闪烁。QT的QWidget在默认情况下有双缓冲机制,但如果你设置了Qt::WA_OpaquePaintEventQt::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项目时,你会发现当年踩过的编译器不匹配、平台插件缺失、定时器不精确这些坑,现在都能提前避开。希望这篇笔记能帮你少走一段弯路,把省下来的时间花在真正值得打磨的游戏手感上。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 21:53:30

Javaer转型Agent开发:Spring AI与LangChain4j学习路线及RAG实战

1. 从Java到Agent&#xff1a;一个老Javaer的转型路线图做了七八年Java后端&#xff0c;CRUD写了无数遍&#xff0c;Spring的源码翻来覆去看了好几轮&#xff0c;突然发现招聘JD上开始频繁出现“Agent开发”“大模型应用”“RAG”这些词。说实话&#xff0c;一开始我是有点抗拒…

作者头像 李华
网站建设 2026/9/24 21:52:24

基于MATLAB的电池SOC估算仿真平台:安时积分与EKF算法对比

做电池管理系统&#xff08;BMS&#xff09;相关开发的朋友应该都吃过SOC估算的亏。公式推导没什么问题&#xff0c;一到真实工况就露馅&#xff1a;电池换一组、温度变一下、电流毛刺多一点&#xff0c;误差就完全不受控制。以前我调试算法的时候&#xff0c;最烦的不是写代码…

作者头像 李华
网站建设 2026/9/24 21:52:04

Unity原生集成Claude Code:AI如何重构游戏开发工作流

1. 这不是“AI游戏”的泛泛而谈&#xff0c;而是工作流正在被重写 9月15日这天&#xff0c;游戏开发圈的晨会、站会、评审会里&#xff0c;突然多了几个高频词&#xff1a;Claude Code、Unity官方插件、AI编码Agent。不是某家创业公司又发了个Demo视频&#xff0c;也不是技术博…

作者头像 李华
网站建设 2026/9/24 21:51:59

WorkBuddy 实战指南:从安装配置到自动化工作流搭建与避坑

刷社区的时候&#xff0c;我几乎每周都能看到同一个问题&#xff1a;“大家都在用 WorkBuddy 做什么&#xff1f;”问的人多了&#xff0c;我干脆把自己这两年的上手经历、踩过的坑&#xff0c;再加上从身边朋友那看到的真实玩法整理了一遍。WorkBuddy 这类 AI 工作台&#xff…

作者头像 李华
网站建设 2026/9/24 21:51:01

从传话筒到流程节点:Agent、IM与OpenAPI协同架构实战

1. 从“传话筒”说起&#xff1a;AI 落地两年后最真实的困境“AI 用了两年&#xff0c;我们却成了它的传话筒&#xff1f;”这句话第一次看到的时候&#xff0c;我正在给一个客户做内部工具链的复盘。会议室里坐着业务、研发、运维三方&#xff0c;大家对着大屏上那张“AI 提效…

作者头像 李华
网站建设 2026/9/24 21:49:58

电脑无法启动的三大根源:供电、固件、加载阶段排查指南

1. 开机瞬间的“静音”不是故障&#xff0c;而是系统在向你发送求救信号电脑按下电源键后毫无反应——风扇不转、屏幕不亮、硬盘没声&#xff0c;连指示灯都懒得闪一下&#xff1b;或者能听到风扇嗡嗡转、硬盘咔哒响几下&#xff0c;但屏幕始终黑着&#xff0c;卡在Logo画面不动…

作者头像 李华