简介:一套面向C++与Qt学习者的俄罗斯方块游戏毕业设计完整资料包,覆盖从算法设计到论文答辩的全流程,适合高校学生和游戏开发入门者学习图形界面与经典游戏逻辑的融合实现。压缩包共四十五个文件,大小约四点六九兆字节,以十一个cpp源码和三个h头文件为主,附有十四个编译中间文件及exe可执行程序,可直接运行体验;另有四份doc文档包含毕业设计论文、任务书、中期检查表,以及一份pptx答辩演示。已有六百三十七人学习下载。源码基于Qt框架,采用QGraphicsView绘制画面,实现了方块随机生成、下落、旋转与左右移动,并通过碰撞检测处理边界与堆叠,满行自动消除并计分,同时支持暂停、继续与结束状态切换;论文对技术选型、系统架构、模块设计与性能优化有详细剖析,答辩PPT提炼了核心成果,便于快速通读与展示。
1. 用QT/C++做俄罗斯方块,是入门游戏程序设计里最划算的事
用QT/C++做俄罗斯方块,是入门游戏程序设计里最划算的事。它不需要美术资源,不需要物理引擎,却把事件循环、定时器、绘图、数据结构和碰撞检测全走一遍。很多人以为难点是七种方块怎么转动,真正写起来才发现,坐标系换算和方块落到边界那一刻的处理才是玄学。这篇笔记从游戏设计讲到QT实现,最后把论文和答辩PPT的素材怎么组织也一并交代掉,照着做,课设或毕设都能少踩一半坑。
2. 核心玩法拆解:数据结构、方块旋转与消行判定
2.1 用10x20网格和7种方块定义游戏状态
俄罗斯方块的核心是一个二维网格。标准玩法是10列20行,超出顶部判定游戏结束。不要用动态数组乱搞,直接固定宽高,方便后面做碰撞检测和消行。我一般把网格数据放在Board类里,用一个QVector<QVector<int>>存储,0表示空,非0表示有方块下落并固定过。颜色信息另存一个二维QVector<QColor>,或者用一个整形编号到QColor的映射表。这样绘制和逻辑分离,答辩时能讲清楚“数据驱动视图”。
7种方块的形状定义用4x4矩阵最省事。I方块横着放是1行4个,竖着放是4行1列,如果只用2x2或3x3矩阵,旋转后还要处理平移,徒增复杂度。O方块是2x2,也可以放进4x4,代价是空行多一点,换来统一的旋转算法。下面是Tetromino类的骨架,实际代码里S、Z、J、L请补全,不要偷懒省略。
// tetromino.h #ifndef TETROMINO_H #define TETROMINO_H #include <QVector> #include <QColor> enum TetrominoType { I, O, T, S, Z, J, L, Count }; class Tetromino { public: explicit Tetromino(TetrominoType type) : m_type(type) { m_shape = shapeFor(type); } QVector<QVector<int>> shape() const { return m_shape; } TetrominoType type() const { return m_type; } void setShape(const QVector<QVector<int>>& shape) { m_shape = shape; } private: static QVector<QVector<int>> shapeFor(TetrominoType type) { switch (type) { case I: return {{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}}; case O: return {{1,1,0,0},{1,1,0,0},{0,0,0,0},{0,0,0,0}}; case T: return {{0,1,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}}; case S: return {{0,1,1,0},{1,1,0,0},{0,0,0,0},{0,0,0,0}}; case Z: return {{1,1,0,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}}; case J: return {{1,0,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}}; case L: return {{0,0,1,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}}; default: return {}; } } QVector<QVector<int>> m_shape; TetrominoType m_type; }; #endif这段代码的关键参数是固定4x4矩阵。所有旋转算法只对这个宽高有效,如果你想省空间改成4x2、3x3之类,旋转公式要同步改。shapeFor里每个方块的第一行代表它的出生姿态,后续旋转都从出生姿态开始,避免状态累积误差。还有一点,不清除前一帧残留:每次setShape都重新赋值整个矩阵,不要原地修改,否则旋转一次再旋转一次会得到变形体。
位置信息我建议用QPoint保存,x是列,y是行。为什么不用两个int?因为后面做踢墙偏移、移动判断时,QPoint + QPoint可以直接得到新位置,代码短一半。碰撞检测时,当前方块的每个非零格子与网格对应位置相加,越界或者已有格子非空,就认为碰撞。
网格宽10高20,方块边长建议设30像素。窗口大小就是1030加边距,高2030。不要硬编码到每个函数里,定义成常量constexpr int kColumnCount = 10;和constexpr int kRowCount = 20;,后面做等级加速和预览界面都要复用这两个数。
2.2 旋转的实现:从旋转矩阵到踢墙
顺时针旋转90度,标准矩阵变换是(x', y') = (-y, x),但直接套到屏幕坐标系容易搞晕。用嵌套循环更直白:转置后每一行倒序。我写一个独立函数,别放在旋转事件里,方便单元测试。
// board.cpp 或独立的 geometry.cpp #include <QVector> QVector<QVector<int>> rotateClockwise(const QVector<QVector<int>>& src) { const int n = src.size(); // 默认4x4 QVector<QVector<int>> dst(n, QVector<int>(n, 0)); for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { dst[j][n - 1 - i] = src[i][j]; } } return dst; }逻辑说明:dst[j][n-1-i]把源矩阵的第 i 行第 j 列放到目标矩阵的第 j 行倒数第 i 列。以I方块为例,横着的[1,1,1,1]会旋转成竖着的一列。参数src.size()要求矩阵是方阵,所以前面才强调统一用4x4。如果哪天改成非4x4,这里要换成独立的cols和rows参数。
旋转后不能直接落到网格上,得先做有效性检查。isValidPosition要遍历当前方块的形状矩阵,把每个1的格子坐标加上方块左上角的网格坐标,判断是否在0~9和0~19范围内,以及网格该位置是否为0。旋转时常见的坑是:方块贴在墙边时,旋转后有一部分跑出边界。这时应该尝试左右平移,而不是直接判断失败。这就是简化版踢墙。
bool Board::tryRotate(Tetromino& tet) { QVector<QVector<int>> rotated = rotateClockwise(tet.shape()); if (isValidPosition(tet.pos(), rotated)) { tet.setShape(rotated); return true; } const int kicks[] = {-1, 1, -2, 2}; for (int offset : kicks) { QPoint newPos(pos().x() + offset, pos().y()); if (isValidPosition(newPos, rotated)) { tet.setShape(rotated); setPos(newPos); return true; } } return false; }参数说明:kicks数组是四种可能的横向偏移量,先左1、再右1、再左2、再右2。这是SRS踢墙规则中JLSTZ方块旋转时的常用偏移组合,但真正的SRS还分旋转方向和不同踢墙表,课设做到这个简化版已经足够。如果你以后做更完整的版本,记得把踢墙表抽成一个数组,按(旋转前方向, 旋转后方向, 尝试序号)索引。另外,O方块根本不旋转,直接return true;I方块的踢墙偏移是别的值,但简化后也可以通用。这里我用了pos()和setPos(),实际得看你把位置存在哪个对象里,建议统一放在Board里的当前方块成员中。
旋转的边界条件最容易出bug,建议写一个打印函数,把网格和方块用QDebug输出。不要在脑子里转,把旋转前后的矩阵打印出来肉眼对比一次,比单步调试快得多。
2.3 消行判定与计分:一次循环解决
消行的标准做法是从下往上扫,发现某一行全部非空就删除它,然后在这个网格顶部插入一行空行。用QVector的好处是removeAt和prepend都是现成的,如果用C数组,你还得手动memmove,容易翻车。
int Board::clearLines() { int cleared = 0; for (int row = m_grid.size() - 1; row >= 0; --row) { bool full = true; for (int col = 0; col < m_grid[row].size(); ++col) { if (m_grid[row][col] == 0) { full = false; break; } } if (!full) continue; m_grid.removeAt(row); m_grid.prepend(QVector<int>(kColumnCount, 0)); ++cleared; ++row; // 关键:删掉的当前行被新行占据,需要重新检查同一索引 } return cleared; }说明:prepend会在头部插入一行全0,让所有已经固定的方块整体下移一行。++row这个操作很反直觉,它的作用是让循环里的--row被抵消,从而留在原地继续检查。因为删掉第row行后,原来第row-1行现在变成了第row行,如果这个补进来的行也是满的,必须接着消。为什么需要这样?因为比如一行满、上一行也满,如果删除当前行后不调整,下一次循环检查的是下面一行,就漏掉上面那行。
计分规则我建议按消行数给基础分:1行100、2行300、3行500、4行800,再乘以当前等级倍数。分数不需要在消行函数内部算,让clearLines只返回消掉的行数,由TetrisWidget更新分数。这样做的好处是逻辑分层,答辩时能说“Board只负责游戏规则,界面只负责展示”,经得起追问。大纲里若写了“软件工程”相关内容,这正好是例子。
消行后还需要刷新界面,但不要在clearLines里发信号,回到KeyPress或Timer的gameTick函数里统一调用update()。QTimer的tick流程是:尝试移动当前方块,如果没法继续下移就固化到网格上,然后调用clearLines,最后生成新方块并判断是否死亡。到这一步,游戏的核心逻辑已经齐了。
3. 搭建QT最小可玩版本:项目文件、定时器与键盘事件
3.1 创建QWidget工程:pro文件与类划分
很多人上来就用Qt Creator自带的“Qt Widgets Application”模板,但生成的代码里有MenuBar、StatusBar,对俄罗斯方块是多余的。我建议清空这些,直接自定义一个继承QWidget的类。工程文件尽量精简,下面是能编译通过的tetris.pro。
QT += core gui widgets TARGET = tetris TEMPLATE = app CONFIG += c++11 SOURCES += \ main.cpp \ Board.cpp \ TetrisWidget.cpp HEADERS += \ Board.h \ Tetromino.h \ TetrisWidget.h参数说明:QT += core gui widgets在Qt 5里必须写全,少了widgets会报“找不到QWidget”;如果用的是Qt 6,widgets依然是独立模块,一样要加。CONFIG += c++11是为了支持QVector<int>列表初始化,编译器默认可能按C++98处理,Qt 5.15的MSVC套件不写这一行容易报莫名其妙的重载错误。
类怎么划分?我见过一个文件写两千行的,答辩时自己都说不清。分成三个文件就够了:Board管网格、当前方块、碰撞、消行;Tetromino管形状和颜色;TetrisWidget管绘图、定时器、键盘事件和分数显示。这样将来加“下一块预览”只在TetrisWidget里加一个成员变量,加“音效”也只用动界面层。论文里的“系统设计”直接照这个类图画UML,比重新画框图诚实得多。
3.2 绘制与刷新:paintEvent + QTimer 双循环
游戏不是一帧一帧用循环驱动,而是由一个QTimer定时“滴答”驱动一下落。每滴答一下,改变状态,然后调用update()请求重绘。paintEvent里只做绘制,不要在绘制里改游戏状态,这是Qt的世界观。
// TetrisWidget.cpp 片段 void TetrisWidget::initGame() { m_board = new Board(kColumnCount, kRowCount, this); m_timer = new QTimer(this); m_timer->setInterval(500); // 每500ms下落一格 connect(m_timer, &QTimer::timeout, this, &TetrisWidget::onGameTick); m_timer->start(); } void TetrisWidget::paintEvent(QPaintEvent*) { QPainter painter(this); painter.fillRect(rect(), QColor(32, 34, 38)); // 1. 绘制已经固定的格子 for (int row = 0; row < m_board->rowCount(); ++row) { for (int col = 0; col < m_board->columnCount(); ++col) { int cell = m_board->cellAt(row, col); if (cell != 0) { drawCell(painter, col, row, colorForId(cell)); } } } // 2. 绘制当前活动方块 const Tetromino& cur = m_board->currentTetromino(); QPoint p = m_board->currentPos(); for (int i = 0; i < cur.shape().size(); ++i) { for (int j = 0; j < cur.shape().at(i).size(); ++j) { if (cur.shape().at(i).at(j)) { drawCell(painter, p.x() + j, p.y() + i, cur.color()); } } } }逻辑说明:颜色映射我存了一个QColor colorForId(int id),这个id是网格里存的值,比如1代表I方块,2代表O方块。不用存整个QColor网格是为了省内存,也方便序列化到存档文件。drawCell内部根据列行号换算像素坐标,方块边长是m_cellSize,像素坐标就是col * m_cellSize + margin之类。
参数说明:setInterval(500)是最慢的下落速度,500毫秒一格;后面做等级加速时,这个值从500开始,最低到80或100就不要再减了,否则没法玩。QTimer的精度在Windows下是15ms左右,对500ms来说无感。注意不要把interval设成0,那样timer会以大约零延迟连续触发,CPU占用百分之百是小事,键盘按一下会直接失控。
3.3 键盘控制与软硬降落的实现
QWidget重写keyPressEvent就能收到键盘,但有几个前置条件:窗口要有焦点,并且没有被其他控件抢走。如果你在界面上放了QLabel或按钮,方向键可能被按钮消费掉。对策是给主窗口setFocusPolicy(Qt::StrongFocus),并且不要添加不必要的按钮,开始和暂停用键盘快捷键更好。
void TetrisWidget::keyPressEvent(QKeyEvent* event) { if (m_paused) { if (event->key() == Qt::Key_P) { setPaused(false); } return; // 暂停时屏蔽其他键 } switch (event->key()) { case Qt::Key_Left: m_board->moveLeft(); break; case Qt::Key_Right: m_board->moveRight(); break; case Qt::Key_Down: if (m_board->softDrop()) { m_score += 1; // 软降一格加1分 updateScoreLabel(); } break; case Qt::Key_Space: m_board->hardDrop(); settleCurrentBlock(); break; case Qt::Key_Up: m_board->tryRotate(); break; case Qt::Key_P: setPaused(true); break; default: QWidget::keyPressEvent(event); break; } update(); }逻辑说明:软降落一次只移动一格,并且加分1;硬降落直接用循环把方块落到底部,然后立刻固化并进入下一块。注意按空格后不经过定时器,直接在事件里调用固化流程,否则方块会在空中停留半秒,体验很差。
参数说明:Qt::Key_Z也可以用作旋转,很多人习惯Z顺时针、X逆时针,如果做两个旋转方向,需要在Board里加入逆时针旋转函数。键盘事件默认不会自动重复,但如果你按住方向键不放,系统会按操作系统的键盘重复率连续触发keyPressEvent,这个效果对左右移动挺好,但对旋转就不太好。可以在事件里检查event->isAutoRepeat(),如果是自动重复就忽略旋转键。
3.4 让代码能跑通的最小main.cpp和TetrisWidget核心连接
把所有东西串起来的是main.cpp。注意不要用MainWindow那个模板自带的大框架,一个干净入口足够。
// main.cpp #include <QApplication> #include "TetrisWidget.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); TetrisWidget widget; widget.setWindowTitle("俄罗斯方块 - QT/C++"); widget.resize(340, 700); // 10 * 30 + 40 边距, 20 * 30 + 100 分数区 widget.show(); return app.exec(); }参数说明:resize的340对应10列方块边长30加上左右边距20,700对应20行方块边长30加上顶部分数区100。这里的数值要和paintEvent里的margin及topOffset保持一致,否则窗口边缘会漏出未绘制区域。不要在main里写死逻辑,程序入口只负责创建窗口和进入事件循环。
TetrisWidget的构造函数里要初始化Board、创建timer、设置初始分数、并setFocusPolicy。我还习惯在构造函数里调用newBlock()生成第一个方块,这样界面一出来就有东西。注意顺序:先Board后Timer,否则第一次tick访问空指针。
到这里,一个能看到方块下落、能按键操作、能消行的俄罗斯方块就已经跑起来了。剩下的工作是把它变成一份能交差的作业和一场问不倒的答辩。
4. 论文与答辩PPT的素材组织:从源码讲清楚设计而不是贴代码
4.1 论文结构:需求分析、方案设计、核心算法、测试与结论
很多人的论文是从网上找“俄罗斯方块设计与实现”改成自己的课程名,结果答辩被问一句“这个类的作用是什么”就卡壳。我建议论文跟代码一一对应,每个模块都能从源码里找到它。下面是我常用的一版结构,适合本科课设,也适合硕士课程的项目报告。
| 章节 | 内容要点 | 对应工程文件 |
|---|---|---|
| 需求分析 | 功能性需求、非功能需求、用例图 | 无 |
| 总体设计 | 系统模块图、线程/事件模型 | TetrisWidget.h |
| 详细设计 | 数据结构、关键算法流程 | Board.cpp, Tetromino.h |
| 核心实现 | 旋转、消行、绘制、定时器 | TetrisWidget.cpp |
| 系统测试 | 测试用例、结果截图、边界情况 | 运行日志 |
| 总结 | 遇到的难点、解决思路、可扩展点 | 无 |
写需求分析时,不要抄书上的“俄罗斯方块的起源”,要写你自己这个版本的功能清单。比如:具备下落、左移右移、旋转、软降、硬降、消行计分、等级加速、暂停继续。每一条都要能对应到代码里的一个函数。非功能需求建议写“按键到画面反馈不超过100ms”和“1小时连续运行不崩溃”,这就是QTimer 500ms tick之外,按键事件立即update所带来的保证。
详细设计里,数据结构必须画图。画出4x4矩阵和10x20网格怎么对应,旋转公式怎么变换。用文字描述一遍算法流程,比贴一段代码更让老师相信是你自己写的。答辩时老师追问“你的时间复杂度”,你要能答出:消行扫描是O(行数*列数),旋转是O(16),因为固定4x4所以是常数级。这种细节比“界面美观”值钱。
4.2 答辩PPT怎么讲:演示顺序、截图和关键代码的取舍
PPT页数控制在10到12页,建议15分钟以内讲完。演示顺序比内容更重要:先现场跑一遍游戏,让老师看到效果,再讲实现。如果一上来就讲源代码,10页PPT根本放不下,观众也没有直观印象。
我习惯的PPT结构是:第一页题目+框架;第二页需求分析;第三页总体设计,画一个简单的模块关系图;第四页核心数据结构,就是网格和方块类;第五页旋转和消行的伪代码;第六页运行效果截图,截三四张不同状态,包括正在消行、游戏结束;第七页测试结果;第八页总结和展望。每页放图为主,文字不要超过五行。代码最多放三行关键判断,比如旋转时的越界判定,不要放整个函数。
特别建议录制一小段GIF或视频,时长不超过1分钟,内容是从开始游戏到消掉4行。因为现场答辩可能手滑或程序抽风,有视频兜底。当然程序能跑才是根本,这个视频只防万一。
4.3 源码注释与命名规范:答辩提问的隐藏加分项
代码注释不需要多,但要在关键算法开头写一两句“这里为什么这么做”。比如消行后++row这个处理,如果你不注释,答辩老师一眼看到会觉得你有bug,注释了就是你有意识处理了连续消行。
命名规范上用统一的PascalCase类名、camelCase函数名,枚举用大写前缀。变量不要用a、b、c这种。给老师看的源码,还要保持缩进一致。Qt本身风格是4空格缩进,你就别用Tab混着来。
答辩前问自己几个问题:为什么用QVector而不是std::vector?为什么用QTimer而不用Windows的SetTimer?为什么颜色用int映射而不用QColor数组?每个问题都能用“因为Qt的对象生命周期管理”“因为跨平台和信号槽集成”这种理由回答就行。这些问题全部会从源码里来,所以你的代码必须是自己一行行敲的。
如果你最后要交的是“源码论文答辩ppt.rar”这类压缩包,建议在源码目录里加一个README,写清运行环境、打开步骤、按键说明。论文放最终版,PPT放演示版和打印版,打印版用白底黑字,因为深色界面打印出来会一团黑。这些细节老师不会写在评分细则里,但会直接影响第一印象。
5. 避坑:QT版本、MSVC编译器、路径与中文乱码的实战排错
5.1 现象:编译报错 dependent '......\qt\5.15.2\msvc2019_64\include\qtwidgets' 找不到
这个报错是Qt Creator在用MSVC套件打开项目时常见的:它尝试去依赖一个相对路径,但你的Qt安装在别的位置。明明环境变量里配了QTDIR,还是会失败。原因是工程文件用了$$PWD或相对路径包含头文件,或者Kit中“Qt版本”和“编译器”的位数不匹配。解决方法是:统一使用MSVC2019 64位Kit和Qt 5.15.2 MSVC2019_64库,在“工具-选项-Kits”里确认编译器版本是2019而不是2022;如果还报错,删除项目目录下的.qmake.stash和Makefile,然后执行“构建-清理”后重新qmake。这个错误不是代码问题,不要改源码,浪费一下午。
5.2 现象:中文注释或字符串变成乱码
MSVC编译器默认按照本地代码页读取源文件,Qt Creator保存为UTF-8时,中文字面量在Windows上会显示成乱码,甚至在编译时报错。解决:在pro文件里加一行msvc: QMAKE_CXXFLAGS += /utf-8,告诉MSVC使用UTF-8编码。如果你的编译器是MinGW,一般没问题,但为了保险,所有源文件统一用UTF-8 no BOM格式保存,并将Qt Creator的“文件编码”设为UTF-8。还有个不推荐但有效的办法:全部用英文注释,论文里再翻译成中文。
5.3 现象:键盘没反应或方向键触发两次
键盘没反应,先看窗口是否处于活动状态。如果窗体上还有子控件,键盘事件会先传给子控件,导致主窗口keyPressEvent收不到。解决:在构造函数的最后调用setFocusPolicy(Qt::StrongFocus),并且不要加入QPushButton。触发两次通常是操作系统自动重复,按住空格键时hardDrop被连续调用,游戏瞬间刷新。解决:在keyPressEvent里对旋转和硬降检查if (event->isAutoRepeat()) return;,自动重复事件也是QKeyEvent,只是isAutoRepeat为true。我一般只在旋转和硬降里排除自动重复,左右移动保留自动重复方便连续移动。
5.4 现象:QTimer刷新太快导致方块闪烁或CPU占用高
很多人为了让方块移动流畅,把interval设成1,结果整个界面闪烁,CPU飙到30%。原因:QTimer最小精度受系统限制,而且每次update都会重绘全部网格。解决:把interval控制在80毫秒以上,下落速度用单独的变量控制,不要用timer的interval控制游戏速度。绘制方面,不要在paintEvent里new对象,QColor、QBrush这些可以做成成员变量。如果还闪,试试在窗口构造函数里setAttribute(Qt::WA_OpaquePaintEvent, true),减少背景重绘;或者用update()的矩形参数只刷新变化区域。
这里补充一个经验:俄罗斯方块的画面只有方块变化时才需要重绘,不像动画每帧都不同。我后来改成每次tick只update(m_prevRect)和update(m_currentRect),CPU占用从25%降到2%,效果立竿见影。课设做到这份上可以当作性能优化亮点写进论文。
5.5 现象:高分榜存文件后运行目录找不到文件
代码里用了QFile file("scores.txt"),但程序从Qt Creator里运行时的当前工作目录是build文件夹,不是源码目录。所以明明文件就在旁边,程序就是找不到。解决:所有运行时文件都放到一个明确路径,用QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)或者简单点,用qApp->applicationDirPath()拼接绝对路径。代码示例:
QString dir = QCoreApplication::applicationDirPath(); QString filePath = dir + "/scores.txt"; QFile file(filePath);注意:如果你把打包后的exe换目录,scores.txt会在exe旁边生成,相对路径的问题就没有了。另外,Qt 5.15对应的QStandardPaths在QtCore里,需要#include <QStandardPaths>。读取时要检查file.exists(),不存在就创建一个默认分数字典。
6. 进阶收尾:给俄罗斯方块加上预览、等级加速和暂停恢复
6.1 下一块预览的最小实现
只需要在TetrisWidget里加一个成员Tetromino m_next;,每固化一个方块,就把m_next赋值给current,再从方块类型池中随机生成新的m_next。绘制时在右侧预留80像素宽的面板,用同样drawCell函数画一个4x4小图。注意把预览方块的颜色和形状原样画出来,而不是只用文字表示。这是论文里“扩展功能”的加分点。
6.2 等级与下落速度的映射表
用一个数组把等级映射到毫秒,每消10行升一级。
| 等级 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 速度(ms) | 500 | 400 | 300 | 200 | 120 |
达到5级后不再加速,否则手速跟不上。等级越高,软降落加分可以乘以等级系数,制造更明显的成就感。这里注意:等级变化后要调用timer->setInterval,并且把新值打印到控制台或界面上,方便验证。
6.3 验证方法:手动测试清单
写代码的时候我习惯准备一个测试流程:开局第一块能否正常下落;按左右移动一格是否精确;旋转后是否仍然在边界内;一行满时消一行;连续消四行;方块堆到顶部时是否触发结束;暂停后再继续是否恢复。每次改完代码,照着清单跑一遍,比写单元测试更快。我吃过亏:改了一个坐标系偏移,觉得“这么简单不用测”,结果消行从底部漏了。
最后说一句习惯:俄罗斯方块这种小游戏,真正的复杂度不在功能多少,而在状态转换的完备性。把每一个“操作后是否要立即固化、是否要刷新下一块”都画成状态图再动手,能少掉很多翻车。希望帮到你。
本文还有配套的精品资源,点击获取