简介这是一份基于Linux与Qt框架完成的贪吃蛇小游戏工程适合正在学习C/Qt图形界面开发或需要课程设计的开发者。整个项目按功能划分为界面显示、游戏逻辑、等级管理、音乐播放、英雄榜与帮助六大模块结构清晰便于理解Qt信号槽、事件处理与资源文件的使用方式。压缩包内含71个文件以cpp/h源文件、ui界面文件、png/jpg图片素材及wav音频资源为主并附带pro工程文件、Makefile和数据库文件解压后即可在Qt 5.4.1环境下打开构建。包体大小约17.22MB已有1473人学习浏览。通过这套代码读者可掌握使用QPainter绘制游戏画面、QTimer驱动游戏循环、SQLite保存高分记录、QMediaPlayer播放背景音乐等常用技巧同时也能参考其模块化组织方式快速迁移到其他小游戏或Qt项目实战中。 我入行那会儿第一个正经图形界面项目就是贪吃蛇。当时选的是Linux Qt的组合原因特别朴素手里的开发机就是Ubuntu而Qt的QPainter绘图和信号槽机制做这种网格类小游戏几乎是量身定做。这次重新把这个老项目翻出来整理发现“贪吃蛇”这种看起来不起眼的小玩意儿其实把Qt开发里最容易忽略又最要命的几个点全串起来了事件循环、定时器、绘图刷新、焦点处理还有打包发布时Linux平台插件的坑。整个项目完整代码量不大但每一块逻辑背后都有值得展开的设计考量。想把Qt基础打牢的读者或者想找一个能快速跑通的图形界面练手项目的人跟着走一遍应该会很有收获。1. 项目拆解贪吃蛇到底练了什么1.1 为什么选Qt而不是其他GUI方案Linux下的图形界面方案其实不少GTK、wxWidgets、SDL、甚至纯终端的curses库都能做贪吃蛇。我最终选Qt主要是看中几件事。第一是信号槽机制。游戏里的“食物被吃”“游戏结束”这类事件天然适合用信号槽去解耦。按下键盘产生一个事件事件改变蛇的方向方向改变触发表单重绘。如果用传统回调函数写代码会越堆越乱而信号槽的连接方式让代码路径非常清晰。第二是QPainter绘图。贪吃蛇的界面本质上就是在一个固定网格上画方块QPainter对这类2D绘图的支持非常舒服画矩形、画网格线、画圆角几行代码就能搞定。不用像SDL那样自己管理窗口表面和刷新缓冲。第三是跨平台迁移。Linux下开发完后续如果想弄到Windows上演示Qt的源码迁移成本几乎是零。CMake或qmake工程文件稍微配一下重新编译就能跑这对练手项目相当友好。1.2 核心功能拆解与模块划分一个完整的贪吃蛇游戏拆开来看其实就这几件事蛇身的存储与移动蛇身体是一串坐标点每走一步头部朝当前方向前进一格尾部去掉一格保证总长度不变吃到食物后尾部不删长度加一。定时驱动游戏不能靠死循环跑得用一个定时器定时触发“走一步”的逻辑。这一步决定了游戏速度也就是传统“帧”的概念。碰撞检测撞到边界直接输撞到自己身体也要输。食物生成食物不能出现在蛇身上每次生成位置要随机但要保证有效。键盘控制方向键改变蛇的朝向需要屏蔽掉“原地掉头”这种无效操作。界面反馈分数显示、暂停提示、结束画面这些交互反馈。这六个模块要是能在一个工程里理清楚Qt的事件驱动模型基本就入门了。2. 环境搭建与工程结构2.1 Linux下安装Qt的几种方式Linux装Qt常见的路子有两条。第一条是用Qt官方在线安装器。从官网下载安装器给执行权限后运行选择需要的组件装就行。在线安装好处是组件齐全官方维护缺点是安装器体积不小国内下载速度也是个问题。第二条是用离线安装包。不会经常需要换版本的话离线包可以直接拷到有网环境下载然后装到任意目录。装完之后在/opt/Qt或你自定义的目录里就能看到一整套Qt工具链。我用离线包比较多因为开发机上可能不只一个Qt版本离线包可以按版本号放在不同目录切换的时候只需要改环境变量。装完之后有个很容易踩的坑编译Qt项目时提示找不到GL/gl.h或者链接不到OpenGL相关的库。原因是系统没装OpenGL开发头文件。Ubuntu/Debian系执行一下sudo apt install build-essential libgl1-mesa-devCentOS/RHEL系则是sudo yum install gcc-c mesa-libGL-devel提示有些精简版系统连基本的C编译器都没有第一步先确认g --version能正常输出再谈Qt安装。2.2 创建工程与目录组织工程创建我建议直接用Qt Creator省去手写pro文件的麻烦。新建项目时选择Qt Widgets Application基类选QMainWindow或QWidget都行贪吃蛇这个场景用QWidget反而更轻量。建完后的目录结构大概是这样的SnakeGame/ ├── SnakeGame.pro ├── main.cpp ├── mainwindow.cpp ├── mainwindow.h ├── mainwindow.ui └── resources.qrcmainwindow.ui在贪吃蛇项目里其实用不太上我一般直接删掉整个界面都在paintEvent里画。这样代码更集中新手理解起来也直观。SnakeGame.pro文件里有几个重点参数新手容易忽略QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET SnakeGame TEMPLATE appTARGET决定编译出来的可执行文件名和项目名不一致时会容易混淆建议保持一致。3. 核心代码实现从蛇身到界面渲染3.1 数据结构与移动逻辑贪吃蛇的蛇身存储最直观的方式是用一个QVectorQPoint或者QListQPoint每一项表示蛇身某一个节点的坐标。为什么用容器而不是直接定义一堆成员变量因为蛇身长度是动态变化的容器天然支持头插尾删。我习惯定义成QVectorQPoint m_snake; QPoint m_direction; // 当前前进方向例如 (1,0) 表示向右移动逻辑看着简单但有个关键点不管蛇有没有吃到食物算法都是先让头部走一格然后判断是否吃到食物吃到了就保留尾部没吃到就删掉尾部。每次移动时新头部坐标是当前头部坐标加上方向向量QPoint newHead m_snake.first() m_direction; m_snake.prepend(newHead); if (!m_foodEaten) { m_snake.removeLast(); }这个方案比“整体移动每个节点”更高效也是业界最常见的写法。注意prepend在QVector里其实没有push_front直观但Qt的容器接口跨平台统一用习惯就好性能差异在这个规模下完全无感。3.2 QTimer驱动与碰撞检测游戏循环我直接用QTimer它的本质是基于事件循环的定时触发。传统游戏可能用多线程跑循环但Qt里定时器方案更干净也避免线程修改UI导致崩溃。m_timer new QTimer(this); m_timer-setInterval(150); // 每150毫秒走一格大约每秒7格 connect(m_timer, QTimer::timeout, this, GameWidget::updateGame);为什么选150毫秒这个值纯个人经验太快新手反应不过来太慢玩起来着急。150毫秒在20x20的棋盘上体验比较均衡。后续在第四章我会提到如何通过动态改这个值来实现加速机制。碰撞检测分两类。一类是撞墙一类是撞自己。// 撞墙检测 if (newHead.x() 0 || newHead.x() m_cols || newHead.y() 0 || newHead.y() m_rows) { gameOver(); return; } // 撞自己检测注意排除尾部因为尾部马上要移动 for (int i 0; i m_snake.size() - 1; i) { if (m_snake.at(i) newHead) { gameOver(); return; } }撞自己这里有个很容易忽略的细节比较时要不要排除最后一个节点当蛇没有吃到食物时最后一个节点这一帧会尾删所以蛇头移动到原来尾部的位置其实是安全的。不加这个判定会出现“明明没碰到自己却宣布游戏结束”的诡异情况我早期调试时被这个坑过。3.3 QPainter绘制与刷新策略绘制是贪吃蛇里最有意思的部分。Qt的绘制paintEvent会被系统频繁调用所以不能在里面做复杂计算只负责画。先定义网格参数const int m_rows 20; // 20行 const int m_cols 20; // 20列 const int m_blockSize 25; // 每个格子25像素画网格线用一个for循环纵线和横线分别画颜色用浅灰色让整个棋盘有参照感。蛇身绘制我推荐用圆角矩形视觉上比纯方块舒服很多代码也就是多填两个参数的事painter.setBrush(QBrush(QColor(46, 139, 87))); painter.setPen(Qt::NoPen); painter.drawRoundedRect(rect, 4, 4);食物用红色画实心圆比方块醒目也不容易和蛇身混淆。这里必须提一个刷新策略的问题。很多人在更新完蛇的位置后直接调用this-update()这个函数只是向事件系统发出一个“需要重绘”的请求并不是立即重绘。真正执行绘制是在事件循环下次处理绘制事件时。这个机制是Qt高效的关键——多次调用update()系统会自动合并成一次重绘。如果贪蛇游戏里发现运动轨迹“拖影”或者闪烁通常不是update的锅而是没有在绘制前清空背景。正确做法是绘制第一行就填充背景色painter.fillRect(rect(), QColor(255, 250, 240));3.4 键盘控制与焦点处理贪吃蛇的方向控制重写keyPressEvent就行void GameWidget::keyPressEvent(QKeyEvent *event) { switch (event-key()) { case Qt::Key_Up: if (m_direction ! QPoint(0, 1)) m_direction QPoint(0, -1); break; case Qt::Key_Down: if (m_direction ! QPoint(0, -1)) m_direction QPoint(0, 1); break; case Qt::Key_Left: if (m_direction ! QPoint(1, 0)) m_direction QPoint(-1, 0); break; case Qt::Key_Right: if (m_direction ! QPoint(-1, 0)) m_direction QPoint(1, 0); break; } }注意每个case里都有一次反向判断当蛇正在向左走你按右键这时候如果直接改变方向蛇会瞬间穿过自己的身体这在经典贪吃蛇里是不允许的。屏蔽反向是硬性要求。键盘事件不响应的坑几乎每个新手都会遇到。最常见的原因有两个第一个是焦点问题。如果界面上有其他控件抢占了焦点键盘事件就发不到QWidget上。解决办法是在构造和显示后设置焦点策略setFocusPolicy(Qt::StrongFocus);第二个是重写事件时忘记调用父类实现。有些情况但不处理方向键时最好在末尾调用QWidget::keyPressEvent(event)避免吞掉其他键的事件。4. 玩法打磨状态机、计分与扩展方向4.1 游戏状态管理与界面联动最开始做这个项目时我直接用布尔变量控制游戏状态比如bool isRunning、bool isPaused后来发现状态一多各种组合判断能把人绕晕。比如暂停状态下按空格要恢复运行游戏结束按空格要重新开始开始前的“按任意键开始”也是一套逻辑。后面我改成状态机用枚举定义状态代码清晰不止一个量级enum GameState { STATE_READY, // 等待开始 STATE_RUNNING, // 游戏中 STATE_PAUSED, // 暂停 STATE_GAMEOVER // 结束 };在这里使用switch处理按键事件时逻辑路径会非常明确该在哪个状态响应什么按键一目了然。状态切换用独立函数changeState()做进入某个状态时统一处理“启动/停止定时器”“刷新界面文字”不会出现状态改了但组件没跟上更新的情况。4.2 计分、加速与扩展方向基础版已经能跑之后加分功能可以层层叠加。计分逻辑最简单吃到一次食物加10分直接用QLabel显示在窗口顶部。食物生成时要检查随机坐标是否在蛇身上做法是一个while循环不断生成直到不在蛇上为止。数据量小不会有多大的性能损耗。加速机制的做法是每次吃到食物后把定时器的间隔缩短一点比如减少5毫秒下限设在80毫秒int interval m_timer-interval(); m_timer-setInterval(qMax(80, interval - 5));这个设计会让游戏逐渐变紧张可玩性明显上升。再往后的扩展方向就多了加音效可以用QSoundEffect加关卡可以改成障碍物地图加排行榜可以用QSettings存本地记录实现AI自动寻路可以尝试A*算法。我在另一篇文章里看到有人还用Qt里的QCustomPlot去画贪吃蛇的轨迹图虽然没有必要但确实说明Qt的绘图生态很够用。5. 常见问题与排查技巧实录5.1 高概率翻车点速查表我把这几年在Qt开发里反复遇到的问题整理成了一个速查表这些坑不少都是在带新人时被反复踩过值得直接抄走。现象根本原因解决方案键盘方向键没反应焦点没有落在游戏控件上调用setFocusPolicy(Qt::StrongFocus)界面闪烁、有残影绘制前没清空背景paintEvent第一行填充背景色蛇移动速度时快时慢定时器间隔设置不合理或CPU占用过高检查setInterval避免在绘制里做耗时操作编译报找不到GL/gl.h缺OpenGL开发包安装libgl1-mesa-dev中文显示为乱码源文件编码不是UTF-8代码文件统一存为UTF-8用tr包裹字符串边跑边调试卡顿调试模式下断言检查拖慢逻辑无特殊处理正常现象蛇撞自己误判碰撞检测没排除即将删除的尾节点遍历时排除m_snake.size() - 1这几个坑里“蛇撞自己误判”我认为最隐蔽逻辑上差一行代码表现上却像随机Bug排查起来很费时间。建议初始写碰撞逻辑时就直接把尾部排除写好免得后面绕回来改。5.2 发布打包与依赖处理项目开发完要发给别人跑Linux下的发布比Windows更麻烦些。Windows上有windeployqt一键收集DLLLinux下的对应工具是linuxdeployqt但使用起来对Qt版本和系统环境的依赖比较敏感偶尔会遇到平台插件路径不对或者依赖库缺失的问题。github上有不少linuxdeployqt的现成release可用下载后配合打包脚本操作起来会方便许多。建议将工程目录里编译好的可执行文件与依赖的Qt库放到同一个目录运行前设置LD_LIBRARY_PATH指向Qt的lib目录这样切换版本时影响最小。一个特别容易出错的地方是开发机编译的程序拿到另一个干净环境运行启动时报错qt.qpa.plugin: Could not find the Qt platform plugin xcb。这个问题本质是系统里缺少xcb相关运行库把依赖补齐一般就能解决sudo apt install libxcb-xinerama0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0提示如果只在自己的机器上跑直接设置export LD_LIBRARY_PATH/opt/Qt/5.15.2/gcc_64/lib就能解决大部分加载问题角度再多一点程序里用绝对路径写QLibraryInfo::location()查看Qt库路径对排查也很有帮助。另外打包之前记得用ldd查一下可执行文件依赖了哪些动态库命令输出里如果看到not found就是缺失项。手动把缺失的.so文件拷到程序目录再用patchelf设置rpath发布体验会稳定不少。最后再分享一个围绕Qt排障的个人习惯遇到这类平台插件初始化报错第一反应不是重装Qt而是先用ldd查依赖、用环境变量打印调试信息比如QT_DEBUG_PLUGINS1这能直接显示出到底是哪个插件加载失败了。这个习惯让我少跑了好几次冤枉路。后面再往深了玩Qt你会发现绘图、串口、网络每个方向都有对应的坑但排查思路基本相通。先把这个小游戏跑通了后面真正做项目时心里会特别有底。本文还有配套的精品资源点击获取