新闻详情

新闻详情

首页 / 资讯中心 / 详情

C++ Qt实现2048小游戏:核心算法与课设避坑指南

发布时间:2026/9/24 21:50:57来源:尧图网络
C++ Qt实现2048小游戏:核心算法与课设避坑指南
简介基于QT框架完成的2048小游戏完整课程设计资料面向学习C与GUI编程的高校学生可作为高级语言程序设计大作业参考。项目采用int[4][4]数组管理棋盘涵盖初始化得分与清空格子、随机生成数字2、检测空格及游戏结束逻辑、paintEvent绘制界面等核心模块代码结构清晰便于理解事件响应与界面刷新机制。压缩包共7个文件包含2个cpp源文件、1个头文件、1个ui界面文件、1个工程配置文件及1份项目报告docx整体仅45KB轻量易用。目前已有330人学习下载。资料附带项目报告详细说明设计思路与实现过程适合需要快速搭建类似小游戏或撰写课程报告的同学参考。1. 2048这个C大作业真正的难点不在界面而在那套合并规则很多人拿到“基于QT实现2048小游戏”这个题目第一反应是去调界面把格子颜色做得花里胡哨结果答辩时老师随便按几下方向键屏幕上出现“2 2 4”却被你合并成“8”的诡异结果整个项目瞬间翻车。基于Qt的C版2048核心价值不在窗口多漂亮而在于你能否把移动、合并、随机生成、判死这套规则用C的类、容器和事件机制组织得干净、可测、能讲。这篇笔记按实际做课设的顺序来写先定数据结构与接口再写核心算法最后才碰界面中间穿插编译和运行时的踩坑记录。适合正在选C大作业题目的学生也适合想把Qt事件驱动和标准容器操作一起复习一遍的开发者。2. 动手前先把类和接口定死MVC拆分的取舍与接口签名2.1 棋盘模型用QVector还是裸数组为什么我选了QVector2048的棋盘只有4乘4共16个格子用int board[4][4]完全够用很多参考代码也是这么写的。但我在课设里更推荐QVectorQVectorint理由有四条。第一移动算法里不可避免要“抽出一行、去掉0、合并、补0、写回”这些操作涉及按值构造临时行、反转、拷贝赋值QVector提供了一整套现成接口代码能短三分之一。第二二维数组作为函数参数会退化成指针接口签名写起来很别扭QVector是Qt容器可以按值传递底层隐式共享拷贝代价很低也可以传const引用。第三QVector可以一行初始化成4x4全零棋盘QVectorQVectorint(4, QVectorint(4, 0))裸数组还得写双重循环去清零。第四答辩时老师经常问“能不能扩展成5x5”用const int BOARDSIZE 4管理维度改一个常量就行裸数组需要连带改所有循环边界。有人会担心QVector比裸数组慢。实际上对于16个int的规模两者性能差异在硬件计时器上都测不出来键盘操作每秒也就几次完全感知不到。课设的性能瓶颈从来不在棋盘存储而在动画写太重导致刷新卡顿这一点后面避坑章节会细说。2.2 GameModel的接口骨架move、isGameOver、spawnRandomTile先定下来写代码前先把类设计写清楚不要边写边加成员。我这里采用一个GameModel类专职管游戏逻辑不碰任何QWidget相关代码界面另用一个MainWindow类只负责显示和键盘转发。先看头文件// GameModel.h #pragma once #include QObject #include QVector #include QPair class GameModel : public QObject { Q_OBJECT public: enum Direction { Up, Down, Left, Right }; static const int BOARDSIZE 4; explicit GameModel(QObject* parent nullptr); void startNewGame(); // 清空棋盘重新开局 bool move(Direction dir); // 尝试移动返回棋盘是否发生变化 bool isGameOver() const; // 棋盘满且无相邻相同块 int tileAt(int row, int col) const; // 只读访问某个格子的值 int score() const { return m_score; } signals: void boardChanged(); // move成功后发射通知界面刷新 void scoreChanged(int newScore); // 分数变化时发射 void gameOver(); // 判死时发射 private: void spawnRandomTile(); // 在随机空位生成2或4 int compressLine(QVectorint line); // 对一行做去零合并补零返回新增分数 bool boardsEqual(const QVectorQVectorint a, const QVectorQVectorint b) const; QVectorQVectorint m_board; int m_score; };几个接口设计上的参数说明。tileAt设计成只读访问器界面16个Label每帧刷新时调用它取数值。move返回bool表示这次按键是否真的改变了棋盘这个返回值在界面层可以做“没动就不刷新”的短路优化。spawnRandomTile是私有方法因为只有startNewGame和move内部需要调用它对外暴露意义不大反而容易让调用顺序出错。compressLine接受一个QVectorint引用直接修改传入的行新增分数通过返回值带出来这和算法实现里“合并”和“计分”同步进行的设计是对应的。信号槽方面boardChanged让界面在移动成功后统一刷新16个格子scoreChanged只负责分数Label的更新gameOver在判死时弹出提示。这样的切分在报告里也能讲清楚界面是显示层Model是业务层信号槽是两者之间的松耦合桥梁。2.3 MainWindow只管刷新与转发信号槽在这里比直接调用干净在哪MainWindow在构造时接收一个GameModel*指针通过connect把三个信号分别连接到自己的刷新槽函数。这里值得注意的是不需要在MainWindow里定义任何游戏逻辑函数它拿到的永远只是“模型已经变了你来刷新显示”。对比一下不这么做的写法很多代码会把棋盘数组直接放在MainWindow里键盘事件里直接操作数组再把结果画到界面上。这种写法的问题是键盘事件函数会越来越长先判断方向再处理移动再处理合并再判断结束再刷新界面最后在报告里你根本没法把“游戏规则”单独拎出来讲。而拆出GameModel之后你可以写一个独立的小测试程序不创建窗口直接调model.move(Left)用qDebug()打印棋盘验证合并规则对不对。这种可测试性在答辩时就是你最大的底气。信号槽相比直接函数调用还有一个好处将来你想加一个“AI自动演示”功能只需要在另一个对象里调用model.move()界面通过槽函数自动跟着刷新不需要让AI模块持有MainWindow的指针。对于大作业这种规模这可能用不上但接口一旦定成这样扩展起来确实顺手。3. 2048核心算法手把手实现移动、合并、随机生成与判死逻辑3.1 先把方向的映射剥开上下左右统一走“取行-压缩-写回”2048的移动规则本质是对每行或每列执行同一个压缩操作去掉空位、相邻相同合并、后面补零。四个方向的区别只在于“从哪个方向取数据”。我建议把方向映射单独处理而不是为每个方向写四个几乎一样的函数。// GameModel.cpp 部分代码 #include GameModel.h #include QRandomGenerator void GameModel::startNewGame() { m_board QVectorQVectorint(BOARDSIZE, QVectorint(BOARDSIZE, 0)); m_score 0; spawnRandomTile(); spawnRandomTile(); emit boardChanged(); emit scoreChanged(m_score); } int GameModel::compressLine(QVectorint line) { // 第一步去掉所有0 QVectorint filtered; for (int v : line) { if (v ! 0) filtered.append(v); } // 第二步相邻相同合并注意只合并一轮 int gained 0; QVectorint merged; for (int i 0; i filtered.size(); i) { if (i 1 filtered.size() filtered[i] filtered[i 1]) { merged.append(filtered[i] * 2); gained filtered[i] * 2; i; // 消耗掉被合并的那个格子 } else { merged.append(filtered[i]); } } // 第三步后面补0凑齐4个 while (merged.size() BOARDSIZE) merged.append(0); line merged; return gained; }compressLine是整个游戏算法的心脏。它假定传入的line是一个长度为4的数组已经按照“从头部往尾部”的方向排列好了。比如按左方向时一行原始数据是{2, 0, 2, 4}压缩后变成{4, 4, 0, 0}新增分数4。如果按右方向我们需要把同一行反向取出来作为line压缩后再反向写回。合并循环里i这行是关键。它保证了一次操作中每个格子最多参与一次合并也就是{2, 2, 2, 2}会变成{4, 4, 0, 0}而不是连锁合并成{8, 0, 0, 0}。这个细节如果丢了后面第5章的避坑部分会专门展开。3.2 四个方向统一处理move函数的坐标映射方向映射最简单的实现是把“取出某一行”和“写回某一行”分别做成两个小函数或者直接在move里用switch控制行列坐标。我的做法是用一个双重循环外层固定按行遍历内层按方向映射取坐标。bool GameModel::move(Direction dir) { QVectorQVectorint newBoard(BOARDSIZE, QVectorint(BOARDSIZE, 0)); int totalGained 0; bool moved false; for (int i 0; i BOARDSIZE; i) { QVectorint line; line.reserve(BOARDSIZE); // 根据方向从棋盘取出一行/一列数据 for (int j 0; j BOARDSIZE; j) { int r i, c j; switch (dir) { case Up: r j; break; case Down: r BOARDSIZE - 1 - j; break; case Left: /* ri, cj 保持不变 */ break; case Right: c BOARDSIZE - 1 - j; break; } line.append(m_board[r][c]); } int gained compressLine(line); totalGained gained; // 压缩后的数据写回newBoard for (int j 0; j BOARDSIZE; j) { int r i, c j; switch (dir) { case Up: r j; break; case Down: r BOARDSIZE - 1 - j; break; case Left: /* 不变 */ break; case Right: c BOARDSIZE - 1 - j; break; } newBoard[r][c] line[j]; } } if (newBoard ! m_board) { m_board newBoard; m_score totalGained; emit boardChanged(); emit scoreChanged(m_score); return true; } return false; }这里比较重要的一点是newBoard ! m_board的整盘比较。QVector重载了相等运算符逐元素比较省去手写boardsEqual的麻烦头文件里那个私有方法也就不需要了。如果移动前后棋盘完全相同说明本次按键不产生任何有效移动此时不需要生成新方块逻辑上保持棋盘不变就行这也符合原版2048的行为。另一个细节在写回newBoard之前newBoard初始是全零所以compressLine补的0会自然落在正确的位置上。上移方向取出的是每一列从上到下压缩后写回同一列相当于把每个数字向上“滑”到底。下移则是从下往上取行压缩后写回原列模拟向下滑动。四个方向只改内层循环的坐标映射压缩逻辑完全不碰。3.3 随机数字的生成QRandomGenerator的使用与弃用qrand的原因每次有效移动之后需要在空白格里生成一个新方块原版规则是90%概率生成210%概率生成4。void GameModel::spawnRandomTile() { QVectorQPairint, int emptyCells; for (int r 0; r BOARDSIZE; r) { for (int c 0; c BOARDSIZE; c) { if (m_board[r][c] 0) { emptyCells.append({r, c}); } } } if (emptyCells.isEmpty()) return; int idx QRandomGenerator::global()-bounded(emptyCells.size()); int value (QRandomGenerator::global()-bounded(10) 0) ? 4 : 2; m_board[emptyCells[idx].first][emptyCells[idx].second] value; emit boardChanged(); }QRandomGenerator::global()-bounded(n)返回[0, n-1]区间内的随机整数线程安全不需要自己管理种子。不要再用qsrand和qrand的组合这两个函数在Qt 5.15里虽然还能用但已经标记为弃用Qt 6直接移除。有些旧教程教的qsrand(QTime::currentTime().msec())这种种子写法在现代Qt里属于拉低代码印象分的旧习惯。bounded(10) 0表示十分之一的概率生成4反过来说就是九成概率生成2和原版2048一致。补充一个审题层面的细节startNewGame里连续调用两次spawnRandomTile保证开局有两个数字。两次调用之间没有额外延迟这是线程安全的因为QRandomGenerator是全局单例内部有锁。3.4 判死要不要每次移动后都做isGameOver的两种触发时机游戏结束条件有两个棋盘16格全部填满且任意相邻两格都不相等。注意是“上下左右相邻”不是斜对角也不是整行整列相同才算。bool GameModel::isGameOver() const { for (int r 0; r BOARDSIZE; r) { for (int c 0; c BOARDSIZE; c) { if (m_board[r][c] 0) return false; // 还有空位 if (c 1 BOARDSIZE m_board[r][c] m_board[r][c 1]) { return false; // 左右相邻可合并 } if (r 1 BOARDSIZE m_board[r][c] m_board[r 1][c]) { return false; // 上下相邻可合并 } } } return true; }调用时机放在move()返回true之后、界面刷新过程中判断。也就是MainWindow的键盘事件里移动成功后先刷新棋盘再判断是否结束。不要在每个spawnRandomTile里判死因为开局生成两个数字后棋盘还剩14个空位此时判死没有意义只会白白消耗性能。这里有个容易忽略的场景玩家按了一次方向键但盘面没有变化。此时move返回false不需要生成新方块也不需要判死因为棋盘状态和按键之前完全一致。如果项目里做了“连续按同方向键”的误触保护就依赖这个返回值来短路。4. 用Qt Widgets把棋盘画出来QGridLayout、keyPressEvent与分数联动4.1 QGridLayout加16个QLabel比QPainter自绘更适合课设有不少同学看到网上一些炫酷版本用QPainter在paintEvent里绘制整个棋盘然后为了处理窗口缩放重新计算方块坐标调试一下午画出来的方块边缘还对不齐。课设阶段我的建议是用QGridLayout摆16个QLabel每个Label负责显示一个格子的数字和背景色简单直接。// MainWindow.h 关键成员 class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(GameModel* model, QWidget* parent nullptr); protected: void keyPressEvent(QKeyEvent* event) override; private slots: void updateBoard(); // 刷新16个格子的显示 void updateScore(int score); // 更新分数Label private: GameModel* m_model; QLabel* m_scoreLabel; QLabel* m_tiles[GameModel::BOARDSIZE][GameModel::BOARDSIZE]; };构造函数的布局部分可以贴在MainWindow.cpp的初始化列表之后// MainWindow.cpp 构造与布局 MainWindow::MainWindow(GameModel* model, QWidget* parent) : QMainWindow(parent), m_model(model) { auto* central new QWidget(this); auto* outerLayout new QVBoxLayout(central); m_scoreLabel new QLabel(分数: 0, central); m_scoreLabel-setAlignment(Qt::AlignCenter); outerLayout-addWidget(m_scoreLabel); auto* grid new QGridLayout; grid-setSpacing(8); for (int r 0; r GameModel::BOARDSIZE; r) { for (int c 0; c GameModel::BOARDSIZE; c) { m_tiles[r][c] new QLabel(, central); m_tiles[r][c]-setAlignment(Qt::AlignCenter); m_tiles[r][c]-setMinimumSize(90, 90); grid-addWidget(m_tiles[r][c], r, c); } } outerLayout-addLayout(grid); setCentralWidget(central); setFixedSize(420, 480); connect(m_model, GameModel::boardChanged, this, MainWindow::updateBoard); connect(m_model, GameModel::scoreChanged, this, MainWindow::updateScore); setFocusPolicy(Qt::StrongFocus); updateBoard(); }setSpacing(8)控制格子间距视觉上比默认的0间距舒服。每个Label设了90x90的最小尺寸窗口固定420x48060留给了分数栏和四周留白这样游戏窗口不会在玩家无意拖拽时变形。setFixedSize一步锁死省得写resizeEvent。4.2 重写keyPressEvent方向键与WASD双配的写法键盘事件的焦点问题是个典型的坑。QMainWindow默认不会主动获得键盘焦点如果界面上还有按钮或别的控件方向键会先落在那个控件上你的keyPressEvent压根不会触发。解决办法是构造函数的最后调用setFocusPolicy(Qt::StrongFocus)然后在窗口显示后调用setFocus()。void MainWindow::keyPressEvent(QKeyEvent* event) { GameModel::Direction dir; bool handled true; switch (event-key()) { case Qt::Key_Up: case Qt::Key_W: dir GameModel::Up; break; case Qt::Key_Down: case Qt::Key_S: dir GameModel::Down; break; case Qt::Key_Left: case Qt::Key_A: dir GameModel::Left; break; case Qt::Key_Right: case Qt::Key_D: dir GameModel::Right; break; default: handled false; } if (handled) { bool moved m_model-move(dir); if (moved m_model-isGameOver()) { QMessageBox::information(this, 游戏结束, 棋盘已满且无法继续合并); } } else { QWidget::keyPressEvent(event); } }这里handled变量用来区分“方向键被处理”和“其他按键交给基类默认处理”。WASD方案并不是原版2048有的但很多同学笔记本上开着中文输入法方向键会被输入法拦截这时WASD就是救命方案。注意如果QMessageBox弹出后焦点转移窗口需要再次调用setFocus()才能继续接收键盘事件可以用QMessageBox::information的返回值处理完后再设一次焦点。4.3 分数与界面的刷新链路信号连接与统一刷新updateBoard()槽函数负责把16个Label全部重刷一遍。数字为0时显示空字符串非0时显示数字本身同时根据数值用QSS切换背景色。void MainWindow::updateBoard() { for (int r 0; r GameModel::BOARDSIZE; r) { for (int c 0; c GameModel::BOARDSIZE; c) { int value m_model-tileAt(r, c); QLabel* lbl m_tiles[r][c]; if (value 0) { lbl-setText(); lbl-setStyleSheet(background-color: #bbada0; border-radius: 8px;); continue; } lbl-setText(QString::number(value)); // 数字越大颜色越深这是2048玩家熟悉的一套色板 QString bg; switch (value) { case 2: bg #eee4da; break; case 4: bg #ede0c8; break; case 8: bg #f2b179; break; case 16: bg #f59563; break; case 32: bg #f67c5f; break; case 64: bg #f65e3b; break; default: bg #edcf72; break; } lbl-setStyleSheet( QString(background-color: %1; border-radius: 8px; font-size: 28px; font-weight: bold;) .arg(bg)); } } } void MainWindow::updateScore(int score) { m_scoreLabel-setText(QString(分数: %1).arg(score)); }每次都重建整个StyleSheet字符串在16个Label的规模下性能完全没问题不用做什么缓存优化。颜色值沿用2048经典配色答辩时一眼就能认出是这个游戏。128以上的数字统一用#edcf72深金色省得为2048单独配一个颜色。字体大小固定28px四位数的“1024”在90x90的格子里也能放得下。5. Qt 5.15踩坑与排查版本混用、linuxfb插件、合并规则与焦点问题这一章专门写编译和逻辑调试中容易翻车的五个点。每一条都是我先看到现象、再定位原因、最后给出解决办法的真实过程按顺序查就能解决大部分问题。5.1 兼容性翻车fatal: cannot mix incompatible qt library (version ex50601) with this library现象编译C/Qt项目时报错提示中带ex50601字样说当前库和另一个Qt库版本不兼容。常见于Windows上装了多个Qt版本或者用CMake时把系统路径的Qt头文件和自带Qt的lib文件混在一起了。原因Qt5.15.2对应的版本宏是QT_VERSION_STR 5.15.2如果头文件是5.15.2、链接库是5.12的或者反过来链接器立刻报错。ex50601这个数字串其实是版本编码50601表示5.6.1说明某处有一条旧版Qt库混了进来。最常见的情形是系统环境变量PATH里先写了另一个Qt的bin目录你在命令行里直接qmake时用的是旧版而mingw32-make用的是新版。解决先确认当前Qt的安装版本命令行执行qmake -v看输出是否和你构建用的版本一致。在Qt Creator里检查构建套件Kit的Qt版本和编译器是否配对。如果是从旧的.pro文件打开项目删掉build-开头的构建目录执行“清理项目”后再重新构建。还有CMake用户检查CMAKE_PREFIX_PATH是否指向了你真正想用的Qt安装目录不要依赖系统默认路径。5.2 编译通过运行黑匣子Could not find the Qt platform plugin linuxfb现象代码编译顺利通过在Linux服务器或嵌入式板卡上运行可执行文件弹出qt.qpa.plugin: Could not find the Qt platform plugin linuxfb in ...之后进程崩溃。窗口没有出现终端里只留下这一行错误。原因Qt的窗口系统插件是运行时动态加载的。桌面Linux上用xcb插件没有显示服务器或使用嵌入式平台时需要linuxfbLinux帧缓冲插件。发行包没有把这个插件目录带过去或者运行时没有设置QT_QPA_PLATFORM环境变量Qt就找不到平台插件。解决分两步检查。第一步看你的Qt安装目录下plugins/platforms/里有没有libqlinuxfb.so没有就说明安装时没勾选对应组件重新运行Qt安装器补装第二步在运行脚本里显式指定# run.sh export QT_QPA_PLATFORMlinuxfb export QT_QPA_PLATFORM_PLUGIN_PATH/opt/Qt/5.15.2/gcc_64/plugins/platforms ./2048_game如果目标是部署到嵌入式设备linuxfb之外还有eglfs可选后者利用GPU渲染性能更好但需要板子有对应的显示驱动支持。课设阶段先用linuxfb跑通就行不要一次性追求复杂渲染。5.3 合并规则写错的翻车现场{2,2,4}被算成了8现象游戏过程中一行显示2 2 4按一次左移后变成8 0 0 0而不是标准的4 4 0 0。越玩分数涨得越离谱但很多人玩到一半才发现。原因压缩算法在发现filtered[0] filtered[1]后立即合并合并后的结果4又被拿去和后面的4比较发生了连锁合并。也就是说合并循环里没有“每个格子只参与一次合并”的限制。标准2048的规则是一行内的同值合并只做一轮{2, 2, 4}只能把前两个2合并成4然后这个新4和原来的4并排放在一起不能再合并成8。解决回到第3章的compressLine合并循环里命中相等分支后执行i跳过一个格子确保合并后的块不参与本轮后续比较。这段逻辑需要单独验证不要通过手工玩游戏来试写一个临时测试函数最稳// 临时验证代码可在main函数里直接调用 QVectorint test {2, 2, 4}; GameModel model; int gained model.compressLine(test); qDebug() test; // 期望输出 {4, 4, 0, 0} qDebug() gained; // 期望输出 4{2, 2, 2, 2}的用例也要测正确结果应该是{4, 4, 0, 0}新增分数8而不是{8, 0, 0, 0}。这类逻辑bug在报告里写“单元测试”章节时是很好的素材。5.4 方向键无效的焦点问题setFocusPolicy忘了设现象运行程序后鼠标点击窗口里的格子区域方向键完全没有反应但如果点击的是窗口标题栏再按方向键偶尔有用。加了按钮控件后焦点被按钮吃掉的概率更高。原因QMainWindow默认的focusPolicy是Qt::NoFocus键盘事件不会直接派发给窗口。窗口上有任何子控件时焦点停留在某个子控件上按键事件被发往该控件而不是窗口。你重写的keyPressEvent根本没被调用。解决构造函数里加一行setFocusPolicy(Qt::StrongFocus)窗口就能主动获取键盘焦点然后在show()之后调用setFocus()。如果界面上有按钮比如“重新开始”按钮点击按钮后焦点会跑到按钮上此时方向键又失效了。处理办法是给按钮的点击槽函数末尾重新调用setFocus()void MainWindow::onRestartClicked() { m_model-startNewGame(); setFocus(); // 把焦点抢回来否则下一次方向键会失灵 }另一种更省心的方案是在keyPressEvent里不调用QWidget::keyPressEvent(event)而是直接event-accept()吞掉所有按键事件但这样按钮的快捷键会受影响不推荐。5.5 中文乱码细节MSVC下的/utf-8选项现象在Windows上用MSVC编译器编译源代码里写的中文字符串比如“游戏结束”“重新开始”在界面上显示成乱码。用MinGW编译器则一切正常。原因MSVC默认把源文件按本地代码页GBK解析而Qt Creator保存文件时默认是UTF-8编码。两者不一致字符串字面量在内存里就成了错误字节序列。解决最稳妥的办法是在.pro文件里给MSVC加编译选项强制按UTF-8解析源文件# 2048.pro QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET 2048_game TEMPLATE app CONFIG c17 # MSVC下强制UTF-8源码解析避免中文乱码 msvc { QMAKE_CXXFLAGS /utf-8 } SOURCES \ main.cpp \ GameModel.cpp \ MainWindow.cpp HEADERS \ GameModel.h \ MainWindow.h如果用的是CMake等价写法是给target_compile_options添加$$CXX_COMPILER_ID:MSVC:/utf-8。顺带注意所有源文件统一用UTF-8保存不要在Qt Creator的设置里把默认编码改成GBK否则换到Linux服务器上编译又会出现一串警告。6. 从能玩到拿高分悔棋、最高分存档和项目报告的写法课设做到能玩只是及格想让答辩时老师眼前一亮可以在这三个方向上各加一点一步悔棋、最高分持久化、以及一份能和代码对得上的报告。悔棋的实现思路很简单在move调用前把整盘数据快照保存到m_lastBoard再在需要悔棋时把快照覆盖回去。最高分用QSettings存到本地注册表或配置文件程序重启后仍能读取。第三步是和项目报告配合的边界设计。// 悔棋的完整实现要点 void GameModel::saveSnapshot() { m_lastBoard m_board; // QVector隐式共享赋值开销极小 m_lastScore m_score; } bool GameModel::undo() { if (m_lastBoard.isEmpty()) return false; // 没有历史记录 m_board m_lastBoard; m_score m_lastScore; m_lastBoard.clear(); // 悔棋一次后清空历史防重复撤销 emit boardChanged(); emit scoreChanged(m_score); return true; }调用顺序上move()内部在改动棋盘之前先调用saveSnapshot()因此玩家每次有效移动都会保存一份“移动前”的状态。undo只能回退一步二次撤销直接返回false避免玩家无限悔棋。QSettings记录最高分只需要两个调用。写入放在scoreChanged信号的处理里如果当前分数大于已存最高分就更新文件。读取在MainWindow构造时加载一次显示在分数Label右侧。这段代码不要和游戏逻辑纠缠单独放在界面层里。项目报告的写法我建议按这个结构组织每个章节都能在代码里找到对应物报告章节对应代码或设计需求分析游戏规则描述移动、合并、随机生成2/4、结束条件总体设计GameModel和MainWindow两个类的职责划分信号槽关系图文字描述即可详细设计compressLine的算法流程、方向映射switch、spawnRandomTile随机策略测试与运行键盘操作说明、WASD附加方案、第5章的单元测试用例表总结遇到的问题与解决过程挑踩坑章节里的一条写够500字答辩时老师最常问的三个问题提前准备答案“为什么用QVector不用数组”、“2048判死条件怎么写的”、“合并会不会连锁”。这三个问题在这篇笔记对应的章节里都有答案用自己的话复述一遍即可。我当年就是把动画特效做得太复杂结果DEMO时玩家AI自动移动一次刷新卡了半秒反而被老师指出效率问题。如果你也想加动画控制在“数字弹出”这一层就够了不要为每个格子做滑动补间性能上不适合QLabel方案。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

专科生必看:10个高效降AIGC工具,轻松降低AI检测率 2026/9/24 23:13:04

专科生必看:10个高效降AIGC工具,轻松降低AI检测率

专科生必看!10个高效降AIGC工具推荐,告别AI检测!我这段时间后台私信快被问爆了。一堆大三、大二的专科生朋友,毕业论文、实习报告、课程设计都被“AI味检测”卡得死死的,降重平台一查就是一排红。有人甚至把稿子翻来覆…

阅读更多 →
野火IM服务端TCP MQTT连接管理全解析:从初始化到心跳保活 2026/9/24 23:13:04

野火IM服务端TCP MQTT连接管理全解析:从初始化到心跳保活

先纠正一个细节:标题里写着 “im-servier”,实际上野火IM服务端目录名是 im-server,后面提到时我都统一用 im-server。之前这个系列聊过整体架构、协议选型、模块规划,这一篇我想把服务端启动时 TCP MQTT 的初始化流程、连接生命周…

阅读更多 →
起偏与检偏:马吕斯定律的物理图像、实验演示与常见误区解析 2026/9/24 23:13:04

起偏与检偏:马吕斯定律的物理图像、实验演示与常见误区解析

起偏和检偏这件事,可以说是我当年学大学物理光学部分时,觉得最“有意思”也最“绕”的一段内容。原因很简单:偏振光看不见摸不着,不像干涉衍射还能看到亮暗条纹,偏振这个东西纯粹靠逻辑推演和实验现象来建立直觉&#…

阅读更多 →
非标设备物联网联网:从数据孤岛到预测性运维的实战路径 2026/9/24 23:13:04

非标设备物联网联网:从数据孤岛到预测性运维的实战路径

1. 这不是“要不要联网”的选择题,而是“还能不能活下去”的生存线非标设备、物联网联网、传统运维——这三个词凑在一起,听起来像技术部门在开务虚会时念的PPT标题。但如果你正管着十几台定制化产线设备、三五套老式包装机、七八台客户现场调试出来的专…

阅读更多 →
野火IM服务端拆解:TCP MQTT连接管理与消息路由实践 2026/9/24 23:13:04

野火IM服务端拆解:TCP MQTT连接管理与消息路由实践

野火IM的源码我前后翻过好几遍,尤其服务端的连接处理部分,越看越觉得有必要单独写一篇来拆。im-server是野火IM服务端的核心模块,它做的事情用一句话概括就是:监听TCP端口,接收客户端长连接,基于MQTT协议完…

阅读更多 →
Python微博情感分析系统实战:从数据采集到可视化全链路 2026/9/24 23:12:56

Python微博情感分析系统实战:从数据采集到可视化全链路

简介:这是一套面向高校计算机相关专业学生的微博情感分析系统毕业设计完整源码,适合作为毕业设计、期末大作业或课程设计参考,也方便零基础学习者上手实战。项目基于Python实现,围绕微博文本的采集、清洗与情感倾向判别展开&#…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞