资讯动态

用Qt、C++和队列构建“别踩白块”游戏:从随机生成到定时器

发布时间:2026/10/1 4:11:45 来源:尧图企业网站定制
简介基于Linux、QT、C实现的“别踩白块儿”小游戏完整源码包面向有一定C与Qt基础的开发者可作为图形界面编程、定时器机制与游戏逻辑设计的入门练习项目。包内共52个文件主要包含34个png图片素材、6个cpp源文件、5个h头文件以及2个ui界面文件、pro工程配置和qrc资源文件等素材与代码分离便于按需查看界面资源与逻辑实现。项目实现了4x4棋盘随机黑块生成、30秒倒计时、得分与历史最高分记录使用qrand配合时间种子确保每次黑块位置随机同时通过工厂模式生成黑白块并用queue容器存储点击黑块后删除队头整列、追加新块并统一下移完整展现了游戏循环与状态更新。已有1182人学习下载适合用来理解Qt事件循环、随机数应用及常见设计模式在游戏开发中的落地。1. 别踩白块儿一套用 Qt、C 和队列把玩法闭环串起来的小游戏我刚拿到这套在 Linux 上用 Qt C 写的“别踩白块儿”小游戏资源时第一反应是这不就是一个点击反馈 demo 吗真上手拆完才发现qrand 随机黑块位置、工厂模式生产黑白块、queue 队列滚动刷新、100ms 定时器倒计时四条线拧在一起才是完整的游戏闭环。它特别适合三类人正在学 Qt 自定义控件和 QGraphics 场景的入门者需要交一个带完整界面的课程设计的学生以及在嵌入式 Linux 板子上想验证触摸屏交互的工程师。下面我按工程文件、核心逻辑、编译运行、踩坑记录一路拆下去最后给你几个能直接改着玩的进阶思路。2. 工程拆开看文件结构与 UI 资源在游戏里各自扮演什么角色2.1 先搞清楚谁在干活.pro、main.cpp 与三个核心窗口类整个压缩包解开之后第一眼是十几个 .cpp/.h外加 .ui 和一堆 png。新手最容易犯的错是把所有文件一股脑拖进某个空白工程结果编译报一堆未定义引用。正确顺序是先看 baikuaiGame.pro它是 qmake 的构建描述文件决定了哪些文件参与编译、链接哪些 Qt 模块。按这套资源的文件列表补全.pro 大概是这样一个形状QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets TEMPLATE app TARGET baikuaiGame SOURCES \ main.cpp \ startwidget.cpp \ gameview.cpp \ gamesetting.cpp \ block.cpp \ mybutton.cpp HEADERS \ startwidget.h \ gameview.h \ gamesetting.h \ block.h \ mybutton.h FORMS \ startwidget.ui \ gameview.ui RESOURCES \ images.qrc第一行QT core gui是 Qt 的基础模块第二行greaterThan(QT_MAJOR_VERSION, 4): QT widgets是为了兼容 Qt4 到 Qt5 的语法实际在 Qt5/Qt6 里 widgets 模块必须显式打开不然后面所有 QPushButton、QGraphicsScene 全是未定义类型。TEMPLATE app表示生成可执行程序TARGET决定二进制名字跑起来之后就叫 baikuaiGame。FORMS里只有 startwidget.ui 和 gameview.uigamesetting 没有 .ui说明它完全靠代码手写界面。main.cpp 的角色是程序入口它只做三件事创建 QApplication、显示 startwidget、进入事件循环。#include startwidget.h #include QApplication int main(int argc, char *argv[]) { QApplication app(argc, argv); startwidget w; w.show(); return app.exec(); }从 main 的写法可以看出游戏启动后的第一屏是 startwidget也就是封面菜单。等到玩家点击“开始”再由 startwidget 内部跳转到 gameview。gamesetting.cpp/h 从命名看是设置页配合资源里的商城、抽奖素材原本应该承担模式选择或商店功能。这里建议拿到工程后先编译一遍 main.cpp startwidget gameview 这条主线确定能跑起来再碰 gamesetting排查问题范围会小很多。提示压缩包里的 baikuaiGame.pro.user 是原作者机器的 Qt Creator 用户配置在你自己机器上直接使用通常会让编译器套件错乱。建议删掉它再用 Qt Creator 打开 .pro 重新生成。2.2 图片资源目录从素材命名反推游戏场景这套资源最直观的“含金量”在 images.qrc 和 resources 目录里的四十多张 png/jpg。它们不是随便堆的素材从命名能反推出界面结构。素材前缀/关键字对应场景用途推断cover_b0~cover_b2、menu_Text主菜单封面背景与游戏标题running_bg、img_bg_level_1/2游戏主场景关卡背景pause_b01~b42、b_paused、b_resume暂停弹窗暂停按钮与继续按钮message_b_chal1~4、message_b_over1/2、message_b_no1/2消息提示挑战成功/失败/弹窗背景challenge_time_bg、challenge_time_line挑战模式倒计时条 UImall_buy1/2、mall_close1/2、lottery_b_1设置/商店页商城购买与抽奖入口这里要提醒一句qrc 资源是在编译期被嵌入可执行文件的不是运行时按相对路径去读。只要 qrc 里的路径写对程序跑到任何机器上图片都能显示。反过来如果 qrc 里某个文件路径写错编译不会报错只有运行到那个界面才会出现空白这种故障定位起来很费时间。第 5 章我会把这类坑专门展开。2.3 自定义按钮与遮罩Qt 界面美化的最小实现资源文件里有 mybutton.cpp/h这是一个典型的自定义按钮类。项目里大量按钮都用 png 切图换肤而不是用 QSS 去写 border-radius因为小游戏界面讲究“按下有反馈”直接换图比 QSS 状态伪类更可控。下面给出 mybutton 的核心实现逻辑// mybutton.h #ifndef MYBUTTON_H #define MYBUTTON_H #include QPushButton class MyButton : public QPushButton { Q_OBJECT public: explicit MyButton(const QString normalPic, const QString pressedPic, QWidget *parent nullptr); protected: void mousePressEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; private: QString m_normalPic; QString m_pressedPic; }; #endif// mybutton.cpp #include mybutton.h #include QPixmap #include QIcon #include QMouseEvent MyButton::MyButton(const QString normalPic, const QString pressedPic, QWidget *parent) : QPushButton(parent), m_normalPic(normalPic), m_pressedPic(pressedPic) { QPixmap pix(normalPic); setFixedSize(pix.size()); // 按钮尺寸跟随图片 setIcon(QIcon(pix)); setIconSize(pix.size()); setFlat(true); // 去掉默认边框 } void MyButton::mousePressEvent(QMouseEvent *e) { if (e-button() Qt::LeftButton) { setIcon(QIcon(m_pressedPic)); // 按下换 pressed 图 } QPushButton::mousePressEvent(e); } void MyButton::mouseReleaseEvent(QMouseEvent *e) { if (e-button() Qt::LeftButton) { setIcon(QIcon(m_normalPic)); // 松开还原 } QPushButton::mouseReleaseEvent(e); }这个类的关键在于setFixedSize(pix.size())。如果不固定尺寸QPushButton 默认会按 QSS 的 sizeHint 撑开图片和按钮热区错位玩家点击时总觉得“没点到”。另一个容易翻车的地方是mousePressEvent里先切图再调用父类这样点击信号clicked仍然会正常发射不会因为提前 return 而丢失。3. 核心玩法是怎么转起来的随机生成、队列消费与 30 秒倒计时3.1 黑块位置的随机性qsrand 时间种子与取模运算的边界游戏的基础规则是 4×4 界面每一行只有一块黑块。黑块位置要产生在 0~3 这个范围内项目摘要里明确写了使用 qrand 函数并对 4 取余。最直接的实现是// 每次生成新一行时调用 qsrand(QTime::currentTime().msec() qrand()); int blackIndex qrand() % 4; // 0~3决定黑块落在第几列这里有两个细节值得展开。第一qsrand是设置随机种子QTime::currentTime().msec()取当前时间的毫秒部分保证不同时刻启动游戏得到的序列不同。第二后面追加一个qrand()是为了“搅乱种子”如果玩家手速非常快两次生成新行发生在同一毫秒内单纯用msec()做种子会让两次黑块位置一模一样体验很糟。加上一个旧的qrand()值等于把上一轮随机状态混入下一轮重复概率明显下降。顺着这个思路如果你拿到的是 Qt6 环境qrand/qsrand这套老 API 在 Qt6 里已经不再维护推荐直接用随机数引擎更省心#include QRandomGenerator int blackIndex QRandomGenerator::global()-bounded(4);bounded(4)返回 0~3 的整数内部线程安全不需要手动播种。对这套资源来说原工程用的是 qrand我不建议你为了“升级”而大改逻辑只要种子更新位置对qrand 完全够用。3.2 工厂模式 QQueue点击黑块时队列如何滚动刷新黑块和白块的生成用到了工厂模式存储结构是 std::queue 或 Qt 的 QQueue。项目摘要里的原话是“点击黑块时delete 队头的 4 个块并 pop 弹出再加入 4 个新块最后将队列中所有的块 Y 坐标增加”。这段话是整个游戏的核心我拆成三步看先删旧行、再补新行、最后整体下移。Block 类本身是 QGraphicsObject 的子类负责把自己的矩形画成黑色或者白色。工厂类负责决定“当前行的哪个位置是黑的”// block.h 核心结构 #include QGraphicsObject class Block : public QGraphicsObject { Q_OBJECT public: explicit Block(bool black, int x, int y, QGraphicsItem *parent nullptr); void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; bool isBlack() const { return m_black; } private: bool m_black; };// 简单工厂一行 4 个仅 blackIndex 位置是黑块 class BlockFactory { public: static QListBlock* createRow(int blackIndex) { QListBlock* row; for (int i 0; i 4; i) { bool isBlack (i blackIndex); row.append(new Block(isBlack, i, 0)); } return row; } };队列滚动部分长这样// 玩家点击了黑块 while (m_blockQueue.size() 4) { for (int i 0; i 4; i) { Block* old m_blockQueue.dequeue(); m_scene-removeItem(old); // 先摘除出场景 delete old; // 再释放内存 } int newBlackIndex getRandomBlackIndex(); QListBlock* newRow BlockFactory::createRow(newBlackIndex); for (Block* b : newRow) { m_blockQueue.enqueue(b); m_scene-addItem(b); } // 所有块整体下移一个步长 for (Block* b : m_blockQueue) { b-setY(b-y() m_dropStep); } }这里有几个顺序上的讲究必须先在队头把一整行移除再加入新行否则队列长度瞬间变成 8、12界面出现错位removeItem必须在delete之前否则场景里还挂着已释放对象的悬空指针下一次渲染直接崩溃整体下移放在最后是为了让新加入的顶行也参与移动视觉上看起来所有块都在往下滚而不是新行原地不动。队列容量的不变量是 4 的倍数初始几行后每次点击删除 4 个、新增 4 个画面始终保持 4×4 的密度。我在调试时会用qDebug() m_blockQueue.size()验证这个倍数关系一旦出现非 4 倍数说明删行或加行逻辑跑飞了。3.3 100ms 定时器与倒计时游戏结束条件的三个触发点倒计时是另一条独立于队列的线。摘要里写了初始时间 30 秒定时器每 100ms 发出一次信号刷新时间。用 QTimer 实现很直接m_leftTime 30.0; // 剩余秒数 m_timer new QTimer(this); m_timer-setInterval(100); // 100ms 触发一次 connect(m_timer, QTimer::timeout, this, []() { m_leftTime - 0.1; // 时间累减 QString text QString::number(m_leftTime, f, 1); ui-timeLabel-setText(text); if (m_leftTime 0.0) { m_timer-stop(); gameOver(); // 时间耗尽 } }); m_timer-start();setInterval(100)的单位是毫秒槽函数里每次减 0.1 秒。显示时用f, 1保留一位小数玩家能看到 29.9、29.8 这种连续变化而不是整秒跳变观感差别很大。要注意定时器不是精确定时器它依赖事件循环的调度如果界面卡顿可能 100ms 的周期会漂移。所以倒计时数值只做显示不拿它做精确碰撞判定。游戏结束的条件不止倒计时归零。常规玩法里还有两个触发点玩家点到白块直接结束黑块移出底部边界没被点击也结束。判断黑块出界的常见做法是在定时器刷新里检查队列中每个块的 Y 坐标如果某个块的 y 值超过场景可视高度就停表并弹提示。这个判断要放在下移之后否则会晚一帧反馈手感变肉。4. 在 Linux 上从编译到运行Qt 环境配置与 .pro 构建实战4.1 安装 Qt 开发环境不同发行版怎么选包这套工程是 Qt Widgets 项目不是 QML最吃紧的依赖其实就三个qtbase 开发库、qmake 工具、Qt Creator 编辑器。在 Debian/Ubuntu 系上我一般这样装sudo apt update sudo apt install qtbase5-dev qt5-qmake qtchooser qtcreator qmake -vqtbase5-dev提供 libQt5Widgets 和头文件qt5-qmake提供 qmake 本体qtchooser负责在多个 Qt 版本间切换。装完先跑qmake -v确认显示的版本是你期望的 Qt5。如果系统里同时存在 Qt5 和 Qt6Ubuntu 22.04 之后的新包可能默认把 qmake 指向 Qt6这时用qtchooser -qt5 -run qmake -v可以临时指定 Qt5。如果你在 Fedora/RHEL 系包名对应是qt5-qtbase-devel和qt5-qmake命令换成 dnf install。实在不想用系统包管理可以去 Qt 官网下开源版离线安装包。这里注意一点离线安装包解压完是一整个目录里面有独立的 qmake 和 libQt5 库。如果用自带 qmake 构建运行时一定要保证动态库搜索路径指向这个目录不然就会出现后面要讲的库版本混用翻车。4.2 从 .pro 到可执行文件Qt Creator 与命令行两条路最简单的方式是用 Qt Creator 直接打开 baikuaiGame.pro。打开后注意 Qt Creator 左下角的构建套件Kit要选对如果机器上同时装了 Qt5 和 Qt6选到 Qt6 编译大概率会报qrand 未声明之类的错。选好套件后直接点构建Output 窗口会依次执行 qmake、make、链接最后在 build 目录生成 baikuaiGame 可执行文件。如果你习惯命令行流程更透明cd baikuaiGame qmake baikuaiGame.pro make -j4 ./baikuaiGame第一行qmake读取 .pro 生成 Makefile第二行-j4表示 4 个编译任务并行机器核数少就写成-j2第三行是运行。这里有个血泪经验改了 .pro 文件比如新增了一个 .cpp之后不要直接 make一定要重新跑一遍 qmake让它重新生成 Makefile否则新加的文件不会被编译进去。如果遇到“make: Nothing to be done”但功能没更新十有八九是忘了重新 qmake。4.3 运行时检查清单ldd、file、QT_QPA_PLATFORM编译过了不代表能跑起来。Linux 下 Qt 程序的运行期问题集中在动态库搜索路径和平台插件两个方向。我的排查顺序是file baikuaiGame ldd baikuaiGame | grep Qtfile看可执行文件的架构和动态链接类型确认编译产物没被交叉编译搞坏。ldd列出所有依赖的 Qt 动态库重点看它们来自哪个目录。如果显示多个不同 Qt 版本路径或者提示某个 libQt5Core.so 找不到直接export LD_LIBRARY_PATH/path/to/qt5lib指过去。平台插件是另一个经典黑匣子。桌面 Linux 上 Qt 默认走 xcb 插件不带显示器的最小环境下要换成 linuxfb 或 offscreenexport QT_QPA_PLATFORMlinuxfb ./baikuaiGame这个环境变量告诉 Qt“我现在没有 X11/Wayland请用 linuxfb 插件直接写显存”。嵌入式触摸屏场景里这套小游戏改成 linuxfb 后能跑得很流畅代价是失去窗口管理画面全屏。第 5 章我会专门写如果没有对应插件时那个报错长什么样、怎么修。5. 避坑记录运行期最容易翻车的五个细节5.1 点击黑块崩溃队列删除与场景移除的顺序现象连点几个黑块后程序直接段错误退出Qt Creator 输出窗口提示“Segmentation fault”或触发断言。原因Block 对象已经 delete但 QGraphicsScene 里还存着指向它的指针或者场景已经把它移除但又被二次删除。最常见的是先 delete 再 removeItemdelete 之后内存已释放removeItem 操作的是悬空指针。解决严格按“从场景摘除 → 删除 → 指针置空”的顺序。Block* old m_blockQueue.dequeue(); m_scene-removeItem(old); // 1. 场景先忘记它 delete old; // 2. 再释放对象 old nullptr; // 3. 防止重复释放这个顺序没有玄学本质是 Qt 的 item 不能同时属于场景又属于裸内存。从那以后我每次动队列都先检查是否成对出现 removeItem 和 delete。5.2 黑块位置连续重复时间种子的更新时机不对现象玩两三行就出现同一列连续是黑块或者某一行有两个黑块。原因qsrand只在 startwidget 构造时调用一次后面每次生成新行都不重新播种。更隐蔽的是如果每次生成新行用的种子是QTime::currentTime().msec()而玩家手速极快两次点击发生在同一毫秒内种子相同得到的黑块位置自然相同。解决把种子更新逻辑收敛到一个函数里每次生成新行强制做一次混合。int GameView::randomBlackIndex() { qsrand(QTime::currentTime().msec() m_blockQueue.size() * 17); return qrand() % 4; }这里把队列长度混进种子纯粹是“破坏规律性”的土办法实际效果是即使同一毫秒内连续调用队列长度不同也会让种子变化。更干净的方案是用QRandomGenerator::global()-bounded(4)但在不动原逻辑的前提下种子更新时机比选哪个随机 API 更关键。5.3 Qt 库版本混用编译通过但运行报 incompatible现象编译全程无报错运行的一瞬间弹出fatal: cannot mix incompatible Qt library (version ex50601) with this library。原因qmake 生成的 Makefile 里写死的是某个 Qt 头文件路径而链接或运行时 LD_LIBRARY_PATH 指向了另一个版本的 Qt 库。ex50601 这种编码表示 Qt 5.6.1出现这个字符串说明编译头文件版本与运行库版本不匹配。解决先查 qmake 和库的真实路径再统一环境。qmake -v which qmake ldd baikuaiGame | grep Qt如果 qmake 指向/usr/bin/qmake但库却来自/opt/Qt/5.15.2/gcc_64/lib卸载重装不如直接指定工具链。我一般在 Qt Creator 里重新选择一次构建套件让编译器、qmake、库三者来自同一个 Qt 前缀然后make clean make全量重编。5.4 linuxfb 平台插件找不到无桌面环境下跑游戏的必踩坑现象嵌入式板子或纯命令行环境运行报qt.qpa.plugin: could not find the qt platform plugin linuxfb然后退出。原因安装 Qt 时没有附带 linuxfb 平台插件或者插件路径没有被 Qt 找到。linuxfb 是 Qt 为嵌入式 Linux 提供的帧缓冲插件必须显式安装并指定才生效。解决确认插件文件存在并设置平台插件搜索路径。export QT_QPA_PLATFORMlinuxfb export QT_QPA_PLATFORM_PLUGIN_PATH$(qmake -query QT_INSTALL_PLUGINS) ./baikuaiGame$(qmake -query QT_INSTALL_PLUGINS)会打印 Qt 安装目录下的 plugins 路径Qt 运行时从这个路径加载 libqlinuxfb.so。如果该文件不存在说明你的 Qt 发行版没带嵌入式插件需要补装对应包或换完整版安装包。5.5 图片资源加载空白qrc 路径与目录大小写问题现象程序能跑但按钮、背景全透明只有文字还在。原因qrc 文件里写的路径与 resources 目录实际文件不匹配大小写、目录层级错一个都加载不到。qrc 在编译期只登记文件名不校验图片能否被QPixmap读取所以问题延迟到运行期才暴露。解决改 images.qrc 里的路径写法让 alias 指向正确资源。qresource prefix/images file aliasrunning_bgresources/running_bg.png/file file aliaspause_b01resources/pause_b01.png/file /qresource代码里加载就用QPixmap(:/images/running_bg)。这里最容易错的是qrc 里resources/running_bg.png是相对 qrc 文件所在目录的路径如果 qrc 在工程根目录那资源文件确实应该在resources/子目录里。改完记得重新跑一次 qmake资源文件列表变了不会自动触发重编。6. 改造成自己的版本最高分存档、难度曲线与调试验证6.1 用 QSettings 把历史最高分落盘原工程已经有历史最高分显示但重启游戏之后分数还在不在取决于有没有做持久化。我习惯用 QSettings 存比手写文件简单且跨平台安全。#include QSettings QSettings settings(BaiKuaiGame, ScoreHistory); int best settings.value(history/best, 0).toInt(); if (currentScore best) { settings.setValue(history/best, currentScore); }Linux 下这个配置默认落在~/.config/BaiKuaiGame/ScoreHistory.conf第一次读不存在的键时返回默认值 0。如果做成课程设计想直接打包带走可以改成当前目录的 Ini 文件QSettings settings(./score.ini, QSettings::IniFormat);这样工程目录下会生成一个 score.ini拷给老师看演示时分数记录也跟着走不会因为换机器而丢。6.2 按得分动态加速下落步长与得分绑定原版固定 30 秒速度恒定玩到后面容易腻。把难度曲线做进去的核心不是缩短倒计时而是加快黑块下落速度。定时器每 100ms 触发一次我不建议在游戏运行中反复改setInterval那样会打乱刷新节奏。更平顺的做法是增大每次下移的像素步长int level currentScore / 20; // 每 20 分升一档 int step 10 level * 3; // 初始 10px每档加 3px m_dropStep qMin(step, 40); // 封顶 40px避免完全点不到分数越高黑块滚得越快但封顶防止难度无限增长。配合倒计时 30 秒不变玩家会在固定时间里挑战更高分最高分存档的意义才真正体现出来。6.3 验证玩法逻辑加日志与断言检查队列状态改造完最怕逻辑回归。我在每次点击黑块的槽函数里加两行调试输出能快速确认队列长度和黑块位置是否符合预期qDebug() queue size: m_blockQueue.size() blackIndex: blackIndex; Q_ASSERT(m_blockQueue.size() % 4 0); Q_ASSERT(blackIndex 0 blackIndex 3);Q_ASSERT在 Debug 构建下生效条件不满足会直接中断并打印源码位置。Release 构建会剥离断言但不影响相信它——断言帮我把“队列不是 4 的倍数”这类逻辑错误在开发期掐死在摇篮里。以前我也写过一版没有先 removeItem 就 delete 的结果被段错误教育了一晚上。从那以后凡是动 scene 里的 item我都强制走一遍“先从场景摘除再 delete最后置空指针”的顺序凡是改队列长度都让Q_ASSERT(size % 4 0)守在旁边。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑