Linux下用Qt+C++开发别踩白块儿的完整实践
简介这是一份基于Linux平台、采用Qt框架与C开发的“别踩白块儿”经典节奏类小游戏完整源码工程面向C初学者及Qt跨平台开发学习者帮助掌握GUI编程、定时器机制、随机数生成、工厂模式设计与容器队列管理等核心实践技能。资源共52个文件包含6个CPP实现文件、5个H头文件、2个UI界面文件、1个PRO项目配置、1个QRC资源文件及34张PNG/UI图片素材整体压缩包仅736KB结构清晰便于编译运行与代码剖析。已有1182人学习下载提供从启动页、游戏主界面到挑战模式的完整功能链4×4动态黑块布局、30秒倒计时、实时得分与历史最高分记录并通过queue管理方块生命周期结合Y坐标偏移实现流畅下落效果是理解Qt事件驱动与游戏逻辑解耦的优质入门案例。1. 为什么在 Linux 上用 Qt C 写“别踩白块儿”不是炫技而是练透 GUI 开发基本功的硬核路径你可能见过 Python 的 PyGame 版“别踩白块儿”也刷过网页版的轻量实现但真正把鼠标事件响应、帧率控制、像素级绘制、资源生命周期管理全链路串起来的还是原生 C 配 Qt 在 Linux 桌面环境下的组合。这不是复古情怀——Ubuntu 22.04/Debian 12 默认搭载 Qt 5.15.2国产 Linux 发行版如统信 UOS、麒麟 V10 的应用商店里80% 的本地 GUI 工具链底层都依赖 Qt而“别踩白块儿”这个看似简单的节奏游戏恰恰覆盖了 Qt Widgets 模块中QPainter绘图、QTimer精确节拍、QKeyEvent/QMouseEvent多通道输入、QSoundEffect音效触发、QSettings本地配置持久化等 7 类高频 API。它不依赖第三方游戏引擎不引入复杂状态机却能暴露 C 内存管理如QListQRect动态块管理、Qt 对象树父子关系this作为 parent 的隐式内存归属、信号槽跨线程安全主循环 vs 音效线程等真实坑点。如果你刚配好 VSCode 的 C/C 插件、装完qtbase5-dev和libqt5widgets5又想避开 WebAssembly 或 Android NDK 的环境干扰这个项目就是你在 Linux 桌面端验证 Qt C 工程能力的第一块试金石。2. 从零构建 Qt Widgets 工程CMake 项目结构与核心类职责拆解2.1 创建可编译的最小 Qt C 项目骨架Linux 下 Qt 开发首选 CMake 而非 qmake因其对多版本 Qt 共存、交叉编译、VSCode 集成更友好。先建立标准目录结构mkdir -p beatwhite/{src,resources} cd beatwhiteCMakeLists.txt必须显式声明 Qt 版本和组件避免find_package(Qt5 REQUIRED)自动降级到 Qt6当前多数发行版默认仍为 Qt5cmake_minimum_required(VERSION 3.10) project(BeatWhite LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 显式指定 Qt5 路径防止系统存在 Qt6 时误链接 find_package(Qt5 REQUIRED COMPONENTS Core Widgets Gui Multimedia) add_executable(beatwhite src/main.cpp src/gameboard.cpp src/block.cpp src/scoremanager.cpp ) target_sources(beatwhite PRIVATE src/main.cpp src/gameboard.h src/gameboard.cpp src/block.h src/block.cpp src/scoremanager.h src/scoremanager.cpp ) target_link_libraries(beatwhite Qt5::Core Qt5::Widgets Qt5::Gui Qt5::Multimedia ) # 关键启用 AUTOMOC 处理信号槽元对象编译 set_target_properties(beatwhite PROPERTIES AUTOMOC ON AUTORCC ON )提示若find_package(Qt5 REQUIRED)报错找不到执行sudo apt install qtbase5-dev libqt5multimedia5-devUbuntu/Debian或dnf install qt5-qtbase-devel qt5-qtmultimedia-develFedora。不要用qtchooser -install qt5 /usr/lib/x86_64-linux-gnu/qt5/bin/qmake这类过时方案。2.2 GameBoard 类承载游戏逻辑与绘制的核心 QWidgetsrc/gameboard.h定义主画布类继承QWidget并声明关键成员#ifndef GAMEBOARD_H #define GAMEBOARD_H #include QWidget #include QTimer #include QList #include QRect #include QKeyEvent #include QPaintEvent #include QSoundEffect class Block; // 前向声明避免头文件循环依赖 class GameBoard : public QWidget { Q_OBJECT public: explicit GameBoard(QWidget *parent nullptr); ~GameBoard() override; protected: void paintEvent(QPaintEvent *event) override; void keyPressEvent(QKeyEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void resizeEvent(QResizeEvent *event) override; private slots: void onTimerTimeout(); // 帧更新槽函数 private: void initGame(); void spawnBlock(); void moveBlocks(); void checkCollision(int x, int y); // 鼠标点击坐标转块索引 void updateScore(int points); QTimer *m_timer; // 主游戏循环定时器60fps QListBlock* m_blocks; // 当前屏幕上的所有块指针 int m_speed; // 块下落速度px/frame随分数递增 int m_score; // 实时得分 QSoundEffect *m_clickSound; QSoundEffect *m_failSound; }; #endif // GAMEBOARD_Hsrc/gameboard.cpp中paintEvent是性能关键区必须用QPainter手动绘制而非QLabel堆叠void GameBoard::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(Qt::NoPen); // 绘制背景深灰渐变 QLinearGradient bgGradient(0, 0, 0, height()); bgGradient.setColorAt(0, QColor(30, 30, 40)); bgGradient.setColorAt(1, QColor(15, 15, 25)); painter.fillRect(rect(), bgGradient); // 绘制所有块 for (Block *block : m_blocks) { if (block-isVisible()) { painter.setBrush(block-isBlack() ? Qt::black : Qt::white); painter.drawRect(block-rect()); // 白块加浅灰边框增强对比 if (!block-isBlack()) { painter.setPen(QColor(200, 200, 200)); painter.drawRect(block-rect().adjusted(0, 0, -1, -1)); } } } // 绘制顶部提示栏 painter.setPen(Qt::white); painter.setFont(QFont(Sans, 12, QFont::Bold)); painter.drawText(10, 25, QString(SCORE: %1).arg(m_score)); }注意QPainter必须在paintEvent内创建且不能缓存为成员变量——Qt 的绘图设备绑定机制要求每次重绘都新建实例。setRenderHint(QPainter::Antialiasing)对圆角矩形无效本例用直角但开启后对后续可能添加的粒子特效有兼容性保障。2.3 Block 类封装单个方块的状态与行为src/block.h定义块的抽象基类为未来扩展彩色块预留#ifndef BLOCK_H #define BLOCK_H #include QRect #include QObject class Block : public QObject { Q_OBJECT public: enum Type { BLACK, WHITE }; explicit Block(const QRect rect, Type type, QObject *parent nullptr); ~Block() override; QRect rect() const { return m_rect; } bool isBlack() const { return m_type BLACK; } bool isVisible() const { return m_visible; } void setVisible(bool visible) { m_visible visible; } void setRect(const QRect rect) { m_rect rect; } // 下落一帧y speed void fall(int speed) { m_rect.translate(0, speed); } private: QRect m_rect; Type m_type; bool m_visible; }; #endif // BLOCK_Hsrc/block.cpp构造函数需确保矩形尺寸符合游戏规则4列×动态行数Block::Block(const QRect rect, Type type, QObject *parent) : QObject(parent), m_rect(rect), m_type(type), m_visible(true) { // 强制校验宽度必须为窗口宽度/4高度固定为60px Q_ASSERT(rect.width() 0 || rect.width() parentWidget()-width() / 4); Q_ASSERT(rect.height() 60); }3. 实现核心游戏循环60fps 帧同步、碰撞检测与音效反馈3.1 初始化定时器与游戏状态GameBoard构造函数中完成关键初始化GameBoard::GameBoard(QWidget *parent) : QWidget(parent), m_timer(new QTimer(this)), m_score(0), m_speed(5) { setFocusPolicy(Qt::StrongFocus); // 启用键盘焦点 setAttribute(Qt::WA_TranslucentBackground, false); // 禁用透明背景避免闪烁 setStyleSheet(background:transparent;); // 但允许子控件透明 // 配置 60fps 定时器1000ms / 60 ≈ 16.666ms m_timer-setInterval(16); connect(m_timer, QTimer::timeout, this, GameBoard::onTimerTimeout); // 加载音效资源路径需提前部署 m_clickSound new QSoundEffect(this); m_clickSound-setSource(QUrl::fromLocalFile(:/sounds/click.wav)); m_clickSound-setVolume(0.7); m_failSound new QSoundEffect(this); m_failSound-setSource(QUrl::fromLocalFile(:/sounds/fail.wav)); m_failSound-setVolume(0.9); initGame(); m_timer-start(); }initGame()方法生成初始 4 个白块底部一行void GameBoard::initGame() { m_blocks.clear(); const int blockWidth width() / 4; const int blockHeight 60; const int startY height() - blockHeight; for (int i 0; i 4; i) { QRect rect(i * blockWidth, startY, blockWidth, blockHeight); Block *block new Block(rect, Block::WHITE, this); m_blocks.append(block); } }3.2 帧更新逻辑移动、生成、销毁三步闭环onTimerTimeout()是游戏心跳必须严格按顺序执行void GameBoard::onTimerTimeout() { // 步骤1移动所有现存块 moveBlocks(); // 步骤2检查是否需要生成新行最上层块Y坐标 -60 if (!m_blocks.isEmpty() m_blocks.first()-rect().top() -60) { spawnBlock(); } // 步骤3清理移出屏幕的块Y height() auto it m_blocks.begin(); while (it ! m_blocks.end()) { if ((*it)-rect().top() height()) { delete *it; // Qt 对象树自动释放 it m_blocks.erase(it); } else { it; } } }moveBlocks()中需处理速度递增逻辑每 500 分提速void GameBoard::moveBlocks() { int newSpeed 5 (m_score / 500) * 2; // 每500分2px/frame if (newSpeed 25) newSpeed 25; // 封顶防失控 m_speed newSpeed; for (Block *block : m_blocks) { block-fall(m_speed); } }spawnBlock()实现随机生成一行4块中1黑3白void GameBoard::spawnBlock() { const int blockWidth width() / 4; const int blockHeight 60; const int startY -blockHeight; // 随机选一个列索引置黑块 int blackCol QRandomGenerator::global()-bounded(0, 4); for (int i 0; i 4; i) { QRect rect(i * blockWidth, startY, blockWidth, blockHeight); Block::Type type (i blackCol) ? Block::BLACK : Block::WHITE; Block *block new Block(rect, type, this); m_blocks.append(block); } }3.3 输入响应鼠标点击与空格键双模操作mousePressEvent和keyPressEvent共享同一套碰撞检测逻辑void GameBoard::mousePressEvent(QMouseEvent *event) { if (event-button() Qt::LeftButton) { checkCollision(event-x(), event-y()); } } void GameBoard::keyPressEvent(QKeyEvent *event) { if (event-key() Qt::Key_Space) { // 空格键触发中心区域点击模拟鼠标 checkCollision(width() / 2, height() / 2); } else { QWidget::keyPressEvent(event); // 其他键交由父类处理 } } void GameBoard::checkCollision(int x, int y) { // 从底向上遍历优先响应最上层可见块 for (int i m_blocks.size() - 1; i 0; --i) { Block *block m_blocks[i]; if (block-isVisible() block-rect().contains(x, y)) { if (block-isBlack()) { // 踩中黑块加分音效 updateScore(100); m_clickSound-play(); block-setVisible(false); } else { // 踩中白块失败 m_failSound-play(); // 重置游戏实际项目应弹窗确认 initGame(); m_score 0; m_speed 5; } break; // 响应首个命中块即退出 } } }提示QRandomGenerator::global()替代已废弃的qrand()避免srand(time(nullptr))在多线程下的竞争问题。QSoundEffect比QSound更适合高频短音效且支持setVolume()精细调节。4. 资源管理与跨平台适配图标、音效、字体及 Qt5/Qt6 兼容策略4.1 使用 Qt Resource System 打包二进制资源创建resources.qrc文件统一管理 assetsRCC qresource prefix/sounds fileclick.wav/file filefail.wav/file /qresource qresource prefix/images fileicon.png/file /qresource /RCC在CMakeLists.txt中注册资源文件qt_add_resources(RESOURCES resources.qrc) target_sources(beatwhite PRIVATE ${RESOURCES} )注意.wav文件必须是 PCM 编码16-bit, 44.1kHzLinux 下可用sox转换sox input.mp3 -r 44100 -b 16 -c 1 click.wav。避免使用.ogg格式——QSoundEffect在 Qt5 中对 Ogg 支持不稳定。4.2 字体与 DPI 适配应对高分屏与国产 Linux 发行版在main.cpp中注入全局字体策略#include QApplication #include QFont #include QScreen int main(int argc, char *argv[]) { QApplication app(argc, argv); // 适配 HiDPI 屏幕UOS/麒麟常见 QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); // 设置全局字体优先使用文泉驿微米黑国产 Linux 预装 QFont font; font.setFamily(WenQuanYi Micro Hei); font.setPointSize(10); app.setFont(font); // 若系统无文泉驿则回退到 Noto Sans CJK SCUbuntu 22.04 默认 if (font.family() ! WenQuanYi Micro Hei) { font.setFamily(Noto Sans CJK SC); app.setFont(font); } GameBoard w; w.resize(800, 600); w.show(); return app.exec(); }4.3 Qt5 与 Qt6 兼容的条件编译方案当项目需同时支持 Qt5 和 Qt6如某些国产 OS 提供双版本 Qt时在头文件中做宏判断// src/gameboard.h #if QT_VERSION QT_VERSION_CHECK(6, 0, 0) #include QMediaPlayer #include QAudioOutput using SoundEffect QMediaPlayer; #else #include QSoundEffect using SoundEffect QSoundEffect; #endif对应gameboard.cpp中音效初始化改为#if QT_VERSION QT_VERSION_CHECK(6, 0, 0) m_clickSound new QMediaPlayer(this); auto *audioOutput new QAudioOutput(this); m_clickSound-setAudioOutput(audioOutput); m_clickSound-setSource(QUrl::fromLocalFile(:/sounds/click.wav)); audioOutput-setVolume(0.7); #else m_clickSound new QSoundEffect(this); m_clickSound-setSource(QUrl::fromLocalFile(:/sounds/click.wav)); m_clickSound-setVolume(0.7); #endif5. 编译、调试与性能调优Linux 下实测有效的 5 个关键技巧5.1 编译命令链从源码到可执行文件的一键流程在项目根目录执行以下命令以 Ubuntu 22.04 为例# 1. 创建构建目录并进入 mkdir build cd build # 2. 配置 CMake显式指定 Qt5 路径避免 find_package 错误 cmake -DCMAKE_BUILD_TYPERelease \ -DQt5_DIR/usr/lib/x86_64-linux-gnu/cmake/Qt5 \ .. # 3. 编译-j$(nproc) 启用全部 CPU 核心 make -j$(nproc) # 4. 运行无需安装直接执行 ./beatwhite提示若cmake报错Could not find a package configuration file provided by Qt5, 执行dpkg -L qtbase5-dev | grep cmake查找真实路径。常见位置为/usr/lib/x86_64-linux-gnu/cmake/Qt5或/usr/lib/cmake/Qt5。5.2 使用perf定位绘制性能瓶颈当游戏运行卡顿时用 Linux 内置工具分析# 记录 5 秒性能数据 perf record -g -p $(pgrep beatwhite) sleep 5 # 生成火焰图需安装 perf script 和 FlameGraph perf script | ~/FlameGraph/stackcollapse-perf.pl | ~/FlameGraph/flamegraph.pl flame.svg重点关注QPainter::drawRect和QTimer::timerEvent占比。若drawRect超过 40%说明块数量过多——此时应启用QGraphicsView替代QWidget绘图但会增加架构复杂度本项目暂不采用。5.3 内存泄漏检测Valgrind 快速扫描编译时加入调试符号运行 Valgrind# 重新编译 Debug 版本 cmake -DCMAKE_BUILD_TYPEDebug .. make clean make # 检测内存泄漏忽略 Qt 内部泄漏 valgrind --leak-checkfull \ --show-leak-kindsall \ --suppressions/usr/lib/x86_64-linux-gnu/valgrind/qt.supp \ ./beatwhite关键观察点definitely lost行数应为 0若出现still reachable检查Block对象是否被正确deleteQList::erase后必须delete指针。5.4 音效延迟优化设置 ALSA 后端低延迟在main.cpp中强制使用 ALSA比 PulseAudio 延迟更低// 在 QApplication 构造后、窗口显示前插入 qputenv(QT_QPA_PLATFORM, xcb); qputenv(QT_ALSA_DEVICE, default); qputenv(QT_QPA_GENERIC_PLUGINS, alsa);5.5 国产 Linux 发行版适配UOS/麒麟的特殊处理针对统信 UOS V20 和银河麒麟 V10需额外处理问题现象解决方案命令/代码窗口无法获取键盘焦点在GameBoard构造函数中添加setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint)setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);音效播放无声安装alsa-plugins-pulse并重启音频服务sudo apt install alsa-plugins-pulse sudo systemctl restart pulseaudio字体渲染模糊强制启用字幕平滑qputenv(QT_FONT_RENDERING_OVERRIDE, 1);最终可执行文件体积控制在 12MB 以内静态链接 Qt 会暴涨至 100MB故推荐动态链接。通过ldd ./beatwhite | grep Qt验证链接的 Qt 库路径是否指向系统/usr/lib/x86_64-linux-gnu/libQt5*确保无冗余依赖。本文还有配套的精品资源点击获取