资讯动态

C++与QT俄罗斯方块课设:从逻辑到界面的高分实践指南

发布时间:2026/10/4 5:25:36 来源:尧图企业网站定制
简介这份基于C与Qt实现的俄罗斯方块游戏课程项目专为需要完成课程设计、毕业设计或期末大作业的计算机专业学生精心准备也适合想通过经典小游戏入门Qt桌面开发的初学者。源码包含详细注释新手也能快速理解游戏逻辑与界面编程思路并配有项目文档对整体架构、关键类和运行流程进行说明下载后简单配置即可运行或二次扩展。资源压缩包共42个文件以17个头文件、9个C源文件为主体另含UI界面文件、动态链接库、配置文件及Word文档等覆盖开发、编译和运行所需环节整体大小11.87MB目录层次清晰。目前已有316人学习下载。项目为个人独立开发并获得导师认可代码涵盖方块变形、消行计分、键盘控制、界面刷新等核心模块能够帮助读者深入掌握Qt信号槽、事件处理与绘图机制是冲击高分课程设计和积累实战经验的优质模板。1. 基于C与QT的俄罗斯方块课设为什么说它是性价比最高的高分方向拿到“基于C与QT的俄罗斯方块游戏源代码项目文档”这个题目时大多数人的第一反应是“这不就是个抄代码的活”。我做过几年C相关开发也帮人审过不少课设代码可以负责任地说俄罗斯方块是C/QT课设里性价比最高的题目之一。它麻雀虽小五脏俱全——你要用纯C处理数据结构与算法方块旋转矩阵、碰撞检测、消行判定要用QT解决界面渲染与事件循环QPainter绘图、QTimer定时器、QKeyEvent键盘事件还要用面向对象思想把游戏逻辑和UI解耦。做完这一个项目等于把C课设考核的三大件全练了一遍。这个方向适合两类人一类是C基础还行、但没做过完整GUI项目的可以通过它把QT的常用组件和信号槽机制串起来另一类是时间紧、需要保底分数的俄罗斯方块逻辑成熟、参考资料多只要按正确的架构写跑通不难加分点等级计分、方块预览、暂停恢复也好扩展。这篇笔记就按我自己搭这类项目的顺序来讲先定架构、再写核心逻辑、最后套QT界面末尾附上我踩过的坑和答辩加分思路。2. 先立架构再写代码GameField核心类与数据设计2.1 为什么把游戏逻辑和界面分开一个类负责“规则”一个类负责“画画”我第一次写QT小游戏时犯过最蠢的错误把游戏状态全堆在MainWindow里按钮回调里直接改数组、直接调update()。初期跑得欢加了暂停和等级之后代码就成了黑匣子——改一处崩三处。正确的做法是让游戏逻辑和界面各自独立C部分只关心“当前面板长什么样、方块能不能动、消了几行”QT部分只关心“怎么把这块面板画出来、按键后调哪个函数”。这个思路对应到项目里是两个核心类GameField游戏逻辑和MainWindowQT界面。GameField不include任何QT头文件这样你甚至可以用命令行程序单测它的逻辑MainWindow只负责把GameField暴露出的状态画出来。后面的调试会舒服非常多——逻辑出问题不用看QT报错界面出问题不用翻算法。2.2 面板数据与方块形状用二维数组和形状表定义全部游戏状态先定义游戏面板。经典俄罗斯方块面板是10列宽、20行高我用一个二维int数组存储0表示空格非0表示有方块。用int不用bool是因为后面做消行计数、等级特效时单元格的值还能扩展。// gamefield.h #pragma once #include vector #include utility class GameField { public: static constexpr int BOARD_WIDTH 10; static constexpr int BOARD_HEIGHT 20; static constexpr int TETROMINO_SIZE 4; GameField(); // 方块操作 bool moveLeft(); bool moveRight(); bool moveDown(); bool rotate(); void hardDrop(); // 状态查询 int getCell(int row, int col) const; // 供QT界面绘制用 bool isGameOver() const { return m_gameOver; } int getScore() const { return m_score; } int getLevel() const { return m_level; } // 下一个方块预览 int getNextBlockType() const { return m_nextBlockType; } private: bool canMove(int newRow, int newCol, const int shape[TETROMINO_SIZE][TETROMINO_SIZE]) const; void lockBlock(); // 方块落地后固化为面板数据 void clearFullRows(); int m_board[BOARD_HEIGHT][BOARD_WIDTH] {}; int m_currentShape[TETROMINO_SIZE][TETROMINO_SIZE] {}; int m_currentRow 0; int m_currentCol BOARD_WIDTH / 2 - 2; int m_nextBlockType 0; int m_score 0; int m_level 1; bool m_gameOver false; };逻辑说明m_board存的是已经固定的方块m_currentShape存当前正在下落的4x4矩阵。初始列取BOARD_WIDTH / 2 - 2是因为每个方块形状定义在4x4矩阵里实际占2到3列居中放置得留出偏移。static constexpr是C17的写法在头文件里直接定义常量不会出现链接重复问题如果你用的是老标准C11/14需要改成static const int并在cpp文件里再定义一次——这是QT旧版本项目里常见的编译报错来源。参数说明面板宽度10、高度20是俄罗斯方块的经典尺寸直接照搬即可。TETROMINO_SIZE 4必须固定——标准俄罗斯方块所有方块都能放进4x4矩阵改大了旋转算法要重写改小了长条I会出界。2.3 七种方块形状表用坐标对定义而不是4x4数组新手喜欢直接用7个4x4数组写死方块形状比如这样// 直观但不好旋转 int shapes[7][4][4] { // O方块 {{0,1,1,0}, {0,1,1,0}, {0,0,0,0}, {0,0,0,0}}, // I方块 {{0,0,0,0}, {1,1,1,1}, {0,0,0,0}, {0,0,0,0}}, // ... };这个写法能跑但做旋转时你要写四套旋转后的数组而且长条I和方块O在4x4里驻留的行列位置很容易错位。我一般用坐标对的方式定义初始形状配合统一的旋转公式逻辑更干净// gamefield.cpp 部分实现 #include gamefield.h // 每种方块由若干行,列坐标对定义均落在4x4矩阵内 static const std::vectorstd::vectorstd::pairint,int SHAPE_TABLE { // O {{0,1},{0,2},{1,1},{1,2}}, // I横向便于左移用手感 {{1,0},{1,1},{1,2},{1,3}}, // T {{0,1},{1,0},{1,1},{1,2}}, // S {{0,1},{0,2},{1,0},{1,1}}, // Z {{0,0},{0,1},{1,1},{1,2}}, // J {{0,0},{1,0},{1,1},{1,2}}, // L {{0,2},{1,0},{1,1},{1,2}} }; GameField::GameField() { m_nextBlockType rand() % SHAPE_TABLE.size(); m_currentRow 0; m_currentCol BOARD_WIDTH / 2 - 2; // 用下一块类型初始化当前形状 for (int i 0; i 4; i) { m_currentShape[m_currentRow][m_currentCol] 1; } }逻辑说明SHAPE_TABLE用vectorpairint,int保存每个方块占用的格子坐标省掉了大段0/1数组。初始化时把第一块方块放到面板顶部中央。这里要注意rand()需要先srand(time(nullptr))如果你在主函数里忘了播种每次启动游戏第一块方块永远一样——这个问题我见过不止一个同学挂在答辩上。参数说明坐标对的数值范围必须全部落在0到3之间这是后续旋转算法正确工作的前提。I方块的初始形态建议放横向占第1行不要放竖向——竖向I从顶部生成时方块底部会直接越出边界。3. 让方块真正动起来移动、旋转、碰撞与消行的C实现3.1 碰撞检测先“试着移动”再决定“能否移动”方块移动的核心不是“移动后检查是否合法”而是“检查目标位置是否合法后再移动”。两种写法结果一样但后者代码路径更清晰也方便后面批量测试。bool GameField::canMove(int newRow, int newCol, const int shape[TETROMINO_SIZE][TETROMINO_SIZE]) const { for (int r 0; r TETROMINO_SIZE; r) { for (int c 0; c TETROMINO_SIZE; c) { if (shape[r][c] 0) continue; int boardRow newRow r; int boardCol newCol c; // 超出左右边界或超出底部 if (boardCol 0 || boardCol BOARD_WIDTH || boardRow BOARD_HEIGHT) { return false; } // 与已固定方块重叠 if (boardRow 0 m_board[boardRow][boardCol] ! 0) { return false; } } } return true; }逻辑说明遍历当前形状4x4矩阵里的每一个“实心”格子换算到面板坐标后做两类检查一是边界二是与m_board中已固定方块的重叠。注意boardRow 0的情况没有判false——当方块从顶部生成时部分格子暂时在面板上方这是合法状态游戏还没结束等它下来就行只有超出底部才判死。参数说明newRow和newCol是“目标位置”也就是当前行/列加上偏移量后的值。移动函数应把“尝试移动”和“落子”分开——比如moveDown()是“先算newRow m_currentRow 1若canMove则更新m_currentRow”。你如果先修改了m_currentRow再调用canMove那canMove检查的就是已经移动后的位置逻辑就反了。3.2 旋转的数学转置加行反转一次搞定四种朝向方块旋转本质是矩阵变换。顺时针旋转90度在坐标表示下等价于“先矩阵转置再把每一行左右反转”。我用一个临时4x4矩阵做旋转然后调用前面写好的canMove判断旋转后位置是否合法。bool GameField::rotate() { int rotated[TETROMINO_SIZE][TETROMINO_SIZE] {}; // 转置行变列 for (int r 0; r TETROMINO_SIZE; r) { for (int c 0; c TETROMINO_SIZE; c) { rotated[c][r] m_currentShape[r][c]; } } // 每一行反转向右旋转90度 for (int r 0; r TETROMINO_SIZE; r) { for (int c 0; c TETROMINO_SIZE / 2; c) { std::swap(rotated[r][c], rotated[r][TETROMINO_SIZE - 1 - c]); } } // 如果旋转后位置合法才应用否则保持原样 if (canMove(m_currentRow, m_currentCol, rotated)) { memcpy(m_currentShape, rotated, sizeof(rotated)); return true; } return true; // 即使不能旋转也不报错只是不改变 }逻辑说明转置加行反转是原地实现顺时针90度旋转的经典做法不需要单独记忆四个方向的数组。memcpy把旋转结果写回m_currentShape。注意最后一行return true——这在语义上表示“旋转动作被尝试过”即使因为碰撞没有生效也不应该让游戏进入异常状态。如果你希望做“踢墙”效果方块在墙边旋转时自动平移一步可以在这里补一个逻辑旋转失败后依次尝试左移一格、右移一格后再旋转。这个旋转算法有个新手易踩的坑O方块旋转前后长得一样但它在4x4矩阵里的坐标是偏左还是偏右会影响旋转后的中心点位置。我用坐标对定义O方块时特意让它占第0/1行、第1/2列这样旋转后中心点基本不偏移手感正常。3.3 消行与落定从下往上扫描合并加分数方块落到无法继续下移时把它写进m_board然后检查有没有满行。消行的实现我建议直接“从底部向上扫描 覆盖删除”而不是用STL的vector做insert/erase——二维数组在固定尺寸下用数组下标操作最快也好讲。void GameField::lockBlock() { // 1. 将当前形状固化到面板 for (int r 0; r TETROMINO_SIZE; r) { for (int c 0; c TETROMINO_SIZE; c) { if (m_currentShape[r][c] ! 0) { int boardRow m_currentRow r; int boardCol m_currentCol c; if (boardRow 0) { m_gameOver true; // 方块被锁定在面板上方游戏结束 return; } m_board[boardRow][boardCol] 1; } } } // 2. 消行从底往上满行则整行下移一行 int linesCleared 0; for (int row BOARD_HEIGHT - 1; row 0; ) { bool full true; for (int col 0; col BOARD_WIDTH; col) { if (m_board[row][col] 0) { full false; break; } } if (full) { // 把上面所有行下移一行 for (int r row; r 0; --r) { memcpy(m_board[r], m_board[r - 1], sizeof(m_board[r])); } memset(m_board[0], 0, sizeof(m_board[0])); linesCleared; // 注意当前行仍然需要重新检查因为上面掉下来一行 } else { --row; } } // 3. 按消行数加分每10行升一级 static const int SCORE_TABLE[] {0, 100, 300, 500, 800}; m_score SCORE_TABLE[linesCleared 4 ? linesCleared : 4]; if (m_score 0 m_score / 1000 m_level) { m_level; } // 4. 生成下一个方块 m_currentBlockType m_nextBlockType; m_nextBlockType rand() % SHAPE_TABLE.size(); m_currentRow 0; m_currentCol BOARD_WIDTH / 2 - 2; // 这里需要重新构造 m_currentShape根据m_currentBlockType查SHAPE_TABLE }逻辑说明消行循环里for (int row BOARD_HEIGHT - 1; row 0; )故意不写--row只在“当前行不是满行”时才递减。这是消行算法的核心细节——当你把上方所有行往下移一行后当前位置[row]处已经换成了新内容必须原地重查一次否则会漏消。memcpy整行覆盖效率高但这行代码只适用于固定宽度数组如果你改成vectorvectorint要换成assign。参数说明计分规则各不相同关键在于让“消四行”Tetris的收益显著高于消一行。SCORE_TABLE里800对应四行是100行的8倍这是经典任天堂计分规则的比例。等级用m_score / 1000 m_level判断也就是说每得1000分升一级这个阈值要和后面的下落速度联动调——等级升太快玩家来不及反应升太慢界面显得没变化建议初始500ms每级递减50ms、最低100ms。4. 让画面动起来QT信号槽、定时器与界面渲染4.1 用QTimer驱动游戏循环为什么游戏不需要while循环控制台版本的俄罗斯方块可以用while(true)加sleep塞延迟但在QT里绝对不能这么写——while循环会阻塞UI线程的事件循环窗口直接变成“未响应”。QT的做法是用QTimer周期性发出超时信号每次超时执行一次“方块下落”动作然后主动刷新界面。// mainwindow.h #include QMainWindow #include QTimer #include gamefield.h class GameFieldWidget : public QWidget { Q_OBJECT public: explicit GameFieldWidget(QWidget *parent nullptr); protected: void paintEvent(QPaintEvent *event) override; void keyPressEvent(QKeyEvent *event) override; private slots: void onTimeout(); private: GameField m_game; QTimer m_timer; int m_previewBlock; // 下一个方块类型从m_game取 };// mainwindow.cpp 关键实现 GameFieldWidget::GameFieldWidget(QWidget *parent) : QWidget(parent) { setFocusPolicy(Qt::StrongFocus); // 关键让按键事件能落到这个控件 m_timer.setInterval(500); // 初始500ms下落一格 connect(m_timer, QTimer::timeout, this, GameFieldWidget::onTimeout); m_timer.start(); } void GameFieldWidget::onTimeout() { if (!m_game.moveDown()) { m_game.lockBlock(); } update(); // 触发重绘 }逻辑说明整个游戏循环的骨架就是“定时器到点 → 尝试下移 → 不能下移就锁定 → 重绘”。moveDown()返回false表示底部有阻碍此时应立刻lockBlock()。为什么lockBlock()不放在GameField的moveDown()内部自动调用因为“锁定”和“生成新方块”在交互上需要明确时机显式调用更清晰也便于你在锁定后加消行音效、动画之类的特判。参数说明m_timer.setInterval(500)是毫秒数。千万不要用m_timer.start(500)这个重载——它每次调用都会重置计时起点暂停/恢复逻辑会混乱。setFocusPolicy(Qt::StrongFocus)必须设置否则键盘事件会被父窗口吞掉你按下方向键游戏毫无反应——这是QT小游戏最常见的翻车点之一。4.2 用QPainter绘制方块的几种策略直接画矩形远比贴图靠谱绘制俄罗斯方块界面有两种主流方案一是用QLabel加样式表拼格子二是用QPainter在paintEvent里直接画矩形。前者写起来快但性能一般后者是标准做法。我推荐后者核心代码很短。void GameFieldWidget::paintEvent(QPaintEvent *) { QPainter painter(this); const int cellSize 30; // 每个格子30像素 const int offsetX 20, offsetY 20; // 面板左边距和上边距 // 1. 画背景网格 painter.fillRect(offsetX, offsetY, GameField::BOARD_WIDTH * cellSize, GameField::BOARD_HEIGHT * cellSize, QColor(30, 30, 30)); // 2. 画已固定的方块 for (int row 0; row GameField::BOARD_HEIGHT; row) { for (int col 0; col GameField::BOARD_WIDTH; col) { if (m_game.getCell(row, col) ! 0) { painter.fillRect(offsetX col * cellSize, offsetY row * cellSize, cellSize, cellSize, QColor(0, 180, 255)); } } } // 3. 画正在下落的方块 // 需要GameField暴露当前形状的4x4矩阵和位置 int currentRow m_game.getCurrentRow(); int currentCol m_game.getCurrentCol(); const auto shape m_game.getCurrentShape(); for (int r 0; r GameField::TETROMINO_SIZE; r) { for (int c 0; c GameField::TETROMINO_SIZE; c) { if (shape[r][c] ! 0) { painter.fillRect(offsetX (currentCol c) * cellSize, offsetY (currentRow r) * cellSize, cellSize, cellSize, QColor(255, 200, 0)); } } } }逻辑说明paintEvent每次被调用都会全量重绘整个面板。对于俄罗斯方块这种格子数不超过200的游戏全量重绘的代价完全可以忽略不要去做“只重绘变化区域”的优化——那会引入复杂到失控的脏矩形管理性能收益却不明显。QPainter::fillRect比drawRect少一个描边操作绘制纯色格子用fillRect绘制边框才用drawRect。参数说明cellSize 30是经验值——10列30像素面板宽度300像素加上边距正好能放进默认的QMainWindow窗口而不拥挤。如果你要做“下一个方块预览”在旁边再画一个4x4的小面板那个小面板的cellSize用12到15比较合适视觉层级更清晰。4.3 键盘事件的两种写法重写keyPressEvent还是用QShortcut按键控制方块移动可以直接重写keyPressEvent也可以用QShortcut注册快捷键。我建议小游戏用重写keyPressEvent因为你要同时处理“按键方向”和“按键是否触发硬降”两个逻辑还可能要处理长按自动连发。void GameFieldWidget::keyPressEvent(QKeyEvent *event) { bool handled true; switch (event-key()) { case Qt::Key_Left: m_game.moveLeft(); break; case Qt::Key_Right: m_game.moveRight(); break; case Qt::Key_Down: m_game.moveDown(); break; case Qt::Key_Up: m_game.rotate(); break; case Qt::Key_Space: m_game.hardDrop(); m_game.lockBlock(); // 硬降后立即锁定并结算 break; case Qt::Key_P: if (m_timer.isActive()) m_timer.stop(); else m_timer.start(); break; default: handled false; break; } if (handled) { update(); // 按键导致游戏状态变化主动重绘 } QWidget::keyPressEvent(event); }逻辑说明Qt::Key_Space触发的“硬降”其实是循环调用moveDown()直到失败然后立刻锁定。这里有个小坑hardDrop()内部最好只做下落动作不要包含锁定动作因为锁定可能触发游戏结束的检测这个逻辑我们希望由外部控制方便统一在lockBlock()之后刷新分数UI。Qt::Key_P是暂停/恢复的常见约定m_timer.stop()和m_timer.start()之间有个细节——重新start()会从头计时所以如果你设置了500ms周期暂停10秒后恢复方块要等满500ms才动第一格这是正常现象不用修。参数说明event-key()拿的是Qt键码不是ASCII码。新手常犯的错误是写if (event-key() a)这肯定是编译不过的。另外如果你希望长按方向键连续移动需要自己维护一个“按键状态定时器”Qt默认的键盘重复事件和游戏节奏不一定匹配我一般不做这个功能——课设答辩时按一次动一格已经够用。5. 高分项目怎么炼成的QT课设常见避坑清单5.1 编译时报错找不到QT头文件路径与构建套件不匹配现象新建QT Widgets项目后#include QMainWindow报红错误信息类似:-1: error: dependent ..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwidgets does not exist。原因这种dependent ... does not exist错误九成是构建套件选错或QT版本不匹配。下载QT时官方提供MSVC2019、MSVC2022、MinGW三套预编译库你装了5.15.2 msvc2019_64但编译器用的是MinGW头文件路径对不上。解决到“工具 → 选项 → Kits构建套件→ 编译器”检查当前套件。要么安装配套的MinGW版QT要么把编译器切换成MSVC。我一般建议直接统一用MSVC——Qt5.15之后的MSVC版本对调试器支持更稳MinGW在断点查看STL容器内容时经常显示一堆加密的内部字段排查问题非常痛苦。注意切换套件后一定要执行“清理(qmake)”再重新构建旧缓存里残存的Makefile会让路径错误反复出现。5.2 方块能移动但画面不刷新忘记让QT重绘现象按下方向键后控制台里能看到方块位置变了界面上却纹丝不动鼠标拖动窗口边缘时画面突然跳变到正确位置。原因游戏逻辑更新了m_game的数据但你没有调用update()。QT的绘制是事件驱动的数据变了不会自动触发重绘要由你显式通知。而拖动窗口会强制重绘所以看起来“拖动一下才刷新”。解决在所有改变游戏状态的地方按键处理结束、定时器超时、锁定、消行、加分之后统一调用update()。我自己的习惯是在MainWindow里加一个私有方法refresh()内部只做一件事update();然后转发分数和等级变更信号。这样不会漏掉刷新点。5.3 方块旋转时“穿墙”或“卡进墙里”旋转算法少了碰撞后回退现象方块靠左贴墙时按上键方块有一部分直接出现在墙外画面左上角出现残缺块或者方块贴右墙时旋转后直接消失。原因rotate()函数在调用canMove判断失败后没有做任何恢复动作。更早的写法是“先旋转矩阵再检查是否越界越界就反向旋转一次”但反向旋转的计算如果写错方块就会永久偏移。解决严格按“先算出旋转结果再碰撞检测检测通过才写入”的顺序编码。如果要做踢墙在旋转失败后按顺序尝试“左移一格、右移一格、左移两格、右移两格”每次尝试都用同样的canMove(rotated)判定。这个流程可以在rotate()里加一个for循环完成不要图省事直接放弃旋转——贴墙旋转是玩家高频操作做不好被扣分很冤。5.4 消行时整行原地消失但上方有残留色块竖直方向坐标搞反现象消掉一行后被消掉的行没了颜色但上面某一行出现了一个只有半截的色块。原因绘制时把行列坐标写反了——fillRect(y * cellSize, x * cellSize, ...)把行当列画了。俄罗斯方块的坐标系统是“行号向下、列号向右”很多线段从Qt示例里复制的代码用的是x, y命名迁移到二维数组时很容易把row和col对调。解决在paintEvent里统一用offsetY row * cellSize作为yoffsetX col * cellSize作为x凡是出现row * cellSize的地方检查它传给了x参数没有。这个坑属于“现象极其诡异、原因极其简单”的典型调试方法是在有方块的格子处打印row, col和对应坐标对比两三次就能定位。5.5 方块下落速度随等级提升没变化定时器间隔改完没生效现象分数涨到升级阈值m_level打印出来变了但方块下落速度感觉一模一样。原因setInterval()并不会重置一个已经start()的定时器你必须先stop()再重新设置间隔最后再start()或者直接调用m_timer.setInterval(newInterval)后再手动m_timer.start()。很多人只在GameField::setLevel()里改了数字没让定时器重新加载。解决把“响应等级变化”的逻辑放在QT侧void MainWindow::onLevelChanged(int level) { int interval 500 - (level - 1) * 50; if (interval 100) interval 100; m_timer.setInterval(interval); m_timer.stop(); m_timer.start(); }其中500 - (level - 1) * 50做到最低100ms的档位。注意stop()后的start()会重新计价玩家会感觉“升级后下一格来得特别快”这是正常现象——比起慢慢变快这种突变的“卡一下”感反而能强化升级反馈。6. 从跑通到加分答辩展示的细节与最后的强化方向到这里你已经有一套逻辑完整、界面能看的俄罗斯方块了。但“高分项目”和“能跑的项目”之间还有一段距离。我可以给你几个投入产出比最高的加分方向按难度排序第一给方块加颜色。m_board里存的值不再固定为1而是存方块的类型编号7种绘制时用七种不同颜色。这个改动只在lockBlock()里加一行赋值收益却很大——答辩现场一片彩色方块比全蓝格子观感好太多评委一眼就能看出你确实做了数据建模。第二做“下一个方块预览框”。在GameField里已经预留了m_nextBlockType你只要在界面右侧加一个QWidget在它的paintEvent里用同样的QPainter逻辑画一个4x4小网格即可。这个功能能体现你对“状态管理”的理解——当前方块和下一块必须同时存在、互不干扰。第三加一个简单的开始界面和结束界面。开始界面用QDialog加一个“开始游戏”按钮结束界面在m_game.isGameOver()返回true时弹出一个QMessageBox显示最终分数。这串逻辑能把你对QT信号槽的理解完整展示出来而且实现难度很低。至于项目文档我建议按“需求分析 → 设计思路附类图 → 核心算法说明附旋转矩阵推导、碰撞检测伪代码 → 运行截图 → 未来改进”五段来写。答辩时评委最常问的三个问题基本是游戏主循环怎么实现的、方块旋转矩阵怎么处理的、内存管理有没有自己管new/delete。你准备到“定时器驱动 矩阵转置 QT父子对象机制自动析构”这三个回答基本就能从容应对。这套架构和代码我前前后后带过几届学生走完过最大的体会是俄罗斯方块课设翻车通常不是算法难而是大家急着写界面、把逻辑和UI搅在一起最后哪个都修不动。我当时第一次写也犯了这个错花了两天时间在paintEvent里改数组现在回头看最初花半小时把类拆开后面全程都在享受写代码。希望这篇笔记能帮你绕过那些我踩过的坑早点把时间花在真正能加分的地方。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑