ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Qt与C++的俄罗斯方块课程设计:从工程结构到答辩完整指南

基于Qt与C++的俄罗斯方块课程设计:从工程结构到答辩完整指南 简介一套基于C与Qt的俄罗斯方块课程设计源码及配套项目文档面向计算机专业需要完成期末大作业、课程设计或毕业设计的学生。代码注释完整结构清晰即使新手也能快速读懂核心逻辑部署简单下载解压后稍作配置即可运行。资源包共42个文件以17个头文件、9个C源文件为主配合Qt界面文件、JSON工程配置以及依赖库完整覆盖游戏渲染、方块控制、消行计分等模块整体大小约11.87MB。目前已有316人学习使用。配套文档系统说明了整体设计、关键类和游戏流程可作为项目报告的重要参考。从实际成果看该作品曾获较高评分并得到导师认可适合作为类似题目的高分模板也可在此基础上扩展难度或界面特效。1. 基于Qt与C的俄罗斯方块大作业不是交差了事的是能直接上会答辩的完整工程期末周最常见的翻车现场代码写了一个星期按 F5 跑起来黑窗一闪就没了导师凑过来看一眼问你类图在哪、设计思路是什么当场哑火。这份基于c与QT的俄罗斯方块游戏源代码属于工程味很正的高分大作业——注释齐全、逻辑和界面分层、CMake 构建、VSCode 调试配置还附带一份项目手册个人手打的98分作品。它不是让你照着抄的模板而是给你一条拆结构、加功能、写文档的完整路线。适合C课程设计、期末大作业也适合想用 Qt 做毕设演示模块的同学工作后没写过 Qt 的人拿来当入门工程拆一遍同样不亏。2. 拆工程结构三个信号、CMake 配置与 VSCode 调试2.1 文件树给出的三个信号解开压缩包先别急着找 .exe先看目录结构。这个是 qtetris-master 一个主目录顶层是 CMakeLists.txt、CMakeSettings.json还有 .vscode 下的 settings.json 和 launch.json源码全部在 src 里另外挂着 libs、dbgLibs、rlzLibs 和一份“手册.1.docx”。第一眼就能确认三件事。第一它用 CMake 而不是 qmake。CMake 工程在 Qt Creator、Visual Studio、VSCode 三端通用这也是答辩时导师最认的一点——它没有把代码写死在一个 IDE 里。CMakeSettings.json 说明作者在 Visual Studio 的 CMake 配置下也做过适配这不是一份“只会在自己电脑上跑的作业”。qmake 的 .pro 文件简单但 CMake 的工程描述更能体现对构建系统的理解。第二src 下的类名划分非常干净。mainwindow 负责窗口壳tetris/game 负责游戏逻辑gamespace 负责网格数据tetrisview 负责视图绘制customGraphItem 是自定义图元。逻辑归逻辑、界面归界面这是后面能加功能、能写文档的地基。很多低分项目把方块数据、绘制、按键响应全塞在 mainwindow.cpp 一个文件里出 bug 连定位都难更别说讲清楚给导师听。第三.vscode 里带着 launch.json 和 settings.json说明作者在 VSCode 里配置过完整的调试链路。用 VSCode 调试 Qt 程序有几个约定俗成的坑点这份文件相当于把踩坑记录直接交给了你。就算你平时用 Qt Creator 打开工程这套 CMake 结构也一样能跑不影响复现。2.2 CMakeLists.txt 最少可用配置对课程设计来说CMakeLists 不用写得花哨能把 AUTOMOC 驱动起来、把 .ui 文件编进来、链上 Qt5::Widgets 就够了。我按这个项目的目录结构整理了一份最小可用版本你可以直接套在自己的工程上改cmake_minimum_required(VERSION 3.16) project(qtetris) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) # 处理头文件里的 Q_OBJECT生成 moc_*.cpp set(CMAKE_AUTOUIC ON) # 处理 .ui 文件自动生成 ui_*.h set(CMAKE_AUTORCC ON) # 如果有 .qrc 资源文件才会用到先开着不亏 find_package(Qt5 REQUIRED COMPONENTS Widgets) set(SOURCES src/main.cpp src/mainwindow.cpp src/tetris.cpp src/game.cpp src/gamespace.cpp src/tetrisview.cpp src/customGraphItem.cpp ) set(HEADERS src/mainwindow.h src/tetris.h src/game.h src/gamespace.h src/tetrisview.h src/customGraphItem.h ) add_executable(qtetris ${SOURCES} ${HEADERS} src/mainwindow.ui ) target_link_libraries(qtetris PRIVATE Qt5::Widgets)逻辑说明三个 AUTOMOC/AUTOUIC/AUTORCC 开关是 Qt 工程的命门。AUTOMOC 负责扫描头文件里的 Q_OBJECT 宏生成对应的 moc 文件AUTOUIC 把 mainwindow.ui 转换成 C 头文件你不需要手动 include 任何 ui_*.hAUTORCC 只在出现 .qrc 资源文件时参与构建没有资源文件的工程开着它也不会有副作用。参数说明CMAKE_PREFIX_PATH是构建时告诉 CMake“Qt 装在哪”的变量。用命令行构建时如果报Could not find Qt5常见做法是显式指定cmake -DCMAKE_PREFIX_PATHD:/Qt/5.14.2/mingw73_64 -S . -B build注意路径要指向包含lib/cmake的那个套件目录Qt 5.14.2 装完后会自动生成 mingw73_64、msvc2017_64 这类子目录选错了连 Qt5Config.cmake 都扫不到。target_link_libraries里的 PRIVATE 关键字表示链接关系不向外传递课程设计项目小用 PUBLIC 也不会出错但养成 PRIVATE 习惯更规范。2.3 main.cpp 入口与 VSCode 调试配置main.cpp 是这个工程最小的入口。把 Qt 程序理解成“事件循环驱动”而不是“顺序执行”这一步就不会写错#include QApplication #include mainwindow.h int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow w; // 窗口对象在栈上创建即可不强制 new w.show(); // show() 只负责显示不阻塞 return app.exec(); // exec() 进入事件循环窗口关闭才返回 }逻辑说明QApplication 承担 Qt 的资源管理和事件分发必须在窗口对象之前构造。w.show()执行完不会让程序退出因为app.exec()启动了事件循环按键、定时器、paint 事件全部在这个循环里流转。窗口关闭时 exec() 返回返回值直接作为 main 的返回值。再来看 .vscode 里的 launch.json。用 VSCode 调试 Qt 程序时最常见的问题是按 F5 提示“无法启动调试”十有八九是 program 路径不对。配套配置一般长这样{ version: 0.2.0, configurations: [ { name: Qt Debug, type: cppdbg, request: launch, program: ${workspaceFolder}/build/qtetris.exe, args: [], cwd: ${workspaceFolder}, miDebuggerPath: gdb, preLaunchTask: build } ] }参数说明program 指向构建产物如果 CMake 配置了CMAKE_RUNTIME_OUTPUT_DIRECTORY这里的路径要跟着改。cwd 工作目录设置成工程根目录否则相对路径加载资源会翻车。preLaunchTask 对应 tasks.json 里的构建任务名它让 F5 先重新编译再启动调试避免改了代码没生效还在查老问题。settings.json 里一般还会配C_Cpp.default.configurationProvider: ms-vscode.cmake-tools让 IntelliSense 吃 CMake 的编译参数不然代码补全会不认识 Qt 头文件。3. 核心玩法逻辑方块数据、旋转碰撞与消行计分3.1 七种方块怎么存静态数组与 C 数组初始化这一章开始进入真正值分钱的部分。俄罗斯方块玩法表面简单核心只有三件事形状、移动旋转、消行。先看形状怎么存。常见做法是不做运行时动态拼接而是把所有方块的初始形态写成静态数组放 tetris.h 里让编译期就定死// tetris.h —— 只放形状数据和声明不放界面相关代码 #ifndef TETRIS_H #define TETRIS_H const int ROWS 20; // 游戏区行数经典取值 20 const int COLS 10; // 游戏区列数经典取值 10 // 七种方块的初始形态1 表示有方块0 表示空 // 统一用 4x4 矩阵旋转时以矩阵中心为轴 const int SHAPES[7][4][4] { // I { {0,0,0,0}, {1,1,1,1}, {0,0,0,0}, {0,0,0,0} }, // O { {0,0,0,0}, {0,1,1,0}, {0,1,1,0}, {0,0,0,0} }, // T { {0,0,0,0}, {0,1,0,0}, {1,1,1,0}, {0,0,0,0} }, // S { {0,0,0,0}, {0,1,1,0}, {1,1,0,0}, {0,0,0,0} }, // Z { {0,0,0,0}, {1,1,0,0}, {0,1,1,0}, {0,0,0,0} }, // J { {0,0,0,0}, {1,0,0,0}, {1,1,1,0}, {0,0,0,0} }, // L { {0,0,0,0}, {0,0,1,0}, {1,1,1,0}, {0,0,0,0} }, }; #endif逻辑说明用全局 const 数组而不是在构造函数里循环赋值有两个好处。第一是数据在编译期就放进了只读区不依赖对象初始化顺序第二是调试时可以直接对着矩阵数格子方块长什么样一眼就能对上。I 和 O 型其实用不到 4x4 的容量但统一成 4x4 矩阵后旋转逻辑可以共用一条代码路径不用为每一种方块写特判。这段代码本身就是 C 数组初始化的一种典型写法——双重花括号逐行赋值。很多人写方块形状时喜欢用std::vectorstd::vectorint在构造函数里 fill不是不行但写进课程设计文档里远没有这种符号表直观。以后你自己要加新形状往 SHAPES 数组里追加一个[4][4]块就行后面的旋转、碰撞、绘制代码完全不用改。3.2 旋转与碰撞检测四个边界条件旋转是俄罗斯方块最容易翻车的点。顺时针旋转 90 度的数学表达式是新矩阵的行等于原矩阵的列列坐标反转。核心代码很短// tetris.cpp —— 顺时针旋转当前方块 void rotateShape(int shape[4][4]) { int tmp[4][4]; for (int r 0; r 4; r) for (int c 0; c 4; c) tmp[c][3 - r] shape[r][c]; for (int r 0; r 4; r) for (int c 0; c 4; c) shape[r][c] tmp[r][c]; }参数说明tmp[c][3 - r] shape[r][c]是顺时针旋转如果想做逆时针把 3-r 换到行坐标上即tmp[3 - c][r] shape[r][c]。旋转之前必须先复制到临时数组如果原地赋值后面的元素会读到被覆盖过的数据旋转一次图形就烂了。旋转完能不能落位得看碰撞检测。这一块常见的做法是提供一个独立函数网格和形状都是参数不依赖任何界面状态// 判断形状放在 (x, y) 位置是否合法 // x形状左上角的列偏移y形状左上角的行偏移 bool canPlace(const int grid[ROWS][COLS], const int shape[4][4], int x, int y) { for (int r 0; r 4; r) for (int c 0; c 4; c) { if (!shape[r][c]) continue; // 空位不用判断 int gx x c; int gy y r; if (gx 0 || gx COLS) return false; // 左右出边界 if (gy ROWS) return false; // 落到底部之下 if (gy 0 grid[gy][gx]) return false; // 与已固定方块重叠 } return true; }逻辑说明gy 0的情况必须放行。方块出生在网格顶部上方如果把gy 0也判定为非法方块一生成就撞自己的初始化坐标游戏直接进入 GameOver这是新手最容易踩的坑。if (gy 0 grid[gy][gx])这一句限定了只检查已经落进网格的行上面的隐形区域没有网格数据跳过就好。四个边界条件归纳一下左边界gx 0、右边界gx COLS、下边界gy ROWS、堆叠碰撞grid[gy][gx]非空。旋转、左右移动、加速下落全部复用同一个canPlace改动一个判定三处行为同步生效。答辩时导师问“碰撞怎么处理的”你直接把这两个函数翻给他看比背概念有用得多。3.3 消行与状态机一次完整落定的流程方块落定后要写进网格、判断整行是否填满、满行移除并计分。消行的经典实现是“从下往上扫满行就整体下移一行”// gamespace.cpp —— 移除满行并计分返回本次消除的行数 int clearLines(int grid[ROWS][COLS], int score) { int removed 0; for (int r ROWS - 1; r 0; --r) { bool full true; for (int c 0; c COLS; c) if (!grid[r][c]) { full false; break; } if (full) { // 把 r 行以上的所有行整体下移一行 for (int rr r; rr 0; --rr) for (int c 0; c COLS; c) grid[rr][c] grid[rr - 1][c]; for (int c 0; c COLS; c) grid[0][c] 0; // 顶部补空行 r; // 重新检查当前行防止漏判 removed; } } score removed * removed * 100; // 一次消四行奖励远高于逐行消 return removed; }参数说明r这行极其关键。某一行挪下去之后原本 r-1 位置的内容到了 r 行如果这里不把 r 加回去for 循环的--r会直接跳过这行导致连续满行时只消一半。计分公式用removed * removed * 100刻意放大单次四消的收益这是俄罗斯方块“一次消四行最爽”体验的数学来源。这一层的状态流转放 game.cpp。用枚举把游戏状态定死避免到处用 int 当状态位enum GameState { Ready, Running, Paused, GameOver }; // game.cpp —— 加速下落触发的完整回合流程 void Game::tick() { if (state_ ! Running) return; if (canPlace(grid_, current_, x_, y_ 1)) { y_; // 还能往下走 } else { lockToGrid(); // 固定当前方块到网格 int lines clearLines(grid_, score_); emit scoreChanged(score_); if (lines 4) emit fourLinesCleared(); spawnNext(); // 生成下一个方块 if (!canPlace(grid_, current_, x_, y_)) { state_ GameOver; emit gameOver(score_); } } }逻辑说明tick()由定时器周期性驱动逻辑层完全不关心界面怎么画。落不下去就把方块写进网格、消行、更新分数、生成新方块新方块出生点直接撞墙判定 GameOver。这种“逻辑在下、视图在上”的分层是答辩时最有说服力的设计点。再补一句不要让 Game 类里出现任何QGraphicsView的调用边界越干净后面写单元测试越容易。4. 视图层与交互QGraphicsView 自绘与信号槽解耦4.1 自定义图元 customGraphItem 的画法游戏面板用 QGraphicsView 承载每个格子是一个自定义 QGraphicsItem这是 Qt 里很正统的经典做法。项目里 customGraphItem 干的就是这件事。核心代码在 paint 里自己画矩形、自己上色不用准备任何美术素材// customGraphItem.h #include QGraphicsItem #include QPainter class CustomGraphItem : public QGraphicsItem { public: explicit CustomGraphItem(const QColor c, qreal size) : color(c), cellSize(size) {} QRectF boundingRect() const override { // 必须实现视图层靠它做碰撞和裁剪 return QRectF(0, 0, cellSize, cellSize); } void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override { Q_UNUSED(option); Q_UNUSED(widget); painter-setBrush(QBrush(color)); painter-setPen(QPen(Qt::black, 1)); painter-drawRect(0, 0, cellSize, cellSize); // 加一条亮边模拟立体感答辩展示会有明显质感 painter-setPen(QPen(color.lighter(150), 2)); painter-drawLine(0, 0, cellSize, 0); } private: QColor color; qreal cellSize; };逻辑说明boundingRect 返回图元的矩形包围盒paint 只在这个区域内绘制。color.lighter(150)是 QColor 自带的方法返回调亮 50% 的颜色用一条亮边就能在矩形上做出轻微立体效果。别小看这行代码很多国产 Qt 小游戏画面扁平就是少了这条高光边线。cellSize由外部传入通常取 24 到 30 像素配合窗口宽度计算。用 QGraphicsItem 而不是重写 QWidget 的 paintEvent优势在于每个格子都是独立对象可以单独做动画、变色、删除重建。消行动画想做得炫一点遍历这一行的 Item 做缩放或渐隐就行这是 QPainter 手绘整张网格做不到的。4.2 按键事件与信号槽方向键怎么进到逻辑层键盘交互是视图层和逻辑层之间的桥梁。TetrisView 继承 QGraphicsView重写 keyPressEvent但不直接改逻辑状态而是发射信号让 MainWindow 去连接 Game 的行为。这是 Qt 信号槽的标准解耦姿势// tetrisview.cpp void TetrisView::keyPressEvent(QKeyEvent *event) { switch (event-key()) { case Qt::Key_Left: emit moveLeft(); break; case Qt::Key_Right: emit moveRight(); break; case Qt::Key_Down: emit softDrop(); // 加速下落一格 break; case Qt::Key_Up: emit rotateOnce(); // 顺带把旋转绑到上键 break; case Qt::Key_Space: emit hardDrop(); // 空格硬降到底 break; default: QGraphicsView::keyPressEvent(event); } }逻辑说明视图层只负责“用户按了什么键”这件事把语义发给别人。“按左键应该左移、按空格应该硬降”——这些决策全部放在 Game 层由 MainWindow 里的 connect 统一装配。这样改键位不用动 Game改游戏规则不用动视图。MainWindow 里对应的连接代码必须写在工作区 setup 之后// mainwindow.cpp 构造函数 connect(view_, TetrisView::moveLeft, game_, Game::moveLeft); connect(view_, TetrisView::rotateOnce, game_, Game::rotateCurrent); connect(view_, TetrisView::hardDrop, game_, Game::hardDrop);参数说明TetrisView::moveLeft这种写法是函数指针语法编译器会检查信号与槽的签名是否匹配。Qt5 之后推荐这种新语法原因是可以编译期查错比旧的SIGNAL()/SLOT()宏在运行时才发现拼写错误要强得多。这里必须强调一个高频坑QGraphicsView 默认不接受键盘焦点必须在构造函数或初始化时调用setFocusPolicy(Qt::StrongFocus)否则窗口弹出来按键全无反应还以为是 Qt 崩溃。4.3 主窗口组装.ui 与代码各管一摊mainwindow.ui 是 Qt Designer 拖出来的界面描述文件负责静态布局QLabel 显示分数、QPushButton 管开始暂停重置、QGraphicsView 放游戏面板。这些控件在代码里直接用变量名访问// mainwindow.cpp MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { ui.setupUi(this); // AUTOUIC 生成的 setupUi 会装配所有控件 game_ new Game(this); view_ new TetrisView(ui.gamePanel); // 把视图挂到 UI 预留的容器里 connect(ui.btnStart, QPushButton::clicked, game_, Game::start); connect(ui.btnPause, QPushButton::clicked, game_, Game::pause); connect(ui.btnReset, QPushButton::clicked, game_, Game::reset); connect(game_, Game::scoreChanged, ui.lblScore, static_castvoid(QLabel::*)(int)(QLabel::setNum)); }逻辑说明ui.setupUi(this)是 .ui 文件与代码的桥。AUTOUIC 在构建时生成 ui_mainwindow.h类内一个Ui::MainWindow ui;成员就能访问所有控件。QLabel::setNum是重载函数用static_cast显式指定int版本否则 connect 模板推导会报 ambiguous。这也顺带说明为什么界面布局放进 .ui 比纯代码手写更省事设计器的网格布局可以实时调整控件多了也不会乱。一句话总结这章的设计思路逻辑层在 game/tetris/gamespace 里独立运转界面层在 view/item/mainwindow 里负责呈现和转发两边用信号槽和纯函数接口对接。答辩时被问“你的架构是什么”就说“模型视图分离视图不修改模型状态只发请求”这句话能顶十分钟的提问。5. 编译部署与常见问题避坑从下载到跑起来的六个坎5.1 版本选型Qt5 还是 Qt6现象拿到的工程按 Qt6 Creator 直接打开CMake 配置阶段报Could not find Qt5或者编译时报Qt5::Widgets不是目标名。原因这份工程按 Qt5 的 find_package 写法组织的Qt6 把模块名改成了 Qt6::WidgetsCMake 指令不通用。另外 Qt6 对 C 标准要求更高部分旧工程在编码和文件路径上也会报新错误。解决课程设计不建议在这个时间点折腾 Qt6常见做法是装 Qt 5.14 或 5.15 的 MinGW 64 位套件版本能对上原工程的构建预期。如果你坚持用 Qt6把 find_package 一行改成find_package(Qt6 REQUIRED COMPONENTS Widgets)target 改成Qt6::Widgets核心代码一般不需要动。Qt 5.15 在部分官方发行版里叫 qt5-default装的时候注意套件名。对比项Qt 5.14/5.15Qt 6.xfind_package 模块名Qt5::WidgetsQt6::WidgetsQGraphicsView 接口稳定兼容部分行为微调中文编码处理MSVC 需 pragma同上课程设计生态教程和现成工程最多相对少5.2 CMake 阶段Qt5 一直找不到现象按上面 2.2 的命令执行后cmake 输出Could not find a package configuration file provided by Qt5后面跟一串标准路径列表。原因CMake 不知道 Qt 装在哪。Windows 下 Qt 安装目录结构很长标准搜索路径扫不到 Qt5Config.cmake 所在的套件子目录。解决用-DCMAKE_PREFIX_PATH显式指定套件路径。Qt 5.14.2 默认安装到D:/Qt/5.14.2/mingw73_64命令写成cmake -DCMAKE_PREFIX_PATHD:/Qt/5.14.2/mingw73_64 -S . -B build注意路径必须指向包含 lib/cmake 的套件目录填到 Qt 安装根目录依然找不到。配完之后cmake --build build直接编译生成了 qtetris.exe 才算这一关过了。如果在 Qt Creator 里打开这一步会在套件选择界面自动完成不用手动传参。5.3 运行阶段缺 DLL 与 0xc000007b现象自己机器上编译完能跑把这个 build 目录拷到 U 盘发给同学双击 exe 报Qt5Core.dll not found或者弹0xc000007b应用无法正常启动。原因Release 模式的 exe 依赖 Qt 的多个 DLL这些动态库没跟着程序走。0xc000007b 多半是 MinGW 编译的程序跑到了 MSVC 的运行库环境架构或运行库不匹配。解决用 Qt 自带的 windeployqt 工具部署。打开 Qt 的命令行环境执行windeployqt --release build/qtetris.exe它会自动把需要的 Qt5Core.dll、Qt5Widgets.dll、platforms/qwindows.dll 拷贝到 exe 旁边。部署完之后整个目录可以打压缩包提交。发给同学之前再确认对方机器装没装对应架构的 VC 运行库MinGW 套件一般依赖libwinpthread-1.dll和libgcc_s_seh-1.dll这两个也在部署目录里一并带过去最稳的验证方式是扔进一台没装过 Qt 的电脑跑一遍。5.4 编译阶段MSVC 编码与中文乱码现象源码里的中文注释在 MSVC 编译时报警告 C4819运行后界面中文变成乱码文件路径含中文时连编译都过不去。原因项目源码是 UTF-8 编码MSVC 默认按本地 ANSI 代码页解析两者对中文的字节解释不一致。MinGW 工具链对 UTF-8 宽容很多这也是为什么课程设计推荐 MinGW 套件。解决源码统一保存 UTF-8 编码在首次包含头文件的地方加一句 MSVC 专用声明#if defined(_MSC_VER) #pragma execution_character_set(utf-8) // 仅 MSVC 需要 #endif界面上的中文文本用QStringLiteral(暂停)而不是裸字符串确保编码在编译期被正确转换成 Unicode。这里有个更省事的方案直接全工程用 Qt Creator MinGW 编译MSVC 的编码玄学基本不碰。答辩演示时用 MinGW 构建的版本最稳别在老师电脑上暴露编码问题。5.5 交互失灵按键没反应、画面不刷新现象窗口能正常弹出游戏也显示但方向键怎么按都没反应分数明明变了网格视图还停在原地。原因QGraphicsView 实例默认不接收键盘焦点必须显式设定焦点策略画面不刷新则是数据更新后没有调用视图的 update 触发重绘或者定时器没有启动。解决构造 view 后强制指定焦点策略并抢占焦点view_-setFocusPolicy(Qt::StrongFocus); view_-setFocus();定时器方面逻辑驱动下落用 QTimer 就行间隔设成timer_-setInterval(500); // 每 500ms 下落一格 timer_-start();关键习惯只在数据变化后调用 update()比如方块移动、消行、分数变化这些时间点不要在 paint 里做任何逻辑计算。用timer_-setInterval(0)跑无限制重绘是性能自杀小工程可能跑不出来但答辩时放大窗口或开录屏就会开始卡顿属于给自己埋雷。5.6 扩展源码加了 .cpp 还是 undefined reference现象自己新增了一个src/levellog.cpp控制难度编译时主程序报undefined reference to LevelLog::LevelLog()但代码看起来没毛病。原因CMake 的源码列表里没有这个文件。add_executable 只编译列表里已有的文件新增的 .cpp 不手动加进去链接阶段符号就找不全。解决改 CMakeLists.txt把文件补进 SOURCES 变量set(SOURCES ... src/levellog.cpp # 新文件必须加在这里 )从这往后我养成了一个习惯VSCode 里新建一个源码文件之后第一件事改 CMakeLists而不是先写代码。很多 Qt 老手也会被这种“找不到符号”的报错卡半小时其实原因就一条——文件没进构建。这个项目自带 CMakeLists 是现成模板复制一份改名加文件比直接改源文件安全得多。6. 从能玩到高分文档、验证流程与答辩弹药6.1 高分文档的四段式骨架这份资源里附带的手册.1.docx结构上建议这样拆第一章写需求分析讲清楚方块种类、计分规则、操作方式第二章写总体设计贴模块划分图和类关系第三章是核心算法说明把旋转、碰撞、消行三个函数挑出来细讲这是导师最爱看的部分第四章写运行环境、构建步骤和操作说明配上三张运行截图其中一张必须是消行瞬间的。注意截图要截窗口整体不要只截游戏区否则看不出工程上下文。6.2 提交前强制跑一遍的验证流程我会把部署验证写进交付清单先删掉 build 目录重新走一遍 configure 和 build确认不是靠残留文件才能编过再执行一次 windeployqt确认 exe 目录下 DLL 齐全最后把整个目录拷到另一个没有安装 Qt 的目录或机器里跑五分钟方向键、旋转、硬降、暂停、重开五个操作各点一遍分数显示、消行动画、游戏结束弹窗全部正常才算一份作业可以交。从那以后我每次要交带 UI 的 Qt 作业都会强制走完这一整遍流程包括录一段 1 分钟的操作演示视频放进文档附录。同样是能跑的作业这一段细节足够把答辩从“他做了个俄罗斯方块”拉到“这工程挺完整”的位置。6.3 三个低成本扩展玩法不写大功能也能有加分点第一分数本地保存用QSettings写注册表或 ini记最高分并在标题栏显示代码不超过二十行第二等级加速每消十行把 QTimer 的 interval 从 500ms 往下降到 100ms难度曲线立刻有了第三硬降动画hardDrop 时让方块在一帧内落到目标位置并闪一下视觉反馈明显增强。答辩时你主动讲这三个小扩展比等老师提问要稳得多。如果这份资源帮你把大作业撑过去了希望它能帮到你接下来每一次要动手做 Qt 项目的时候。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进