简介:这份QT C++俄罗斯方块源代码面向希望入门QT框架或提升C++游戏开发能力的初学者与进阶开发者,通过一个完整可运行的经典游戏项目,帮助读者理解GUI绘制、事件驱动与面向对象设计在实际工程中的落地方式。压缩包共12个文件,约58KB,包含4个cpp实现文件、3个h头文件、1个pro工程文件,以及png、jpg截图与user配置,分别承担游戏逻辑、方块类定义、界面绘制与项目构建等职责,结构清晰便于按模块阅读。目前已有465人学习下载,适合作为课程设计或自学练手参考。代码中可重点研究QGraphicsView与QGraphicsItem绘制游戏区域、QTimer控制下落节奏、信号与槽处理旋转输入,以及碰撞检测、行消除与得分计算等核心逻辑,同时体会头文件与实现文件分离、qmake构建的工程组织方式,为后续独立开发小型QT应用打下基础。
1. 拆开这份 QT C++ 俄罗斯方块源码:它到底能帮你省下多少造轮子的时间
很多人第一次想用 QT 写点带界面的东西,脑子里蹦出来的练手项目就是俄罗斯方块。原因很直接:规则清晰、状态机简单、又能把 QT 的绘图、定时器、事件处理串一遍。但真动手写,卡住的地方往往不是游戏逻辑,而是 QT 那一套 QGraphicsView 坐标系怎么摆、QTimer 的节奏怎么调、方块旋转后碰撞检测为什么总差一格。这份 QT C++ 俄罗斯方块源代码,就是把这些已经踩平的坑打包给你看。它用 QT 框架加 C++ 实现经典俄罗斯方块,代码里能看到 QGraphicsView 和 QGraphicsItem 负责绘制游戏区、方块和网格线,QTimer 控制下落节奏,信号槽处理旋转和移动输入。适合两类人:刚学完 C++ 语法想找个完整项目练手的,以及用过 QT 但没写过游戏循环、想看看别人怎么组织代码的。下面我按实际拆包和复现的顺序,把这份源码的结构、编译、参数和坑一条条讲清楚。
2. 源码结构与 QT 绘图机制:先搞懂它怎么把方块画出来
2.1 文件布局与类职责划分
拿到压缩包,先别急着打开 .pro 文件编译。花五分钟把目录扫一遍,后面改代码会顺很多。这份源码的典型布局是头文件和实现文件分离,.h 放类声明,.cpp 放具体实现,这是 C++ 项目的基本功。从项目正文提到的截图和源文件布局图来看,核心类大致分三层:方块类、游戏逻辑类、界面类。
方块类通常叫 Tetrix 或 Block 之类,每个方块类型有自己的形状矩阵、颜色和旋转行为。游戏逻辑类一般叫 Game 或 Tetris,管当前方块、下一个方块、已固定方块、得分、行消除判断。界面类继承 QGraphicsView 或 QWidget,负责把逻辑层的数据画出来,同时接收键盘事件转发给逻辑层。
我一般会先找 main.cpp,看它创建了哪个窗口类,再顺着这个类往下追。这样能快速建立调用链:main 创建窗口 → 窗口初始化场景和定时器 → 定时器触发下落 → 下落时做碰撞检测 → 碰撞后固定方块并检查消行。
提示:如果源码里类名和上面说的不一样,不用纠结,按「谁持有方块数据、谁负责画、谁接收输入」这三个问题去定位就行。
2.2 QGraphicsView 场景坐标系与方块绘制
QT 的 QGraphicsView 框架用场景坐标来定位图元,默认原点在场景中心,x 向右为正,y 向下为正。俄罗斯方块的游戏区通常是一个固定列数乘行数的网格,比如 10 列 20 行。每个格子对应一个固定像素尺寸,常见是 20 到 30 像素。方块就是一组 QGraphicsItem 或 QGraphicsRectItem 的集合,按当前形状矩阵里的位置摆到对应格子上。
下面这段是常见的方块绘制逻辑,我按源码里大概率会出现的写法整理成可读版本:
// 假设格子边长 24 像素,游戏区 10 列 20 行 const int CELL_SIZE = 24; const int COLS = 10; const int ROWS = 20; // 把一个逻辑坐标 (col, row) 转成场景坐标 QPointF gridToScene(int col, int row) { // 场景左上角对齐游戏区左上角 qreal x = col * CELL_SIZE; qreal y = row * CELL_SIZE; return QPointF(x, y); } // 创建一个方块格子图元 QGraphicsRectItem* createCell(int col, int row, const QColor& color) { QGraphicsRectItem* cell = new QGraphicsRectItem(0, 0, CELL_SIZE, CELL_SIZE); cell->setPos(gridToScene(col, row)); cell->setBrush(QBrush(color)); cell->setPen(QPen(Qt::black, 1)); return cell; }逻辑说明:gridToScene 做的是逻辑网格到场景像素的映射,把列号和行号乘上格子边长。createCell 创建一个矩形图元,设置位置、填充色和边框。参数方面,CELL_SIZE 决定视觉大小,改大改小不影响游戏逻辑,但会影响窗口整体尺寸;COLS 和 ROWS 决定游戏区大小,改这两个值要同步改碰撞检测的边界判断,否则方块会跑出可视区域。
常见做法是把所有已固定的方块也放在同一个场景里,当前下落方块单独用一组图元表示,每次下落时更新这组图元的位置。这样消行时只需要移除对应行的图元,不用重绘整个场景。
2.3 QTimer 节奏控制与信号槽连接
游戏的下落节奏靠 QTimer 驱动。源码里一般会有一个 QTimer 成员,设置一个间隔比如 500 毫秒,超时信号连接到下落槽函数。每触发一次,当前方块下移一行,如果下移后发生碰撞,就固定方块、检查消行、生成新方块。
// 在窗口或游戏类构造函数里 dropTimer = new QTimer(this); connect(dropTimer, &QTimer::timeout, this, &Game::onDropTick); dropTimer->start(500); // 500 毫秒下落一格 void Game::onDropTick() { if (!tryMoveDown()) { lockPiece(); // 固定当前方块 clearFullRows(); // 检查并消除满行 spawnNewPiece(); // 生成新方块 if (isGameOver()) { dropTimer->stop(); } } }逻辑说明:tryMoveDown 尝试把当前方块下移一行,返回是否成功。失败说明碰到了底部或已有方块,这时走固定流程。参数上,start 的毫秒数直接决定难度,500 毫秒偏慢,适合调试;200 到 300 毫秒接近常见游戏手感。改这个值不用动其他代码,但要注意如果同时有加速下落逻辑,别让两个定时器互相干扰。
信号槽在这里的作用是把定时器事件和游戏逻辑解耦。定时器只管发信号,具体怎么处理由槽函数决定。键盘事件也是类似,keyPressEvent 里根据按键调用左移、右移、旋转、硬降等函数,这些函数内部再发信号或直接改状态。
3. 编译与运行:从 qmake 到可执行文件的完整路径
3.1 环境准备与 QT 版本选择
这份源码用的是 QT 框架,编译前需要装好 QT 开发环境和 C++ 编译器。Windows 上常见组合是 QT 5.15.2 加 MinGW 或 MSVC,Linux 上可以用系统包管理器装 qt5-default 和 g++。QT 6 也能编,但部分 API 有变动,比如 QGraphicsView 相关类基本没动,但有些信号槽语法和模块划分变了。如果你装的是 QT 6,遇到编译报错先看是不是头文件路径或模块名的问题。
我一般会先用qmake --version确认 qmake 在 PATH 里,再用g++ --version或cl确认编译器可用。QT Creator 用户可以直接打开 .pro 文件,它会自动识别套件。命令行用户按下面步骤走。
3.2 用 qmake 构建的完整命令
# 进入源码目录,确认有 .pro 文件 cd /path/to/tetris-source # 生成 Makefile qmake tetris.pro # 编译,-j 后面跟 CPU 核心数,加快速度 make -j4 # Windows MinGW 环境下用 mingw32-make mingw32-make -j4 # 运行生成的可执行文件,名字看 .pro 里的 TARGET ./tetris逻辑说明:qmake 读取 .pro 文件里的配置,生成对应平台的 Makefile。make 根据 Makefile 调用编译器编译每个 .cpp 并链接成可执行文件。参数 -j4 表示并行编译四个任务,核多可以调大。如果 .pro 里 TARGET 写的是别的名字,运行时就换成那个名字。
注意:如果 qmake 报「cannot mix incompatible Qt library」这类错误,通常是系统里装了多个 QT 版本,PATH 里的 qmake 和链接时用的库版本不一致。解决办法是明确用某个 QT 版本的 qmake 全路径,比如
/opt/Qt/5.15.2/gcc_64/bin/qmake,然后重新 make clean 再 make。
3.3 在 QT Creator 里打开与调试
不想敲命令的话,QT Creator 是最省事的。打开 QT Creator,File → Open File or Project,选中 .pro 文件,选择套件,点 Configure Project。然后直接点左下角绿色三角运行。调试的话在关键函数里打上断点,比如 onDropTick、tryMoveDown、clearFullRows,按 F5 进入调试模式,能一步步看方块状态怎么变。
常见做法是在 clearFullRows 里打断点,观察满行判断的条件。如果消行不生效,先看行遍历的范围对不对,再看判断满行的条件是不是要求所有列都有方块。有时候源码里会用if (rowFull)这种布尔标记,检查它有没有在每行开始前重置。
4. 核心逻辑拆解:碰撞检测、旋转与消行的参数细节
4.1 碰撞检测的边界条件
碰撞检测是俄罗斯方块里最容易出玄学 bug 的地方。逻辑本身不复杂:当前方块每个格子移动后,判断是否超出左右下边界,或者和已固定方块重叠。但边界条件写错一格,就会出现方块贴边卡住、旋转后穿墙、落地后还能左右移动等问题。
bool Game::isValidPosition(const QVector<QPoint>& cells) { for (const QPoint& p : cells) { int col = p.x(); int row = p.y(); // 左右边界和下边界 if (col < 0 || col >= COLS || row >= ROWS) { return false; } // 和已固定方块重叠 if (row >= 0 && board[row][col] != 0) { return false; } } return true; }逻辑说明:cells 是当前方块所有格子相对游戏区的坐标。col 小于 0 或大于等于列数说明出左右边界,row 大于等于行数说明到底。row 小于 0 的情况要放行,因为方块刚生成时可能有部分在顶部以上。board 是已固定方块的占用表,非零表示有方块。
参数上,COLS 和 ROWS 必须和绘图时的网格数一致。board 的大小也要匹配,常见是int board[ROWS][COLS]或QVector<QVector<int>>。如果改游戏区大小,这三个地方要同步改,漏一个就会越界或判断失效。
4.2 旋转实现与踢墙处理
旋转通常是把方块的形状矩阵转置再翻转,或者直接查表。简单实现是维护每个方块的几种旋转状态,旋转时切换到下一个状态,然后做一次碰撞检测。如果旋转后位置非法,就尝试左右微调,这就是常说的踢墙。
void Game::rotatePiece() { QVector<QPoint> rotated = currentPiece.rotatedCells(); // 先试原位旋转 if (isValidPosition(rotated)) { currentPiece.applyRotation(rotated); return; } // 原位不行,试左移一格 QVector<QPoint> leftShifted; for (const QPoint& p : rotated) { leftShifted.append(QPoint(p.x() - 1, p.y())); } if (isValidPosition(leftShifted)) { currentPiece.applyRotation(leftShifted); return; } // 再试右移一格 QVector<QPoint> rightShifted; for (const QPoint& p : rotated) { rightShifted.append(QPoint(p.x() + 1, p.y())); } if (isValidPosition(rightShifted)) { currentPiece.applyRotation(rightShifted); return; } // 都不行就放弃旋转 }逻辑说明:先算旋转后的格子坐标,原位合法就直接用。不合法就尝试左移一格和右移一格,这是最基础的踢墙。更完整的实现还会尝试上移,但源码里不一定有。参数上,左移右移的偏移量是 1 格,如果格子尺寸变了不影响这个逻辑,因为这里用的是逻辑坐标。
提示:如果旋转后方块偶尔穿到已固定方块里面,先检查 rotatedCells 返回的坐标有没有重复或遗漏,再检查 isValidPosition 有没有把当前方块自身也算进 board 里。
4.3 消行判断与得分计算
消行逻辑是遍历每一行,如果该行所有列都被占用,就标记为满行。然后从下往上把满行以上的行整体下移,顶部补空行。得分通常按一次消除的行数给不同分值,比如一行 100 分,两行 300 分,三行 600 分,四行 1000 分。
int Game::clearFullRows() { int cleared = 0; 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) { ++cleared; // 把这一行以上的所有行下移 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 变成了新行,需要重新检查 ++row; } } // 按 cleared 数量加分 if (cleared == 1) score += 100; else if (cleared == 2) score += 300; else if (cleared == 3) score += 600; else if (cleared >= 4) score += 1000; return cleared; }逻辑说明:从下往上遍历,发现满行后把该行以上的行整体下移,顶部补零。注意++row这步,因为下移后当前索引位置变成了原来上一行的内容,需要重新检查。得分规则可以按需改,改分值不影响逻辑。
参数上,board 的初始值全为 0,方块固定时把对应格子设为非零,通常用方块类型编号或颜色值。如果消行后画面没更新,检查绘图部分有没有在消行后重新根据 board 重建图元。
5. 避坑与排查:编译、运行和逻辑上的常见翻车点
5.1 编译时报找不到 QGraphicsView 相关头文件
现象:qmake 或 make 报fatal error: QGraphicsView: No such file or directory。
原因:QT 的模块没在 .pro 里声明。QGraphicsView 属于 QtWidgets 模块,QT 5 之后需要显式加QT += widgets。
解决:打开 .pro 文件,确认有QT += core gui widgets。如果用的是 QT 6,还要确认greaterThan(QT_MAJOR_VERSION, 4): QT += widgets这行在。改完重新 qmake 再 make。
5.2 程序运行后窗口空白或方块不显示
现象:编译通过,运行后有窗口但游戏区一片空白,或者只有背景没有方块。
原因:常见是场景没设置或图元没加到场景里。QGraphicsView 需要 setScene,图元需要 scene->addItem。另一个可能是场景矩形没设对,图元加到了可视区域外面。
解决:在窗口初始化里检查scene = new QGraphicsScene(this); view->setScene(scene);这两步。再检查图元创建后有没有scene->addItem(cell)。如果用了自定义坐标,确认场景的 sceneRect 覆盖了游戏区范围。
5.3 方块下落速度异常或键盘无响应
现象:方块下落要么快得看不清,要么按键盘没反应。
原因:QTimer 间隔设得太小或太大,或者键盘事件没被正确接收。QGraphicsView 默认可能不接收键盘焦点,需要在 view 上 setFocusPolicy(Qt::StrongFocus) 或把 keyPressEvent 写在正确的类里。
解决:先调 QTimer 的 start 参数到 500 毫秒左右测试。键盘问题检查 view 或窗口有没有 setFocus,以及 keyPressEvent 有没有被重写。如果用的是 QGraphicsView,事件可能先被场景吞掉,需要在 view 里重写 keyPressEvent 并调用基类或直接处理。
5.4 消行后画面和逻辑不一致
现象:逻辑上消了行,但画面上被消的方块还在,或者画面消了但逻辑没消。
原因:绘图和逻辑用了两套数据,消行后只更新了其中一套。常见是 board 更新了但图元没删,或者图元删了但 board 没改。
解决:统一以 board 为准,消行后清空场景里所有方块图元,再根据 board 重建。重建时遍历 board,非零格子创建图元。这样虽然每次消行重绘开销大一点,但不会出现不一致。优化做法是只删对应行的图元,但初学阶段先保证正确。
5.5 旋转时方块偶尔重叠或穿墙
现象:旋转后方块和已固定方块重叠,或者部分格子跑到边界外。
原因:旋转后的坐标计算有误,或者踢墙逻辑没覆盖所有情况。有些源码只做了原位旋转,没做踢墙,贴边旋转就会失败或穿墙。
解决:先确认 rotatedCells 返回的坐标是相对方块原点的,再确认 isValidPosition 对每个格子都做了边界和重叠检查。如果贴边旋转失败,加上左右各试一格的踢墙逻辑。还不行就检查 board 的索引有没有越界。
6. 进阶改造与验证:把这份源码变成你自己的项目
拆完这份源码,最值的动作不是原样跑一遍,而是改几个参数看效果,再动手加一两个小功能。我一般会先改 QTimer 的间隔,从 500 毫秒调到 200 毫秒,感受下落速度对游戏难度的影响。然后改 COLS 和 ROWS,比如从 10x20 改成 12x24,观察绘图和碰撞检测哪里会出问题。这一步能快速暴露哪些地方硬编码了尺寸。
再进一步,可以加一个「下一个方块预览」区域。做法是在界面右侧放一个小 QGraphicsView,根据下一个方块的形状矩阵画出来。逻辑上需要在 Game 类里维护 nextPiece,每次生成新方块时把 nextPiece 赋给 currentPiece,再随机生成新的 nextPiece。绘图部分单独写一个函数,把 nextPiece 的格子画到预览场景里。
验证改动是否正确的办法很简单:每改一个参数或加一个功能,先跑一遍看画面和逻辑是否一致。消行、旋转、落地固定这三个流程各测几次。如果加了预览,重点看预览方块和实际下落方块是不是同一个形状。
还有一个容易忽略的点是随机数生成。源码里可能用 qrand 或 QRandomGenerator 来生成方块序列。如果发现方块序列总是重复,检查随机种子有没有初始化。QT 5 里可以用qsrand(QTime::currentTime().msec()),QT 6 里 QRandomGenerator 默认就带种子,不用手动设。
// QT 5 随机种子初始化,放在 main 或 Game 构造函数里 #include <QTime> qsrand(QTime::currentTime().msec()); // 生成 0 到 6 的随机数,对应七种方块 int type = qrand() % 7;逻辑说明:qsrand 设置随机种子,用当前时间的毫秒数保证每次运行序列不同。qrand 返回随机整数,取模 7 得到 0 到 6。如果用的是 QT 6,把 qrand 换成QRandomGenerator::global()->bounded(7)。
参数上,取模的 7 对应方块种类数,如果你加了新方块类型,这个数要同步改。另外注意 qrand 在 QT 5 里不是线程安全的,游戏里单线程用没问题。
从那以后我每次拆这类源码,都强制自己先跑通编译,再改一个参数验证理解,最后加一个小功能收尾。只读不跑,很多坑根本发现不了。希望这份拆解能帮你少走点弯路,把这份 QT C++ 俄罗斯方块源码真正用起来。
本文还有配套的精品资源,点击获取