简介这份资源是面向 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 中每帧检测 QListQGraphicsItem * 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_castint(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状态显示提示文字按空格进入RunningGameOver停止所有定时器显示重开按钮。状态切换时记得重置小鸟位置、速度、分数和所有管道否则重开后管道还在原地小鸟一出来就撞。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后必须重新执行 qmakeQt 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()。注意删除项时要用removeItemdelete只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.findChildQGraphicsView *(); 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 代码量不大但每一行都值得你亲手敲一遍而不是复制粘贴。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?