简介:这是一份面向C++与Qt跨平台开发初学者的Linux桌面端贪吃蛇游戏实战项目,适用于高校嵌入式/图形界面课程实践、Qt入门项目练手及Linux下GUI应用开发学习。资源完整包含游戏核心六大模块:界面显示与菜单切换、实时游戏逻辑与控制、等级动态管理、背景音乐与音效播放、SQLite高分榜持久化存储、以及交互式帮助系统,覆盖从UI设计到数据存储的全链路开发能力训练。压缩包共71个文件,含10个C++源码(cpp/h)、8个UI界面文件(ui)、21张资源图片(png/jpg)、3个音频(wav)、1个Qt项目配置(pro)、1个资源编译脚本(qrc)及1个轻量级SQLite数据库(db),总大小17.22MB,结构清晰,模块职责分明,便于逐层理解与二次开发。目前已有1474人学习下载,提供可直接编译运行的完整工程,含Debug构建产物与多窗口Widget实现(如Gamewidget、Mainwidget、Sqlitewidget等),是掌握Qt信号槽机制、事件处理、多窗口管理与Linux平台部署的优质参考范例。
1. 一个在终端里敲make && ./snake就能跑起来的 Qt 贪吃蛇:不依赖 IDE、不改系统配置、连 qmake 都能绕开——适合刚配好 Linux 开发环境、想用 C++ 实战练手又怕被 Qt 构建系统劝退的新手
你可能试过网上那些“Qt 贪吃蛇教程”,下载源码后卡在qmake -project报错,或cmake ..提示找不到 Qt5Widgets,甚至装完 Qt Creator 还得手动配 Kit、改 kit 名称、等十分钟索引完成……最后关掉终端,默默打开 Python 版。这个项目不是那样。它是一份「最小可运行 Qt 图形界面」的硬核压缩包:整个工程只有 3 个.cpp、2 个.h、1 个.pro(且内容仅 8 行),编译命令就一行make,运行直接./snake。它不调用任何高级控件(QTableView、QGraphicsView 全不用),所有绘制逻辑直写QPainter,键盘事件只监听Qt::Key_Up/Down/Left/Right四个键,连食物生成都用qrand() % width硬算坐标。它存在的唯一目的,就是让你在 Ubuntu 22.04 / CentOS 7 / Debian 12 上,从git clone到看到小蛇扭动,控制在 90 秒内。如果你正卡在“学完 C++ 基础但没写过带窗口的程序”,或者需要一份能嵌入嵌入式 Linux 设备(如树莓派 Zero W)的轻量游戏 Demo,这份代码就是你的第一块垫脚石——它不炫技,但每行都经得起gdb单步调试。
2. 项目结构与核心模块拆解:从main.cpp到SnakeGame::paintEvent()的完整链路
2.1 目录结构与文件职责:5 个源文件撑起整个游戏循环
项目解压后目录极简,无子文件夹,所有文件平铺:
snake/ ├── main.cpp # 程序入口:创建 QApplication、实例化 SnakeGame 窗口、调用 exec() ├── snakegame.h # 游戏主类声明:继承 QWidget,定义蛇身 vector、方向枚举、定时器指针等 ├── snakegame.cpp # 游戏主类实现:构造函数初始化、keyPressEvent 处理输入、timerEvent 驱动移动 ├── food.h # 食物类声明:封装 x/y 坐标、随机生成逻辑、碰撞检测接口 ├── food.cpp # 食物类实现:重载 operator== 用于蛇头坐标比对,避免重复生成 └── snake.pro # qmake 工程文件:仅指定 TARGET、TEMPLATE、SOURCES、HEADERS、QT += widgets提示:
.pro文件中未显式写CONFIG += c++11,但实际编译时 g++ 默认启用 C++11(GCC 5.4+),因此std::vector和auto可直接使用;若你在旧版 GCC(如 4.8)下编译失败,只需在.pro中追加CONFIG += c++11即可。
关键设计选择在于完全规避 Qt Widgets 高级容器:没有QMainWindow,没有QLayout,没有QPushButton。整个窗口就是一个QWidget,所有绘制(蛇身方块、食物圆点、分数文字)全部在paintEvent()中用QPainter手绘。这意味着:
- 无需管理 widget 层级关系;
- 帧率由
QTimer控制(默认 150ms/帧),而非依赖repaint()触发时机; - 坐标系原点在左上角,x 向右增,y 向下增,与传统数学坐标系相反——这是新手最容易画错位置的地方。
2.2SnakeGame类核心逻辑:状态机驱动的四向移动模型
snakegame.h中定义了游戏状态的核心数据结构:
// snakegame.h 关键片段 class SnakeGame : public QWidget { Q_OBJECT public: explicit SnakeGame(QWidget *parent = nullptr); ~SnakeGame(); protected: void paintEvent(QPaintEvent *event) override; void keyPressEvent(QKeyEvent *event) override; void timerEvent(QTimerEvent *event) override; private: enum Direction { UP, DOWN, LEFT, RIGHT }; std::vector<QPoint> snakeBody; // 蛇身坐标列表,[0] 为蛇头 QPoint foodPos; // 当前食物位置 Direction currentDir; // 当前移动方向 int score; // 当前分数 int timerId; // 定时器 ID,用于 stop/start static const int GRID_SIZE = 20; // 网格单位大小(像素),决定蛇身和食物尺寸 };这里GRID_SIZE = 20是整个游戏物理世界的基石:所有坐标均以GRID_SIZE为单位对齐。蛇每次移动一格,即x += GRID_SIZE * dx,y += GRID_SIZE * dy。这种设计让碰撞检测变得极其简单——只需判断蛇头坐标是否等于食物坐标(snakeBody[0] == foodPos),而无需做矩形相交计算。
snakegame.cpp中timerEvent()是游戏心跳:
// snakegame.cpp 关键片段 void SnakeGame::timerEvent(QTimerEvent *event) { if (event->timerId() != timerId) return; // 1. 根据当前方向计算新蛇头位置 QPoint newHead = snakeBody[0]; switch (currentDir) { case UP: newHead.setY(newHead.y() - GRID_SIZE); break; case DOWN: newHead.setY(newHead.y() + GRID_SIZE); break; case LEFT: newHead.setX(newHead.x() - GRID_SIZE); break; case RIGHT: newHead.setX(newHead.x() + GRID_SIZE); break; } // 2. 检查是否撞墙(超出窗口边界) if (newHead.x() < 0 || newHead.x() >= width() || newHead.y() < 0 || newHead.y() >= height()) { gameOver(); return; } // 3. 检查是否自撞(新蛇头与蛇身其他部分重合) for (size_t i = 0; i < snakeBody.size(); ++i) { if (snakeBody[i] == newHead) { gameOver(); return; } } // 4. 无碰撞:将新蛇头插入头部,移除尾部(实现“移动”效果) snakeBody.insert(snakeBody.begin(), newHead); snakeBody.pop_back(); // 保持蛇长不变,除非吃到食物 // 5. 检查是否吃到食物 if (newHead == foodPos) { score += 10; generateFood(); // 重新生成食物,确保不与蛇身重叠 // 注意:此处未增长蛇身!增长逻辑在 generateFood() 后手动追加 snakeBody.push_back(snakeBody.back()); // 在尾部复制一个点,使蛇长+1 } }这段代码暴露了一个关键细节:蛇身增长不是在移动时自动发生的,而是在吃到食物后,通过push_back()显式延长。这解释了为什么generateFood()必须在snakeBody.push_back()之前调用——否则新食物可能生成在刚延长的蛇尾上,导致下一帧立即碰撞。这是一个典型的“顺序敏感”逻辑,也是新手调试时容易忽略的血泪经验点。
2.3paintEvent()绘制逻辑:纯 QPainter 手绘,无资源加载、无样式表
paintEvent()是 Qt 窗口重绘的唯一入口,本项目在此完成全部视觉输出:
// snakegame.cpp 中 paintEvent 实现 void SnakeGame::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); // 绘制蛇身:每个坐标点画一个实心矩形 painter.setBrush(Qt::darkGreen); for (const QPoint &point : snakeBody) { painter.drawRect(point.x(), point.y(), GRID_SIZE, GRID_SIZE); } // 绘制食物:画一个实心圆 painter.setBrush(Qt::red); painter.drawEllipse(foodPos.x(), foodPos.y(), GRID_SIZE, GRID_SIZE); // 绘制分数文字:固定位置,白色字体 painter.setPen(Qt::white); painter.setFont(QFont("Arial", 12)); painter.drawText(10, 20, QString("Score: %1").arg(score)); }注意三个细节:
setRenderHint(QPainter::Antialiasing, true)启用抗锯齿,让矩形边缘更平滑(虽对像素风游戏非必需,但能避免初学者误以为“画歪了”);drawRect()的第 3、4 参数是宽高,而非右下角坐标,因此drawRect(x, y, w, h)绘制的是以(x,y)为左上角的矩形;drawText(10, 20, ...)的y=20是基线位置,不是文字顶部——若设y=0,文字会大部分被裁剪出窗口上方,这是新手常踩的“文字看不见”坑。
3. 编译与运行全流程:从零开始,不装 Qt Creator,只用终端和系统包管理器
3.1 环境准备:确认 Qt5 开发库已安装(Ubuntu/Debian/CentOS 通用)
项目依赖 Qt5 Widgets 模块,需确保系统已安装对应开发包。不同发行版命令如下:
Ubuntu/Debian 系:
sudo apt update sudo apt install build-essential qt5-default libqt5widgets5 libqt5widgets5-devCentOS/RHEL 系:
sudo yum groupinstall "Development Tools" sudo yum install qt5-qtbase-devel qt5-qtbase # 或对于较新版本(CentOS 8+): sudo dnf install qt5-qtbase-devel验证是否成功:执行
qmake -v应输出 Qt 版本(如Using Qt version 5.15.3 in /usr/lib/x86_64-linux-gnu);执行which qmake应返回路径(如/usr/bin/qmake)。若提示command not found,说明qt5-default或qt5-qtbase-devel未正确安装,需重装。
3.2 构建三步法:qmake → make → run,全程终端操作
进入项目根目录后,严格按以下顺序执行:
# 第一步:用 qmake 生成 Makefile(注意:必须在项目目录下执行) qmake -o Makefile snake.pro # 第二步:调用 make 编译(会自动调用 g++,链接 Qt5Widgets 库) make # 第三步:运行可执行文件(默认生成名为 'snake' 的二进制) ./snake关键参数说明:
qmake -o Makefile snake.pro:-o指定输出 Makefile 文件名,避免生成Makefile.Release等变体;snake.pro是工程描述文件,内容极简,仅含必要字段;make不带参数:因snake.pro中未指定CONFIG += debug_and_release,默认只构建 release 版本,生成的snake二进制体积小、启动快;- 若需调试版本(带符号表,可用
gdb断点),可先清理再重建:make clean && qmake CONFIG+=debug && make。
3.3 运行时交互与基础操作
程序启动后,窗口默认大小为800x600(在snakegame.cpp构造函数中调用resize(800, 600)设置)。操作方式如下:
| 操作 | 键位 | 效果 |
|---|---|---|
| 向上移动 | ↑或W | 蛇头向上一格(y 减GRID_SIZE) |
| 向下移动 | ↓或S | 蛇头向下一格(y 加GRID_SIZE) |
| 向左移动 | ←或A | 蛇头向左一格(x 减GRID_SIZE) |
| 向右移动 | →或D | 蛇头向右一格(x 加GRID_SIZE) |
| 暂停/继续 | Space | 调用killTimer(timerId)或startTimer(150)切换状态 |
| 退出游戏 | Esc | 调用close()关闭窗口 |
注意:方向键切换有防反向机制——若当前方向为
UP,按下DOWN键会被忽略(避免蛇立即自撞)。该逻辑实现在keyPressEvent()中:if ((currentDir == UP && key == Qt::Key_Down) || (currentDir == DOWN && key == Qt::Key_Up) || (currentDir == LEFT && key == Qt::Key_Right) || (currentDir == RIGHT && key == Qt::Key_Left)) { return; // 忽略反向按键 }
4. 常见问题排查:5 条真实翻车现场与对应解法
4.1 现象:qmake -o Makefile snake.pro报错Project ERROR: Unknown module(s) in QT: widgets
原因:系统安装了 Qt5,但未安装libqt5widgets5-dev(Ubuntu/Debian)或qt5-qtbase-devel(CentOS)开发包。qmake能运行,但找不到 Widgets 模块的头文件和链接库路径。
解决:
- Ubuntu/Debian:
sudo apt install libqt5widgets5-dev - CentOS:
sudo yum install qt5-qtbase-devel - 验证:
pkg-config --modversion Qt5Widgets应输出版本号(如5.15.3)
4.2 现象:make编译通过,但运行./snake提示error while loading shared libraries: libQt5Widgets.so.5: cannot open shared object file
原因:运行时动态链接器找不到 Qt5 库路径。常见于手动编译 Qt 或某些精简版发行版(如 Alpine Linux),系统库路径未包含/usr/lib/x86_64-linux-gnu或/usr/lib64/qt5/lib。
解决:
- 临时方案(当前终端有效):
export LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH - 永久方案:将上述
export行加入~/.bashrc,然后source ~/.bashrc - 更健壮做法:用
patchelf修改二进制 RPATH(适用于部署到无 Qt 环境的设备):sudo apt install patchelf patchelf --set-rpath '/usr/lib/x86_64-linux-gnu' ./snake
4.3 现象:窗口打开但一片空白,或只有分数文字,看不到蛇和食物
原因:paintEvent()未被触发,通常因QWidget未正确设置setAttribute(Qt::WA_PaintOnScreen, true)或resize()被错误覆盖。但本项目更可能是GRID_SIZE与窗口尺寸不匹配导致绘制区域溢出。
解决:
- 检查
snakegame.cpp构造函数中resize(800, 600)是否被注释或修改; - 确认
GRID_SIZE = 20下,蛇初始位置snakeBody = {QPoint(400, 300)}是否在窗口内(400 < 800,300 < 600,符合); - 强制触发重绘:在
timerEvent()结尾添加update()(虽 Qt 会自动调用,但加此行可排除事件循环异常)。
4.4 现象:蛇移动时出现“瞬移”或“跳格”,不是匀速一格一格走
原因:QTimer间隔设置过短(如startTimer(50)),导致timerEvent()频繁触发,而paintEvent()未跟上,造成视觉残留。或GRID_SIZE被意外修改为非整数倍值。
解决:
- 检查
startTimer()参数:默认应为150(毫秒),对应约 6.67 FPS,肉眼可见流畅移动; - 确认
GRID_SIZE在snakegame.h中为static const int,未被宏定义覆盖; - 若需更高帧率,建议同步调整
GRID_SIZE(如改为10)并增大窗口尺寸,避免像素过小看不清。
4.5 现象:按下方向键无反应,或按一次键蛇移动多格
原因:keyPressEvent()中未调用event->accept(),导致事件被父类拦截;或QTimer未在构造函数中正确启动(timerId = startTimer(150)缺失)。
解决:
- 检查
snakegame.cpp中keyPressEvent()结尾是否有event->accept()(本项目已包含); - 确认构造函数中
timerId = startTimer(150)已执行,且timerId != 0(startTimer返回 0 表示失败); - 在
timerEvent()开头加日志:qDebug() << "Timer fired";,运行时观察终端输出是否规律出现。
5. 二次开发实战:3 个可立即上手的改造方案与验证技巧
5.1 方案一:增加难度等级,让蛇越吃越快(动态调整定时器间隔)
核心思路:不修改GRID_SIZE,而是缩短QTimer间隔。startTimer()返回的timerId可被killTimer()停止,再用新间隔重启。
改造步骤:
- 在
snakegame.h的private区域添加成员变量:int currentInterval; // 当前定时器间隔(毫秒) static const int MIN_INTERVAL = 50; // 最快不能超过 20 FPS - 在
snakegame.cpp构造函数中初始化:currentInterval = 150; timerId = startTimer(currentInterval); - 修改
timerEvent(),在吃到食物后缩短间隔:if (newHead == foodPos) { score += 10; generateFood(); snakeBody.push_back(snakeBody.back()); // 难度升级:每吃 3 个食物,加快 10ms,最低到 MIN_INTERVAL if (score % 30 == 0 && currentInterval > MIN_INTERVAL) { killTimer(timerId); currentInterval -= 10; timerId = startTimer(currentInterval); } }
验证技巧:运行后观察分数栏,当Score达到30、60、90时,蛇移动速度应有明显提升。用qDebug()打印currentInterval值可精确确认。
5.2 方案二:支持暂停功能(Space 键切换运行/暂停状态)
本项目已预留Space键处理逻辑,但未实现暂停。需扩展状态管理。
改造步骤:
- 在
snakegame.h添加状态标志:bool isPaused; // true 表示暂停 - 在构造函数中初始化
isPaused = false; - 修改
keyPressEvent():if (key == Qt::Key_Space) { if (isPaused) { timerId = startTimer(currentInterval); } else { killTimer(timerId); } isPaused = !isPaused; update(); // 立即重绘,显示暂停状态(可选:加"PAUSED"文字) return; } - (可选)在
paintEvent()末尾添加暂停提示:if (isPaused) { painter.setPen(Qt::yellow); painter.setFont(QFont("Arial", 24, QFont::Bold)); painter.drawText(rect(), Qt::AlignCenter, "PAUSED"); }
验证技巧:按Space键,蛇应立即停止移动,再按一次恢复。注意killTimer()后timerEvent()不再触发,update()仅重绘,不驱动逻辑。
5.3 方案三:保存最高分到本地文件(跨会话持久化)
利用 Qt 的QSettings类,无需手动读写文件,自动适配平台(Linux 下存于~/.config/CompanyName/ProductName.conf)。
改造步骤:
- 在
snakegame.h添加:QSettings *settings; // 指向配置对象 int highScore; // 当前最高分 - 在构造函数中初始化:
settings = new QSettings("MySnakeGame", "Settings"); highScore = settings->value("highScore", 0).toInt(); - 在
gameOver()函数(需自行添加)中更新最高分:void SnakeGame::gameOver() { if (score > highScore) { highScore = score; settings->setValue("highScore", highScore); } // ... 原有游戏结束逻辑,如弹窗或重置 } - 在
paintEvent()中显示最高分:painter.drawText(10, 40, QString("High Score: %1").arg(highScore));
验证技巧:运行游戏,故意撞墙结束,记下分数;关闭程序再启动,High Score应保持不变。手动删除~/.config/MySnakeGame/Settings.conf可重置最高分。
从那以后我每次给新手推 Qt 入门项目,都会先确认对方环境里有没有qmake和libqt5widgets5-dev——不是因为它们难装,而是因为一旦卡在这两步,人就容易怀疑自己“不适合学 Qt”。这个贪吃蛇项目,我把它当成一块“校准石”:它不教你怎么写信号槽,也不讲 Model-View 架构,就专注把QPainter的drawRect和QTimer的timerEvent这两个最基础的砖块,严丝合缝地砌成一个能跑、能看、能改的完整闭环。当你第一次亲手改出“越吃越快”的效果,看着终端里qDebug()打印的currentInterval数字跳动,那种掌控感,比任何教程里的“恭喜你完成了!”都实在。希望帮到你。
本文还有配套的精品资源,点击获取