简介:这是一份基于QT框架与C++语言实现的经典俄罗斯方块游戏源代码,面向希望深入理解QT应用开发与游戏编程的初学者及有经验的开发者。项目通过QGraphicsView与QGraphicsItem绘制游戏区域、方块和网格线,借助QTimer控制下落节奏,并利用信号与槽机制处理旋转、移动等用户输入,完整呈现了面向对象设计、事件驱动编程与碰撞检测、得分计算等核心逻辑。压缩包共12个文件,包含4个cpp实现文件、3个h头文件、1个pro工程文件,以及png、jpg截图和user配置,整体约58KB,结构清晰,便于按模块阅读与编译运行。目前已有465人学习下载。通过研读这份代码,读者可以掌握QT图形界面组件的实际用法、C++类与模块化设计思路,以及游戏主循环、随机方块生成、行消除判定等关键实现,是提升QT开发与游戏编程能力的实用参考。
1. 拿到 QT C++俄罗斯方块源代码.zip 之后:先别急着点运行
很多人下载完 QT C++俄罗斯方块源代码.zip,第一反应是双击 .pro 文件,然后被一堆报错劝退。我见过太多人卡在这一步:Qt 版本不匹配、编译器没配、中文路径乱码,最后得出“这代码跑不起来”的结论。其实问题不在代码,在于你没先搞清楚这份源码的工程结构和技术栈。俄罗斯方块这个项目,麻雀虽小五脏俱全,它涉及 Qt 事件循环、QPainter 绘图、定时器驱动、二维数组碰撞检测,是一个非常适合拿来练手 Qt 桌面开发的完整案例。这篇文章面向两类人:一是刚学完 C++ 基础、想找个能跑起来的小游戏项目练手的;二是已经会写 Qt 界面、但没做过游戏循环、想看看别人怎么组织代码的。我会从工程导入、编译配置、核心模块拆解、参数调优到常见报错排查,一步步带你把这套源码跑通、改懂、用起来。
2. 把源码跑起来:Qt 版本选择与工程导入的完整路径
2.1 Qt 5.15.2 还是 Qt 6?先看源码里的信号槽写法
拿到一份 Qt C++ 源码,第一件事不是打开 Qt Creator,而是用文本编辑器扫一眼 .pro 文件和头文件里的 include。这一步决定了你该装哪个版本的 Qt。常见做法是:如果源码里出现#include <QtGui/QApplication>或者QApplication::setGraphicsSystem,那基本是 Qt 4 时代的产物,直接上 Qt 5.15.2 最稳。如果看到QRegularExpression、QRandomGenerator这类类名,说明至少是 Qt 5.10 以后的代码,Qt 5.15.2 或 Qt 6.x 都能编。
Qt 5.15.2 是目前存量项目兼容性最好的 LTS 版本,网上大量俄罗斯方块源码都是基于它写的。Qt 6 虽然新,但部分模块的 API 有破坏性变更,比如QPainter的高 DPI 缩放默认行为变了,QTextCodec被移到了核心外。如果你不确定,先装 Qt 5.15.2,跑通之后再考虑迁移。
安装时有个坑:Qt 5.15.2 的在线安装器默认只勾选最新版 Qt,你需要手动展开 “Archive” 节点,找到 5.15.2 并勾选对应的编译器套件。Windows 下推荐勾选 MinGW 8.1.0 64-bit 和 MSVC 2019 64-bit 两套,MinGW 开箱即用,MSVC 适合后续发布。
2.2 用 Qt Creator 导入 .pro 并配置构建套件
假设你已经装好了 Qt 5.15.2 和 Qt Creator,接下来按步骤操作:
# 先解压到一个纯英文、无空格的路径下 # 比如 D:/projects/tetris_qt # 不要放在 C:/Users/你的中文名/Desktop/ 这种路径下解压后找到 .pro 文件,用 Qt Creator 打开。Qt Creator 会提示你选择构建套件(Kit),这里选 Desktop Qt 5.15.2 MinGW 64-bit。如果列表是空的,说明安装时没勾选编译器,回到 Qt 维护工具里补装。
打开 .pro 文件后,检查几行关键配置:
QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = Tetris TEMPLATE = app SOURCES += main.cpp mainwindow.cpp tetrisboard.cpp HEADERS += mainwindow.h tetrisboard.hQT += widgets这一行必须有,否则QMainWindow、QWidget都找不到。如果源码里用了多媒体或网络模块,还会看到QT += multimedia或QT += network,对应模块没装的话编译会报 “Unknown module”。
配置好之后点左下角的锤子图标构建,再点绿色三角运行。如果一切正常,你会看到一个窗口弹出来,方块开始往下掉。
2.3 编译报错的三个高频原因和对应修法
第一个高频报错是fatal: cannot mix incompatible Qt library (version 0x50601) with this library。这个错误的意思是:你链接的 Qt 库版本和编译时用的头文件版本不一致。常见于系统里装了多个 Qt 版本,或者 PATH 环境变量指向了另一个 Qt 的 bin 目录。解决办法是清理构建目录,在 Qt Creator 的 “项目” 设置里确认 Kit 的 Qt 版本,然后重新构建。
第二个是qt.qpa.plugin: could not find the Qt platform plugin "windows"。这通常发生在你直接双击 exe 运行时,因为 exe 找不到 Qt 的插件目录。开发阶段在 Qt Creator 里运行不会遇到,发布时需要把platforms/qwindows.dll一起拷过去。
第三个是中文乱码。Qt 5 默认用 UTF-8 处理源码,但如果你的源文件是 GBK 编码,字符串会显示成乱码。在 .pro 里加一行QMAKE_CXXFLAGS += -finput-charset=GBK -fexec-charset=UTF-8可以强制转换,但更推荐用 Qt Creator 把文件编码统一改成 UTF-8。
提示:每次改完 .pro 文件,记得先执行 “构建 → 执行 qmake”,再重新构建,否则新配置不生效。
3. 拆开看骨架:俄罗斯方块源码里真正值得读的四个模块
3.1 用 QTimer 驱动游戏循环:为什么不是 while(1)
很多从 C 语言转过来的开发者,第一反应是用while(1)加sleep来做游戏循环。这在 Qt 里是灾难性的,因为while(1)会阻塞事件循环,界面直接卡死,按钮点不动,窗口拖不了。Qt 的正确做法是用QTimer。
俄罗斯方块源码里通常有一个QTimer成员变量,在构造函数里设置间隔:
// 在 TetrisBoard 构造函数中 timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &TetrisBoard::onTick); timer->start(500); // 500 毫秒下落一格onTick()槽函数里做三件事:尝试把当前方块下移一格、检测碰撞、如果碰撞就固定方块并生成新方块。QTimer的好处是它运行在 Qt 事件循环里,界面保持响应,你还可以随时timer->stop()暂停游戏。
参数怎么调:start(500)里的 500 是毫秒数,越小方块掉得越快。一般初级难度设 800,中级 500,高级 200。如果你想做随分数加速,可以在每次消行后动态调整:timer->setInterval(qMax(100, timer->interval() - 20))。
3.2 QPainter 绘制方块:坐标计算与双缓冲
俄罗斯方块的绘制全部在paintEvent里完成。源码里一般会定义一个常量BLOCK_SIZE,比如 30 像素,然后每个方块用QRect表示:
void TetrisBoard::paintEvent(QPaintEvent *event) { QPainter painter(this); // 绘制已固定的方块 for (int row = 0; row < ROWS; ++row) { for (int col = 0; col < COLS; ++col) { if (board[row][col] != 0) { QRect rect(col * BLOCK_SIZE, row * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); painter.fillRect(rect, QColor(0, 160, 233)); painter.setPen(Qt::black); painter.drawRect(rect); } } } // 绘制当前下落方块,逻辑类似 }这里的关键是坐标映射:col * BLOCK_SIZE是 x 坐标,row * BLOCK_SIZE是 y 坐标。board是一个二维数组,0 表示空,非 0 表示已固定方块的颜色索引。
Qt 的paintEvent本身有双缓冲机制,不需要你手动开。但如果你在绘制里做了大量计算,可以先把内容画到QPixmap再一次性贴上去,减少闪烁。俄罗斯方块这种规模,直接画完全够用。
3.3 二维数组碰撞检测:边界判断和方块旋转的数学
碰撞检测是俄罗斯方块的核心逻辑。源码里通常有一个bool checkCollision()函数,判断当前方块的位置是否合法:
bool TetrisBoard::checkCollision(const Tetromino &piece, int newX, int newY) { for (int i = 0; i < 4; ++i) { for (int j = 0; j < 4; ++j) { if (piece.shape[i][j] == 0) continue; int boardX = newX + j; int boardY = newY + i; // 左右边界和下边界 if (boardX < 0 || boardX >= COLS || boardY >= ROWS) return true; // 与已固定方块重叠 if (boardY >= 0 && board[boardY][boardX] != 0) return true; } } return false; }piece.shape是一个 4x4 的矩阵,表示当前方块的形状。旋转就是把这个矩阵顺时针转 90 度:newShape[i][j] = shape[3-j][i]。旋转之后要立刻调一次checkCollision,如果碰撞就回退旋转,这就是所谓的 “墙踢” 简化版。
参数注意:newY可能为负数,因为方块刚生成时有一部分在屏幕上方。所以判断boardY >= 0才能访问board数组,否则越界。
3.4 信号槽解耦:分数更新和下一块预览怎么联动
一份结构清晰的俄罗斯方块源码,不会把所有逻辑塞进一个类。常见做法是TetrisBoard负责游戏逻辑,MainWindow负责界面布局,两者通过信号槽通信:
// TetrisBoard 中定义信号 signals: void scoreChanged(int newScore); void nextPieceChanged(const Tetromino &piece); // MainWindow 中连接 connect(board, &TetrisBoard::scoreChanged, this, &MainWindow::updateScoreLabel); connect(board, &TetrisBoard::nextPieceChanged, this, &MainWindow::updateNextPreview);这样TetrisBoard不需要知道QLabel的存在,MainWindow也不需要知道分数是怎么算出来的。你后续想加一个 “等级” 显示,只需要在TetrisBoard里多发一个信号,MainWindow里多连一个槽,互不干扰。
注意:自定义信号如果传了自定义类型(比如
Tetromino),需要用qRegisterMetaType<Tetromino>()注册,否则跨线程连接会失败。单线程下不注册也能跑,但建议养成习惯。
4. 改出你自己的版本:参数调优与功能扩展的实操点
4.1 调整下落速度、消行得分和难度曲线
跑通默认版本之后,最值得改的就是游戏参数。这些参数通常散落在几个文件里,我一般会先把它们集中到一个config.h:
// config.h #ifndef CONFIG_H #define CONFIG_H const int ROWS = 20; const int COLS = 10; const int BLOCK_SIZE = 30; const int INIT_INTERVAL = 800; // 初始下落间隔 ms const int MIN_INTERVAL = 100; // 最快下落间隔 const int SPEED_STEP = 30; // 每消一行减少的间隔 const int SCORE_PER_LINE = 100; // 每消一行的基础分 #endif然后在onTick和消行逻辑里引用这些常量。这样做的好处是:你想调难度,只改一个文件,不用满工程搜数字。
难度曲线的常见做法是:每消 10 行升一级,每级下落间隔减少SPEED_STEP,但不低于MIN_INTERVAL。得分可以用SCORE_PER_LINE * 当前等级,这样高级别消行收益更大,鼓励玩家撑下去。
4.2 加一个 “下一个方块” 预览面板
预览面板是提升游戏体验最明显的改动。实现思路:在TetrisBoard里维护一个nextPiece成员,每次生成新方块时,把nextPiece赋给当前方块,再随机生成一个新的nextPiece,然后发信号通知界面更新。
界面侧用一个自定义QWidget子类NextPreview,重写paintEvent,把 4x4 的方块形状画在一个小格子里:
void NextPreview::paintEvent(QPaintEvent *) { QPainter painter(this); painter.fillRect(rect(), Qt::black); if (piece.shape.empty()) return; int cell = qMin(width(), height()) / 4; for (int i = 0; i < 4; ++i) for (int j = 0; j < 4; ++j) if (piece.shape[i][j]) { QRect r(j * cell, i * cell, cell, cell); painter.fillRect(r, piece.color); painter.setPen(Qt::white); painter.drawRect(r); } }把这个 widget 放到主窗口右侧布局里,连接nextPieceChanged信号,调用update()触发重绘。
4.3 用 QSettings 做本地最高分记录
每次关掉游戏分数就没了,体验很差。用QSettings可以把最高分存到注册表(Windows)或 plist(macOS)里,代码量很少:
// 读取最高分 QSettings settings("MyCompany", "Tetris"); int highScore = settings.value("highScore", 0).toInt(); // 更新最高分 if (currentScore > highScore) { settings.setValue("highScore", currentScore); }在MainWindow里加一个QLabel显示最高分,游戏结束时比较并更新。QSettings的构造函数参数是组织名和应用名,随便填但要保持一致,否则读不到之前存的值。
4.4 键盘事件处理:为什么你的方向键没反应
俄罗斯方块需要响应方向键:左右移动、上旋转、下加速、空格直落。这些都在keyPressEvent里处理:
void TetrisBoard::keyPressEvent(QKeyEvent *event) { if (gameOver) return; switch (event->key()) { case Qt::Key_Left: if (!checkCollision(currentPiece, currentX - 1, currentY)) currentX--; break; case Qt::Key_Right: if (!checkCollision(currentPiece, currentX + 1, currentY)) currentX++; break; case Qt::Key_Up: rotatePiece(); break; case Qt::Key_Down: if (!checkCollision(currentPiece, currentX, currentY + 1)) currentY++; break; case Qt::Key_Space: while (!checkCollision(currentPiece, currentX, currentY + 1)) currentY++; break; } update(); }如果方向键没反应,检查两点:一是TetrisBoard是否调用了setFocusPolicy(Qt::StrongFocus),没有这行它收不到键盘事件;二是焦点是否被其他控件抢走了,比如按钮。可以在MainWindow里把焦点默认设给TetrisBoard。
提示:
keyPressEvent里改完状态一定要调update(),否则界面不会重绘,你会以为按键没生效。
5. 避坑与排查:跑俄罗斯方块源码时最容易翻车的五件事
5.1 现象:编译通过但运行闪退,没有任何提示
原因:最常见的是board二维数组越界访问。比如方块生成时currentY初始值设成了 0,但方块形状的第一行在 4x4 矩阵的第 0 行,实际绘制时boardY可能为负,访问board[-1][x]直接崩溃。另一个可能是QPainter在paintEvent外使用,Qt 会直接终止程序。
解决:在checkCollision里加if (boardY < 0) continue;跳过屏幕上方区域。用qDebug()在关键位置打印变量值,定位崩溃前最后执行的代码行。Qt Creator 的调试模式按 F5 启动,崩溃时会停在出错行。
5.2 现象:方块重叠在一起,或者消行后留下残影
原因:消行逻辑写错了。常见错误是检测到满行后,把该行清空但没有把上面的行下移,或者下移时循环方向搞反了,导致数据被覆盖。
解决:消行应该从下往上遍历,遇到满行就把该行以上的所有行整体下移一格:
for (int row = ROWS - 1; row >= 0; --row) { bool full = true; for (int col = 0; col < COLS; ++col) if (board[row][col] == 0) { full = false; break; } if (full) { for (int r = row; r > 0; --r) for (int c = 0; c < COLS; ++c) board[r][c] = board[r-1][c]; for (int c = 0; c < COLS; ++c) board[0][c] = 0; row++; // 重新检查当前行,因为上面下移了 } }5.3 现象:Qt Creator 提示 “Unknown module in QT: serialport”
原因:.pro 文件里写了QT += serialport,但安装 Qt 时没有勾选 Qt Serial Port 模块。俄罗斯方块源码一般不会用到串口,这多半是源码作者从别的项目复制 .pro 时没删干净。
解决:直接删掉那一行。如果确实需要串口,打开 Qt 维护工具,在对应版本下勾选 “Qt Serial Port” 重新安装。类似的还有QT += multimedia、QT += sql,用不到就删。
5.4 现象:发布 exe 到别人电脑上打不开,报缺 DLL
原因:Qt 程序动态链接了 Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll 以及平台插件,直接拷 exe 过去肯定缺库。
解决:用 Qt 自带的windeployqt工具自动拷贝依赖:
# 在 Qt 命令行工具里执行 windeployqt.exe D:/release/Tetris.exe --release它会把需要的 DLL 和platforms/qwindows.dll一起拷到 exe 同目录。如果还缺 MinGW 的运行库(libgcc_s_seh-1.dll、libstdc++-6.dll、libwinpthread-1.dll),从 Qt 安装目录的Tools/mingw810_64/bin下拷过来。
5.5 现象:改了源码里的常量,重新编译后没生效
原因:Qt 的构建系统有缓存,改了 .pro 或头文件里的常量,qmake 没有重新生成 Makefile。
解决:在 Qt Creator 里执行 “构建 → 清理所有项目”,然后 “构建 → 执行 qmake”,再重新构建。如果还不行,手动删掉构建目录(通常是build-项目名-Desktop_Qt_5_15_2_MinGW_64_bit-Debug),重新打开 .pro。
6. 从能跑到好用:给俄罗斯方块加一个 “幽灵方块” 落点提示
跑通、改完参数、修完 bug 之后,如果你想让这个项目从 “练手” 变成 “拿得出手”,我建议加一个幽灵方块(Ghost Piece)功能。它会在当前方块正下方显示一个半透明的落点预览,让玩家提前知道方块会落在哪里。这个功能在商业俄罗斯方块里是标配,实现起来也不复杂,但能让你对 Qt 绘图和游戏逻辑的理解再深一层。
核心思路:在paintEvent里,先计算当前方块如果一直下落会停在哪个Y位置,然后在这个位置用半透明颜色画一遍方块形状。计算落点不需要真的移动方块,只需要循环调用checkCollision:
int TetrisBoard::getGhostY() const { int ghostY = currentY; while (!checkCollision(currentPiece, currentX, ghostY + 1)) ghostY++; return ghostY; }然后在paintEvent里,在绘制当前方块之前,先画幽灵:
int ghostY = getGhostY(); if (ghostY != currentY) { QColor ghostColor = currentPiece.color; ghostColor.setAlpha(60); // 半透明 for (int i = 0; i < 4; ++i) for (int j = 0; j < 4; ++j) if (currentPiece.shape[i][j]) { QRect r((currentX + j) * BLOCK_SIZE, (ghostY + i) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); painter.fillRect(r, ghostColor); painter.setPen(QPen(Qt::white, 1, Qt::DashLine)); painter.drawRect(r); } }这里有几个细节值得注意。ghostColor.setAlpha(60)里的 60 是透明度,范围 0 到 255,60 大约相当于 24% 不透明度,既能看清又不抢眼。虚线边框用Qt::DashLine,和实心方块的实线边框区分开。getGhostY()是const函数,因为它不修改任何成员变量,只是查询。
性能方面完全不用担心。getGhostY()最多循环 20 次(棋盘高度),每次checkCollision遍历 4x4 矩阵,总共 320 次判断,对现代 CPU 来说可以忽略不计。paintEvent每秒触发几十次也没压力。
加完幽灵方块之后,你会发现游戏体验明显提升,尤其是快速下落时不用再靠猜。这个功能也是面试时展示 Qt 项目的一个好话题:你可以讲清楚怎么在不修改游戏状态的前提下做预测计算,怎么用QColor的 alpha 通道做半透明绘制,怎么用const正确性约束函数。
我自己写 Qt 游戏时有个习惯:每加一个新功能,先在纸上画出数据流,确认哪些是只读计算、哪些会改状态,然后再写代码。幽灵方块就是典型的只读计算,它不应该影响currentY,也不应该触发任何信号。这个习惯帮我省了很多调试时间,因为大部分 bug 都出在 “以为没改但实际改了” 的地方。
希望帮到你。
本文还有配套的精品资源,点击获取