
简介QT与C实现的俄罗斯方块完整源代码面向正在学习Qt桌面开发或经典游戏逻辑的初学者与进阶者。压缩包共12个文件仅58KB包含4个cpp源文件、3个h头文件、1个pro工程文件及png/jpg预览截图cpp与h分离接口实现配工程文件可直接用qmake构建。项目以Tetris游戏为主线用QGraphicsView/QGraphicsItem绘制游戏区域、方块和网格用QTimer控制下落节奏并通过信号槽机制处理旋转按钮、碰撞检测与消行判断。各方块类型独立成类主逻辑封装在专门控制器中随机生成与行消除函数体现模块化设计动画更新和事件驱动流程清晰适合逐行研读。已有466人学习浏览可从中掌握Qt GUI编程、C面向对象设计、定时器调度和工程构建方法资源体量小巧而结构完整也可作为课程设计或二次开发起点。1. 一份能跑的QT C俄罗斯方块从入门到理解图形界面游戏如果你翻过不少QT教程脑海里的QGraphicsView还停留在“可以画矩形”的认知那这份QT C俄罗斯方块源代码值得下下来拆一遍。它不是那种只有几百行的教学Demo而是一个把经典游戏完整落地的工程方块生成、旋转、碰撞检测、行消除、计分逻辑全部串起来了截图、界面预览、源文件布局图都随包附带对着代码学比对着视频学要踏实得多。适合两类人一是刚学完C和QT面向对象想找个完整项目练手的新手二是被教程案例喂饱但没接触过QTimer驱动动画和QGraphicsItem绘制实战的人。看完你会明白一个游戏循环在QT里是怎么被信号槽和定时器撑起来的。2. 项目全景与技术选型QT的QGraphicsView为什么是这块游戏的正确底座2.1 资源包内容先看布局再动手拿到这个压缩包先别急着解压跑代码按文件名把素材捋一遍会更有效率。包里除了源码还带了三张关键的辅助图捕获.JPG 是程序运行时的真实截图界面预览.png 展示的是窗口布局和控件排布源文件布局.png 则是整个工程的目录树。这三张图对新手特别友好因为它把“正确答案”直接摆在你面前了——你写完代码跑出来的界面应该和截图一致如果类文件组织乱了看一眼布局图就能对回原来的结构。我的习惯是先看界面预览再碰代码这样进入源码时心里已经有一张画面地图哪里是游戏主区域哪里是下一块预览区哪里显示得分。很多初学者一上来就找main.cpp结果被一堆文件绕晕其实先花五分钟看图片后面读代码会顺很多。2.2 技术栈选型为什么是QGraphicsView而不是QWidget实现俄罗斯方块单纯用QWidget和painterEvent也能画出来但这份源码选择了QGraphicsView作为游戏画板这个选择值得反复琢磨。QGraphicsView带来的是一整套“场景-视图-图元”三层架构QGraphicsScene持有所有游戏元素QGraphicsView负责渲染呈现QGraphicsItem则代表每个可交互的方块格子。对于俄罗斯方块这种需要频繁增删大量矩形格子的游戏这个架构天然适合。对比一下就知道差距QWidget方案里每一次画面更新都要调用update()触发整个窗口重绘方块一多性能就肉眼可见地掉帧而QGraphicsScene管理的是一个图元列表每个图元是独立对象移动方块时只需要更新对应item的坐标不需要重绘整个棋盘。源码里的网格线、已落下方块、当前活动方块都各自以QGraphicsItem的形式存在这在调试时也是个福利——你可以单步检查每个item的坐标值而不是面对一整个painter事件。另外一个务实原因是事件分发。QGraphicsView把鼠标和键盘事件先用scene坐标系统做了一层转换对于要做点击旋转、拖拽落下的扩展玩法直接重写scene的event处理方法就行不必自己写坐标换算省掉很多重复劳动。2.3 目录结构与构建方式从这个项目的源码布局来看工程遵循了QT里最经典的“界面与控制分离”结构启动入口main.cpp只负责实例化主窗口并进入事件循环游戏逻辑全部收敛到Board类里方块形状数据则抽象成Piece类。头文件(.h)与实现文件(.cpp)严格分开每个类都保持单一职责这刚好是C面向对象设计里最值得学习的部分。构建方式用的是qmake这是QT传统工程的标准工具链。在终端里跑到源码目录执行qmake tetris.pro makeqmake会读取工程文件里的QT core gui、TARGET、SOURCES等配置自动生成对应平台的Makefile。需要说明的是如果用的是QT Creator直接打开.pro文件点运行即可命令行方式则要保证qmake在当前PATH环境变量里。构建完成后生成的二进制文件会放在Makefile指定的目标路径下在Linux/Unix系统里通常就是当前目录的tetris可执行文件。提示如果你在多版本QT环境里切换过qmake可能指向旧版本。执行qmake --version确认一下避免版本不一致导致链接错误。对于想二次开发的人这个目录组织的可读性很重要改游戏规则去Board类找调整方块样式去Piece类找动界面布局去MainWindow里找视线不需要在几十个文件里乱跳。3. 核心模块拆解一个方块从生成到消行的完整生命周期3.1 方块类型设计用类封装形状、颜色和行为俄罗斯方块一共有七种标准形状每种形状的旋转规则和颜色都不相同。这个项目的Piece类用一组相对坐标点来描述形状这是最直观也最容易扩展的做法。每个方块对象记录着自己当前四个小格子的相对位置以及属于自己的颜色。// piece.h —— 单个方块的形状与状态 class TetrisPiece { public: // 用坐标点集合描述形状每个点相对piece原点的偏移 using Shape QVectorQPoint; TetrisPiece(); // 返回当前旋转状态下的形状坐标 Shape shape() const { return coords; } // 顺时针旋转对每个点做 (x, y) - (-y, x) 变换 void rotate() { for (QPoint p : coords) { int x p.x(); int y p.y(); p.setX(-y); p.setY(x); } } QColor color() const { return mColor; } private: Shape coords; QColor mColor; };这里的核心是rotate函数里的坐标变换公式。顺时针旋转90度本质上是把每个像素点绕原点旋转变换数学表达式就是新的x等于负的旧y新的y等于旧的x。这个变换有个特点连续执行四次会回到初始状态所以代码里不需要额外维护旋转状态直接原地修改坐标即可。初次接触的人容易把旋转写成分段枚举比如为每个方向准备一套坐标那样代码会膨胀四倍而且漏掉任何一个方向都会出bug。使用坐标点而不是二维数组的另一个好处是碰撞检测简单。检测一个方块是否与边界或已填格子冲突只需要把每个相对坐标叠加到方块当前位置上再检查目标格子是否越界或已有内容即可没必要遍历一个4x4矩阵。这个设计直接影响了后面的核心逻辑写法。3.2 游戏主逻辑Board类如何拆分职责游戏的核心状态全部集中在Board类里它维护了一个二维网格数组、当前活动方块、游戏是否开始的标记以及计分数据。这个类继承了QGraphicsScene因为游戏区域本身就是场景键盘事件和绘制都由场景统一接管。// board.h —— 游戏主逻辑承载类 class GameBoard : public QGraphicsScene { Q_OBJECT public: GameBoard(QObject *parent nullptr); void startGame(); void setTimerInterval(int msec); // 外部调节下落速度 protected: void keyPressEvent(QKeyEvent *event) override; private slots: void moveDown(); // 定时器触发的下落动作 private: bool tryMove(const QPoint newPos); // 尝试把当前方块移动到newPos bool hasCollision(const TetrisPiece piece, const QPoint pos); int removeFullRows(); // 消除满行返回消除行数 void draw(); // 把网格和当前方块绘制到场景 int grid[ROW_COUNT][COL_COUNT]; // 记录已落下方块的网格 TetrisPiece curPiece; QPoint curPos; bool isStarted; };这段代码最值得学习的是把“游戏规则”和“界面呈现”揉进了同一个类却没有乱。grid负责逻辑draw负责呈现tryMove负责校验moveDown负责定时推进。如果你刚开始接触游戏编程可能会觉得一个类里既有数据又有界面不太符合“分层”思想但俄罗斯方块这种单窗口小游戏把强关联的状态放一起反而是最清晰的写法。真要拆成game engine和renderer两个大类在这个体量下反而增加理解成本。startGame时做的第一件事不是马上生成方块而是把网格数组清零、把旧的图元从场景中移除。这个“重置状态再开始”的流程容易被新手忽略导致连续玩两局时旧方块还残留在画布上。3.3 绘制链路从scene到item再到视图渲染绘制逻辑的入口是draw函数。它把网格里每个非空格子映射成场景里的一个矩形图元同时把当前活动方块也画上去。这里利用了QGraphicsScene对重复项的自动管理新方块需要显示时直接addItem方块落定后它自然变成网格的一部分不需要额外操作。// board.cpp —— 网格与方块的绘制逻辑 void GameBoard::draw() { QGraphicsRectItem *rect nullptr; // 优先清掉上一帧的活动方块图元避免重叠 for (QGraphicsItem *item : items()) { if (item-data(0).toInt() 1) { removeItem(item); delete item; } } // 画网格中已固定的方块 for (int row 0; row ROW_COUNT; row) { for (int col 0; col COL_COUNT; col) { if (grid[row][col] ! EMPTY) { rect new QGraphicsRectItem(0, 0, CELL_SIZE, CELL_SIZE); rect-setPos(col * CELL_SIZE, row * CELL_SIZE); rect-setBrush(QBrush(gridColor[row][col])); rect-setData(0, 0); // 标记为固定方块 addItem(rect); } } } // 画当前活动方块 for (const QPoint p : curPiece.shape()) { rect new QGraphicsRectItem(0, 0, CELL_SIZE, CELL_SIZE); rect-setPos((curPos.x() p.x()) * CELL_SIZE, (curPos.y() p.y()) * CELL_SIZE); rect-setBrush(QBrush(curPiece.color())); rect-setData(0, 1); // 标记为活动方块 addItem(rect); } }这段代码里的setData是一个容易忽略但很重要的细节。它给图元挂了一个自定义整数标记后续移动当前方块时就能通过data(0)的值快速从场景里找出需要删除的旧图元而不是误删已经固定的方块。如果你不用这个标记draw函数执行两次就会出现“幽灵方块”——同一个位置叠了新旧两层图元颜色还会变深。项目里用这个技巧保证每次重绘只保留最新状态对应的更新策略是删除全部活动块图元左右如果遇到方块平移频繁的场景也可以改成只更新pos但原始工程显然更看重代码直白程度。QGraphicsView配合QGraphicsScene时还有一个常见玄学问题为什么画出来的方块不在期望的位置原因多半是没有正确设置sceneRect。这个项目里场景的原点对应棋盘的左上角每个格子的像素尺寸是固定的CELL_SIZE所以映射关系只要做一次乘法就行。看到坐标错位时先检查是不是这里少乘了一个值。4. 信号槽与碰撞检测游戏交互从按键到状态变更的链路4.1 QTimer驱动游戏循环的骨架俄罗斯方块没有复杂的物理引擎它的“游戏循环”就是定时器反复触发同一个动作。项目里创建了一个QTimer设置固定间隔超时后发射timeout信号连接的槽函数执行下落一步。这个机制把游戏的节奏感变成了一行配置// mainwindow.cpp —— 定时器初始化与信号槽连接 gameTimer new QTimer(this); gameTimer-setInterval(500); // 每500毫秒下落一格 connect(gameTimer, QTimer::timeout, board, GameBoard::moveDown); gameTimer-start();setInterval的单位是毫秒这里500表示半秒一格的初始速度。很多新手在这里翻车把500当成秒导致方块慢得像蜗牛爬或者为了加速把间隔设置成50结果快到没法操作。调速的正确做法是保留一个基准速度变量消行数越多就把间隔设得越小而不是直接硬编码一个极端值。QTimer驱动模式还有个容易被忽略的点它不保证精确的定时精度如果主线程在短时间内处理大量绘制操作timeout信号的发射可能会被挤到后面。在俄罗斯方块这个体量下感受不到但如果以后你拿这套架构去做更复杂的游戏就得考虑把逻辑计算和渲染分离到不同线程。这里用单线程就够了因为每次绘制只有最多一两百个矩形性能压力很小。moveDown槽函数内部其实不是无脑向下移动它会先调用tryMove试探下一步位置是否合法如果不合法就说明当前方块已经落定这时把它的形状写进grid数组然后检查消行、生成新方块。这个“先试探再落定”的模式是整个游戏逻辑的枢纽也是接下来理解碰撞检测的关键入口。4.2 键盘事件处理把按键变成旋转和位移游戏操作依赖keyPressEvent事件重写所有按键动作都集中在游戏场景类里。方向键控制左右移动上方向键旋转下方向键加速下落空格直接硬降到最低点。每个按键分支都调用tryMove或者rotate然后更新画面void GameBoard::keyPressEvent(QKeyEvent *event) { if (!isStarted) return; switch (event-key()) { case Qt::Key_Left: tryMove(curPos QPoint(-1, 0)); break; case Qt::Key_Right: tryMove(curPos QPoint(1, 0)); break; case Qt::Key_Up: curPiece.rotate(); if (!hasCollision(curPiece, curPos)) { draw(); } else { // 旋转后撞墙则回退避免转出一个非法状态 curPiece.rotate(); curPiece.rotate(); curPiece.rotate(); } break; case Qt::Key_Down: moveDown(); break; case Qt::Key_Space: // 硬降循环下移直到触底 while (tryMove(curPos QPoint(0, 1))) { // 每步都更新画面实现连续下落效果 draw(); } moveDown(); break; default: QGraphicsScene::keyPressEvent(event); } }旋转处理这里有个细节值得品执行旋转后马上做了碰撞回退。因为某些方块在靠墙时旋转会越过边界如果不回退方块就会“嵌”进墙里。回退的方式是连续旋转三次恢复原状这利用了旋转公式四倍回到原点的性质比起额外保存旋转前坐标要省事。硬降的写法也很有代表性用while循环不断试探往下移动直到失败那一刻再调用一次moveDown让方块正式落定。4.3 碰撞检测与行消除把边界条件算清楚tryMove是所有移动操作的唯一通道它内部先调用hasCollision判断目标位置是否违规合法才更新坐标。这个收口设计很重要它保证没有任何移动能绕过碰撞检测你不需要担心某个快捷键漏写了校验。hasCollision的判断范围包括左右下三面边界以及网格里已经被占用的格子bool GameBoard::hasCollision(const TetrisPiece piece, const QPoint pos) { for (const QPoint p : piece.shape()) { int nx pos.x() p.x(); int ny pos.y() p.y(); if (nx 0 || nx COL_COUNT) return true; // 左右越界 if (ny ROW_COUNT) return true; // 触底 if (ny 0 grid[ny][nx] ! EMPTY) return true; // 撞到已固定方块 } return false; }这里有意放过了ny 0的情况也就是允许方块刚生成时有一部分在棋盘顶部的上方。这个设定是俄罗斯方块的通行做法给新方块进入画面留出缓冲空间如果你把ny 0也判为碰撞游戏开始在生成第一个方块时就会直接判负。行消除的逻辑稍加变化。从最底层往上扫描只要发现某一行全部非空就把它上面的所有行各自下移一格然后清空最顶行。消除一行后还要原地再看一次当前行因为上方落下来的行可能是满的。int GameBoard::removeFullRows() { int removed 0; for (int row ROW_COUNT - 1; row 0; --row) { bool full true; for (int col 0; col COL_COUNT; col) { if (grid[row][col] EMPTY) { full false; break; } } if (full) { // 当前整行下移一格 for (int r row; r 0; --r) { for (int c 0; c COL_COUNT; c) grid[r][c] grid[r - 1][c]; } // 置空最顶行 for (int c 0; c COL_COUNT; c) grid[0][c] EMPTY; row; // 因为上面落下来一行原地再检查一次 removed; } } return removed; }这段代码里row这个操作是我觉得整份源码里最见功力的地方。如果不原地再查一次连续出现两行同时满时第二行会被漏判。很多人写消行时只消了一半问题就出在这里。消除完成后返回的行数可以用来计算得分消除一行记一分连续多行可以设计加成分。5. 避坑指南编译运行调试中我踩过的五个坑5.1 界面黑屏或方块全部挤在左上角现象程序跑起来游戏区域一片黑方块全部出现在窗口左上角巴掌大的地方网格线也对不齐。原因QGraphicsScene默认的原点在(0,0)但如果你没有显式设置sceneRect视图会按照所有item的内容范围自动缩放导致坐标系和棋盘网格对不上。另一个常见叠加原因是把网格坐标直接当像素坐标用了没乘格子边长。解决在构造场景时显式调用setSceneRect(0, 0, COL_COUNT * CELL_SIZE, ROW_COUNT * CELL_SIZE)并确认所有item的坐标都是“列索引 * CELL_SIZE”而不是直接用列号。如果之后发现方块位置偏离半格多半是pos设置时把原点多算了一格。5.2 qmake成功了一运行就崩溃现象qmake和make都顺利走完双击生成的exe或者从命令行启动窗口都没弹出来就崩了。原因最常见的是QGraphicsItem对象new出来了但没有addItem到scene里或者QTimer连接时用了旧式的字符串语法信号槽参数类型不匹配直到事件循环触发时才炸。尤其是前者QT文档里明确写了QGraphicsItem必须依附于scene否则就是一个悬空对象任何对它的操作都可能段错误。解决所有图元创建后立即调用scene-addItem()connect统一用QTimer::timeout这类新式函数指针写法让编译期替你做参数检查。如果还是崩用调试器看堆栈我踩过的一次就是卡在绘制函数里访问了一个空指针引用。5.3 方块落到底之后还能左右移动甚至嵌进已填充的行现象方块明明已经堆到底了按左右方向键依然会平移甚至会穿进已经固定好的方块堆里。原因tryMove函数只校验了纵向移动左右平移分支另写了一套移动逻辑没有复用hasCollision。两边逻辑不一致是这类小游戏最容易埋雷的地方。解决让左、右、下三种移动全部走同一个入口tryMove这个入口内部统一调用hasCollision。hasCollision里把左右边界和下边界、已占用格子的判断一次性算完不要在三个地方各写一份边界条件。我一般还会加一行调试输出在移动失败的瞬间把目标坐标打出来一眼就能看出是哪个方向的判断漏了。5.4 中文目录下qmake构建失败现象项目放在“某个盘/我的文档/项目/tetrix”这种带中文的路径下make时报错找不到头文件或目标文件拷到纯英文路径就好了。原因Windows下用MinGW工具链时make生成的依赖规则对中文路径处理不友好路径串里的中文编码和反斜杠拼接容易断链。这个问题在旧版本的QT Creator里尤其频繁。解决所有QT工程一律放在纯英文路径下路径中也不要带空格。这个习惯后来帮我避开了大量构建玄学问题毕竟谁都不想在排查了一晚上编译器错误之后最后的解决方案是改文件夹名。5.5 消行之后上面的方块没有掉下来像悬空一样现象凑满一行后被清掉了但头顶上一堆方块没有补齐空白网格里出现一条悬空的裂痕。原因消行时只做了“把当前行清零”没有把该行上方的所有内容整体下移一行。这在画面上的表现就是行确实消失了但上面的方块留在原地没跟着挪下来。解决严格按“从下往上扫描满行就把上面的行逐行下移一格再把顶行置空”的顺序来写。注意下移必须从目标行开始往上遍历而不是从上往下否则会把下面的数据覆盖掉。写完后用连续两行同时满的用例来验证能暴露漏判问题。6. 二次开发思路从复刻经典到做一个有自己风格的版本6.1 改造方向一增加下一个方块预览当前工程只显示活动方块下一个方块的信息藏在逻辑里。实现预览功能并不难在Piece类里增加一个获取形状和颜色的接口MainWindow区域右侧放置一个独立的QGraphicsView每次生成新方块时把下一块的形状画进去。只需要在生成逻辑里多保存一份“预备方块”对象并在消行后触发的更新函数里同步刷新预览场景。这个改动能让你更早接触“多视图共享同一份数据”的QT设计模式。6.2 改造方向二引入计分板与等级变速现有计费逻辑完成度已经足够跑通但节奏是固定的。你可以把下落间隔和消除行数绑定每消满十行把计时器间隔减少50毫秒同时更新界面上的等级数字。实现上只需在removeFullRows的返回值基础上做一个累加判断然后调用setTimerInterval把新的间隔写到QTimer上。要注意设置一个最低间隔上限比如100毫秒否则几十级之后游戏快到完全没法操作瞬间从方块游戏变成抽风游戏。6.3 改造方向三加入暂停与重新开始暂停功能适合检验你对事件驱动模型的理解。加一个bool变量isPaused在keyPressEvent里拦截P键暂停时调用gameTimer-stop()恢复时再start()。重新开始则要求初始化全部网格数据和场景图元注意清理残留的item。从那以后我每次拿到一个QT小游戏源码第一件事不是急着改功能而是先画一遍“按键到状态变更”的链路图把tryMove、hasCollision、draw这条主链标注出来再动手。这次拆QT C俄罗斯方块源码我按这条路线走半天就把整个工程吃透了后面加预览、调速都顺顺利利没再被碰撞边界的边界条件绊住脚。这份源码包里还带了界面截图和布局图下载后对照着文件名看能少走不少弯路。希望帮到你。本文还有配套的精品资源点击获取