Linux下用C++和Qt手搓贪吃蛇:从架构到避坑的完整指南
简介这是一份面向Linux平台、使用C与Qt框架开发的贪吃蛇小游戏完整源码工程适合正在学习Qt界面编程、事件处理与小型游戏架构的初学者和进阶开发者参考。项目按模块划分清晰涵盖界面显示与菜单切换、游戏画面与控制逻辑、等级管理、背景音乐播放与切换、英雄榜分数统计与高分记录更新以及帮助信息展示等功能可作为课程设计或练手项目的完整范例。压缩包共71个文件约17.22MB包含10个cpp源文件与8个头文件承载核心逻辑3个ui界面文件与21张png、8张jpg图片负责视觉资源3个wav音频用于背景音乐另有qrc资源文件、pro工程文件、makefile及player.db数据库文件便于直接编译运行与二次修改。目前已有1474人学习下载读者可从中获取Qt多界面切换、信号槽通信、SQLite分数存储与音频播放等具体实现思路快速理解一个可运行小游戏的完整目录结构与模块协作方式。1. 从零手搓一个 Linux 桌面贪吃蛇为什么我劝你先别急着写游戏循环很多人第一次在 Linux 上碰 Qt都是从贪吃蛇这种小游戏开始的。原因很实在它足够小小到你能在一周内把窗口、绘图、定时器、键盘事件全串一遍又足够完整完整到你能摸到一个桌面程序从启动到退出的全部骨架。这个项目就是干这件事的——用 C 和 Qt 在 Linux 下写一个能跑、能玩、能改的贪吃蛇。它适合两类人一类是刚学完 C 语法、想找个东西练手的另一类是想快速复习 Qt 事件模型和 QPainter 绘图的老手。我见过太多人一上来就写 while 循环加 sleep结果窗口直接卡死所以这篇笔记会先把架构和踩坑讲透再带你跑起来、改起来。2. 技术选型与工程骨架为什么是 Qt Widgets 而不是 QML2.1 选型逻辑C 逻辑 Qt Widgets 绘图这个项目最核心的选型就一句话用 C 写游戏逻辑用 Qt Widgets 做渲染和输入。为什么不用 QML因为贪吃蛇的逻辑是纯 C 的二维数组操作QML 的声明式语法反而会把简单问题复杂化。为什么不用 SDL 或 OpenGL因为 Qt 已经帮你把窗口、事件循环、定时器全封装好了你只需要重写paintEvent和keyPressEvent两个函数就能跑起来。常见做法是继承QWidget把整个游戏塞进一个自定义控件里。这样做的另一个好处是以后想嵌入到更大的 Qt 项目里直接把这个 Widget 当组件用就行不用改架构。2.2 目录结构一个最小可编译的 Qt 工程长什么样一个能跑起来的 Qt Widgets 工程目录其实很干净。我一般会这样组织snake-qt/ ├── snake.pro # qmake 工程文件 ├── main.cpp # 程序入口 ├── snakewidget.h # 游戏窗口类声明 ├── snakewidget.cpp # 游戏逻辑与绘图实现 └── README.md如果你用 CMake把.pro换成CMakeLists.txt即可Qt5 和 Qt6 都支持。这里用 qmake 是因为它在 Linux 下最省事qmake make两条命令就能出可执行文件。.pro文件里关键就三行QT core gui widgets TARGET snake SOURCES main.cpp snakewidget.cpp HEADERS snakewidget.hQT widgets是必须的Qt5 之后 Widgets 模块和 Core 分开了漏写会报找不到QApplication。TARGET是生成的可执行文件名Linux 下没有后缀直接./snake就能跑。2.3 核心类设计一个 QWidget 扛下所有整个游戏的核心就是SnakeWidget这一个类。它的职责边界很清晰维护蛇身坐标、食物坐标、当前方向、游戏状态然后在paintEvent里把这些数据画出来。头文件大概长这样// snakewidget.h #ifndef SNAKEWIDGET_H #define SNAKEWIDGET_H #include QWidget #include QVector #include QPoint #include QTimer #include QKeyEvent #include QPainter class SnakeWidget : public QWidget { Q_OBJECT public: explicit SnakeWidget(QWidget *parent nullptr); protected: void paintEvent(QPaintEvent *event) override; // 负责所有绘制 void keyPressEvent(QKeyEvent *event) override; // 负责方向输入 private slots: void tick(); // 定时器回调驱动游戏前进 private: void initGame(); // 重置游戏状态 void spawnFood(); // 随机生成食物 bool checkCollision(); // 碰撞检测 QVectorQPoint snake; // 蛇身snake[0] 是蛇头 QPoint food; // 食物坐标 int direction; // 0上 1下 2左 3右 int cellSize; // 每个格子的像素大小 int cols, rows; // 网格列数和行数 bool gameOver; // 游戏结束标志 QTimer *timer; // 游戏主定时器 }; #endif这里有几个设计点值得说。第一蛇身用QVectorQPoint存QPoint的 x 和 y 直接对应网格坐标不用自己写结构体。第二方向用int而不是枚举是为了后面做“禁止 180 度掉头”时判断更直接。第三QTimer用指针成员方便在游戏结束时stop()重新开始时start()。注意Q_OBJECT宏不能省否则信号槽和slots关键字都不生效编译会报undefined reference to vtable。3. 核心模块拆解定时器、绘图与碰撞检测的完整链路3.1 游戏主循环为什么用 QTimer 而不是 while这是新手翻车最多的地方。很多人写游戏循环的第一反应是while (true) { update(); sleep(200); }在 Qt 里这么写窗口会直接卡死因为主线程被死循环占住了事件循环根本没机会处理重绘和输入。正确的做法是把“循环”交给QTimer// snakewidget.cpp 构造函数片段 SnakeWidget::SnakeWidget(QWidget *parent) : QWidget(parent) { cellSize 20; cols 30; rows 20; setFixedSize(cols * cellSize, rows * cellSize); setFocusPolicy(Qt::StrongFocus); // 必须设置否则收不到键盘事件 timer new QTimer(this); connect(timer, QTimer::timeout, this, SnakeWidget::tick); timer-start(150); // 150ms 走一格数值越小蛇越快 initGame(); }QTimer::timeout每隔 150 毫秒触发一次tick()tick()里只做逻辑更新然后调用update()请求重绘。update()不会立刻重绘而是把重绘请求丢进事件队列等主线程空闲时统一处理。这样窗口既不会卡输入也不会丢。setFocusPolicy(Qt::StrongFocus)这行是血泪经验。不写的话Widget 默认不接收键盘焦点你按方向键一点反应都没有排查半天以为是keyPressEvent没重写对。3.2 tick 函数一帧里到底发生了什么tick()是整个游戏的心脏它按顺序做四件事算新蛇头、判碰撞、吃食物、更新蛇身。void SnakeWidget::tick() { if (gameOver) return; // 1. 根据当前方向计算新蛇头坐标 QPoint head snake.first(); switch (direction) { case 0: head.ry() - 1; break; // 上 case 1: head.ry() 1; break; // 下 case 2: head.rx() - 1; break; // 左 case 3: head.rx() 1; break; // 右 } // 2. 撞墙检测超出网格范围直接结束 if (head.x() 0 || head.x() cols || head.y() 0 || head.y() rows) { gameOver true; timer-stop(); update(); return; } // 3. 撞自己检测新蛇头不能和现有蛇身重合 if (snake.contains(head)) { gameOver true; timer-stop(); update(); return; } // 4. 插入新蛇头 snake.prepend(head); // 5. 判断是否吃到食物 if (head food) { spawnFood(); // 吃到就不删尾巴蛇变长 } else { snake.removeLast(); // 没吃到就删尾巴蛇保持长度 } update(); // 请求重绘 }这里的关键逻辑是“先加头、再决定删不删尾”。吃到食物时只加头不删尾蛇自然长一格没吃到时加头又删尾蛇看起来就在移动。snake.contains(head)用的是QVector的线性查找蛇短的时候完全够用蛇长到几百节也不会卡因为每帧只查一次。spawnFood()的实现要注意别把食物生成到蛇身上void SnakeWidget::spawnFood() { do { food.setX(QRandomGenerator::global()-bounded(cols)); food.setY(QRandomGenerator::global()-bounded(rows)); } while (snake.contains(food)); }QRandomGenerator是 Qt5.10 之后推荐的随机数类比qrand()更安全也更方便。bounded(cols)返回[0, cols)的随机整数正好对应网格下标。3.3 paintEvent用 QPainter 把数据画成画面绘图部分全部集中在paintEvent里逻辑和渲染彻底分离。这样以后想换皮肤、加网格线、加分数显示都只改这一个函数。void SnakeWidget::paintEvent(QPaintEvent *event) { Q_UNUSED(event); QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, false); // 画背景 painter.fillRect(rect(), QColor(30, 30, 30)); // 画食物红色圆角矩形 painter.setBrush(QColor(220, 60, 60)); painter.setPen(Qt::NoPen); painter.drawRoundedRect(food.x() * cellSize, food.y() * cellSize, cellSize - 2, cellSize - 2, 4, 4); // 画蛇身蛇头用亮绿色区分 for (int i 0; i snake.size(); i) { QColor color (i 0) ? QColor(80, 220, 120) : QColor(60, 160, 90); painter.setBrush(color); painter.drawRoundedRect(snake[i].x() * cellSize, snake[i].y() * cellSize, cellSize - 2, cellSize - 2, 3, 3); } // 游戏结束提示 if (gameOver) { painter.setPen(Qt::white); painter.setFont(QFont(Sans, 24, QFont::Bold)); painter.drawText(rect(), Qt::AlignCenter, Game Over\n按 R 重开); } }cellSize - 2是为了在格子之间留 2 像素缝隙视觉上蛇身是一节一节的不会糊成一条。drawRoundedRect的圆角半径给 3 到 4 像素看起来更柔和。Q_UNUSED(event)是告诉编译器这个参数我不用避免警告。3.4 keyPressEvent方向输入与防 180 度掉头键盘处理看着简单但有个经典 bug蛇正在向右走你快速按了上再按左如果两帧之间方向被连续改两次蛇可能直接反向撞死自己。解决办法是记录“当前方向”和“待处理方向”在tick()里才真正应用。void SnakeWidget::keyPressEvent(QKeyEvent *event) { switch (event-key()) { case Qt::Key_Up: if (direction ! 1) direction 0; // 不能从下直接转上 break; case Qt::Key_Down: if (direction ! 0) direction 1; break; case Qt::Key_Left: if (direction ! 3) direction 2; break; case Qt::Key_Right: if (direction ! 2) direction 3; break; case Qt::Key_R: if (gameOver) initGame(); // 游戏结束后按 R 重开 break; default: QWidget::keyPressEvent(event); } }direction ! 1这种判断就是防掉头当前向上0时按向下1会被忽略。initGame()里记得把gameOver置回false并重新timer-start(150)否则重开后蛇不动。4. 编译运行与避坑Linux 下从 qmake 到可执行文件4.1 环境准备与编译命令在主流 Linux 发行版上装 Qt 开发包最省事的方式是用包管理器。以 Debian/Ubuntu 系为例sudo apt update sudo apt install qtbase5-dev qt5-qmake g make装完之后进入工程目录两条命令编译qmake snake.pro make -j4-j4是并行编译核多可以写-j8。编译成功后当前目录会出现snake可执行文件直接./snake运行。如果报cannot find -lQt5Widgets说明qtbase5-dev没装全重新执行上面的安装命令。4.2 避坑清单五个我真实踩过的坑现象一窗口能弹出但按方向键没反应。原因Widget 没有获取键盘焦点。 解决构造函数里加setFocusPolicy(Qt::StrongFocus)或者在show()之后调用setFocus()。现象二编译报undefined reference to vtable for SnakeWidget。原因类里写了Q_OBJECT宏但没有经过 moc 预处理。 解决确认.pro里HEADERS snakewidget.h写了然后重新qmake make。moc 是根据头文件生成中间代码的头文件没进工程就会漏。现象三蛇移动速度忽快忽慢或者按一下方向键蛇连走好几格。原因QTimer间隔太短或者键盘事件里直接改了蛇身坐标。 解决把timer-start(150)的数值调大同时确保keyPressEvent只改direction不动snake。所有位置更新只在tick()里做。现象四食物生成在蛇身上蛇一吃就“穿模”。原因spawnFood()没有排除蛇身坐标。 解决用do...while循环直到随机出的坐标不在snake里为止。蛇很长时这个循环可能多跑几次但网格有 600 个格子蛇再长也远没占满不会死循环。现象五游戏结束后按 R 没反应。原因initGame()里忘了重置gameOver或者忘了重启定时器。 解决initGame()里把gameOver false、snake清空并重新填入初始三节、direction重置为右、timer-start(150)全部写全。提示如果运行时提示qt.qpa.plugin: Could not load the Qt platform plugin xcb说明系统缺少 X11 相关库装libxcb-xinerama0之类的依赖即可这是 Qt 在 Linux 上的常见环境问题跟代码无关。5. 二次开发改造从能玩到好玩我一般会动哪几刀5.1 加分数与最高分记录最值得先加的肯定是分数。改动很小在SnakeWidget里加一个int score和int highScore吃到食物时score 10游戏结束时更新最高分。最高分可以用QSettings持久化到本地下次启动还能读回来。// 吃到食物时 if (head food) { score 10; if (score highScore) highScore score; spawnFood(); } // 游戏结束时保存 QSettings settings(MyStudio, SnakeQt); settings.setValue(highScore, highScore); // 启动时读取 highScore settings.value(highScore, 0).toInt();QSettings在 Linux 下默认写到~/.config/MyStudio/SnakeQt.conf不用自己管文件路径。然后在paintEvent里用drawText把分数画到左上角就行。5.2 难度分级与速度曲线固定 150 毫秒太单调。我一般会做一个难度选择简单 200ms、普通 150ms、困难 100ms。更进阶一点的做法是动态加速——每吃 5 个食物定时器间隔减 10ms最低不低于 60ms。改法就是在tick()的吃食物分支里判断if (head food) { score 10; eatenCount; if (eatenCount % 5 0 timer-interval() 60) { timer-setInterval(timer-interval() - 10); } spawnFood(); }QTimer::setInterval会立即生效不需要 stop 再 start。这样蛇会越跑越快紧张感自然就上来了。5.3 用 QPainterPath 画更顺滑的蛇身现在的蛇是一节节独立方块想让它看起来像一条连续的蛇可以用QPainterPath把蛇身各节中心点连成折线再用粗画笔描边。核心思路是遍历snake把每个格子的中心坐标(x * cellSize cellSize/2, y * cellSize cellSize/2)依次lineTo最后painter.strokePath()。画笔宽度设成cellSize - 4端点样式设Qt::RoundCap出来的就是一条圆头圆脑的蛇。这个改动只动paintEvent逻辑层完全不用碰正好验证了前面“逻辑与渲染分离”的设计价值。5.4 改造后的验证方法每次改完我习惯按这个顺序过一遍先编译看有没有警告再跑起来看蛇能不能正常移动、吃食物、撞墙结束、按 R 重开。加分数后重点验证最高分能不能跨进程保存——关掉程序再开分数还在才算过。加速度曲线要手动玩到吃 10 个食物以上确认定时器间隔确实在降且没降到负数。画蛇身那步最容易翻车的是坐标算错半格表现是蛇整体偏移这时候把cellSize/2的偏移量打印出来对一下就行。从那以后我每次改这个项目都强制先跑一遍“移动、吃食、撞墙、重开”四步冒烟测试再动新功能。这套流程帮我省了不少回头查 bug 的时间。希望帮到你。本文还有配套的精品资源点击获取