首页 / 资讯中心 / 文章详情

Qt俄罗斯方块源码解析:从编译到运行,掌握QTimer与paintEvent

Qt俄罗斯方块源码解析:从编译到运行,掌握QTimer与paintEvent ★ FEATURED ARTICLE
简介这是一份面向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下是 1y 轴向下为正。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/macOSWindows 下是 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之类的模块声明生成 MakefileCMake 则靠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逻辑属于把已有函数复用出花来的典型。我自己写这类小项目最大的教训是别一上来就追求功能全先把「下落—碰撞—锁定—消行」这条主循环写扎实再往上叠预览、加速、音效。主循环不稳加什么都是空中楼阁。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站