简介:这是一份面向QT初学者与游戏开发爱好者的俄罗斯方块完整源码,基于QT框架与C++实现,适合作为GUI编程、事件驱动与游戏逻辑设计的练手项目。压缩包为rar格式,共11个文件,约15.18MB,包含5个cpp源文件、1个头文件、1个ui界面文件、1个pro工程文件,以及2个wav音效资源,覆盖主程序、方块移动、舞台渲染、计分与界面等模块。源码围绕QGraphicsView与QGraphicsScene构建显示区域,实现了方块生成、旋转、下落、碰撞检测与行消除等核心机制,并通过信号槽响应键盘输入,管理开始、暂停、结束等游戏状态。目前已有526人学习,读者可借此理解QT图形渲染、坐标变换与资源加载的完整流程,对照源码梳理游戏循环与状态切换思路,是提升QT框架理解与游戏开发能力的实用参考。
1. 从一份 Tetris_QT 俄罗斯方块源码说起:它到底能跑出什么
很多人第一次搜「Tetris_QT俄罗斯方块源码」,心里想的其实不是「我要读一份代码」,而是「我想在本地跑起来一个能玩的俄罗斯方块,顺便看看 Qt 是怎么把界面、定时器和游戏逻辑缝在一起的」。这个诉求非常具体:编译能过、窗口能弹、方块能掉、键盘能转、消行能加分。它适合两类人——一类是刚学完 Qt 控件、想找个完整小项目练手的;另一类是写过控制台版俄罗斯方块,但一碰到 QWidget 绘制、QTimer 刷新、信号槽就卡壳的。
俄罗斯方块这个题材的好处是规则封闭、状态机清晰,没有网络、没有数据库、没有第三方服务,一份源码就能把「事件循环 + 定时器 + 重绘 + 键盘输入」这条 Qt 主线全部串起来。所以这篇不打算逐行念代码,而是按「先立住结构,再动手复现,最后讲坑」的顺序,把一份典型的 Tetris_QT 源码拆成你能自己重写的模块。你读完应该能做到:拿到任意一份同类源码,知道先看哪几个文件、哪些参数决定手感、哪些地方最容易翻车。
2. 拆开 Tetris_QT 源码:棋盘、方块和定时器怎么分工
2.1 先认清 Qt 版俄罗斯方块的四个核心对象
一份结构正常的 Tetris_QT 源码,不管作者怎么命名,基本都逃不出四个角色。第一个是棋盘数据,通常是一个二维数组,比如int board[20][10],20 行 10 列是经典比例,行数决定你能堆多高,列数决定横向空间。第二个是当前方块,它至少要有形状矩阵、当前坐标、当前旋转态三个信息。第三个是定时器,Qt 里就是QTimer,它负责每隔一段时间把方块往下推一格。第四个是绘制入口,也就是paintEvent,所有方块最终都是在这里被画到窗口上的。
这四个对象的关系是单向驱动的:定时器触发下落,下落改变方块坐标,坐标变化触发重绘,重绘读取棋盘和方块数据画图。键盘输入是另一条支线,它不直接改画面,而是改方块状态,改完同样触发重绘。理解这条链路,你就知道为什么很多新手改了半天颜色没反应——因为颜色是在paintEvent里根据数据算出来的,你改错了地方。
提示:如果你拿到的源码里棋盘是
[10][20]而不是[20][10],先别急着改,看清楚它是按「列优先」还是「行优先」访问的,两种写法都能跑,混用才会出错。
2.2 用 QTimer 控制下落:interval 决定手感
俄罗斯方块的手感,八成来自下落速度。Qt 里最直接的做法是给一个QTimer设setInterval(),单位是毫秒。经典俄罗斯方块在第一关大约是 800ms 一格,随着等级提升逐步缩短。下面这段是最小可用的下落驱动逻辑:
// 在窗口类构造函数里初始化定时器 dropTimer = new QTimer(this); // 800ms 下落一格,数值越小掉得越快 dropTimer->setInterval(800); // 定时器到点就连到自定义槽函数 connect(dropTimer, &QTimer::timeout, this, &TetrisWidget::onTick); dropTimer->start(); void TetrisWidget::onTick() { // 先尝试把方块整体下移一行 if (canMove(currentPiece, 0, 1)) { currentPiece.y += 1; } else { // 下不动了,说明落地,锁定进棋盘 lockPiece(); clearFullLines(); spawnNewPiece(); } // 数据变了就请求重绘,不要手动调 paintEvent update(); }这里的逻辑说明很关键:canMove是碰撞检测,它要同时判断边界和已锁定方块;lockPiece把当前方块写进棋盘数组;clearFullLines扫描满行并下移;spawnNewPiece生成新方块。参数上,setInterval的数值直接决定难度曲线,我一般会用一个level变量反推 interval,比如interval = 800 - (level-1)*70,并设一个下限 100ms,否则高等级会快到没法玩。
2.3 键盘事件与旋转:别在 keyPressEvent 里直接改画面
键盘处理是第二个高频翻车点。Qt 的keyPressEvent里应该只做状态变更,不要在里面调repaint()或直接画图。方向键左右改x,下键加速下落,上键旋转。旋转最容易出问题的是「贴墙旋转」——方块转到一半超出边界。常见做法是旋转后做一次合法性检查,不合法就尝试左右微移(俗称 wall kick),再不行就放弃这次旋转。
void TetrisWidget::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key_Left: if (canMove(currentPiece, -1, 0)) currentPiece.x -= 1; break; case Qt::Key_Right: if (canMove(currentPiece, 1, 0)) currentPiece.x += 1; break; case Qt::Key_Down: if (canMove(currentPiece, 0, 1)) currentPiece.y += 1; break; case Qt::Key_Up: rotatePiece(); // 内部做合法性检查和微移 break; default: QWidget::keyPressEvent(event); return; } update(); // 统一在这里请求重绘 }参数说明:canMove的偏移量是相对当前坐标的增量,左是 -1,右是 +1,下是 +1(y 轴向下为正)。rotatePiece里建议把旋转后的形状先存到临时变量,验证通过再赋值,避免旋转失败后形状被改坏。这套写法比在事件里直接操作绘制要稳得多,也是我看源码时判断作者水平的一个小信号。
3. 把 Tetris_QT 源码跑起来:编译、绘制与最小验证
3.1 用 qmake 还是 CMake:先看源码里的工程文件
拿到一份 Tetris_QT 源码,第一步不是打开 cpp,而是看根目录有没有.pro或CMakeLists.txt。有.pro就是 qmake 工程,用 Qt Creator 直接打开即可;有CMakeLists.txt就用 CMake 流程。两种方式我都给出来,你按源码实际情况选。
# 方式一:qmake 工程,在源码根目录执行 qmake Tetris.pro make -j4 ./Tetris # Linux/macOS,Windows 下是 release/Tetris.exe # 方式二:CMake 工程 mkdir build && cd build cmake .. -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x/gcc_64 cmake --build . -j4逻辑说明:qmake会读取.pro里的QT += widgets之类的模块声明,生成 Makefile;CMake 则靠find_package(Qt6 COMPONENTS Widgets REQUIRED)定位 Qt。参数上,CMAKE_PREFIX_PATH必须指向你本机 Qt 的安装前缀,写错会报找不到 Qt6Config.cmake,这是最高频的编译失败原因。如果你用的是 Qt 5,把Qt6换成Qt5即可,源码里#include <QtWidgets>这类写法两者基本兼容。
3.2 paintEvent 里到底画了什么:一次讲清坐标换算
paintEvent是俄罗斯方块源码里最值得逐行读的函数。它通常做三件事:画背景网格、画已锁定方块、画当前下落方块。核心是坐标换算——棋盘坐标(col, row)要乘上格子边长才能变成像素坐标。
void TetrisWidget::paintEvent(QPaintEvent *) { QPainter painter(this); const int cell = 30; // 每个格子的像素边长,决定窗口大小 // 画已锁定的棋盘方块 for (int r = 0; r < ROWS; ++r) { for (int c = 0; c < COLS; ++c) { if (board[r][c] != 0) { // 棋盘坐标转像素坐标 QRect rect(c * cell, r * cell, cell, cell); painter.fillRect(rect, colorOf(board[r][c])); painter.drawRect(rect); // 描边,让格子边界清晰 } } } // 画当前正在下落的方块,逻辑同上,坐标加上 currentPiece 的偏移 for (int r = 0; r < 4; ++r) for (int c = 0; c < 4; ++c) if (currentPiece.shape[r][c]) { QRect rect((currentPiece.x + c) * cell, (currentPiece.y + r) * cell, cell, cell); painter.fillRect(rect, colorOf(currentPiece.type)); } }参数说明:cell是唯一需要你调的显示参数,改成 20 窗口就变小,改成 40 就变大,但窗口尺寸要同步用setFixedSize(COLS*cell, ROWS*cell)设置,否则会出现留白或裁切。colorOf是一个把方块类型映射到QColor的辅助函数,建议用switch写死七种颜色,别用随机色,否则同一块方块每帧颜色都在变,看起来像 bug。
3.3 最小验证:先让它掉,再让它消
跑起来之后不要急着玩,按这个顺序验证,能快速定位问题出在哪一层。第一步,窗口是否按COLS*cell和ROWS*cell正确弹出;第二步,方块是否按 interval 稳定下落;第三步,按左右键是否能移动且不越界;第四步,堆满一行是否能消除并加分。这四步分别对应绘制、定时器、键盘、棋盘逻辑四个模块,哪一步断了就去查对应模块,比盲目读全部代码快得多。
注意:如果方块下落但画面不刷新,九成是
onTick里漏了update();如果画面刷新但方块重叠,检查canMove是否把当前方块自身也算进了碰撞。
4. 避坑与排查:Tetris_QT 源码里最容易翻车的 5 个地方
4.1 现象:编译报「undefined reference to vtable」
原因:Qt 的信号槽依赖 moc(元对象编译器)生成额外代码,如果你手动用 g++ 编译而没跑 moc,或者类里声明了Q_OBJECT但没重新执行 qmake,就会缺 vtable。解决:老老实实用 qmake 或 CMake 构建,改过带Q_OBJECT的头文件后重新执行一次qmake再 make,别图省事直接 g++。
4.2 现象:方块旋转后卡在墙里或穿进已有方块
原因:旋转只改了形状矩阵,没做边界和碰撞校验。解决:旋转后立即用canMove检查,不合法就尝试x±1的微移,仍不合法则回滚到旋转前的形状。这是俄罗斯方块源码里最经典的逻辑漏洞,很多能跑的源码其实也有。
4.3 现象:消行后上面的方块没有整体下移
原因:clearFullLines只把满行清零,没有把上方行逐行下移。解决:从下往上扫描,遇到满行就把该行以上的所有行整体下移一行,然后继续检查同一索引位置,别急着++r,否则会漏掉连续满行。
4.4 现象:长时间运行后越玩越卡
原因:在paintEvent里创建了QPainter之外的临时对象,或者每帧都重新分配大数组。解决:棋盘数组和方块形状在构造时一次性分配好,paintEvent里只读不建。如果用了QTableWidget之类的控件展示分数榜,注意大数据量下它本身就会卡,这种场景更适合QTableView加自定义 model,只渲染可见行。
4.5 现象:窗口关闭后进程不退出
原因:QTimer没停,或者有子线程没 join。解决:在窗口的closeEvent里显式dropTimer->stop(),如果源码里用了额外线程做音效或网络,也要在那里做退出处理。这个坑在只做单机俄罗斯方块时不一定遇到,但一旦你往源码里加功能就容易踩。
5. 从能跑到好用:给 Tetris_QT 源码加一个「下一块预览」和难度曲线
把基础版跑通之后,真正让一份 Tetris_QT 源码从「作业」变成「作品」的,是两个小功能:下一块预览和难度曲线。它们代码量不大,但能立刻拉开手感差距。
下一块预览的做法是维护一个nextPiece变量。每次spawnNewPiece时,把nextPiece赋给当前方块,再随机生成新的nextPiece。预览区可以是一个独立的QWidget,也可以在主窗口右侧留一块区域,在paintEvent里用同样的坐标换算画出来,只是原点偏移到预览区。关键是预览区尺寸要按 4x4 格子预留,否则长条方块会画不全。
难度曲线我一般用一个level和linesCleared的映射来做。每消除 10 行升一级,interval 按公式递减并卡下限:
void TetrisWidget::updateSpeed() { // 每 10 行升一级,最低 100ms,防止快到不可玩 int level = linesCleared / 10 + 1; int interval = qMax(100, 800 - (level - 1) * 70); dropTimer->setInterval(interval); }参数说明:800是起始间隔,70是每级加速量,100是下限。这三个数你可以按自己手感调,但下限一定要有,否则高等级下QTimer会以极高频率触发,既难玩又浪费 CPU。linesCleared在每次消行后累加,然后调用updateSpeed。
验证方法很简单:连续消行到 10 的倍数,看 interval 是否变化;用qDebug()打印当前 interval 和 level,比盯着画面猜靠谱。如果你还想更进一步,可以加「暂存块」和「幽灵方块」(显示当前方块落点),这两个功能都依赖同一套canMove逻辑,属于把已有函数复用出花来的典型。
我自己写这类小项目最大的教训是:别一上来就追求功能全,先把「下落—碰撞—锁定—消行」这条主循环写扎实,再往上叠预览、加速、音效。主循环不稳,加什么都是空中楼阁。希望帮到你。
本文还有配套的精品资源,点击获取