简介:这是一套基于C++与Qt框架开发的俄罗斯方块游戏完整工程,面向需要完成课程设计、期末大作业或毕业设计的计算机专业学生,也适合Qt初学者对照学习。项目曾获导师认可的高分成绩,从方块旋转、消行判定到得分统计均有清晰实现,代码注释详细,新手也能看懂。压缩包共42个文件,主要涵盖17个头文件、9个C++源文件、Qt界面文件及json、lib/dll等配置文件,并附有说明文档和构建脚本,结构完整便于部署。资源大小约11.87MB,目前已有316人学习使用。附带的项目文档梳理了系统设计与核心代码思路,结合源码可快速掌握基于Qt的事件循环、图形绘制与游戏逻辑实现方法,对冲刺高分及答辩演示很有帮助。
1. 基于C++与Qt的俄罗斯方块大作业:先搞清楚包里有什么、能解决什么
期末大作业、课程设计节点上,拿到一套基于c++与Qt的俄罗斯方块源代码,最该关心的不是游戏画面够不够炫,而是三件事:能不能在我自己电脑上跑起来、代码能不能讲明白、答辩时老师问到「状态机」「碰撞检测」「渲染」能不能接得住。这套源码包就是冲着这三件事来的,一个个人手打的98分项目,src目录下源码、CMake构建配置、.vscode调试配置、项目文档一次给全。它的价值不在于俄罗斯方块本身,而在于给你一套最小可运行的C++/Qt游戏工程骨架,从Q_OBJECT类的moc处理、QGraphicsScene绘图到键盘事件驱动状态机,全是课程设计里最高频的考点。适合两类人:一类是时间紧、想直接复现并能在答辩中讲清楚原理的课设/毕设学生;另一类是想要一份带注释的Qt游戏项目当底稿、快速改成连连看或贪吃蛇的入门开发者。下面按「先跑通、再拆逻辑、后避坑」的顺序把这包东西过一遍。
2. 把工程跑起来:环境选型、路径配置与两种编译方式
2.1 先看清工程骨架:src、libs、CMake 与文档各管什么
拿到压缩包解压后,根目录是qtetris-master,里面不是一堆散文件,而是按工程习惯分好的结构。先用一张表看清每个目录和文件的定位,后面跑命令时你就知道该改哪里。
| 路径 | 作用 |
|---|---|
CMakeLists.txt | CMake 构建脚本,决定如何找 Qt 库、如何编译 src 下所有源文件 |
CMakeSettings.json | Visual Studio 的 CMake 配置,主要记录构建类型和 Qt 路径 |
.vscode/settings.json | VSCode 里 C/C++ 插件的 includePath、编译参数 |
.vscode/launch.json | VSCode 调试配置,指定可执行文件路径和调试器 |
src/mainwindow.cpp / .h / .ui | 主窗口、菜单布局、键盘事件入口,mainwindow.ui是 Qt Designer 的界面文件 |
src/tetris.cpp / .h | 方块类型定义与棋盘数据结构 |
src/game.cpp / .h与gamespace | 游戏状态机、消行判定、计分逻辑 |
src/tetrisview.cpp / .h | QGraphicsView 子类,负责场景管理和视图刷新 |
src/customGraphItem | 自定义图元类,棋盘里每个格子的绘制都走这里 |
libs/dbgLibs、libs/rlzLibs | Debug 版和 Release 版的预编译依赖库 |
手册.1.docx | 课程设计文档,答辩前重点看这份 |
这个分层是很典型的 Qt 游戏课设写法:数据层(tetris/game)、表现层(tetrisview/customGraphItem)、控制层(mainwindow 接收键盘事件并转发给 game)。答辩时老师问「你代码怎么组织的」,你就按这三层讲,比直接讲某一行代码有说服力得多。.gitignore也在根目录,说明原项目是 git 管理的,这不算什么功能点,但能看出提交规范,课程设计里提一句「团队协作时用 git 管理源码」也算加分项。
2.2 环境选型:Qt 5.15.2 / MSVC2019_64 与 MinGW 的区别,以及 moc 在 Q_OBJECT 里的作用
这套工程在开发机上用的是 Qt 5.15.2 的 MSVC2019_64 构建链,这一点从常见报错路径dependent '..\..\..\Qt\5.15.2\msvc2019_64\include\QtWidgets'就能看出来。Qt 5.15.2 是 Qt 5 系列里最稳的一个长期支持版本,国内镜像站下载也快,课程设计环境一般就装它。如果你本机装的是 Qt 6 或者 MinGW 版,不能直接编译,原因不只在版本号,而在编译器 ABI:MSVC 编译出的库和 MinGW 编译出的库不通用,CMake 在 find_package(Qt5) 时会按路径找对应的Qt5Widgets.dll和导入库,混了会报一堆莫名其妙的链接错误。
还有一个必须先搞懂的机制是 moc。凡是写了Q_OBJECT宏的类,比如mainwindow.h、tetris.h里的类,都不能直接拿 g++/cl 编译,必须先用 Qt 的 moc 工具把 Q_OBJECT 展开成标准 C++ 代码。CMake 里find_package(Qt5 COMPONENTS Widgets)之后会自动处理 moc,所以你基本感觉不到它的存在;但如果你哪天拿裸命令行去编译 src 下的文件,会得到「undefined reference to vtable for MainWindow」这类错误,这就是 moc 没跑。这个知识点属于 Qt 入门必考题,文档里如果写了「基于 Qt 的界面编程」,十有八九会被问到。
2.3 编译前必改的三处路径:CMakeLists、CMakeSettings.json、.vscode/launch.json
多数人在这步翻车。工程是别人机器上写的,Qt 安装路径是绝对路径,换到你机器上必须改,否则 CMake 配置阶段就报错。我一般拿到工程第一件事,不是点构建,而是先看这三个文件里的 Qt 路径。
首先是CMakeLists.txt里大概率有 Qt 路径相关设置,典型的写法是这样:
cmake_minimum_required(VERSION 3.16) project(qtetris) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 关键:告诉 CMake 去哪里找 Qt5 set(CMAKE_PREFIX_PATH "D:/Qt/5.15.2/msvc2019_64") find_package(Qt5 REQUIRED COMPONENTS Widgets Gui) add_executable(qtetris src/main.cpp src/mainwindow.cpp src/mainwindow.h src/tetris.cpp src/tetris.h src/game.cpp src/game.h src/tetrisview.cpp src/tetrisview.h ) target_link_libraries(qtetris PRIVATE Qt5::Widgets Qt5::Gui)这里CMAKE_PREFIX_PATH就是给find_package指路的,值必须是你本机 Qt 的安装根目录,路径里带不带msvc2019_64取决于你装的是哪个套件。如果你的 Qt 装在 C 盘,把D:/Qt/...改成C:/Qt/...就行。注意路径分隔符用正斜杠/,Windows 下用反斜杠\有时会被 CMake 当成转义字符处理。
然后是CMakeSettings.json,这是 Visual Studio 用的,里面有一个environments数组,记录构建环境变量:
{ "configurations": [ { "name": "x64-Release", "generator": "Ninja", "configurationType": "Release", "inheritEnvironments": [ "msvc_x64_x64" ], "buildRoot": "${projectDir}\\out\\build\\${name}", "installRoot": "${projectDir}\\out\\install\\${name}", "cmakeCommandArgs": "-DCMAKE_PREFIX_PATH=D:/Qt/5.15.2/msvc2019_64", "buildCommandArgs": "" } ] }最后是.vscode/launch.json,VSCode 调试时要靠它找到生成好的 exe 和调试器:
{ "version": "0.2.0", "configurations": [ { "name": "QtTetris Debug", "type": "cppvsdbg", "request": "launch", "program": "${workspaceFolder}/build/Debug/qtetris.exe", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "console": "externalTerminal" } ] }program填编译产物 exe 的绝对路径,type用cppvsdbg对应 MSVC 调试器,如果你是 MinGW 环境要换成cppdbg并指定miDebuggerPath为 gdb。这三个文件都指向同一个 Qt 路径,只改一处不改另外两处,就会出现「CMake 能生成、VS 能编译、VSCode 一调试就崩」的割裂状态。
2.4 两种跑通方式:VSCode + CMake Tools 与 VS2022 + Qt VS Tools
路径改完就可以编译了。我常用的方式是 VSCode + CMake Tools,命令干净,输出直观,适合课设演示。在工程根目录执行:
# 生成构建目录,指定 Ninja 生成器和 Qt 路径 cmake -S . -B build -G Ninja -DCMAKE_PREFIX_PATH="D:/Qt/5.15.2/msvc2019_64" # 编译,-j 8 表示 8 线程并行 cmake --build build --config Debug -j 8-S . -B build意思是源码在当前目录、构建目录叫 build;-G Ninja指定生成器,Ninja 比默认的 Visual Studio 生成器快不少;-DCMAKE_PREFIX_PATH覆盖 CMakeLists 里写死的路径。编译成功后在 build/Debug 下会出现qtetris.exe。
另一种方式是用 Visual Studio 2022 加 Qt VS Tools 插件,优点是调试 Qt 信号槽时能直接看到信号发射链,缺点是要额外装插件、还要在 VS 里配置 Qt 版本。如果你本来就装了 VS,直接打开根目录的CMakeSettings.json,VS 会自动加载里面的配置,点「生成」即可。无论哪种方式,第一次生成时如果报错,九成是 2.3 节那三个路径没改干净。
提示:Debug 和 Release 两套构建千万别共用同一个 build 目录。先编 Debug 再编 Release,Ninja 会认为部分目标已是最新而不重新编译,最后得到一个混着 debug 库的 release exe,运行时就报
_ITERATOR_DEBUG_LEVEL不匹配。
3. 核心逻辑拆解:数据结构、碰撞检测与 Qt 渲染路径
3.1 棋盘与方块建模:20×10 棋盘加四形态预置矩阵
俄罗斯方块的游戏规则不复杂,但代码怎么写直接决定答辩时能不能说清楚。这套工程的数据层在tetris.h里,核心是把棋盘抽象成一个二维数组,把方块抽象成预置的形态矩阵。常见写法是这样的:
// tetris.h enum TetrominoType { EMPTY = 0, I, O, T, S, Z, J, L }; // 每种方块的 4 个旋转形态,每个形态用 4x4 矩阵表示 const int SHAPES[7][4][4][4] = { { // I {{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}}, {{0,0,1,0},{0,0,1,0},{0,0,1,0},{0,0,1,0}}, // ... 旋转形态 2、3 }, // ... 其余 6 种方块 }; class Board { public: static const int ROWS = 20; static const int COLS = 10; int grid[ROWS][COLS]; // 0 表示空格,非 0 表示已固定的方块类型 };棋盘用grid[20][10]存,0 表示空,1~7 分别表示七种方块。方块本身没有用坐标列表存,而是用 4×4 矩阵,这样旋转就是查表换一个矩阵,碰撞检测也统一走矩阵遍历,不用为每种形状写特判。答辩时导师问「你方块旋转怎么实现的」,你就答「每种方块预置四个旋转形态,旋转只是切换矩阵下标」,一句话说透,比现场推导旋转公式稳得多。
这里有个可扩展的细节:如果你想加第 8 种方块,只需要在SHAPES数组里加一组 4×4 矩阵,并同步改枚举类型。这就是把方块数据从游戏逻辑里剥离开的好处,也是这份代码能拿高分的一个结构原因。
3.2 游戏状态机与 QTimer 驱动:方向键怎么变成一次下落
游戏逻辑在game.cpp里,核心是一个状态机加一个 QTimer。QTimer 定时触发下落,方向键通过keyPressEvent进入逻辑层,两者在同一个线程里协作,天然避免线程安全问题。典型的骨架长这样:
// game.h class Game : public QObject { Q_OBJECT public: enum State { READY, RUNNING, PAUSED, GAMEOVER }; void handleKey(int key); private slots: void onTimeout(); // 每次定时器超时,方块下移一格 private: State state; Board board; QTimer timer; int currentX, currentY; // 当前方块在棋盘上的位置 int currentShape; // 当前方块类型 }; // game.cpp Game::Game(QObject *parent) : QObject(parent) { connect(&timer, &QTimer::timeout, this, &Game::onTimeout); timer.setInterval(500); // 初始 500ms 下落一格 } void Game::handleKey(int key) { if (state != RUNNING) return; switch (key) { case Qt::Key_Left: tryMove(-1, 0); break; case Qt::Key_Right: tryMove(1, 0); break; case Qt::Key_Down: tryMove(0, 1); break; case Qt::Key_Up: rotate(); break; case Qt::Key_Space: hardDrop(); break; } }handleKey是整个游戏的输入入口,Qt::Key_Left对应左移,Qt::Key_Down在俄罗斯方块里通常表示软降一格,Qt::Key_Space是硬降到底。QTimer::setInterval(500)表示每 500ms 触发一次onTimeout,这个值就是速度档位,下文第四节会讲怎么把它做成等级递增。if (state != RUNNING) return;这行很关键,不加的话,游戏结束或暂停时按方向键也能移动方块,属于低级的逻辑漏洞,答辩挑刺时会很尴尬。
3.3 碰撞检测与消行:先探路再回滚的判定顺序
碰撞检测是所有俄罗斯方块实现里最容易写崩的地方。这里采用的「先尝试移动、失败就回滚」策略,比先算边界再移动要简洁得多,也更好验证。
// game.cpp bool Game::tryMove(int dx, int dy) { int nx = currentX + dx; int ny = currentY + dy; // 遍历当前方块 4x4 矩阵的每个有效格子 for (int r = 0; r < 4; ++r) { for (int c = 0; c < 4; ++c) { if (shapeAt(currentShape, r, c)) { int boardX = nx + c; int boardY = ny + r; // 撞左墙、右墙、底墙,或者撞到已固定方块 if (boardX < 0 || boardX >= COLS || boardY >= ROWS) return false; if (boardY >= 0 && board.grid[boardY][boardX] != 0) return false; } } } // 没有碰撞,更新坐标 currentX = nx; currentY = ny; return true; }这里shapeAt(currentShape, r, c)返回当前旋转形态在 (r,c) 处是否有方块,dx, dy是移动向量,左移传 (-1,0),右移传 (1,0),下落传 (0,1)。注意boardY >= 0的条件不能省,因为 4×4 矩阵有空白行,方块没完全进入棋盘时,ny + r可能是负数,负下标访问数组会产生未定义行为,这是运行时崩溃的常见原因。
消行逻辑写在onTimeout里:方块落定后调用clearRows(),从底往上扫:
int Game::clearRows() { int removed = 0; for (int row = ROWS - 1; row >= 0; --row) { bool full = true; for (int col = 0; col < COLS; ++col) { if (board.grid[row][col] == 0) { full = false; break; } } if (full) { // 上面所有行整体下移一行 for (int r = row; r > 0; --r) { memcpy(board.grid[r], board.grid[r-1], COLS * sizeof(int)); } memset(board.grid[0], 0, COLS * sizeof(int)); ++row; // 继续检查当前行,因为上面又移下来一行 ++removed; } } return removed; }这里用memcpy做整行搬运,效率比逐格赋值高,也少写一层循环。++row是因为 for 循环会再执行--row,等于停在当前行重新检查一遍,否则漏判连续消两行的情况。消行返回值removed会传给计分模块,1 行、2 行、3 行、4 行的分数不同,这一点在答辩时也是常用考点。
3.4 渲染路径:customGraphItem 的 paint() 与 QGraphicsScene 更新
这部分的 Qt 绘图逻辑在customGraphItem类里,它继承自QGraphicsItem,重写paint()绘制棋盘。Qt 的绘图机制是:视图 → 场景 → 图元 三层,QGraphicsView负责接收鼠标键盘事件和显示,QGraphicsScene管理图元列表,每个QGraphicsItem只画自己这一块。
// customGraphItem.cpp class GameBoardItem : public QGraphicsItem { public: QRectF boundingRect() const override { return QRectF(0, 0, COLS * CELL_SIZE, ROWS * CELL_SIZE); } void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override { Q_UNUSED(option); Q_UNUSED(widget); for (int row = 0; row < ROWS; ++row) { for (int col = 0; col < COLS; ++col) { int value = board.grid[row][col]; QColor color = value ? colorOf(value) : Qt::black; painter->fillRect(col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE - 1, CELL_SIZE - 1, color); } } } };boundingRect()返回整个棋盘的矩形范围,作用是告诉视图这个图元占多大区域,也用于碰撞检测的视图裁剪;paint()里按grid[row][col]的值填色,CELL_SIZE - 1留出 1 像素间隙,格子之间自然出现网格线。常见优化是把colorOf(value)的颜色映射表做成静态数组,避免每次 paint 都走一次 switch。当棋盘数据变化时,调用this->update()触发重绘,不要在paint()里改游戏数据,QPainter 在重绘时可能被调用多次,改数据会造成逻辑错乱。
4. 避坑记录:编译、链接、运行与部署的五个真实故障
这类 Qt 课程设计工程,能一次编译通过的少,能换个电脑还跑得动的更少。下面五条是这个资源和同类工程里最常翻车的场景,每条都按「现象 → 原因 → 解决」梳理,基本覆盖从下载到答辩前会遇到的问题。
4.1 编译期:dependent '..\..\..\Qt\5.15.2\msvc2019_64\include\QtWidgets'路径失效
现象:用 CMake 生成构建系统时报错,提示某个 dependent 路径不存在,或者干脆是fatal error C1083: 无法打开包含文件: "qwidget.h"。打开错误信息里的路径一看,指向的是别人机器上的 D 盘 Qt 目录。
原因:工程在开发机上建的时候,CMakeLists 或 CMakeSettings.json 里写死了 Qt 安装的绝对路径。Qt 的include/QtWidgets、include/QtGui目录层级是固定的,但盘符和上级目录跟着机器走,换机器必炸。
解决:不要硬改那串..\..\相对路径,直接在 CMake 配置时指定本机 Qt 根目录:
cmake -S . -B build -DCMAKE_PREFIX_PATH="D:/Qt/5.15.2/msvc2019_64"如果还用命令行工具,Qt 官方提供的qt-cmake会自动注入 Qt 的 bin 目录到 PATH,不需要手动指定 CMAKE_PREFIX_PATH,更省事。改完后再重新生成,构建设置里就全是本机路径了。
4.2 链接期:dbgLibs 与 rlzLibs 混用导致 LNK2038 或找不到 Qt5Widgets.lib
现象:Release 构建时报LNK2038: 检测到 _ITERATOR_DEBUG_LEVEL 的不匹配项,或者提示无法打开文件 Qt5Widgetsd.lib(注意多了一个 d)。Debug 构建正常,Release 必挂。
原因:libs/dbgLibs是 Debug 版依赖库,rlzLibs是 Release 版。如果 CMake 里没按构建类型区分链接目录,Release 构建也会去 dbgLibs 里找带 d 后缀的调试库,而调试库要求代码用 debug 迭代器模式编译,两边宏定义对不上就报 LNK2038。
解决:在 CMakeLists 里按构建类型动态选择库目录:
if(CMAKE_BUILD_TYPE STREQUAL "Debug") target_link_directories(qtetris PRIVATE ${CMAKE_SOURCE_DIR}/libs/dbgLibs) else() target_link_directories(qtetris PRIVATE ${CMAKE_SOURCE_DIR}/libs/rlzLibs) endif()或者更干脆,不手动链接这些 lib 目录,让find_package(Qt5)自动指向 Qt 安装目录下的lib,那里同时有 debug 和 release 版本,Qt 会根据构建类型自动选。
4.3 运行期:程序起来了但方向键完全没反应
现象:exe 能启动,窗口正常显示,但按上下左右、空格键都没反应,方块一开始就卡住不动,游戏像死机一样。
原因:键盘事件没送到Game::handleKey。常见两个来源:一是QGraphicsView默认不抢键盘焦点,按键事件被窗口其他控件吃掉了;二是重写keyPressEvent时没有调用父类方法,事件链断了。
解决:在mainwindow构造里给视图设置焦点策略,并在窗口显示后强制拿焦点:
// mainwindow.cpp 构造函数 ui->gameView->setFocusPolicy(Qt::StrongFocus); ui->gameView->setFocus();同时检查mainwindow.cpp里的keyPressEvent:
void MainWindow::keyPressEvent(QKeyEvent *event) { game->handleKey(event->key()); // 先处理游戏逻辑 QMainWindow::keyPressEvent(event); // 再交给父类,防止事件链断掉 }Qt::StrongFocus表示控件可以通过点击和 Tab 获得焦点,setFocus()是启动时主动把焦点给视图。这两行不加,按键就是进了黑匣子,这是运行期最常见又最难定位的问题之一。
4.4 运行期:下落几秒后界面白屏、CPU 飙满卡死
现象:游戏刚开始正常,玩了十几秒后窗口变白,点哪都没反应,任务管理器里进程 CPU 占用接近 100%。Debug 模式下用 qDebug 打点能看出来卡死前输出停止在某一行。
原因:最常见的是onTimeout里某个 while 循环条件永远为真,比如消行后++row写错变成--row,导致同一行反复检查;或者hardDrop()里用一个 while 循环持续下落,但下落函数内部没有在「已落定」状态置标志位,循环退出条件永远满足不了。
解决:先在onTimeout入口和出口各加一行qDebug() << "tick in/out",看输出停在哪。定位到循环后按下述方式处理:
void Game::hardDrop() { // 每步下落一格,撞到底就停在原地 while (tryMove(0, 1)) { /* 循环体留空,移动到不能再移动为止 */ } // 循环结束后立即固定方块并触发消行 lockBlock(); clearRows(); }这里tryMove(0, 1)返回 false 时循环自然退出,不会再死循环。任何 while 循环都要确保至少有一个「改变循环条件状态」的函数调用,否则就是死循环。
4.5 部署期:换电脑运行提示缺少 Qt5Widgets.dll 或 VCRUNTIME140.dll
现象:在自己机器上编译运行都正常,把整个 build 目录拷到另一台没装 Qt 的电脑上,双击 exe 弹窗:由于找不到 Qt5Widgets.dll,无法继续执行代码。有时候还会附带VCRUNTIME140.dll缺失。
原因:Qt 默认是动态链接,exe 依赖 Qt 的 DLL;同时你用的编译器是 MSVC,运行库 VCRUNTIME140 不在目标机器上。课程设计上交源码还好,如果是交 exe 演示,这两样缺一不可。
解决:用 Qt 自带的部署工具windeployqt把依赖打全,再装一个 VC 运行库:
# 进入构建输出目录 cd build/Release # 用 windeployqt 自动拷贝 Qt 依赖 windeployqt qtetris.exewindeployqt会扫描 exe 的导入表,把 Qt5Widgets.dll、Qt5Gui.dll、Qt5Core.dll 和对应插件目录复制到 exe 旁。跑完后把整个目录打包带走,双击就能跑。VC 运行库体积小,直接装vc_redist.x64.exe即可,注意 x86 和 x64 分开装。部署完的目录里会多出 platforms、styles 等子目录,那些是 Qt 插件,不要删除。
5. 让它更像 98 分:速度档位、计分规则与预览方块扩展
答辩时导师最爱问的两个问题,一个是「难度是怎么递增的」,另一个是「计分规则怎么定的」。原工程把基础框架做扎实了,但这两块通常只是最简实现。下面这套扩展方案是从课设「加分项」角度给的,代码量不大,效果却很明显。
速度档位我用等级表管理,每消除满一定行数升一级,下落间隔线性缩短:
| 等级 | 下落间隔(ms) | 消除 1/2/3/4 行得分 |
|---|---|---|
| 1 | 500 | 100 / 300 / 500 / 800 |
| 2 | 450 | 200 / 400 / 600 / 900 |
| 3 | 400 | 300 / 500 / 700 / 1000 |
| 4 | 350 | 400 / 600 / 800 / 1100 |
| 5 | 300 | 500 / 700 / 900 / 1200 |
实现上,把等级和分数放进 Game 类:
void Game::addScore(int lines) { static const int lineScore[5] = {0, 100, 300, 500, 800}; score += lineScore[lines] * level; // 等级作为倍率 linesCleared += lines; // 每消除 10 行升一级,等级越高下落越快 int newLevel = linesCleared / 10 + 1; if (newLevel != level) { level = newLevel; timer.setInterval(qMax(100, 500 - (level - 1) * 50)); } emit scoreChanged(score, level); }qMax(100, ...)保证最低间隔 100ms,再快就超过人类反应极限了。等级作为计分倍率是常见做法,答辩时解释「鼓励玩家在高速下消除多行」,逻辑自洽。附带加一个「下一个方块预览」也很简单:在 Game 里预生成一个nextShape,落定后把currentShape替换为nextShape,再随机生成新的nextShape,界面上用一个小QGraphicsView渲染它即可。这块代码不复杂,但演示效果非常直观,答辩时导师看到预览、等级、分数一起联动,整段演示流程就完整了。
这套工程我前后复现过几次,最早的教训是拿到手的第一个动作就错了——直接点开 build,而不是先改 CMakeLists 里的路径。结果跑出 4.1 节那个 dependent 报错后,又顺手把.vscode/launch.json也改岔了,白白浪费半小时。从那以后我每次拿到别人写的 Qt 工程,强制自己按固定顺序走一遍:先看CMakeLists.txt里的CMAKE_PREFIX_PATH,再看CMakeSettings.json的环境变量,最后检查launch.json的program路径,三处都指向本机目录才允许点构建。这份带注释的源码和项目文档,适合作为你课设的底稿边看边跑,希望帮到你。
本文还有配套的精品资源,点击获取