C++ Qt4 2048小游戏源码详解:二维数组与信号槽实战

📅 发布时间:2026/10/17 9:12:32
C++ Qt4 2048小游戏源码详解:二维数组与信号槽实战
简介这是一份基于C与Qt4框架编写的2048小游戏源码面向初学C及图形界面编程的读者。项目包含完整的游戏逻辑与界面实现六个文件涵盖两个cpp源文件、一个pro工程文件、一个ui界面文件、一个头文件及一个user配置文件其中cpp与头文件构成核心代码pro与ui用于工程与界面构建压缩包整体约7KB结构精简适合对照学习。游戏采用GameBoard管理棋盘状态与合并规则、Tile表示数字方块、GameManager控制游戏流程通过Qt4的QGraphicsView与QGraphicsItem绘制界面演示了面向对象封装、继承、事件处理及信号槽通信方式。已有266人学习下载读者可借此理解2048合并规则、键盘/滑动事件响应、游戏状态管理及得分判定等核心流程也可作为Qt4入门或课程设计的参考模板。项目虽小却完整覆盖了GUI程序从初始化、交互到结束判定的典型链路有助于初学者建立模块化编程思维。1. 用 C 和 Qt4 撸 2048这份资源到底值不值得下如果你刚学完 C 语法正愁找不到一个能跑起来、能看界面、又不太复杂的练手项目那 2048 几乎是最优解玩法人人都懂规则不超过三句话但实现起来恰好覆盖了二维数组操作、随机数、键盘事件、界面刷新和信号槽这些 C/Qt 入门必踩的坎。手头这份名为 2048qt小游戏C初学 的压缩包就是用 Qt4 写的一套完整实现——不用你从零憋架构直接打开工程看代码、改参数、跑起来再对着源码把每个模块拆明白。它适合两类人一是刚学完 C 基础、想看看真实小项目长什么样的新手二是要交课设、想快速搞懂游戏循环 事件驱动到底怎么配合的在校生。下面我按玩法逻辑 → 界面层 → 编译运行 → 踩坑的顺序把这份资源从头到尾拆给你看。2. 核心玩法拆解二维数组、滑动合并与胜负判断2.1 棋盘的数据结构为什么用固定大小二维数组打开资源里的 main.cpp 或 gameboard.cpp你会发现棋盘几乎必然是一个 4×4 的二维数组。常见做法是声明为int board[4][4]或QVectorQVectorint前者更直观、性能更好后者写起来稍灵活。初学阶段用固定数组就对了因为 2048 的棋盘大小不会变不需要动态分配。int board[4][4] {0}; // 全 0 表示空格这里用 0 代表空格2、4、8 等代表具体方块。选择 0 作为空位的理由是判断能不能移动只需要比较格子上的值是否相等合并或是否为 0移动不用额外维护一套状态标记。逻辑上数组本身既是数据模型又承担了界面要渲染的全部信息一份数据源搞定所有事这是新手最容易理解的设计。2.2 滑动合并算法先移动后合并顺序不能乱滑动是整个游戏的核心操作方向分上下左右四种。如果你自己从零写最常犯的错误是直接在原始数组上边遍历边合并导致同一个格子被重复合并。标准做法是分两步先把所有非零元素朝目标方向挤过去再对每行或每列做一次相邻合并。// 以向左滑动为例 void slideLeft(int board[4][4], int score) { for (int row 0; row 4; row) { // 第一步把非零元素紧凑到左侧 int insertPos 0; for (int col 0; col 4; col) { if (board[row][col] ! 0) { board[row][insertPos] board[row][col]; // 注意如果 insertPos-1 不等于原 col需要把原位置清零 if (insertPos - 1 ! col) { board[row][col] 0; } } } // 第二步从左到右合并相邻相等元素 for (int col 0; col 3; col) { if (board[row][col] ! 0 board[row][col] board[row][col 1]) { board[row][col] * 2; score board[row][col]; // 后面的元素往前挪一位 for (int k col 1; k 3; k) { board[row][k] board[row][k 1]; } board[row][3] 0; } } } }这段代码的思路是移动和合并分两个循环做移动阶段只负责紧凑不改变数值合并阶段只做相等即融合融合完再把后面的元素整体前移。参数score用引用传递因为合并产生的分数需要累加到主循环里。关键坑在于移动阶段的清零操作。insertPos指向下一个非零元素应该放的位置如果它和当前列号不一致说明发生了移动原位置必须清零否则数组里会出现重复数据。合并阶段的循环边界也要注意col 3而不是col 4因为每次比较的是col和col 1越界会导致读到一个不确定值。四个方向怎么处理最省事的方案是把向右、向上、向下都转换成向左操作。向右就是先把每行反转向左滑再反转回来向上是转置向左滑再转置。不过初学者更直接的做法是写一个通用函数通过参数控制遍历方向比如向右滑就每行从右往左扫描。资源里如果用的是后者的写法你能看到遍历的下标不是简单的col而是col--这就是方向控制的体现。2.3 随机生成新方块位置和数值的权重逻辑每次有效滑动后棋盘空位要随机生成一个方块通常是 90% 概率出 210% 出 4。void spawnTile(int board[4][4]) { // 收集所有空位坐标 QVectorQPairint, int emptyCells; for (int i 0; i 4; i) { for (int j 0; j 4; j) { if (board[i][j] 0) { emptyCells.append(QPairint, int(i, j)); } } } if (emptyCells.isEmpty()) return; // 随机选一个空位 int index rand() % emptyCells.size(); int row emptyCells[index].first; int col emptyCells[index].second; // 90% 概率生成 210% 生成 4 board[row][col] (rand() % 10 0) ? 4 : 2; }先用双重循环扫一遍棋盘把所有空位坐标存进QVector然后随机选一个下标赋值为 2 或 4。rand() % 10 0的意思是生成的随机数在 0 到 9 之间只有等于 0 的时候才出 4也就是 10% 概率。这里有个细节随机数种子。Qt 程序里通常在主函数或构造函数中调一次qsrand(QTime(0,0,0).secsTo(QTime::currentTime()))让种子随当前时间变化。如果不调种子rand()每次启动程序生成的序列都一样玩起来第一局和第二局出方块的位置完全相同体验会很奇怪。2.4 游戏结束判定没有空位也没有可合并项判断游戏是否结束不能只看棋盘满了就完事。如果棋盘满了但还有相邻相等的格子玩家仍然可以通过一次滑动产生空位游戏还能继续。所以标准判定条件是没有空位且任意相邻格子上下或左右都不相等。bool isGameOver(int board[4][4]) { // 有空位就没结束 for (int i 0; i 4; i) { for (int j 0; j 4; j) { if (board[i][j] 0) return false; } } // 检查相邻相等 for (int i 0; i 4; i) { for (int j 0; j 4; j) { if (j 1 4 board[i][j] board[i][j 1]) return false; if (i 1 4 board[i][j] board[i 1][j]) return false; } } return true; }这个判断逻辑放在每次滑动和生成新方块之后调用。注意两个循环都要检查上下和左右两个方向漏掉任何一个方向游戏该结束的时候不结束或者结束太早都会有。3. Qt4 GUI 层拆解信号槽、按键捕获与界面刷新3.1 为什么选 Qt4 而不是其他 GUI 库这份资源用的是 Qt4主要原因是很多学校的教材和课程设计还在用 Qt4 的 API而且 Qt4 的信号槽语法和控件命名跟 Qt5 有差异。Qt4 的类名和头文件与 Qt5 略有不同比如QMainWindow在 Qt4 下对应的头文件是QtGui/QMainWindow而 Qt5 是QMainWindow。编译时如果直接报头文件找不到多半就是 Qt 版本不匹配的问题。另外 Qt4 的QApplication头文件是QtGui/QApplication而 Qt5 需要QApplication。这些差异在代码里能看到明显痕迹——如果源码里到处是#include QtGui/...那基本可以确定是 Qt4 工程。3.2 主窗口搭建不用 Designer 手写代码的方式资源里大概率用的是纯代码构建界面而不是 .ui 文件。好处是源码一目了然不用额外打开设计器。主窗口继承QMainWindow在构造函数里设置标题、固定尺寸然后把核心控件布局进去。#include QtGui/QMainWindow #include QtGui/QGridLayout #include QtGui/QLabel #include QtGui/QKeyEvent class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent 0); ~MainWindow(); protected: void keyPressEvent(QKeyEvent *event); private: QLabel *cell[4][4]; // 每个格子对应一个 QLabel int board[4][4]; int score; };用QLabel数组表示棋盘格子每个QLabel显示一个数字或空。构造函数里用QGridLayout把 16 个QLabel按 4×4 排布并设置固定大小和字体。信号槽在 Qt4 里的连接方式跟 Qt5 区别明显Qt4 用宏connect(button, SIGNAL(clicked()), this, SLOT(onButtonClicked()));Qt5 之后可以直接写connect(button, QPushButton::clicked, this, MainWindow::onButtonClicked);如果你把 Qt4 项目放到 Qt5 环境编译SIGNAL/SLOT 宏仍然兼容但反过来不行。初学者最容易翻车的就是明明信号槽没生效却查不到编译错误——因为 SIGNAL 和 SLOT 宏本质上是字符串只有在运行时连接失败才会在控制台打印提示。3.3 按键控制重写 keyPressEvent 而不是用按钮2048 是靠键盘方向键控制的所以主窗口需要捕获按键事件。在 Qt 里最直接的方式是重写keyPressEventvoid MainWindow::keyPressEvent(QKeyEvent *event) { bool moved false; switch (event-key()) { case Qt::Key_Up: moved slideUp(board, score); break; case Qt::Key_Down: moved slideDown(board, score); break; case Qt::Key_Left: moved slideLeft(board, score); break; case Qt::Key_Right: moved slideRight(board, score); break; default: QMainWindow::keyPressEvent(event); return; } if (moved) { spawnTile(board); updateBoard(); if (isGameOver(board)) { // 弹窗提示游戏结束 } } }关键点在于moved这个返回值。很多新手忽略这一步无论是否真的发生移动都调用spawnTile结果就是玩家按了无效方向比如向上滑但所有格子都顶到顶部了也会刷出新方块游戏难度凭空增加。好的做法是每个滑动函数返回一个bool表示这次滑动是否改变了棋盘布局。updateBoard()是界面刷新函数遍历 16 个QLabel根据board数组里的值设置文字和背景颜色void MainWindow::updateBoard() { for (int i 0; i 4; i) { for (int j 0; j 4; j) { int value board[i][j]; cell[i][j]-setText(value 0 ? : QString::number(value)); // 根据 value 设置背景色比如 2 浅黄、4 橙、8 红等 } } }颜色映射可以按数字大小做区间判断。2048 经典的配色是浅色底加深色字具体色值抄官方就行。这段逻辑不复杂但它是数据 → 界面的桥梁理解了这个函数你就明白了什么叫界面只是数据的另一种表达形式。3.4 界面刷新与性能为什么不需要双缓冲2048 属于低频率刷新游戏——只有按一次键才更新一次而且每次只改 16 个格子的文字和颜色。Qt4 的 QLabel 自动处理了绘制不存在闪烁问题也不需要像射击游戏那样搞双缓冲。资源里如果直接调用update()或repaint()都没问题后者强制立刻重绘前者合并到事件循环再重绘对于这种低频率更新两种方式差别可以忽略。有一点要注意setText会触发 QLabel 的update信号如果 16 个格子连续更新每个格子都会发一次重绘请求。初学阶段不用优化但如果想了解优化点可以把所有setText做完后手动调用一次repaint()把多次绘制合并成一次——不过这只是个概念实际效果在这个项目里肉眼看不出来。4. 动手复现从源码编译到跑起来的完整流程4.1 环境准备Qt4 和编译器版本选型编译这份资源需要装 Qt4 开发库。不同系统安装方式不一样。Ubuntu/Debian 系sudo apt-get install qt4-qmake libqt4-dev g makeWindows 上比较复杂Qt4 官方已经停止支持你需要找 Qt 4.8.x 的离线安装包装 MinGW 版本。装完记得把qmake.exe所在目录加进 PATH 环境变量否则命令行找不到 qmake。macOS 上装 Qt4 更麻烦现在 Homebrew 默认只提供 Qt5/Qt6建议直接用 Linux 虚拟机或 Windows 环境。4.2 用 qmake 编译pro 文件的写法与常见编译错误Qt4 工程的标准构建流程是先写 .pro 文件再用 qmake 生成 Makefile最后 make。资源包里的 .pro 文件大概长这样QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET 2048 TEMPLATE app SOURCES main.cpp gameboard.cpp HEADERS gameboard.h第三行greaterThan(QT_MAJOR_VERSION, 4)是 Qt 官方为兼容 Qt4/Qt5 写的宏判断Qt4 不需要widgets模块Qt5 必须加。如果你用的是纯 Qt4 环境这行可以删掉直接写QT core gui即可。编译执行两步qmake -project qmake make第一条命令会根据当前目录的源文件自动生成 .pro第二条生成 Makefile第三条开始编译。如果 qmake 报错说找不到 Qt 模块先检查qmake -v输出版本是不是 4.x如果环境里装了多个 Qt 版本qmake -query QT_VERSION能查到当前指向哪个版本。编译过程最常见的报错是main.cpp:3:36: error: QtGui/QMainWindow: No such file or directory这几乎可以肯定是编译器版本不对——你大概率在 Qt5 的 include 路径里找 QtGui 目录Qt5 把这些头文件拆到了 QtWidgets 目录下。解决办法是改 include 路径而不是换 Qt 版本。如果资源本身是 Qt4 的写法建议直接装 Qt4 环境别强行在 Qt5 下改——改头文件、改 QApplication 那一套折腾下来够你写半个项目了。4.3 代码运行与调试启动黑屏或窗口不弹出的排查路径编译成功不代表能跑。运行程序窗口没弹出来有以下可能./2048黑一下屏就退先看主函数#include QtGui/QApplication #include mainwindow.h int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow w; w.show(); return app.exec(); }app.exec()必须放在最后一行它是进入 Qt 事件循环的入口。如果括号位置写错窗口 show 完就立刻 return 0程序闪退。另一个常见问题是MainWindow构造函数里的setFixedSize写成了resize导致窗口尺寸和布局不一致某些系统下会出现窗口有了但内容空白。运行时如果方向键没反应检查主窗口是否设置了焦点策略。QMainWindow 默认能接收键盘事件但窗口内如果有按钮或其他控件抢了焦点keyPressEvent 就不会触发。常见修复是setFocusPolicy(Qt::StrongFocus);这句写在 MainWindow 构造函数里强制窗口持有焦点。5. 避坑指南Qt4 版 2048 的五个经典翻车点5.1 编译报错SIGNAL 和 SLOT 宏不匹配现象程序编译通过运行后按钮点击、按键响应完全没有反应控制台打印QObject::connect: No such slot MainWindow::xxx()。原因Qt4 的信号槽连接本质是字符串比较SIGNAL 和 SLOT 宏里的函数签名必须完全一致包括参数类型和有无 const。比如connect(btn, SIGNAL(clicked()), this, SLOT(onClick(int)))信号 clicked() 不带参数槽带 int 参数运行时必然连接失败。解决把信号槽的签名逐一对照。槽函数的声明必须写在头文件里并且类要加Q_OBJECT宏。写完新槽函数后需要重新运行 qmake因为 Q_OBJECT 会生成 moc 文件没有 moc 文件槽函数就跟普通成员函数一样不会进入 Qt 的元对象系统。5.2 滑动方向反了或合并错乱现象按上箭头方块往下走按左箭头左边的格子没有合并到最左边。原因你看了资源里的滑动算法方向处理没改对。常见的反转写法是把行号当列号用了或者转置之后的二次交换没做全。解决逐方向打印中间结果。在滑动函数里加一段调试输出把滑动前后的 board 数组打印出来。具体到代码就是qDebug() Before:; for (int i 0; i 4; i) { QString line; for (int j 0; j 4; j) { line QString::number(board[i][j]) ; } qDebug() line; }跑一步看一步很快能定位是先紧凑出了问题还是合并出了问题。记住一个原则四个方向尽量复用同一个核心逻辑靠行列索引变换去适配方向别四个方向写四套近乎重复的代码——重复代码出错面最大。5.3 中文字符串乱码现象界面上的提示文字全是菱形问号或乱码。原因Qt4 默认的编码处理与 Qt5 不同源文件如果是 UTF-8 编码含中文的字符串字面量会被系统按本地编码解析。Windows 下本地编码通常不是 UTF-8就显示了乱码。解决方案一是源文件用 GBK 编码保存方案二是使用QString::fromLocal8Bit(游戏结束)最省心的方案是放弃中文界面提示全用英文初学项目完全够用。5.4 玩一局后新方块不再生成现象游戏进行到中盘按方向键没反应棋盘也不出新的 2 或 4。原因spawnTile里的空位收集逻辑没有考虑滑动无效的情况。如果你把moved判断写漏了或者滑动函数在有合并动作但无位置变化时返回了 false就被keyPressEvent里的if (moved)拦住了。解决合并但位置不变到底算不算移动严格来说不算因为棋盘布局没变不应该生成新方块。检查你的滑动函数在合并发生时是否更新了moved标志。常见错误是只检查了有没有非零元素换位置没检查有没有合并发生。5.5 qmake 指向了错误的 Qt 版本现象编译时提示QApplication: No such file or directory或者Qt 5.15.2 ... not installed。原因系统里同时装了 Qt4 和 Qt5PATH 环境变量里的 qmake 是 Qt5 的。运行qmake -v就能看到版本号。解决临时指定 Qt4 的 qmake 路径/usr/lib/qt4/bin/qmake或者把 Qt4 的 bin 目录加到 PATH 前部。Windows 下则需要注意Qt4 安装目录和 Qt5 安装目录里的bin谁在前面谁生效。这个问题非常隐蔽因为编译错误提示的往往是头文件而不是 qmake 版本。6. 把这份资源变成你自己的核心逻辑与界面解耦源码吃透之后下一步是重构。别急着学下一个小游戏先在这份 2048 上做一件事把棋盘逻辑和 Qt 界面完全分开让gameboard类不依赖任何 Qt 头文件。做法是建立一个纯 C 的GameBoard类构造时接收棋盘大小提供move(direction)、isGameOver()、getScore()等方法内部只维护std::vectorstd::vectorint数据。解耦之后立刻能收获两个好处。第一你可以用命令行写测试不启动 Qt 窗口就能验证算法正确性第二以后想迁移到 Qt5 或 Qt6界面层重写逻辑层一行不用改。配套一个实用技巧加保存最高分。Qt4 下最简洁的方式是用QSettings它把数据写进注册表或配置文件不用自己处理文件路径// 构造函数里读取 QSettings settings(MyCompany, Game2048); int highScore settings.value(highScore, 0).toInt(); // 游戏结束时写入 if (score highScore) { settings.setValue(highScore, score); }QSettings的构造函数参数是组织名和应用名写什么都行存的位置由系统决定Windows 下是注册表Linux 下是用户目录的配置文件。这个 API 在 Qt4 和 Qt5 里完全兼容不存在迁移问题。另一个值得加的验证函数是2048 是否达成——这个和游戏是否结束要分开判断。达成 2048 可以继续玩很多人会忽略这一点直接判游戏结束。正确写法是遍历棋盘发现任意格子大于等于 2048 就置一个标志位但游戏继续游戏结束条件仍然只看无处可走。改完这些这份资源才真正变成了你自己的项目。我自己带过的学生里能把滑动算法不看源码默写出来的人不多——大多数人要栽跟头的地方就是moved状态和重复合并标记。从那以后我每次帮人 debug 这个项目都强制先跑一遍连续按左、上、下、右各十次中途不开可视化的脚本检查棋盘数组的输出是否满足不变式。这个方法看起来土但比任何人肉点击都可靠。希望帮你省下这一步的摸索时间也让这份入门资源发挥它该有的价值。本文还有配套的精品资源点击获取