ARTICLE DETAIL

资讯详情

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

Linux环境下用Qt与C++开发“别踩白块儿”小游戏

Linux环境下用Qt与C++开发“别踩白块儿”小游戏 简介基于Linux、Qt与C开发的“别踩白块儿”小游戏完整工程源码面向有一定C或Qt基础、希望将面向对象思想和常用容器应用到实际游戏项目中的学习者。项目使用工厂模式创建黑块与白块以queue容器保存方块序列每次生成行时调用带时间种子的qrand随机函数用对4取余的结果决定黑块所在列。游戏内置30秒倒计时、当前得分与历史最高分记录交互流程覆盖开始界面、游戏主视图和结束反馈。压缩包共52个文件主要包括6个cpp源文件、5个头文件、2个ui界面文件与1个pro工程文件另含34张png图片、2张jpg背景图和1个qrc资源文件整体仅736KB目录中图片素材与代码模块对应清晰便于打开工程直接阅读和二次开发。已有1182人学习下载。阅读源码可理解Qt定时器、信号槽、随机数生成、工厂模式及队列数据结构在游戏开发中的综合运用并可在现有框架上扩展难度、音效或排行榜。1. 在 Linux 上从零写一个“别踩白块儿”为什么是 QT C而不是 Web 或 Python如果你是个刚接触 Linux 的 C 学习者想找一个能同时练到“界面绘制、事件响应、定时器、数据结构”的小项目“基于 Linux、QT、C 的别踩白块儿小游戏”几乎是绕不开的选择。它的难度刚好卡在“入门之上、进阶未满”不需要处理复杂的网络和并发但足以让你把 QT 的事件循环、QPainter 绘图、QTimer 驱动动画这些核心机制全部摸一遍。更关键的是你最终拿到的不是黑窗口里的命令行程序而是一个带图形界面、能发布到别的 Linux 机器上跑起来的桌面应用——这对简历和实际开发手感都是实打实的加分项。我见过不少人拿 Python 的 pygame 或网页版 JS 实现类似游戏但坦白说在“运行环境受限的国产 Linux 桌面”上QT 的跨发行版兼容性和 C 的性能余量才是真实场景里最省心的组合。这篇文章我会从环境搭建、核心逻辑、绘图交互一路讲到发布打包和踩坑记录全程给可复现代码目的是让你在周末两天内把它跑起来并看懂每一行在干什么。2. 先把地基打牢Linux 下 QT C 环境搭建与最小项目跑通2.1 装对 Qt 版本apt 源安装还是官网下载在 Linux 上装 QT第一道坎是“选哪个版本、用什么方式装”。常见的做法有两种一是用发行版自带的软件源安装比如 Ubuntu 上的apt install qt5-default qtcreator二是从 QT 官网下载在线安装器选指定的套件安装。我的建议是如果你只是为了跑这个小游戏优先用软件源里的版本。因为软件源里的 Qt 已经针对当前发行版做完了编译匹配依赖装得少。官网安装器虽然版本新但容易和系统自带的库冲突特别是后面要在无桌面环境里跑的时候平台插件路径不对会直接让你看到qt.qpa.plugin: could not find the qt platform plugin linuxfb这类报错。下面是我实际用的安装命令sudo apt update sudo apt install qtbase5-dev qt5-qmake qtcreator g libgl1-mesa-devqtbase5-dev是核心开发包包含 Widgets、Gui、Core 这些基础模块qt5-qmake提供构建工具libgl1-mesa-dev是 OpenGL 相关的依赖QT 的窗口渲染会用得到。装完之后检查一下版本qmake --version g --version能看到输出里出现 QMake version 和 g 版本号说明工具链通了。这一步最常翻车的是qt5-default这个包在新版本 Ubuntu 里已经不存在所以直接装qtbase5-dev更稳妥。2.2 用 qmake 还是 CMake单文件小游戏我用 qmake关于构建系统很多人一上来就纠结。CMake 是行业主流但 QMake 对单个 .pro 工程的配置极其简单。这个项目规模不大我建议你用 qmake因为它能让你把精力放在 C 和 QT 的逻辑上而不是在find_package和target_link_libraries里消耗热情。工程文件whiteblock.pro长这样QT core gui widgets TARGET whiteblock TEMPLATE app SOURCES \ main.cpp \ gameboard.cpp HEADERS \ gameboard.hQT core gui widgets声明了依赖模块注意必须写widgets否则 QApplication 和 QWidget 相关类都找不到。TARGET指定生成的可执行文件名TEMPLATE app表明这是一个应用程序而不是库。加新文件时往 SOURCES 和 HEADERS 里追加就行qmake 会自动处理头文件依赖关系。2.3 跑通第一个最小窗口main.cpp 与空白画布现在写一个最简的main.cpp来验证整个链路#include QApplication #include QWidget int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget window; window.setWindowTitle(别踩白块儿 - 准备中); window.resize(400, 600); window.show(); return app.exec(); }这段代码做的只有三件事创建 QApplication 对象、创建主窗口并设置标题和尺寸、进入事件循环。app.exec()会阻塞在这里直到窗口被关闭。在终端里执行qmake make然后./whiteblock如果弹出一个 400x600 的空白窗口恭喜Qt 的最小运行链路已经打通。提示如果你用的是 Wayland 桌面可能还需要装qtwayland5插件否则 QT 默认走 xcb 会失败。这是环境差异导致的和代码无关。3. 写游戏核心逻辑方块生成、下落节奏与点击判定C 核心3.1 数据结构设计为什么不直接用 QList 存方块在“别踩白块儿”里核心数据是一个 4 列、无限纵向延伸的板子。每一行有且只有一个黑块位置随机。玩家要赶在黑块滚出屏幕之前把它点掉。常见的数据结构是二维数组或 QList但我建议把“一行的四个方块”封装成一个结构体代码可读性会好很多struct Row { int blackIndex; // 0~3 表示本行哪个是黑块 bool clicked; // 是否已被点击 bool passed; // 是否已滚动出可视区 };这里blackIndex用 int 而不是 bool是因为后面扩展难度时可能出现一行多个黑块或者长条黑块占两格的需求int 的可扩展性更好。整个游戏板就是一个QVectorRow每帧按速度向下移动视觉上就是白块往上走。3.2 方块下落逻辑QTimer 驱动与帧间隔控制游戏的核心循环由 QTimer 驱动。每触发一次定时器把所有方块的 y 坐标往下移一段距离然后触发重绘。关键参数有三个定时器间隔、每次移动的像素数、方块高度。我用的参数是const int COLUMN_COUNT 4; const int ROW_HEIGHT 150; // 每行高 150 像素 const int MOVE_SPEED 3; // 每帧下移 3 像素 const int TIMER_INTERVAL 16; // 约 60FPS定时器间隔设 16ms配合 3px 的位移下落速度就是 180px/s约 1.2 秒滚完一行。这个速度起步合适后面可以在游戏分数过 20 时把MOVE_SPEED提到 5。我见过有人把间隔设成 10ms 以下结果在低端嵌入式板子上 CPU 占用直接打满界面反而卡得更厉害得不偿失。定时器的实现在GameBoard类里void GameBoard::startGame() { m_timer new QTimer(this); m_timer-setInterval(TIMER_INTERVAL); connect(m_timer, QTimer::timeout, this, GameBoard::onTick); m_timer-start(); } void GameBoard::onTick() { for (int i m_rows.size() - 1; i 0; --i) { m_rows[i].y MOVE_SPEED; } generateNewRowIfNeeded(); removeRowsOutOfView(); update(); // 触发 paintEvent 重绘 }onTick里先移动所有行再补新行、删掉滚出底部的行最后调update()。注意不要直接调用repaint()update()会合并同一帧内的多次请求避免频繁刷新导致的闪烁。捕新行的条件很简单当前最上面一行已经进入可视区就在顶部补一行新的。3.3 点击判定黑块命中、白块误触与漏点漏判点击判定是整个游戏最容易写出 bug 的地方。我建议统一走“坐标换算”的方式而不是去比对控件。鼠标点击的坐标先相对窗口左上角换算成游戏区域的行号与列号再查这一行这一列是不是黑块void GameBoard::mousePressEvent(QMouseEvent *event) { if (m_gameOver) return; QPoint pos event-pos(); int col pos.x() * COLUMN_COUNT / width(); int rowIndex findRowIndexAtY(pos.y()); if (rowIndex 0) return; // 点到了顶部的空白过渡区 Row row m_rows[rowIndex]; if (col row.blackIndex) { row.clicked true; m_score; emit scoreChanged(m_score); } else { gameOver(); // 点到白块直接结束 } }findRowIndexAtY的职责是根据 y 坐标反推行号return (pos.y() - topOffset) / ROW_HEIGHT;。这里要特别注意topOffset可视区域顶部可能有一段留白不参与行号计算不然点到留白区域会被误判成第一行产生“明明没点到白块却 Game Over 的玄学”。漏点判断放在行滚出屏幕的时候void GameBoard::handleRowLeavingView(Row row) { if (!row.clicked !row.passed) { row.passed true; m_missedCount; if (m_missedCount 3) { gameOver(); } } }这里设计成漏掉 3 个黑块才结束而不是漏一个就结束因为摸过的节奏游戏都知道“手滑点空”是常态连续 3 次漏点才是真的跟不上。4. 用 QT 把画面画出来QPainter 绘图、鼠标事件与界面刷新4.1 为什么用 QPainter 绘制而不是堆 QLabel很多新手拿到这个项目会想到用 QGridLayout 加 QLabel 铺出一个 4xN 的棋盘然后改背景色模拟方块移动。这个思路在视觉上没问题但性能和开发效率都很差每次下落都要移动几十个控件、触发多次布局计算而且把逻辑和界面高度耦合后面想加动画或者变形改起来想死。更干净的做法是用 QPainter 在paintEvent里一次性把整个画面画出来。绘制逻辑非常直白void GameBoard::paintEvent(QPaintEvent *) { QPainter painter(this); painter.fillRect(rect(), Qt::white); for (const Row row : m_rows) { int x row.blackIndex * (width() / COLUMN_COUNT); int y row.y; int w width() / COLUMN_COUNT; int h ROW_HEIGHT; if (row.clicked) { painter.fillRect(x, y, w, h, QColor(80, 80, 80)); // 已点击的方块变灰 } else { painter.fillRect(x, y, w, h, Qt::black); } } }painter.fillRect是填色Qt::black就是黑块。已点击的方块置灰能给玩家“这格已经处理过了”的正反馈。注意绘制顺序因为白块是背景色只需要画黑块每行一个黑块整行铺白色背景视觉上就形成了黑块在白色画布上往下掉的效果。这里的 width() 是窗口实际宽度除以 4 得列宽窗口拉伸时方块会自动自适应非常方便。4.2 鼠标事件重写 mousePressEvent 与事件过滤器的取舍鼠标点击有两种接法。第一种是直接重写GameBoard::mousePressEvent适合事件处理逻辑比较单一的场景我上面的代码就是这么写的。第二种是安装事件过滤器installEventFilter适合你要同时监听多个子控件事件的场景。小游戏玩家只会在主窗口上点击没有子控件抢事件所以重写 坐标换算是最短路径。如果你后面想加“点击特效动画层”可以把这个动画层作为子控件然后在父窗口装事件过滤器统一转发那是后话。坐标换算有个容易忽略的坑event-pos()返回的是相对本控件的坐标如果你的游戏窗口内嵌在另一个更大的布局里记得用event-pos()而不是event-globalPos()。后者相对屏幕要手动减去窗口偏移多写一行代码就多一处出错的机会。4.3 计分、游戏状态与结束画面简单的状态机不用引什么设计模式直接用一个枚举管住游戏状态就够了enum GameState { Ready, Playing, Paused, Over };状态切换的时机很明确点击“开始”按钮从 Ready 到 Playing点击黑块继续 Playing点到白块或漏满 3 次切到 OverOver 界面显示最终分数和一个“再来一局”按钮。整个状态机在GameBoard里用switch维护每个状态只做自己的事void GameBoard::onTick() { switch (m_state) { case Ready: break; case Playing: moveRows(); generateRows(); checkMisses(); update(); break; case Over: m_timer-stop(); break; } }有人会问“暂停”怎么处理我通常不单独做暂停逻辑因为QTimer::stop()一停整个动画就冻结了效果和暂停一致不需要额外状态。唯一的区别是 Over 状态要停掉计时并且不再响应鼠标而 Paused 可能还要响应“继续”按钮所以用枚举区分是有必要的。5. 避坑指南Linux 下 QT C 小游戏最常见的 4 个翻车现场5.1 启动即报错qt.qpa.plugin: could not find the qt platform plugin linuxfb现象在纯命令行环境或者精简版 Linux 上执行./whiteblock终端直接输出qt.qpa.plugin: Could not find the Qt platform plugin linuxfb in 进程退出。原因QT 启动需要指定“平台插件”在桌面 Linux 上一般是 xcb 或 wayland在嵌入式环境里是 linuxfbLinux Framebuffer。你的系统里没装对应的插件包或者 QT 的插件搜索路径不对。解决先确认你是在桌面环境里跑桌面环境下设置export QT_QPA_PLATFORMxcb再启动。如果你确实要无桌面跑需要安装 framebuffer 插件sudo apt install libqt5gui5然后运行前export QT_QPA_PLATFORMlinuxfb。排查时优先用QT_DEBUG_PLUGINS1 ./whiteblock看详细日志它会打印出 QT 扫描了哪些路径、哪些插件加载失败、失败原因是什么。5.2 编译期报错cannot mix incompatible Qt library (version ex50601) with this library现象编译链接顺利通过运行到QApplication构造时崩溃报错内容是cannot mix incompatible Qt library (version ex50601) with this library。原因系统里同时存在两套 Qt 库比如/usr/lib/x86_64-linux-gnu/libQt5Core.so.5是一个版本你从官网装的 Qt 在/opt/Qt/5.15.2/gcc_64/lib是另一个版本程序加载时混用了。通常是.pro文件里手动加了INCLUDEPATH或LIBS指向了非系统路径导致的。解决卸载手动安装的套件或者检查qmake指向的是不是同一个版本which qmake qmake --version ldd ./whiteblock | grep Qt5ldd输出里如果出现两条不同路径的 Qt5 库就能确认是混用了。删掉.pro里多余的LIBS和INCLUDEPATH统一走QT core gui widgets声明式依赖让编译器自己找系统库。5.3 画面卡顿QTimer 间隔设置不合理导致的假死现象游戏运行几秒钟后画面明显掉帧点击响应滞后CPU 占用接近满核。原因我在前面提过定时器间隔低于 10ms 时QT 事件循环被频繁打断paintEvent反复触发而每帧绘制包含大量fillRect和容器遍历低配机器扛不住。另一个隐蔽原因是你在paintEvent里做了 new 操作比如动态创建 QColor 或 QBrush导致频繁分配内存。解决定时器间隔保持在 16ms 以上把 QColor 声明成 static 常量避免重复构造static const QColor CLICKED_COLOR(80, 80, 80);检查是否有泄漏可以用top命令看内存占用是否持续上涨。如果还卡就把MOVE_SPEED降下来用“加大步长 减少刷新频率”的组合代替“小步长高频刷新”。5.4 中文乱码或方块Linux 字体缺失与编码问题现象窗口标题、按钮文字、“分数100”里的中文全部显示成方块或问号。原因Linux 桌面环境没装中文字体或者保修用的字体库不在 QT 的字体搜索路径里。QT 默认从系统字体配置读取找不到对应字形就退化成豆腐块。解决先装字体sudo apt install fonts-wqy-microhei然后在代码里指定字体QFont font(WenQuanYi Micro Hei, 14); QApplication::setFont(font);这行要放在窗口创建之前。注意不同发行版字体的字族名可能不同查看系统已有字体可以用fc-list :langzh确认字族名后写进代码别硬编码“宋体”。6. 最后一章发布可执行文件与两个值得做的进阶改造游戏跑通之后下一个问题是怎么把它带给别人玩。Linux 上没有 Windows 那种绿色 exe 概念但可以打一个“免安装运行包”。最简单的方式是静态编译加动态库拷贝先编译一遍再用ldd找出所有 .so 依赖把它们拷到同一个目录写一个启动脚本。更省事的做法是用linuxdeployqt自动打包但会多引入一个工具链。我通常先用ldd手动排查一次因为能看到哪些库是你系统特有的哪些是通用的这对后续部署到别的发行版有帮助ldd ./whiteblock cp /usr/lib/x86_64-linux-gnu/libQt5Widgets.so.5 ./release/ cp /usr/lib/x86_64-linux-gnu/libQt5Gui.so.5 ./release/ cp /usr/lib/x86_64-linux-gnu/libQt5Core.so.5 ./release/静态编译需要系统安装对应模块的静态库配置上要加-static我很少在小游戏上做全静态因为体积会从 1MB 膨胀到 30MB 以上收益不大。进阶改造我建议优先做两件事性价比最高一是连击加分和速度自适应每 10 分加速一次加一个 Combo 字符串显示在顶部视觉效果非常明显二是音效用QSoundEffect播放一个极短的 click 音提升操作反馈感。这两个点都不需要重构现有结构各自几十行代码搞定。我在给同事演示这个项目时加完音效和速度变化后游戏的“手感”完全不是一回事这也是我留下的一条习惯——先做完整能玩的版本再加反馈层不要一开始就追求炫酷。希望帮到你。本文还有配套的精品资源点击获取
返回列表