
简介这是一套基于QT/C的俄罗斯方块游戏毕业设计资料包包含完整可运行的MyTetrix源码、太原理工大学毕业设计论文最终版、答辩PPT、任务书及中期检查表面向计算机专业学生、Qt入门开发者及需要完成同类课设/毕设的人群。资源实现涵盖QGraphicsView界面绘制、随机方块生成与旋转、碰撞检测、消行计分、暂停/继续等核心逻辑并配有可执行程序便于直接运行验证。压缩包共45个文件约4.69MB主要类型包括cpp/h源文件、o编译中间文件、doc文档、pptx演示文稿、qrc资源文件及exe可执行程序等目录区分Debug与Release构建版本适合对照源码理解Qt项目组织方式。目前已有637人学习下载读者既能获得可运行的实战项目也能参考毕业设计论文结构与答辩PPT的展示思路是学习Qt桌面应用开发和完成课程设计的实用资料。1. 基于QT_C的俄罗斯方块游戏设计与实现这门课设到底在做什么每年计算机专业的大作业季总有那么一批同学在同一个题目上焦虑基于QT_C的俄罗斯方块游戏设计与实现。这个标题看起来是源码论文答辩ppt.rar三件套实际上反映的是课设交付的真实要求——光能跑还不够还得有论文能过查重、有PPT能撑住答辩现场十分钟。我见过太多人死磕代码写了一周最后在论文格式和答辩演示上翻车。这篇文章就把整套方案讲透游戏核心逻辑怎么拆、Qt框架怎么搭、论文和PPT怎么对应代码、以及那些让Qt项目崩溃的坑到底在哪。适合正在做课设的学生、想拿Qt练手的C初学者以及需要快速交付一个完整小游戏项目的工程师。2. 俄罗斯方块核心玩法拆解坐标系、方块旋转与消行判定不管界面做得多花哨俄罗斯方块的灵魂就三件事方块怎么描述、怎么旋转、怎么判定消除。这三件事全部建立在坐标系的定义上。我一般用两个坐标系一个是游戏面板的固定坐标系10列×20行左上角为原点另一个是方块自身的局部坐标系4×4网格。所有算法都在局部坐标系里算完再映射到面板坐标系上这样碰撞检测和绘制逻辑都能保持单纯。2.1 七种方块的数据建模4x4矩阵、颜色表与初始形态最常见的做法是用一个int[4][4]二维数组描述每种方块的初始形态1 表示该格子被占用0 表示空。七种方块I、O、T、S、Z、J、L统一定义在一个常量数组里下标同时对应颜色表避免维护七份独立代码。// tetris_types.h #ifndef TETRIS_TYPES_H #define TETRIS_TYPES_H #include QColor const int ROWS 20; // 面板行数 const int COLS 10; // 面板列数 const int CELL 30; // 单个格子的像素边长 const int BLOCK_SIZE 4; // 方块局部网格边长 // 每种方块在 4x4 网格中的初始形态1 表示占用 static const int SHAPES[7][BLOCK_SIZE][BLOCK_SIZE] { // 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} } }; // 与 SHAPES 下标一一对应的填充颜色 static const QColor COLORS[7] { QColor(#00bcd4), QColor(#ffeb3b), QColor(#9c27b0), QColor(#4caf50), QColor(#f44336), QColor(#2196f3), QColor(#ff9800) }; #endif // TETRIS_TYPES_H这段代码的关键在设计决策ROWS和COLS定义面板尺寸CELL决定后续绘制时每个格子占多少像素。QColor数组用QColor(#00bcd4)这种十六进制字符串构造比 RGB 三参数更直观也方便在 Qt Designer 里对色值。注意COLORS的下标从 0 到 6实际存进面板网格的数值建议从 1 开始0 表示空绘制时取COLORS[grid[y][x] - 1]来避免访问数组越界。2.2 方块旋转的两种方案旋转矩阵公式与硬编码状态表旋转是俄罗斯方块里最容易出玄学 bug 的地方。常见做法有两种第一种是每帧用旋转公式把 4x4 数组整体旋转 90 度第二种是预先给每种方块硬编码 4 个旋转状态运行时按下标取数组。对于课设我推荐第二种代码量多但逻辑直白答辩时能讲清楚第一种更接近工程实践但要处理旋转后偏移问题。// 方案A旋转公式法——顺时针旋转 4x4 网格 void rotateClockwise(int grid[BLOCK_SIZE][BLOCK_SIZE]) { int temp[BLOCK_SIZE][BLOCK_SIZE] {0}; for (int row 0; row BLOCK_SIZE; row) { for (int col 0; col BLOCK_SIZE; col) { temp[row][col] grid[BLOCK_SIZE - 1 - col][row]; } } memcpy(grid, temp, sizeof(temp)); }公式temp[row][col] grid[BLOCK_SIZE - 1 - col][row]的含义是原数组的第BLOCK_SIZE - 1 - col行、第row列搬到新数组的row行、col列。用一个 4x4 矩阵实际转一下就会发现左上角的格子会跑到右上角整条 I 方块从横向变纵向这是标准的矩阵顺时针旋转。这里的参数grid是方块的局部坐标数组旋转结果仍保持在 4x4 范围内不会污染游戏面板数据。// 方案B硬编码状态表——以 T 方块为例定义四个旋转状态 static const int T_SHAPES[4][BLOCK_SIZE][BLOCK_SIZE] { { // 0° {0, 0, 0, 0}, {0, 1, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0} }, { // 90° {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0} }, { // 180° {0, 0, 0, 0}, {1, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0} }, { // 270° {0, 0, 1, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0} } };方案 B 的四个状态必须手动验证特别是 T、S、Z 这种不对称方块转完以后很容易多一格少一格。我给课设学生的建议是用方案 A 的公式自动生成状态把它塞进初始化函数调试时打印旋转后的数组对比方案 B 的手写结果两边一致才算过关。注意 O 方块旋转没有视觉变化可以跳过旋转逻辑节省一次碰撞检测I 方块旋转后位置会发生偏移需要在旋转后做一次边界回推墙踢下文避坑章节会展开。2.3 碰撞检测与消行判定边界检查、重叠判断与整行下移碰撞检测的原则很简单移动或旋转后先把方块的每个小格映射到面板坐标再逐个判断是否越界或与已落定方块重叠。消行则从面板底部往上扫发现整行非空就把该行以上所有行整体下移一行。// board.cpp bool Board::canPlace(int shape[BLOCK_SIZE][BLOCK_SIZE], int offsetX, int offsetY) { for (int row 0; row BLOCK_SIZE; row) { for (int col 0; col BLOCK_SIZE; col) { if (shape[row][col] 0) continue; int boardX offsetX col; int boardY offsetY row; if (boardX 0 || boardX COLS) return false; if (boardY ROWS) return false; if (boardY 0 m_grid[boardY][boardX] ! 0) return false; } } return true; }offsetX和offsetY是方块左上角在面板上的坐标shape[row][col]是方块局部坐标。boardY 0的判断很关键方块刚生成时可能有一部分在面板顶部之外负坐标这时只检查越界不检查重叠否则会误判成碰撞导致游戏一开始就无法操作。m_grid存的是 0 或 1~70 表示空格非 0 表示已落定方块的颜色索引。int Board::clearFullRows() { int cleared 0; for (int y ROWS - 1; y 0; --y) { bool full true; for (int x 0; x COLS; x) { if (m_grid[y][x] 0) { full false; break; } } if (full) { // 该行以上全部下移一行 for (int yy y; yy 0; --yy) for (int x 0; x COLS; x) m_grid[yy][x] m_grid[yy - 1][x]; for (int x 0; x COLS; x) m_grid[0][x] 0; cleared; y; // 重新检查当前行因为上面的行移下来了 } } return cleared; }这段代码的循环顺序是有讲究的。从底部往上扫遇到满行就下移然后y让同一行再检查一次——因为下移后当前行可能又满了。如果你漏了y会出现隔一行漏消的经典翻车现场。cleared返回值要传给上层做计分返回 0 说明这一回合没有消行不要在这里直接修改分数把计分逻辑放到主控制器里保持模块单一职责。3. 用Qt搭建游戏框架从主窗口到定时器驱动代码逻辑再漂亮跑不起来等于零。Qt 项目从选版本到搭框架有一套固定的套路选对 Qt 版本和编译套件用QTimer驱动游戏节奏用QPainter画界面用keyPressEvent接收键盘输入。这四个环节环环相扣任何一个出问题整个游戏都会黑屏或卡死。3.1 选对Qt版本与工程类型Widgets与QML怎么选课设和中小型工具项目我基本只推 Qt 5.15.2 以上的 Qt 5 LTS 分支配 Widgets 模块。Qt 5 的安装包在官网有开源版安装时勾选 MSVC 或 MinGW 对应套件建议同时勾上 Qt Designer 和 Qt Creator。QML/Qt Quick 适合动画密集的移动端界面但俄罗斯方块这种传统网格游戏用 Widgets 更稳QPainter绘制 10×20 的格子性能完全够代码也更容易被答辩老师看懂。Qt Designer 界面设计工具在这里主要用于搭主窗口布局比如放一个分数标签、一个预览区域剩余的网格绘制全部交给自定义的Board控件去paintEvent。别试图用 Qt Designer 去画游戏面板那是在给自己上刑。3.2 用QTimer驱动游戏循环下落节拍、等级加速与暂停恢复俄罗斯方块不是每帧都在动的游戏而是每隔固定时间下落一格。QTimer是最合适的选择比QThread开子线程简单得多也比while(true)sleep文明得多因为 UI 线程不会被阻塞。// mainwindow.cpp 构造函数中的关键代码 m_timer new QTimer(this); connect(m_timer, QTimer::timeout, this, MainWindow::tick); m_timer-setInterval(intervalForLevel(m_level)); // 初始 1000ms m_timer-start(); // 等级加速每过 10 行提高一级下落间隔缩短 int MainWindow::intervalForLevel(int level) { int interval 1000 - (level - 1) * 80; return qMax(interval, 100); // 兜底最快 100ms 一格 }intervalForLevel的设计要注意两个参数基数 1000 是第一级的下落间隔毫秒步长 80 是每级缩短的时间。qMax(interval, 100)防止等级高了以后间隔变成负数导致QTimer行为异常。QTimer的timeout信号默认在 UI 线程执行所以槽函数里不要做耗时操作俄罗斯方块的逻辑量级一次移动加一次碰撞检测完全在安全范围内。暂停功能直接m_timer-stop()恢复时start()不需要重新设置间隔QTimer会记住之前的值。3.3 用QPainter绘制游戏面板坐标换算、双缓冲与局部刷新绘制是新手最容易卡住的地方核心问题只有一个怎么把m_grid[y][x]的数组坐标变成屏幕上的像素矩形。答案是乘上CELL常量x * CELL算出列像素起点y * CELL算出行像素起点。// board.cpp void Board::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.fillRect(rect(), QColor(#1a1a2e)); // 背景底色 // 画网格线 painter.setPen(QPen(QColor(#3a3a4a))); for (int x 0; x COLS; x) painter.drawLine(x * CELL, 0, x * CELL, ROWS * CELL); for (int y 0; y ROWS; y) painter.drawLine(0, y * CELL, COLS * CELL, y * CELL); // 画已落定方块 for (int y 0; y ROWS; y) { for (int x 0; x COLS; x) { if (m_grid[y][x] 0) continue; QColor color COLORS[m_grid[y][x] - 1]; painter.fillRect(x * CELL 1, y * CELL 1, CELL - 2, CELL - 2, color); } } // 画当前活动方块 const int (*curShape)[BLOCK_SIZE] m_currentShape; for (int row 0; row BLOCK_SIZE; row) { for (int col 0; col BLOCK_SIZE; col) { if (curShape[row][col] 0) continue; int px (m_curX col) * CELL 1; int py (m_curY row) * CELL 1; painter.fillRect(px, py, CELL - 2, CELL - 2, m_curColor); } } }注意fillRect里用了CELL - 2并偏移1这是给每个方块之间留 1 像素的缝隙让网格线露出来视觉效果更好。如果你直接填CELL大小缝隙消失整个面板变成一块纯色板课设截图会非常难看。paintEvent不需要手动调用当你需要刷新画面时调用update()Qt 会在合适的时机重绘。千万不要在逻辑代码里直接调用paintEvent那是把绘制线程和逻辑线程搅在一起高负载时会界面闪烁。3.4 键盘事件处理软降硬降、旋转键位与事件过滤键盘处理的关键不在怎么写switch而在怎么防止按键重复触发和防止无响应。keyPressEvent默认支持按住重复但如果你的游戏没有对自动重复做处理按住左键会连续移动方块手感很飘。void MainWindow::keyPressEvent(QKeyEvent *event) { switch (event-key()) { case Qt::Key_Left: m_board-moveLeft(); break; case Qt::Key_Right: m_board-moveRight(); break; case Qt::Key_Down: m_board-softDrop(); // 软降下移一格并加分 break; case Qt::Key_Up: m_board-rotate(); break; case Qt::Key_Space: m_board-hardDrop(); // 硬降直落到底并落定 break; case Qt::Key_P: togglePause(); break; default: QMainWindow::keyPressEvent(event); break; } }softDrop和hardDrop的区别要讲清楚软降是一次下移一格并触发计分操作频繁硬降是一次性下落到碰撞位置然后立刻落定并产生新方块。硬降在部分省市的课设评分标准里属于加分项因为涉及快速落定的状态切换。这里有个血泪经验如果你让Board获得了焦点MainWindow可能收不到按键事件。解决方法是设置board-setFocusPolicy(Qt::StrongFocus)或者干脆在MainWindow上重写keyPressEvent让事件从主窗口下发这也是我推荐的方式。Qt::Key_P这种不常用的功能按键用event-isAutoRepeat()判断一下按住 P 键时避免暂停状态疯狂切换。4. 论文与答辩PPT的设计代码之外的另一半分数很多人的课设死在答辩环节代码能跑但讲不出设计思路论文结构和代码对不上PPT 上贴满大段代码没有逻辑。这一章我直接讲论文结构怎么组织、PPT 页面怎么排、现场演示怎么不翻车。这三件事的核心都是同一句话让老师在三分钟内找到他想看到的模块和设计依据。4.1 论文六章结构与代码模块的对照关系课程设计论文的标准结构是六章绪论、需求分析、总体设计、详细设计、测试、总结。关键技巧是把每一章和代码文件对应起来老师问这个类在论文哪里你得能指着目录说在第4章第3节。论文章节对应代码模块写作要点第1章 绪论无课题背景、俄罗斯方块的历史演进、选题意义第2章 需求分析无功能需求列表移动、旋转、消行、计分、暂停、用例图第3章 总体设计mainwindow.cpp系统架构图、模块划分、类图MainWindow、Board、Tetromino第4章 详细设计board.cpp、tetromino.cpp数据结构定义、旋转算法、碰撞检测流程、消行逻辑第5章 测试全部测试用例表格、边界测试、运行截图第6章 总结与展望无完成情况、不足、未来改进如联机对战在第 3 章总体设计里类图不要画得太复杂三到四个类就够了。主窗口类管界面和定时器棋盘类管网格数据和碰撞游戏逻辑类管方块生成和计分。类图画多了答辩老师追问起来你自己都容易解释不清。第 4 章详细设计是核心要把rotateClockwise的公式推导写出来把canPlace的边界条件列出来用 UML 时序图画出一次硬降的完整调用链。老师判断论文是不是抄的基本就看这一章的图和代码注释的一致性。4.2 答辩PPT的八页结构与讲解节奏答辩 PPT 控制在八页左右一页讲一分钟。很多人的 PPT 二十页起步讲到最后老师都失去耐心了。八页的结构我固定如下封面页标题、姓名、学号、指导老师、选题背景与意义、需求分析功能点列表、总体设计架构图加类图、核心模块实现旋转加消行两段关键代码附流程图、界面展示三到四张运行截图、测试结果表格列出用例和结果、总结与致谢。核心模块实现页是重点不要贴完整代码只贴rotateClockwise的公式和clearFullRows的循环示意图。用 Visio 或 draw.io 画一张消行流程的示意图比任何大段文字都有说服力。界面展示页建议放三张截图游戏运行中的正常界面、消行瞬间的界面、Game Over 界面。这三张图能覆盖老师对功能是否全部实现的一半疑问。4.3 现场演示的翻车点与备手方案现场演示的翻车点我踩过太多列几条最典型的。第一用 Debug 模式在 Qt Creator 里直接跑台上突然弹出崩溃对话框。解决提前用 Release 构建一个独立 exe放到桌面双击运行确认从双击到游戏界面出现不超过两秒。第二演示到一半游戏窗口失去焦点键盘事件全部失灵。解决演示前先点击一下游戏窗口讲完背景和需求分析之后再切到演示环节避免先切窗口再按键导致事件丢失。第三游戏在投影仪上画面偏小或模糊。解决在main.cpp里设置QApplication::setAttribute(Qt::AA_EnableHighDpiScaling)并且在绘制时用devicePixelRatio做缩放适配。另外建议录一份五分钟的演示视频存 U 盘万一电脑现场死机视频兜底这是所有答辩准备的后悔药。5. Qt环境与俄罗斯方块项目常见问题排查避坑这一章是血泪经验的汇总全是 Qt 开发新手最常撞上的坑每条都按现象、原因、解决的结构写。这些问题里有一半属于环境配置问题不解决的话你代码写得再对也白搭。5.1 Qt库版本混用cannot mix incompatible Qt library现象编译通过但程序启动时崩溃控制台输出fatal: cannot mix incompatible Qt library (version ex50601) with this librar后面通常会跟一串版本号。原因你的系统里装了多个 Qt 版本或者编译时链接的 Qt 库和运行时加载的 Qt 库一个是 5.x 一个是 6.x。常见于从下载站拖来一堆 Qt DLL、或者之前的旧项目残留了 Qt6 的环境变量。解决打开 Qt Creator 的构建套件设置确认这个项目用的是哪个 Qt 版本清理项目目录下的build-xxx文件夹删除所有临时缓存检查系统 PATH 环境变量是否被多个 Qt 目录污染只保留当前使用的那个。如果项目是用 qmake 管理的在.pro文件里显式注释掉多余的配置QMAKE会自动带上正确路径但你手动设置过INCLUDEPATH和LIB的话可能把错误版本引进来。最稳妥的办法是换一个干净的目录重新qmake make让 Qt Creator 从头生成构建脚本。5.2 平台插件缺失could not find the qt platform plugin linuxfb现象在 Linux 开发板上或纯终端环境没有桌面系统运行 Qt 程序报错qt.qpa.plugin: could not find the qt platform plugin linuxfb in ...然后程序退出。原因Qt 的 GUI 程序需要一个平台插件来对接显示系统在 PC 上通常是xcb在嵌入式设备上可能是linuxfb。你的发行版没装对应插件或者QT_QPA_PLATFORM环境变量被错误设置。解决先看/opt/Qt/5.15.2/plugins/platforms/目录是否存在里面有libqxcb.so、liblinuxfb.so等文件。缺少就重新安装 Qt 的 platform plugin 组件或者用发行版包管理器安装qt5-qpa-platform-plugin。临时应急可以在程序启动前设置export QT_QPA_PLATFORMxcb桌面环境或export QT_QPA_PLATFORMlinuxfb开发板让它强制走某个插件。注意开发板和 PC 的插件选型完全不同课设如果要在树莓派上演示记得交叉编译时把 platforms 目录一起拷过去这个目录漏了是最常见的黑匣子。5.3 高分屏DPI缩放导致绘制偏移现象游戏在 1080p 上正常换到 Windows 的 150% 缩放比例或 Mac 的 Retina 屏上画面模糊、格子错位、鼠标点击位置偏到离谱。原因Qt 默认按逻辑分辨率绘制高分屏的物理像素和逻辑像素比例不是 1:1QPainter画出的坐标与实际显示区域对不上。解决在main.cpp的main函数最开始QApplication创建之前加两行代码int main(int argc, char *argv[]) { QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); QApplication app(argc, argv); // ... 其余代码 }注意顺序必须在QApplication对象创建之前否则不生效。处理完 DPI 后paintEvent里获取控件尺寸时统一用width()和height()的逻辑值绘制时 Qt 会自动映射到物理像素。如果你在代码里用了fixedSize(300, 600)这种硬编码窗口尺寸在 Retina 屏上会自动放大两倍格子看起来很大但界面还算正常但如果用了setMinimumSize(300, 600)可能两边留出黑边。5.4 C随机数陷阱rand()的伪随机与7-bag随机器现象每次开新游戏第一个方块永远是同一个或者方块序列有明显的周期重复玩几局就能背板。原因用的是srand(time(NULL)) rand()time(NULL)精度是秒级你在一秒内启动两次游戏种子相同序列完全相同。另一个更严重的原因是rand() % 7会偏向小数字在随机数分布要求高的场景下不算真随机。解决C11 开始用random库示例代码如下#include random static std::mt19937 rng(std::random_device{}()); int randomTetrominoType() { static std::uniform_int_distributionint dist(0, 6); return dist(rng); }std::mt19937是梅森旋转算法的随机数引擎std::random_device{}()是真正的硬件随机数种子。更专业的俄罗斯方块实现会用7-bag 随机器把 7 种方块放进一个袋子每次随机抽出一种抽完 7 个再装袋。这样能保证每 7 个方块里 7 种全出现不会连续 5 个都是 I 方块游戏体验更公平。代码就十行std::vectorint bag; int nextPiece() { if (bag.empty()) { bag {0, 1, 2, 3, 4, 5, 6}; std::shuffle(bag.begin(), bag.end(), rng); } int type bag.back(); bag.pop_back(); return type; }这个方案在论文的方块生成模块里也能作为亮点讲比单纯一句rand()%7有价值得多。5.5 中文乱码源码编码与日志输出现象在 Windows 上编译后游戏界面里的分数暂停显示成乱码或者 Qt Creator 的控制台输出中文全部变成锟斤拷。原因Windows 默认代码页是 GBK而 Qt 源码文件存成 UTF-8MSVC 编译器按本地代码页读源码导致中文字符串被错误解释。QString内部用的是 Unicode但字面量在编译期就被破坏了。解决三条路任选。第一条在.pro文件里加QMAKE_CXXFLAGS /utf-8告诉 MSVC 源码按 UTF-8 读取第二条所有中文字符串写英文运行后再用tr(Chinese text)加载翻译文件但课设没必要上 i18n第三条遇到从外部读入的中文文本显式转换QString::fromLocal8Bit(str)或QString::fromUtf8(str)不要直接QString(str)赋值。我个人习惯是源码统一 UTF-8.pro加/utf-8一行解决省心。如果在 Linux 上开发再拷到 Windows 上编译编码问题会进阶成中文字符串在两边显示不同这时候直接用英文 label 最省事。6. 从课设到工程给俄罗斯方块加计分、音效与答辩验证清单课设拿 A 和拿 C 的分水岭往往不在基础功能上而在细节完善度。这一章讲三个具体的加分项和一个验收方法都是小幅改动但能让项目看起来像工程而不是作业。6.1 计分逻辑设计软降硬降与连击奖励基础计分规则要明确写进论文消一行 100 分两行 300 分三行 500 分四行 800 分软降每格 1 分硬降每格 2 分。这个梯度设计不是拍脑袋而是参考经典俄罗斯方块的计分曲线鼓励玩家一次积满四行Tetris。连击奖励是加分项连续两次落定都触发消行每次额外加 50 分乘以连击次数。计分模块建议单独一个ScoreManager类不要写在Board里这样论文里能多画一张类图。// score_manager.cpp 核心逻辑 class ScoreManager { public: void addSoftDrop() { m_score 1; } void addHardDrop(int cells) { m_score cells * 2; } void onLineClear(int lines) { static const int base[] {0, 100, 300, 500, 800}; m_combo (lines 0) ? m_combo 1 : 0; m_score base[lines] m_combo * 50; } private: int m_score 0; int m_combo 0; };m_combo的细节是消行数大于 0 才累加没消行就归零。base数组下表从 0 开始lines的取值范围是 1~4所以base[lines]不会越界但你要在调用处确保lines最大为 4否则要加防御判断。计分规则写进论文第 4 章计分模块设计时最好配一个表格列出消行数与分数对应关系老师就喜欢看到这种对比表。6.2 音效接入与资源文件管理音效用QSoundEffect类最轻量专门播放 WAV 短音效比QMediaPlayer占用小也不用装解码器。做法是把几个 wav 文件放进.qrc资源文件里这样程序打成单个 exe 也不会丢资源。#include QSoundEffect // 在 MainWindow 构造函数里初始化 m_rotateSound new QSoundEffect(this); m_rotateSound-setSource(QUrl(qrc:/sounds/rotate.wav)); m_rotateSound-setVolume(0.5f); m_clearSound new QSoundEffect(this); m_clearSound-setSource(QUrl(qrc:/sounds/clear.wav)); m_clearSound-setVolume(0.8f); // 调用处 void MainWindow::tick() { bool moved m_board-moveDown(); if (!moved) { int cleared m_board-settleCurrentPiece(); if (cleared 0) m_clearSound-play(); else m_rotateSound-play(); } }QSoundEffect的生命周期管理有个坑局部变量会立刻被析构声音根本播不出来。所以必须把音效对象作为成员变量挂在MainWindow或Board上。setVolume参数范围是 0.0 到 1.0Windows 老的 audio 驱动对 0.5f 以下的音量可能表现异常调试时先设 1.0 确认能响再调低。如果播放有爆音检查 wav 文件是不是 16bit PCM 格式压缩过的 wav 会触发 Qt 的音频解码警告。6.3 答辩前的十分钟代码走查清单把这份清单保存在docs/checklist.md答辩前一晚过一遍每项打勾。第一运行 Release 版本确认没有依赖缺失的 DLL 弹窗第二连续玩到 Game Over确认不会闪退第三把窗口缩到最小尺寸再还原确认绘制不崩第四快速按方向键 20 次确认方块没有飞出去第五消行瞬间截图确认计分数字正确第六暂停后按方向键确认方块不动第七关闭窗口确认进程结束不残留后台进程。第八检查代码文件里有没有// TODO和调试用qDebug输出答辩时不希望看到qDebug刷屏。我的习惯是每次课设都强制自己过一遍这份清单哪怕时间再紧。因为大部分答辩现场翻车都不是功能缺失而是边界情况没处理——按键按太快、窗口缩放、暂停瞬间切走焦点。把这些场景在清单里固定下来比答辩前一天临时试玩十局有效得多。希望帮到你。本文还有配套的精品资源点击获取