☰
基于QT与C++的俄罗斯方块课设:从源码到答辩全解析
2026/10/8 19:06:45 网站建设 项目流程

简介:面向中高级C++学习者与计算机相关专业毕业生,这是基于Qt_C++的俄罗斯方块游戏完整毕业设计资料包,完整覆盖从源码实现到论文答辩的全流程。内容包含方块随机生成、移动旋转、碰撞检测、消行计分、暂停结束等核心玩法逻辑,并配套毕业设计论文、任务书、中期检查表与答辩PPT,可帮助读者理解Qt图形视图框架及C++面向对象编程的实际应用。压缩包共45个文件,以cpp源文件、h头文件、doc文档、exe可执行程序及pptx答辩演示为主,整体大小4.69MB,目录划分清晰,便于按模块查阅。目前已有637人学习下载,适合用于课程设计、毕业设计参考或Qt游戏开发入门实践。

1. 基于QT_C++的俄罗斯方块:一份源码+论文+答辩PPT的完整课设方案

每到期末,总有人拿一份“基于QT_C++的俄罗斯方块游戏设计与实现源码论文答辩ppt.rar”来问我,里面有什么、能不能直接跑、答辩会不会被问穿。我的回答很直接:这类压缩包的价值不在文件本身,而在它正好覆盖课设的三道关卡——程序能跑通、报告能写圆、答辩能扛住追问。这里不谈下载和解压,只把从逻辑设计到界面实现,再到论文和答辩PPT的完整链路讲清楚:方块的状态怎么表示、旋转和碰撞怎么做、QT里怎么用QPainter和QTimer搭一个最小游戏循环,哪些编译和运行坑要提前避开。适合正在做QT课设的同学,也适合想快速过一遍最小完整游戏工程的开发者——新手可以照着步骤调,熟手可以直接跳到第4章看边界条件。

2. 从方块到游戏规则:状态、旋转、碰撞与消行的核心实现

动手写QT界面之前,我习惯先把游戏逻辑单独拎出来。俄罗斯方块真正难的不是画图,而是“规则数学化”:方块怎么存储、旋转怎么算、碰撞怎么判断、消除怎么处理。把逻辑层和界面层解耦之后,后面不管用QWidget、QML还是换成控制台程序,游戏规则都不用重写。

2.1 用4x4矩阵表示方块:为什么不用坐标列表

7种俄罗斯方块每一种都由4个单元格组成。常见做法是用一个形状数组把7种方块先定义好。我选择用4x4二维数组,0表示空格、1表示有格子,而不是直接存4个坐标,原因只有一个:旋转能退化成统一的矩阵运算,不用为每种方块单独写旋转分支。

// 用枚举给方块类型编号,方便映射颜色和形状 enum TetrominoType { Type_I = 1, Type_O, Type_T, Type_S, Type_Z, Type_J, Type_L }; // 7 种方块,每种一个 4x4 形状;这里列 T、I、O 三个做示例 static const int shapes[][4][4] = { { // Type_I {0,0,0,0}, {0,0,0,0}, {1,1,1,1}, {0,0,0,0} }, { // Type_O {0,0,0,0}, {0,1,1,0}, {0,1,1,0}, {0,0,0,0} }, { // Type_T {0,0,0,0}, {0,1,0,0}, {1,1,1,0}, {0,0,0,0} } // S、Z、J、L 形状按同样方式依次补全 };

这个数组的索引就是方块类型,绘制和碰撞检测都靠它。用4x4矩阵的另一个好处是旋转公式统一:顺时针旋转90度,坐标变换是newX = oldY,newY = 3 - oldX。旋转函数可以直接写在游戏核心类里,就地修改当前形状:

// 就地旋转 4x4 矩阵:顺时针 90 度 void rotateMatrix(int shape[4][4]) { int temp[4][4]; for (int row = 0; row < 4; ++row) { for (int col = 0; col < 4; ++col) { temp[col][3 - row] = shape[row][col]; } } memcpy(shape, temp, sizeof(temp)); }

row和col是原始矩阵的行列下标,temp[col][3 - row]是旋转后的新位置。这个函数不关心方块具体形状,只要传入4x4数组都能正确旋转。整局游戏里,m_currentShape这个二维数组保存当前活动方块的朝向,旋转就调用一次rotateMatrix。

方块初始形状(4x4 内的非零区域)常见配色思路
I 型中间一行连续4格青色
O 型左上角2x2黄色
T 型上一行中间1格 + 下一行3格紫色
S 型两行错位,像阶梯绿色
Z 型两行错位反向红色
J 型L 形,长边在左蓝色
L 型L 形,长边在右橙色

所有方块数据集中放在一个数组里,界面层根据类型枚举去查颜色,逻辑层根据类型枚举去取形状,这比在代码里散落十几个临时数组要干净得多。

2.2 碰撞检测:先计算目标位置,再判断越界和重叠

碰撞检测是俄罗斯方块最容易写乱的部分。很多新手先移动方块,发现撞了再往回推,结果边界情况堆成山。正确做法是做一个独立函数:传入目标坐标和当前形状,返回“能不能放这里”。移动、旋转、硬降之前都先问它一遍。

// 判断形状放到 (newX, newY) 位置时是否合法 bool GameBoard::isCollision(int newX, int newY, const int shape[4][4]) const { for (int row = 0; row < 4; ++row) { for (int col = 0; col < 4; ++col) { if (shape[row][col] == 0) continue; // 空格子不参与判断 int boardX = newX + col; int boardY = newY + row; // 左右越界或落到底部 if (boardX < 0 || boardX >= 10 || boardY >= 20) return true; // 撞到已经固定的方块;boardY < 0 是出生点上方,要放行 if (boardY >= 0 && m_board[boardY][boardX] != 0) return true; } } return false; }

这里参数newX和newY是当前方块左上角对应的棋盘坐标。判断分三层:先跳过形状里的空格;再检查是否超出棋盘左右边界和底部;最后检查是否撞到固定方块。boardY < 0时必须放行,因为新方块初始位置在棋盘可见区上方,那里没有数组行可以访问,也允许方块先“悬空”出现。

移动方块时,不要直接修改坐标再验证,而是先调用tryMove,能走才更新:

// 尝试把当前方块移动到 (newX, newY) bool GameWidget::tryMove(int newX, int newY) { if (!m_board.isCollision(newX, newY, m_board.currentShape())) { m_board.setCurrentPos(newX, newY); return true; } return false; }

在定时器下落里,tryMove(currentX, currentY + 1)返回false,说明下面被堵住,这时才把当前方块写入棋盘数组,变成固定状态。

2.3 消行与计分:先标志整行,再整体下移

消行逻辑不算复杂,但有两个细节容易写错:一是要从底往上扫,因为消掉一行后上面的行才会下移;二是一次消多行时,下移之后当前行号要重新检查,否则会漏掉连续满行。下面这段直接在棋盘数组上操作:

// 消除满行,返回本次消除的行数 int GameBoard::clearLines() { int cleared = 0; for (int row = 19; row >= 0; --row) { bool full = true; for (int col = 0; col < 10; ++col) { if (m_board[row][col] == 0) { full = false; break; } } if (full) { // 该行以上整体下移一行 for (int r = row; r > 0; --r) { for (int c = 0; c < 10; ++c) { m_board[r][c] = m_board[r-1][c]; } } for (int c = 0; c < 10; ++c) { m_board[0][c] = 0; } ++cleared; ++row; // 当前行被替换成上一行内容,要重新检查 } } return cleared; }

所有数据操作都在m_board这个二维数组上完成,10列20行,下标[row][col]。注意循环里那个++row,它配合for循环自身的--row,让下一轮还是检查同一行——因为该行现在装的是原来上一行的内容,可能又是满行。

计分规则我采用最常见的查表方式:消除1行100分,2行300分,3行500分,4行800分,也就是一次消4行被称为“Tetris”。得分还要乘上当前等级系数:

int scoreForLines(int lines) { const int scores[] = {0, 100, 300, 500, 800}; return (lines >= 0 && lines <= 4) ? scores[lines] : 0; }

固定当前方块之后,还要生成下一个方块。生成逻辑里隐藏着一个游戏结束判断:新方块出生在棋盘顶部,如果出生位置已经被现有方块占用,说明堆积到顶,游戏就结束。这可以写成isGameOver(),在界面层调用。到这里,游戏核心已经能单独工作,不依赖任何窗口类,答辩时被问到“怎么测试逻辑层”,这就是答案——用临时main函数跑一遍,不需要打开界面。

3. 用QT Creator把逻辑变成界面:QPainter绘图、QTimer主循环与键盘控制

逻辑层完成后,下一步是在Qt Creator里建界面。项目模板选Qt Widgets Application,主窗口放一个游戏画布、一个分数面板、一个下一个方块预览区。这里我完全不碰QML,课程设计里QWidget方案代码直观、调试方便,老师也容易看懂,没必要上QML那套更复杂的渲染模型。

3.1 用QPainter在paintEvent里重画整个游戏区

俄罗斯方块的界面核心就是“把格子数据映射成屏幕色块”。在自定义的GameWidget类里重写paintEvent,每次数据变化后调用update()触发重绘。QPainter画矩形既简单又足够快:

void GameWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, false); // 棋盘是像素风格,不开抗锯齿 const int cellSize = 30; // 每个格子的边长,像素 const int boardRows = 20, boardCols = 10; // 画出已经固定的方块 for (int row = 0; row < boardRows; ++row) { for (int col = 0; col < boardCols; ++col) { int type = m_board.cell(row, col); if (type == 0) continue; painter.fillRect(col * cellSize, row * cellSize, cellSize, cellSize, colorForType(type)); painter.setPen(QPen(Qt::gray, 1)); painter.drawRect(col * cellSize, row * cellSize, cellSize - 1, cellSize - 1); } } // 画出当前正在运动的方块 const int (*shape)[4] = m_board.currentShape(); int curX = m_board.currentX(); int curY = m_board.currentY(); for (int row = 0; row < 4; ++row) { for (int col = 0; col < 4; ++col) { if (shape[row][col] == 0) continue; int boardX = curX + col; int boardY = curY + row; if (boardY < 0) continue; // 出生点上方不画,避免越界 painter.fillRect(boardX * cellSize, boardY * cellSize, cellSize, cellSize, colorForType(m_board.currentType())); painter.setPen(QPen(Qt::gray, 1)); painter.drawRect(boardX * cellSize, boardY * cellSize, cellSize - 1, cellSize - 1); } } }

这里cellSize是关键参数,我一般取30。太小在高分屏上看起来很挤,太大又会占满窗口。固定窗口大小时不建议响应resizeEvent动态重算格子大小,那会引入大量边界判断。painter.drawRect里减1是为了让相邻格子之间留一条细线,看起来更像棋盘。

颜色映射colorForType建议用switch按类型返回QColor,和前面表格里的配色保持一致。不要在多个地方散落地写case 1: return QColor(...),后期想换色时容易漏改。

3.2 QTimer驱动下落:主循环不在while里,在事件循环里

不少新手在第一个版本里用while(true)写下落循环,结果是窗口直接卡死。原因是Qt程序靠事件循环驱动,paintEvent、键盘事件、定时器都要在事件循环空闲时才能触发。正确做法是用QTimer定时触发“下落一格”:

// GameWidget 构造函数里初始化定时器 m_timer = new QTimer(this); m_timer->setInterval(500); // 初始下落间隔 500ms connect(m_timer, &QTimer::timeout, this, &GameWidget::onTimerTick); m_timer->start();

定时器到点后做的事,就是之前逻辑层的tryMove:能下落就下落,不能下落就固定方块并产生下一个:

void GameWidget::onTimerTick() { if (m_board.tryMove(m_board.currentX(), m_board.currentY() + 1)) { // 正常下落一格 } else { // 固定当前方块,消行,更新分数,生成下一个方块 m_board.fixCurrentBlock(); int lines = m_board.clearLines(); m_score += scoreForLines(lines) * m_level; m_lines += lines; updateLevel(); m_board.spawnNextBlock(); if (m_board.isGameOver()) { m_timer->stop(); QMessageBox::information(this, "游戏结束", "得分: " + QString::number(m_score)); } } update(); }

等级影响下落速度,我一般把“每消10行升1级”和“每级快40ms”绑在一起:

void GameWidget::updateLevel() { int newLevel = m_lines / 10 + 1; if (newLevel != m_level) { m_level = newLevel; int interval = 500 - (m_level - 1) * 40; m_timer->setInterval(interval > 80 ? interval : 80); // 下限 80ms } }

初始500毫秒比较休闲,想刺激一点可以设400;80ms是硬下限,再快人的反应就跟不上。答辩时老师常问“难度怎么递增”,答案就是这条公式:消行数除以10取整得到等级,等级越高,QTimer的interval越短。

3.3 键盘事件:移动、旋转与硬降的按键处理

在GameWidget里重写keyPressEvent,用event->key()判断按键:

void GameWidget::keyPressEvent(QKeyEvent *event) { if (!m_timer->isActive()) return; // 游戏结束时忽略输入 switch (event->key()) { case Qt::Key_Left: m_board.tryMove(m_board.currentX() - 1, m_board.currentY()); break; case Qt::Key_Right: m_board.tryMove(m_board.currentX() + 1, m_board.currentY()); break; case Qt::Key_Down: if (m_board.tryMove(m_board.currentX(), m_board.currentY() + 1)) { m_score += 1; // 软降一格加 1 分,作为操作奖励 } break; case Qt::Key_Up: rotateCurrentBlock(); break; case Qt::Key_Space: hardDrop(); break; default: break; } update(); }

硬降是一次性落到底,用循环不断向下尝试,落定后立即固定,不等下一次定时器:

void GameWidget::hardDrop() { while (m_board.tryMove(m_board.currentX(), m_board.currentY() + 1)) { m_score += 2; } m_board.fixCurrentBlock(); int lines = m_board.clearLines(); m_score += scoreForLines(lines) * m_level; m_lines += lines; updateLevel(); m_board.spawnNextBlock(); }

键盘响应里有个很常见的坑:按一下方向键,方块连续移动好几格。这不是逻辑问题,而是操作系统的键盘自动重复在起作用——按住方向键不放,keyPressEvent会被系统重复触发。课程设计阶段通常不影响,但想要手感跟手,可以在事件开头加一句if (event->isAutoRepeat()) return;。

分数面板和下一个方块预览都很直接:分数用QLabel更新文本;预览区做一个固定大小的NextShapeWidget,重写paintEvent画m_board.nextShape(),每次固定方块后调用update()。预览窗口的cellSize建议设小一些,比如18像素,视觉上更协调。

到这里,一个能玩的完整版本已经成型。剩下的工作,就是把它整理成课设交付物,并处理各种“我这环境跑不起来”的问题。

4. 常见问题排查:编译依赖、刷新失效、焦点丢失与旋转踢墙的5个踩坑现场

这一章写我帮人调QT课设时反复遇到的现场,每组按“现象 → 原因 → 解决”的节奏来,按这个顺序扫问题,大部分能在十分钟内定位。

4.1 编译报错:依赖的Qt头文件路径打不开

现象:编译时出现类似:-1: error: dependent '..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwidgets' does not exist.的报错,工程文件里能看到一长串相对路径。

原因:这类报错几乎都是Kit环境不匹配。msvc2019_64表示用MSVC编译器,如果机器上没装对应版本的Visual Studio编译器,或者Qt安装目录里根本没有msvc2019_64这个子目录,qmake就会沿相对路径往上找,最终找到错误的依赖。另一个触发条件是工程被解压到特别深的目录,“..\”层层回退超出预期。

解决:不要手动维护INCLUDEPATH里的Qt路径。在Qt Creator里重新选择编译器Kit,比如装了Qt 5.15.2 MSVC 2019 64位,就选Qt 5.15.2 MSVC2019 64bit。如果习惯用Visual Studio 2022,要装Qt Tools扩展,并在扩展里重新指向Qt安装目录。用CMake的话,设置CMAKE_PREFIX_PATH指向Qt根目录,不管VSCode还是Qt Creator都认这个变量。我见过最快修复是把工程放到D盘根目录的短路径下,例如D:\tetris,再重新CMake一次。

提示:下载Qt时,按自己已装的编译器选套件。装的是VS2019就选MSVC2019_64,装的是MinGW就选对应MinGW版本,混着用必出依赖错误。

4.2 画面不刷新或直接崩溃:数据变了界面还是老样子

现象:按方向键有逻辑反应,但画布不动;有时候动一下窗口大小又突然刷新。更严重的是按某个键后程序直接闪退。

原因:画面不刷新是因为改了游戏数据,但没调用update()。Qt的绘图是“被动”的,paintEvent只在该重绘时执行,不是每次数据变化都自动触发。崩溃大多是数组越界:棋盘数组是20行,但绘制或碰撞时没有判断boardY是否小于0,直接拿负下标访问了m_board[-1]。

解决:在所有改变游戏状态的路径末尾统一调用update(),包括键盘处理、定时器回调、旋转和硬降。绘制和碰撞检测里都要先判断boardY < 0 || boardY >= 20。排查时可以临时在paintEvent里加qDebug()输出,看刷新到底触发没有。

4.3 方向键没反应:键盘事件收不到

现象:窗口能正常绘制,方块自动下落,但按任何方向键都没反应。

原因:多数情况是焦点问题。keyPressEvent只有窗口获得键盘焦点才会触发,如果界面上某个QPushButton被点击过,焦点就不在画布上。而且QWidget默认的focusPolicy是NoFocus,自定义的GameWidget根本没资格接收焦点。

解决:在构造函数里设置setFocusPolicy(Qt::StrongFocus);,窗口显示后调用一次gameWidget->setFocus()。如果旁边还有面板控件,确保没有按钮抢走焦点。调试办法:在keyPressEvent第一行打印qDebug() << event->key(),按方向键看有没有输出——没输出就是焦点问题。

4.4 旋转后穿墙或瞬移:旋转中心与踢墙修正

现象:方块旋转后突然跑到棋盘外面,或者贴墙的方块一转就消失。

原因:4x4矩阵的旋转中心在所有方块上并不一样。T型方块左右对称,原地转没问题;但L型、J型在贴边时旋转,新形状会越过边界。如果碰撞检测返回“不合法”就直接丢弃旋转,表现为“转不动”;更差的实现强制写入非法坐标,就是穿墙。

解决:采用简单的单方块踢墙修正,旋转后先试偏移,再决定是否接受:

void GameWidget::rotateCurrentBlock() { int oldShape[4][4]; memcpy(oldShape, m_board.currentShape(), sizeof(oldShape)); int oldX = m_board.currentX(), oldY = m_board.currentY(); rotateMatrix(m_board.currentShape()); // 先就地旋转 // 常见踢墙偏移序列:不动、左1、右1、左2、右2 int kicks[] = {0, -1, 1, -2, 2}; for (int i = 0; i < 5; ++i) { if (!m_board.isCollision(oldX + kicks[i], oldY, m_board.currentShape())) { m_board.setCurrentPos(oldX + kicks[i], oldY); return; } } // 五个位置都不行,恢复旋转前的形状和位置 memcpy(m_board.currentShape(), oldShape, sizeof(oldShape)); m_board.setCurrentPos(oldX, oldY); }

这里kicks数组里每个值代表“在X方向上的偏移尝试”。为什么按这个顺序?因为大多数旋转冲突只需要向左或向右错开一个格子就能解决。这个经验序列够课设用,不需要复刻现代俄罗斯方块的SRS超级旋转系统。

4.5 按住方向键连续移动:系统键盘重复干扰

现象:按一次左键移动一格,长按时方块像打字一样每隔一段时间动一下,手感很差。

原因:Qt的keyPressEvent不仅接收第一次按下,也接收操作系统生成的自动重复事件,event->isAutoRepeat()返回true。

解决:在keyPressEvent开头加if (event->isAutoRepeat()) return;,直接拒绝重复事件。代价是按住不放不会连续移动,需要连续移动就得自己做“长按加速”。一个折中方案是:记录当前按键方向,用另一个短间隔QTimer在按住期间驱动移动,松开时停掉。课设阶段用isAutoRepeat()拒绝重复足够了,把这点写进论文的“问题与改进”反而是加分项。

5. 从源码到论文再到答辩PPT:课程设计交付物怎么组织才不容易被问倒

很多同学把源码写完就松口气,结果答辩时被问到“代码结构怎么设计”“为什么这样实现”直接被问住。源码、论文、PPT其实是同一件事的三种表达,必须按同一套思路组织,前后对得上。

5.1 源码目录怎么分:界面与逻辑解耦

拿到一份俄罗斯方块源码,我第一眼看目录结构。专业课设里常见的丢分点是所有代码塞进两个文件,或者把游戏逻辑写死在按键事件里。我习惯按“核心逻辑”和“界面”两层拆分:

文件/目录职责关键内容
src/core/GameBoard.h/cpp游戏规则棋盘数组、碰撞检测、消行、生成新方块
src/core/Tetromino.h/cpp方块数据7种方块形状、旋转函数、类型枚举
src/ui/GameWidget.h/cpp游戏画布paintEvent、keyPressEvent、QTimer
src/ui/MainWindow.h/cpp主窗口布局、分数面板、下一个方块预览、按钮
main.cpp程序入口创建QApplication和MainWindow

GameBoard类里不出现任何QPainter、QKeyEvent相关代码,逻辑层用纯C++就能测试。答辩时直接说“我先把游戏核心设计成不依赖界面的类,再用QWidget做表现层”,这句话本身就是设计说明里的一节。

交作业前还有一个发布步骤:用Qt命令行工具windeployqt把动态库拷到release目录,再把Visual C++ Redistributable运行库的说明写进使用文档。很多老师不是在Qt Creator里点运行来验收,而是直接解压你的发布包双击exe,没有这两个东西就白给。

5.2 论文和设计报告的写作顺序

论文不是从摘要写到总结的线性过程。我的经验是先把骨架搭出来:第一章绪论,写背景与意义;第二章需求分析,写功能需求和非功能需求;第三章总体设计,写类图和模块划分;第四章详细设计,放核心代码与算法说明;第五章系统测试,列测试用例表;第六章总结。课程设计论文不追求创新,追求自洽,前后呼应。

章节篇幅建议必写内容
摘要300-500字选题目的、用的技术、实现的功能
需求分析800-1000字功能需求:移动、旋转、下落、消行、计分、游戏结束
总体设计1000-1500字模块划分、类图、游戏流程
详细设计2000-3000字isCollision原理、旋转矩阵、消行算法、定时器机制
系统测试600-1000字边界用例:贴墙旋转、顶出方块、快速下降、多行消除

设计报告里最容易加分的是“测试用例表”。比如“贴左墙连续旋转5次,不发生越界”“底行全满时消行并正确计分”。这些用例恰好是上一章踩坑内容的反面,写论文时可以直接复用。

5.3 答辩PPT的10页结构与常见追问

答辩PPT页数不在多,10页以内,每页只讲一个点。最忌把大段代码贴上去,核心逻辑永远用示意图和伪代码。

页码主题该页停留时间
1封面:题目、姓名、学号10秒
2项目背景与目标30秒
3技术选型:为什么用QT和C++40秒
4总体设计:模块划分和类图60秒
5核心逻辑:方块表示与旋转60秒
6核心逻辑:碰撞检测与消行60秒
7界面交互:QTimer、键盘事件40秒
8测试结果与演示录屏60秒
9遇到的问题与解决40秒
10总结与拓展方向30秒

答辩时老师喜欢追问的点,提前准备答案:

  • “碰撞检测怎么实现的?” 回答思路:遍历当前方块4x4形状的所有非空格子,对每个格子计算它在棋盘上的坐标,再检查越界和是否已被占用。
  • “旋转怎么做?” 回答思路:坐标变换实现顺时针90度旋转,旋转后做偏移修正。
  • “计分规则怎么定的?” 回答思路:按消行数查表,1/2/3/4行分别对应100/300/500/800分,再乘等级系数。
  • “难度怎么递增?” 回答思路:消满10行等级加1,每级把QTimer的interval缩短40毫秒,下限80毫秒。

这些回答和论文里的详细设计保持一致,就形成了完整闭环。演示时开场先把窗口拖到合适大小,按一次硬降展示消4行,再贴墙转一次,基本就没人追着问实现细节了。

6. 一点进阶技巧:阴影预览、存档读档与可录制的自动验证

6.1 阴影预览:一行逻辑就能加的效果

给当前方块画一个“落地预览”,叫ghost piece。做法是复制当前方块的位置,不断向下移动直到不能动,再用半透明色画出轮廓。这个功能在答辩演示时非常直观,能证明你理解碰撞检测的复用:

void GameWidget::paintGhost(QPainter &painter) { int ghostY = m_board.currentY(); while (!m_board.isCollision(m_board.currentX(), ghostY + 1, m_board.currentShape())) { ++ghostY; } painter.setOpacity(0.3); // 半透明,和实体方块区分 // 遍历4x4形状,把格子绘制到 (currentX, ghostY) 位置 }

6.2 存档读档:把棋盘序列化成文本

答辩时老师常问“数据怎么持久化”,用文本格式存棋盘就能回答:第一行存分数和等级,后面20行每行10个数字代表棋盘内容。落盘和加载各约30行代码:

void saveGame(const QString &path) { QFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) return; QTextStream out(&file); out << m_score << " " << m_level << "\n"; for (int row = 0; row < 20; ++row) { for (int col = 0; col < 10; ++col) { out << m_board.cell(row, col) << " "; } out << "\n"; } }

6.3 自动验证:用模拟按键代替手工测试

最后分享一个我自己的习惯做法:在main函数里支持一个-test命令行参数,启动后不进入主窗口,而是用固定随机种子初始化游戏,写入一串预定义按键序列,最后打印得分和棋盘状态。具体实现是加一个TestRunner类,调用GameWidget的公开接口模拟按键,不开真实窗口也能做回归验证。对课设来说,这比写单元测试更省时间。

我做这类课设最大的教训是把“能跑”当成了“没问题”。实际上从“能跑”到“能演示、能讲、能扛追问”还差着测试表和预演两步。建议答辩前至少完整打一局,专门验证10行升级、一次消四行、贴墙旋转这三个关键时刻——这几个点是老师最常盯着的。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询