Linux下用Qt+C++开发别踩白块儿的完整实践
2026/9/14 2:45:49 网站建设 项目流程

简介:这是一份基于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++ 内存管理(如QList<QRect>动态块管理)、Qt 对象树父子关系(this作为 parent 的隐式内存归属)、信号槽跨线程安全(主循环 vs 音效线程)等真实坑点。如果你刚配好 VSCode 的 C/C++ 插件、装完qtbase5-devlibqt5widgets5,又想避开 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 beatwhite

CMakeLists.txt必须显式声明 Qt 版本和组件,避免find_package(Qt5 REQUIRED)自动降级到 Qt6(当前多数发行版默认仍为 Qt5):

cmake_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-dev(Ubuntu/Debian)或dnf install qt5-qtbase-devel qt5-qtmultimedia-devel(Fedora)。不要用qtchooser -install qt5 /usr/lib/x86_64-linux-gnu/qt5/bin/qmake这类过时方案。

2.2 GameBoard 类:承载游戏逻辑与绘制的核心 QWidget

src/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) QList<Block*> m_blocks; // 当前屏幕上的所有块指针 int m_speed; // 块下落速度(px/frame),随分数递增 int m_score; // 实时得分 QSoundEffect *m_clickSound; QSoundEffect *m_failSound; }; #endif // GAMEBOARD_H

src/gameboard.cpppaintEvent是性能关键区,必须用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_H

src/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 输入响应:鼠标点击与空格键双模操作

mousePressEventkeyPressEvent共享同一套碰撞检测逻辑:

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))在多线程下的竞争问题。QSoundEffectQSound更适合高频短音效,且支持setVolume()精细调节。

4. 资源管理与跨平台适配:图标、音效、字体及 Qt5/Qt6 兼容策略

4.1 使用 Qt Resource System 打包二进制资源

创建resources.qrc文件统一管理 assets:

<RCC> <qresource prefix="/sounds"> <file>click.wav</file> <file>fail.wav</file> </qresource> <qresource prefix="/images"> <file>icon.png</file> </qresource> </RCC>

CMakeLists.txt中注册资源文件:

qt_add_resources(RESOURCES resources.qrc) target_sources(beatwhite PRIVATE ${RESOURCES} )

注意:.wav文件必须是 PCM 编码(16-bit, 44.1kHz),Linux 下可用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 SC(Ubuntu 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); #endif

5. 编译、调试与性能调优:Linux 下实测有效的 5 个关键技巧

5.1 编译命令链:从源码到可执行文件的一键流程

在项目根目录执行以下命令(以 Ubuntu 22.04 为例):

# 1. 创建构建目录并进入 mkdir build && cd build # 2. 配置 CMake(显式指定 Qt5 路径,避免 find_package 错误) cmake -DCMAKE_BUILD_TYPE=Release \ -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::drawRectQTimer::timerEvent占比。若drawRect超过 40%,说明块数量过多——此时应启用QGraphicsView替代QWidget绘图(但会增加架构复杂度,本项目暂不采用)。

5.3 内存泄漏检测:Valgrind 快速扫描

编译时加入调试符号,运行 Valgrind:

# 重新编译 Debug 版本 cmake -DCMAKE_BUILD_TYPE=Debug .. make clean && make # 检测内存泄漏(忽略 Qt 内部泄漏) valgrind --leak-check=full \ --show-leak-kinds=all \ --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*,确保无冗余依赖。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询