ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Qt+C++实战:用QTimer与碰撞检测打造flappyBird课设项目

Qt+C++实战:用QTimer与碰撞检测打造flappyBird课设项目 简介一套基于Qt框架与C语言开发的FlappyBird游戏完整源码主要面向毕业设计、课程设计及个人项目开发也适合作为Qt界面编程的入门练习。代码结构紧凑包含游戏主循环、管道障碍、碰撞检测、计分等核心逻辑并配有可执行程序与工程文件便于直接运行和二次修改。资源包共27个文件以C源文件和头文件为主体辅以游戏素材图片、背景音乐与音效以及项目配置和资源定义文件其中图片供给界面渲染音频负责反馈与背景声exe文件可直接预览运行效果整体压缩包大小仅3.38MB。目前已有188人学习下载项目源码经过严格测试可在参考基础上放心扩展。对于希望从零完成课程项目或学习事件驱动开发的开发者这套样例提供了完整的实现思路与清晰目录结构性价比不错的实战素材。1. 基于QtC的flappyBird一个能跑、能改、能答辩的课设模板很多同学第一眼看到 flappyBird 会低估它——不就是一根管子一只鸟吗课设选它是不是显得工作量不够。真动手你会发现这个工程量卡得刚刚好QTimer 驱动的游戏循环、重力与速度积分、矩形碰撞检测、c随机数生成管道、键盘鼠标事件、QPainter 渲染再把状态机一拆Qt 日常开发里最常用那部分能力基本全摸了一遍。用 QtC 做 flappyBird不是做个玩具而是用最小成本把事件驱动 定时刷新 自定义绘制这条 GUI 开发主线走通。这篇按我自己做课设带项目的习惯把工程骨架、核心算法、常见坑和答辩改进点一次讲清楚照着敲完你能得到一个真正能玩、能改、能交的作品。2. 项目骨架怎么搭文件结构、类划分与Qt工程配置2.1 最小可运行工程从 new project 到第一个空白窗口打开 Qt Creator 新建工程时选 Qt Widgets Application 而不是 Qt Quick/QML原因很直接课设答辩看重的是 C 逻辑和 Qt 基础组件运用Widgets 方案代码直观老师追问每一行你都能说清楚。工程名我建议叫 FlappyBird路径不要带中文和空格后续编译省掉一堆编码和路径的坑。创建完工程后把自动生成的 MainWindow 删掉换成我们自己的 GameWidget。先看.pro文件这是 qmake 工程的灵魂QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET FlappyBird TEMPLATE app SOURCES \ main.cpp \ gamewidget.cpp \ bird.cpp \ pipe.cpp HEADERS \ gamewidget.h \ bird.h \ pipe.h这里有个非常容易翻车的点QT widgets必须写在greaterThan判断里因为 Qt5 之后 Widgets 从 core/gui 里拆出来了。如果你在 Qt 5.15 的工程里漏了这行编译会直接报cannot find -lQt5Widgets后面第 5 章会专门讲这类链接错误。TARGET是生成的可执行文件名TEMPLATE app表示这是一个应用程序而不是库。然后是入口文件main.cpp所有 Qt Widgets 程序的标准启动方式#include QApplication #include gamewidget.h int main(int argc, char *argv[]) { QApplication a(argc, argv); GameWidget w; w.setFixedSize(420, 640); // 固定窗口大小避免拉伸导致坐标错乱 w.show(); return a.exec(); // 进入事件循环所有定时器和信号槽在这里跑起来 }setFixedSize(420, 640)这一步不能省。flappyBird 的逻辑坐标全部按窗口尺寸设计一旦用户拉大窗口管道底部坐标、鸟的落地判定全都会对不上。固定尺寸是最省事的方案我一般把宽高定义成常量后面所有绘制和碰撞都引用这两个常量。2.2 类怎么拆一个窗口类 两个实体类不少课设把全部代码塞进一个 widget 的 paintEvent 里几百行挤在一起。那样能跑但答辩时老师问你的程序怎么组织的就不好答了。常见做法是按职责拆三个类GameWidget游戏主控持有定时器、状态机、对象列表负责事件分发和整体绘制Bird鸟的物理状态只管自己的坐标、速度、重力、跳跃Pipe管道对象只管自己的位置、尺寸、移动和计分标记。先看bird.h#ifndef BIRD_H #define BIRD_H #include QRectF class Bird { public: Bird(); void reset(); // 回到初始位置和速度 void flap(); // 跳跃给一个向上的初速度 void updatePhysics(double dt); // 每帧推进物理状态 QRectF rect() const; // 鸟的碰撞盒 double y() const { return m_y; } private: double m_y; // 垂直位置窗口坐标系向下为正 double m_vy; // 垂直速度像素/秒 double m_rotation; // 鸟的旋转角度仅用于绘制 static constexpr double kGravity 1200.0; // 重力加速度 static constexpr double kFlapSpeed -380.0; // 跳跃初速度负号表示向上 static constexpr double kSize 30.0; // 鸟的边长 }; #endif // BIRD_H2.3 游戏状态机Ready、Playing、GameOver 三态切换flappyBird 最容易被忽略的设计是状态机。没有状态机就会出现游戏结束了还能跳开始界面就在掉鸟这类逻辑混乱。我的做法是在 GameWidget 里定义一个枚举// gamewidget.h 内部 enum class GameState { Ready, Playing, GameOver };三个状态各司其职。Ready 时鸟悬停在固定高度按空格进入 PlayingPlaying 时定时器驱动物理和碰撞GameOver 时冻结画面按 R 重置。所有输入处理都先查状态再执行动作逻辑一清二楚。这个三态模型也是答辩时讲程序架构最好的切入点——比单纯贴代码讲得有层次。3. 核心玩法拆解重力、随机管道与碰撞判定的实现3.1 游戏循环QTimer 定时刷新而不是 QThread 死循环很多人一上来就想用 while(1) 循环写游戏在 Qt 里这是错误的做法。GUI 程序的主线程必须响应事件死循环会把界面卡死。正确方案是 QTimer// gamewidget.cpp 构造函数 m_timer.setInterval(16); // 约 60 FPS connect(m_timer, QTimer::timeout, this, GameWidget::onTick);16毫秒一轮 ticktimeout信号触发onTickonTick里更新物理、移动管道、检测碰撞最后调用update()触发重绘。Qt 的定时器本质还是事件循环的一部分不会阻塞界面。这里有一个性能误区要澄清60 FPS 不代表死磕 16ms 完成所有计算。这个游戏里每帧只做几十次矩形相交判断CPU 开销可以忽略。真正要小心的是不要在onTick里做字符串拼接、文件读写这类耗时操作否则帧率会肉眼可见地掉。3.2 重力与跳跃固定时间步长 dt 是手感的关键物理更新是游戏的核心也是很多课设翻车的地方。我见过有人写成每帧速度加 2结果在 60Hz 显示器上正常、在 120Hz 显示器上快一倍。要让帧率不影响手感必须用固定时间步长void Bird::updatePhysics(double dt) { // 速度积分v v g * dt m_vy kGravity * dt; // 位移积分y y v * dt m_y m_vy * dt; // 旋转角随速度方向变化只是视觉效果 m_rotation qBound(-30.0, m_vy * 0.06, 60.0); } void Bird::flap() { m_vy kFlapSpeed; // 直接把速度重置为向上初速度 }由于onTick的间隔是 16msdt 取1.0 / 60.0即可。为什么重力是 1200 而不是 10因为单位是像素/秒²设计上是「每秒速度增加 1200 像素/秒」。以 640 高的窗口为参考鸟从静止开始自由落体半秒就能落 150 像素这个强度配上 380 的跳跃初速度手感接近原版。关于 380 这个值有个判断方法跳跃初速度够不够看它能把鸟抬多高。忽略重力损耗理论抬升高度约v² / (2g) 380² / 2400 ≈ 60像素而管道开口高度我设为 150 像素。也就是说单次跳跃的净升量不到开口高度一半玩家需要连续点按才能过管难度适中。m_rotation qBound(-30.0, m_vy * 0.06, 60.0)这行是纯视觉。qBound把旋转角限制在 -30° 到 60° 之间速度向上时鸟抬头下落时鸟低头。这个细节对答辩时的演示效果很加分一眼看上去像个真游戏。3.3 管道生成用 c随机数控制间距与高度管道生成是游戏难度的调节器核心是两件事管道间隔多远出现一根、开口位置在哪里。间隔决定了节奏开口位置由随机数决定。Qt5 以后官方推荐用QRandomGenerator取代老的qsrand/qrand这正好是 c随机数 在 Qt 项目里的标准做法// gamewidget.cpp void GameWidget::spawnPipe() { // 开口中心在 [150, 420) 之间随机避开顶部和底部过近 int gapY QRandomGenerator::global()-bounded(150, 420); const int gapHeight 150; // 开口高度像素 double topBottom gapY - gapHeight / 2; double bottomTop gapY gapHeight / 2; Pipe p; p.x width(); // 从窗口右侧边缘进入 p.topRect QRectF(p.x, 0, p.pipeWidth, topBottom); p.bottomRect QRectF(p.x, bottomTop, p.pipeWidth, height() - bottomTop); m_pipes.append(p); }QRandomGenerator::global()-bounded(150, 420)返回 [150, 420) 区间的整数用全局单例不需要自己管理随机种子按一下跑一遍每次分布都不一样。管道生成的触发机制我不用定时器单独计时而是用一个计数器叠加在游戏循环里// onTick 内部 if (m_pipeSpawnCounter % 90 0) { spawnPipe(); } m_pipeSpawnCounter;90 帧也就是 90/60 1.5 秒生成一根管道。这个频率配合 180 像素/秒的移动速度相邻管道的水平间距大约 270 像素玩家有充足的反应时间。你可以把 90 改成 70 提高难度也可以把gapHeight从 150 改到 120这两个参数是课程设计里功能扩展最容易写进报告的点。3.4 碰撞判定矩形相交 边界判死碰撞判定直接决定游戏是否公平。flappyBird 里所有物体都是轴对齐矩形用QRectF::intersects就能完成检测不需要像素级碰撞。但有个视觉细节鸟画成圆形碰撞盒却默认是外接矩形四角会空撞。所以我实际用的碰撞盒比视觉图形每边缩 4 像素手感立刻变好// gamewidget.cpp bool GameWidget::checkCollision() const { // 画成圆的鸟碰撞盒向内缩 4 像素避免视觉上没碰到却判死 QRectF birdRect m_bird.rect().adjusted(4, 4, -4, -4); for (const Pipe p : m_pipes) { if (birdRect.intersects(p.topRect) || birdRect.intersects(p.bottomRect)) { return true; } } // 撞天花板或落地都判死 if (m_bird.y() 0 || m_bird.y() 30 height()) { return true; } return false; }adjusted(4, 4, -4, -4)是 QRectF 的内缩方法左、上各加 4右、下各减 4。同样地管道我也做了微小内缩避免鸟贴着管道边缘蹭过去却判死的情况。这类细节不会写进需求文档但对实际体验影响极大答辩时你主动讲出来老师会觉得你真的调过手感而不是照着抄的。碰撞判定放在onTick里、物理更新之后。注意检查边界的顺序先判物体相交再判窗口边界两者是或关系顺序不影响结果。4. 界面交互与渲染用 QPainter 画游戏画面并处理输入4.1 自定义绘制QPainter 从零画背景、管道和鸟界面做在 Qt Designer 里还是用代码绘制对于 flappyBird 这种画面完全动态的游戏我用代码绘制不用 qt designer 界面设计。理由很简单管道和鸟每一帧位置都在变Designer 只能管静态控件最终还是要靠 paintEvent 画。与其两套方案混用不如统一用 QPainter 搞定。// gamewidget.cpp void GameWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); // 天空背景纯色填充 painter.fillRect(rect(), QColor(110, 197, 233)); // 管道绿色矩形带上一点立体感 painter.setPen(Qt::NoPen); painter.setBrush(QColor(88, 176, 82)); for (const Pipe p : m_pipes) { painter.drawRect(p.topRect); painter.drawRect(p.bottomRect); } // 鸟黄色圆形 painter.setBrush(QColor(250, 200, 50)); painter.drawEllipse(m_bird.rect()); // 分数顶部居中 QFont font painter.font(); font.setPixelSize(32); font.setBold(true); painter.setFont(font); painter.setPen(QColor(255, 255, 255)); painter.drawText(rect(), Qt::AlignHCenter | Qt::AlignTop, QString::number(m_score)); // 状态提示 if (m_state GameState::Ready) { painter.drawText(rect(), Qt::AlignCenter, tr(按空格或点击屏幕开始)); } else if (m_state GameState::GameOver) { painter.drawText(rect(), Qt::AlignCenter, tr(游戏结束按 R 重新开始)); } }paintEvent 是 Qt 的黑匣子入口——你只管往 painter 上画东西系统在合适时机调用它。画完不用保存任何像素数据下一帧位置变了再画一次就行。双缓冲由 Qt 内部自动处理不用手动做不会闪烁。绘制顺序很重要先背景再管道再鸟最后文字。如果把鸟画在背景之前就会被背景色盖住。这也是新手常见的鸟消失了问题的根源。4.2 输入处理keyPressEvent 与 mousePressEvent 双通道游戏交互要兼容两种输入键盘空格跳跃、鼠标点击跳跃。有的同学电脑用触控板点按比敲空格方便两者都支持是基本的体验设计。void GameWidget::keyPressEvent(QKeyEvent *e) { if (e-key() Qt::Key_Space) { handleJump(); } else if (e-key() Qt::Key_R m_state GameState::GameOver) { resetGame(); } } void GameWidget::mousePressEvent(QMouseEvent *) { handleJump(); } void GameWidget::handleJump() { if (m_state GameState::GameOver) { return; // 游戏结束只能按 R 重置避免误触 } if (m_state GameState::Ready) { m_state GameState::Playing; m_timer.start(); } m_bird.flap(); m_timer.start(); // 兜底确保定时器在运行 }注意两个细节。第一GameOver 状态下点击不触发重置只能用 R 键——这是刻意的防止玩家输了之后一顿乱点误入下一局。第二m_timer.start()是幂等的重复调用不会叠加计时所以可以在每次跳跃时都调一次做兜底。4.3 分数逻辑与游戏重置计分规则很直接管道超出鸟的水平位置且未计过分的算一分。难点在于每根管道只计一次分我用一个布尔标记解决void GameWidget::updateScore() { QRectF birdRect m_bird.rect(); for (Pipe p : m_pipes) { // 管道右缘已经到鸟的左边且这根还没计过分 if (!p.scored birdRect.x() p.x p.pipeWidth) { p.scored true; m_score; // 这里预留音效播放接口 } } }scored字段每次生成管道时初始化为 false管道移动出屏幕被删掉后随对象一起消失不会重复计分。重置函数负责把一切拉回起点void GameWidget::resetGame() { m_pipes.clear(); m_bird.reset(); m_score 0; m_pipeSpawnCounter 0; m_state GameState::Ready; m_timer.stop(); update(); }m_timer.stop()很关键——Ready 状态下不应有任何物理推进否则鸟会在原地掉下去。停掉定时器画面就冻结了等玩家按空格再启动。5. 课设季最常见的 5 个运行与编译坑现象、原因、解法这一章写的是我在课设季给别人调工程时反复见到的真坑全部是现实环境里会撞上的问题按现象 → 原因 → 解决的顺序记下来。5.1 链接阶段报 cannot find -lpublic 或 cannot find -lQt5Widgets现象编译到链接时突然失败报cannot find -lpublic或cannot find -lQt5Widgets前面源码编译都正常。原因.pro文件里QT widgets缺失或写法错误。Qt5 把 Widgets 模块独立出来后不声明这个模块链接器找不到对应的库。-lpublic这个诡异的名字常见于把QT core gui写成了残缺形式qmake 把变量解析出了奇怪的库名。解决打开.pro文件确认写法是QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets改完保存Qt Creator 会自动重新运行 qmake。如果没生效就手动构建 → 清理再重新构建。注意必须清理后再构建只点构建有时不会重新生成 Makefile。5.2 运行时报 qt.qpa.plugin: could not find the qt platform plugin linuxfb现象程序编译通过但在裸机环境或纯命令行 Linux 上双击运行直接崩溃报qt.qpa.plugin: could not find the qt platform plugin linuxfb。原因Qt 的 GUI 程序需要平台插件来对接显示系统。linuxfb 是嵌入式 Linux 下的帧缓冲插件桌面 Linux 上一般没有。程序没找到合适的平台插件于是尝试了默认值失败。解决在启动脚本里显式指定平台export QT_QPA_PLATFORMoffscreen ./FlappyBird-offscreenoffscreen平台不依赖真实显示器适合服务器上做冒烟测试。如果要在开发板上实际显示则需要交叉编译时带上 linuxfb 插件并把 plugins/platforms 目录部署到可执行文件旁边。排查这类问题有个通用命令qmake -v看当前用的哪个 Qt 版本再find / -name libqlinuxfb.so 2/dev/null确认插件在不在。5.3 cannot mix incompatible Qt library (version ex50601) with this library现象换了一台电脑编译运行启动立刻报cannot mix incompatible Qt library (version ex50601)版本号每次还不一样。原因典型的 Qt 版本混装。机器里既装了 Qt 5.15.2又装了 Qt 6.x或者同一个版本编译器和库文件不配套。程序运行时加载了 A 版本的动态库头文件却是 B 版本编译的直接在内存布局上对不上。解决检查环境变量。在终端执行which qmake和qmake -v确认当前 PATH 里指向的是不是你工程用的那套 Qt。Windows 下更隐蔽可能系统 PATH 里残留了某个 Qt 的 bin 目录而 Qt Creator 里用的是另一套。我一般把不需要的 Qt 彻底卸载只保留一个版本这种版本冲突从此再没复发过。这属于环境问题里的玄学类所有 Qt 从业者都经历过不是你代码的问题。5.4 tr() 包裹的中文在界面上显示成乱码现象代码里写的tr(开始游戏)窗口标题和提示文字在运行后变成乱码。原因源码文件编码和编译器默认编码不一致。Windows 上 MSVC 默认按本地代码页读源码如果你的源文件是 UTF-8 无 BOM中文就被拆坏了。Qt5 的tr()处理的是 UTF-8 字面量编译器读进来的字节已经错了后面怎么转都救不回来。解决按项目情况三选一。一是源文件全部存成 UTF-8 with BOM二是在.pro里加编译选项msvc: QMAKE_CXXFLAGS /utf-8三是不用tr()包裹中文直接用QStringLiteral(开始游戏)。我推荐第三种最省事且完全绕开编码转换链。顺便确认.pro文件本身没有中文字符和中文注释省得 qmake 自己也乱。5.5 高分屏下窗口偏移以及帧率不同导致手感不一致现象在 2K 或 4K 缩放的 Windows 笔记本上运行窗口位置偏移、绘制内容缩放后模糊换一台 144Hz 屏幕的电脑游戏明显变快。原因这是两个独立问题叠加。高分屏问题是 Qt5 在 Windows 上默认不走高 DPI 缩放逻辑坐标和物理像素对不上帧率问题是某些代码里用elapsed()实时算 dt物理更新本身没错但不同刷新率下跳跃时机的手感会漂。解决高分屏在main.cpp的QApplication实例化之前加一行QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);帧率问题反过来——不要用真实时间驱动物理固定在onTick里用1.0/60.0作为 dt这样无论屏幕刷新率多少游戏逻辑永远按 60 FPS 推进。这是游戏开发里的通用做法叫固定时间步长也是我在第 3 章强调手感的关键的原因。6. 从能玩到能交向课程设计与答辩看齐的三个改造点6.1 把散落的魔法数收进 GameConfig现在代码里1200、380、90、150这些数字散布在各处答辩时老师问难度怎么调你只能现翻代码。花十分钟建一个命名空间把参数收拢// config.h namespace GameConfig { constexpr double kGravity 1200.0; constexpr double kFlapSpeed -380.0; constexpr double kPipeSpeed 180.0; constexpr int kPipeGap 150; constexpr int kSpawnFrames 90; }所有类只引用GameConfig::xxx改一处全局生效。报告里写通过参数配置实现难度调节就落到了实处。6.2 加音效、最高分持久化和难度递增音效用QSoundEffect播放 WAV跳跃一声、得分一声、落地一声每段只需一两行代码。最高分用QSettings写入注册表或配置文件重开游戏不丢。难度递增最简单的是让kSpawnFrames随分数减少从 90 帧渐降到 60 帧每过 5 分升一档。这三个扩展点不大但足以让课设从能用变成有想法。6.3 打包与答辩追问的准备交付课设通常要一个能直接跑的程序。Windows 上在 Qt Creator 用 Release 构建然后打开命令行进入构建目录执行windeployqt FlappyBird.exe工具会自动把需要的 Qt DLL 和插件复制到同级目录整个文件夹拷贝到任何 Windows 机器都能跑。答辩前把这三个问题准备好为什么用 QTimer 而不是线程循环、碰撞检测的边界为什么要内缩、固定时间步长解决什么问题。能把这三句话说清楚这套 Qt 课设就算真正吃透了。做了这些年项目我养成的习惯是每个 demo 都留一个能听响的音效、一个能看变化的参数这样演示时不冷场老师也能看到你真的在调产品而不是跑通就收工。希望帮到你。本文还有配套的精品资源点击获取
返回列表