资讯详情

基于Qt的贪吃蛇游戏开发:信号槽、QTimer与碰撞检测入门

📅 2026/10/16 15:11:17 | 华诺云谱 👁 阅读
基于Qt的贪吃蛇游戏开发:信号槽、QTimer与碰撞检测入门
简介面向C与Qt初学者的迷你贪吃蛇游戏工程演示如何基于QGraphicsView、QGraphicsScene构建可运行的2D贪吃蛇界面。项目将蛇头、蛇身、食物和边界分别设计为独立图形对象类完整涵盖定时器驱动的移动循环、方向键控制、碰撞检测、吃到食物后增长与重新生成、计分刷新等核心机制并在碰撞后停止游戏循环、展示结束界面适合自学说Qt图形视图框架、事件处理与面向对象封装。压缩包内共8个文件包括2个cpp源文件、1个头文件、1个ui界面文件、1个pro工程文件、2张jpg图片以及1个user配置记录整体约209KB结构紧凑、目录清晰便于直接编译研读。该资源已有263人学习/下载。通过阅读源码可掌握自定义QGraphicsItem的绘制方法、利用信号槽关联用户输入与界面更新、用QTimer控制移动速度等关键技巧是C开发者从控制台转向图形界面编程的一份实用入门参考。1. 基于QT的简略版贪吃蛇一个连源码都能看懂的入门项目如果你刚接触 Qt 和 C正找一个小项目练手我强烈建议从贪吃蛇开始。这个基于 QT 的简略版贪吃蛇游戏代码量不大却把 Qt 最核心的几块东西全摸了一遍QTimer 定时器、QPainter 绘图、键盘事件、信号槽、坐标与碰撞检测。整个项目没有第三方库只有四个普通代码文件编译出来就是一个可以玩的桌面小游戏。对新手来说它能帮你把「面向对象 GUI 事件驱动」从抽象概念落到实打实的代码上对熟手来说它又是一个干净到能一眼看透的 Qt Widgets 项目范本拿来改造成完整游戏框架也很快。2. 项目结构和编译工程Qt Widgets C 怎么组织2.1 技术选型为什么用 Qt Widgets 而不是 QML这个项目面向的是「想搞懂 C 和 Qt 底层机制」的开发者所以选用了 Qt Widgets 模块而不是 QML/Qt Quick。Qt Widgets 是一套基于 QWidget 的传统控件框架所有界面元素都由 C 代码直接创建和绘制没有额外的声明式语言和 JavaScript 运行时。贪吃蛇本身不需要复杂动画和触摸交互用 Widgets 写反而更直白——你能看到窗口怎么创建、定时器怎么触发、每次重绘都发生了什么。QML 当然也能做而且很适合做华丽的界面但它的逻辑大多写在 JavaScript 里C 只负责暴露接口对刚学 C 的人不太友好。Widgets 模式下游戏的所有状态蛇身、方向、食物坐标都直接作为类的成员变量存在绘制函数里用 QPainter 一笔一笔画出来调试起来非常直观。2.2 文件清单与职责main.cpp / snakegame.h / snakegame.cpp / snake.pro这个简略版一共四个文件每个文件的职责非常单一snakegame.h定义SnakeGame类继承自QWidget声明成员变量和槽函数。snakegame.cpp实现游戏全部逻辑包括初始化、定时器处理、键盘响应、绘图。main.cpp创建QApplication和SnakeGame窗口进入事件循环。snake.proQt 的工程文件告诉 qmake 项目里有哪些源文件、链接哪些模块。SnakeGame类里最核心的成员变量是这些// snakegame.h 片段 class SnakeGame : public QWidget { Q_OBJECT public: SnakeGame(QWidget *parent nullptr); protected: void paintEvent(QPaintEvent *event) override; void keyPressEvent(QKeyEvent *event) override; private slots: void gameTick(); private: void initGame(); void spawnFood(); bool isCollision(); void updateTitle(); QVectorQPoint m_snake; // 蛇身索引0是头 QPoint m_food; // 食物坐标 int m_direction; // 当前移动方向 int m_nextDirection; // 待生效方向防止一帧内反向 int m_score; // 得分 int m_gridWidth; // 网格列数 int m_gridHeight; // 网格行数 int m_cellSize; // 每个格子的像素边长 QTimer *m_timer; // 游戏心跳定时器 bool m_gameOver; // 游戏结束标记 };看到Q_OBJECT宏就应该知道这个类要使用 Qt 的信号槽机制所以snakegame.h在编译前必须先经过moc元对象编译器处理。这也是为什么用 Qt 写项目必须交给我们 Qt 提供的qmake或cmake构建系统而不是自己拿 g 直接编——这一点在后面编译时会坑到不少人。m_snake用QVectorQPoint存储QPoint是 Qt 自带的二维坐标类存整型坐标刚刚好。索引 0 固定是蛇头后续索引依次是蛇身。m_direction是当前实际移动方向m_nextDirection是先记下来的「下次移动方向」这样用户在快节奏操作时不至于丢失按键。2.3 .pro 工程文件与信号槽连接snake.pro这一行行内容都很常规QT core gui widgets greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET snake TEMPLATE app SOURCES main.cpp \ snakegame.cpp HEADERS snakegame.h第一行声明依赖core和gui模块。core提供信号槽、事件循环、容器类gui提供窗口和绘图基础类。widgets模块在 Qt5 之后被单独拆了出来因为命令行程序不需要 widgets所以要么用QT widgets要么写成上面对 Qt4 做兼容的写法。TARGET snake决定最终可执行文件叫snake如果你在 Windows 上还会看到snake.exe。信号槽在这个项目里最典型的使用场景是定时器。在构造函数里m_timer new QTimer(this); m_timer-setInterval(100); connect(m_timer, QTimer::timeout, this, SnakeGame::gameTick);QTimer每 100 毫秒发一次timeout信号SnakeGame::gameTick槽函数被调用相当于游戏的一次「心跳」。这种设计把「时间驱动的逻辑」封装成了事件不用自己写 while 死循环窗口界面也不会卡死。setInterval(100)里的数字就是速度100 毫秒一帧也就是每秒移动 10 格后面想加速可以直接改这个参数或者做成动态调速。3. 核心模块拆解蛇移动、食物刷新、碰撞检测的完整实现3.1 游戏数据模型用 QVector 存蛇身贪吃蛇的数据模型是整个程序的地基设计得好后面所有逻辑都好写。我这里用的是QVectorQPoint每个QPoint表示蛇身体的一个格子坐标。比如坐标(5, 3)表示第 5 列第 3 行对应屏幕上的像素位置是(5 * cellSize, 3 * cellSize)。初始化游戏时void SnakeGame::initGame() { m_snake.clear(); m_snake.append(QPoint(m_gridWidth / 2, m_gridHeight / 2)); m_snake.append(QPoint(m_gridWidth / 2 - 1, m_gridHeight / 2)); m_snake.append(QPoint(m_gridWidth / 2 - 2, m_gridHeight / 2)); m_direction Qt::Key_Right; m_nextDirection m_direction; m_score 0; m_gameOver false; spawnFood(); updateTitle(); m_timer-start(); }初始蛇身有三节水平排列在场地中央头在最右侧初始方向向右。注意QPoint的 x 是列坐标y 是行坐标绘制时对应横向和纵向别搞反了。m_direction用了Qt::Key_Right这些枚举值好处是键盘事件里直接拿按键值和它比较就行少做一层转换。QVector的头部追加和尾部删除正好模拟蛇的移动逻辑。下一帧要移动时根据方向计算新头坐标插入到vector最前面再把尾节点弹出。如果吃到食物尾巴就不用弹蛇身长度自然加一。这个操作是贪吃蛇和普通几何体移动最大的区别蛇不是整条平移而是「头进尾出」像一条真正的链条。3.2 游戏循环QTimer 控制刷新节奏gameTick是每一帧要执行的完整逻辑按顺序做五件事更新方向、计算新头、判断碰撞、处理食物、重绘窗口。void SnakeGame::gameTick() { if (m_gameOver) return; // 防反向不能直接往当前方向的相反方向走 if (!isOpposite(m_nextDirection, m_direction)) { m_direction m_nextDirection; } QPoint head m_snake.first(); QPoint newHead head; switch (m_direction) { case Qt::Key_Up: newHead.ry() - 1; break; case Qt::Key_Down: newHead.ry() 1; break; case Qt::Key_Left: newHead.rx() - 1; break; case Qt::Key_Right: newHead.rx() 1; break; default: return; } // 撞墙 if (newHead.x() 0 || newHead.x() m_gridWidth || newHead.y() 0 || newHead.y() m_gridHeight) { m_gameOver true; updateTitle(); update(); return; } // 撞自己 if (m_snake.contains(newHead)) { m_gameOver true; updateTitle(); update(); return; } m_snake.prepend(newHead); // 吃到食物 if (newHead m_food) { m_score 10; updateTitle(); spawnFood(); } else { m_snake.pop_back(); } update(); // 请求重绘 }isOpposite是一个工具函数判断两个按键方向是否相反。比如当前方向是右用户快速按了左这时候如果你直接更新方向蛇头会一头扎进自己身体里。我这里的做法是先让用户按的方向进m_nextDirection但在gameTick里只有当它不是当前方向的相反方向时才生效其余按键被丢弃。这个细节很多简略版贪吃蛇不会处理谁玩谁骂。QPoint::rx()和ry()返回坐标的引用可以用直接修改比重新赋值快也简洁。m_snake.contains(newHead)是QVector自带的线性查找因为蛇身最多几十节性能完全没问题。撞墙和撞自己都直接置m_gameOver true然后调用updateTitle()把窗口标题改为「Game Over」再update()触发重绘让画面停在被撞的那一刻。定时器还在跑但gameTick开头判断了gameOver就直接返回等于游戏冻结。3.3 食物生成与碰撞检测细节食物坐标必须在游戏场地内并且不能落在蛇身上。spawnFood的常规写法是用随机数试void SnakeGame::spawnFood() { bool valid false; int attempts 0; while (!valid attempts 10000) { int x QRandomGenerator::global()-bounded(m_gridWidth); int y QRandomGenerator::global()-bounded(m_gridHeight); QPoint candidate(x, y); if (!m_snake.contains(candidate)) { m_food candidate; valid true; break; } attempts; } // 极端情况蛇几乎占满全屏随机很难命中空格 // 此时应该遍历找第一个空格而不是死循环 if (!valid) { for (int i 0; i m_gridWidth; i) { for (int j 0; j m_gridHeight; j) { QPoint p(i, j); if (!m_snake.contains(p)) { m_food p; return; } } } } }QRandomGenerator::global()-bounded(n)是 Qt5.10 之后推荐用的随机数接口返回 0 到 n-1 的整数代替了老的qrand() % n。我加了attempts 10000的上限防止理论上蛇占满 99% 区域时随机数长时间试不中。如果 10000 次都没成功直接顺序遍历找一个空格保证游戏能继续。碰撞检测分两类边界检测和身体自碰撞。边界检测就在gameTick里用newHead的坐标直接跟m_gridWidth和m_gridHeight比大小小于 0 或大于等于边界都算撞墙。身体自碰撞注意一个细节先判断m_snake.contains(newHead)再prepend因为蛇在移动前身体的集合就是当前m_snake如果新头会落到旧尾巴的位置正常情况下先弹出尾巴、再插入头部其实不算碰撞。但简略版通常保守处理把这种情况也算撞死——毕竟蛇头永远不该钻到尾巴占的那一格。如果你希望更贴近真实规则可以先pop_back再判断但那样容易出现蛇头瞬间和尾巴重叠的渲染问题我建议就按保守版来。3.4 绘制与 UI 刷新QPainter 画布实现绘制放在paintEvent里所有看得见的东西都是手动画出来的void SnakeGame::paintEvent(QPaintEvent *) { QPainter painter(this); // 背景 painter.fillRect(rect(), QColor(30, 30, 30)); // 网格线可选 painter.setPen(QColor(60, 60, 60)); for (int i 0; i m_gridWidth; i) { int x i * m_cellSize; painter.drawLine(x, 0, x, m_gridHeight * m_cellSize); } for (int j 0; j m_gridHeight; j) { int y j * m_cellSize; painter.drawLine(0, y, m_gridWidth * m_cellSize, y); } // 食物 painter.setPen(Qt::NoPen); painter.setBrush(QColor(220, 40, 40)); QRect foodRect(m_food.x() * m_cellSize 2, m_food.y() * m_cellSize 2, m_cellSize - 4, m_cellSize - 4); painter.drawEllipse(foodRect); // 蛇 for (int i 0; i m_snake.size(); i) { QPoint p m_snake.at(i); QRect bodyRect(p.x() * m_cellSize 1, p.y() * m_cellSize 1, m_cellSize - 2, m_cellSize - 2); painter.setBrush(i 0 ? QColor(30, 180, 60) : QColor(70, 200, 100)); painter.drawRect(bodyRect); } // 游戏结束文字 if (m_gameOver) { painter.setPen(Qt::white); painter.setFont(QFont(Microsoft YaHei, 16, QFont::Bold)); painter.drawText(rect(), Qt::AlignCenter, Game Over); } }这里有几个值得注意的细节画蛇时给每个格子留了 1 像素空隙蛇看起来是一段段独立方块画食物用drawEllipse并留 2 像素边距让食物更接近圆形游戏结束时用drawText居中显示 Game Over而没有加额外控件因为简略版不想再引入按钮和布局。窗口尺寸和网格参数在构造函数里设定我固定为 20x20 格子每格 25 像素窗口大小为20 * 25 500像素。如果以后想改场地大小只要改两个常量。update()每次触发paintEventQt 会自动把旧的画面擦掉所以我们不用手动清屏。键盘事件keyPressEvent负责接收方向键void SnakeGame::keyPressEvent(QKeyEvent *event) { int key event-key(); if ((key Qt::Key_Up || key Qt::Key_Down || key Qt::Key_Left || key Qt::Key_Right) !m_gameOver) { m_nextDirection key; } else if (key Qt::Key_Space) { // 空格重置游戏 initGame(); } }这里只更新m_nextDirection真正的方向变化发生在下一个gameTick。这个设计保证了无论用户按键多快一个时间片内蛇只移动一格不会出现一次按下连走两步的“跳帧”手感。4. 编译运行与常见问题排查从 qmake 到键盘响应的血泪避坑4.1 运行步骤qmake、make、启动项目把四个文件放到同一个目录比如snake/然后在终端里执行cd snake qmake snake.pro make -j4 ./snake如果你用的是 Qt Creator直接打开snake.pro点左下角的绿色运行按钮就行。qmake会根据.pro生成对应平台的 Makefilemake用它编译出可执行文件。在 Windows 上Qt Creator 会把整个流程包到 “构建” 按钮里但底层依然是 qmake 编译器。如果你是 Qt6 用户命令不变。唯一要注意的是greaterThan(QT_MAJOR_VERSION, 4)这一行在 Qt5/Qt6 下会额外引入widgets模块所以不要删掉。运行后窗口里默认蛇会向右自动移动按方向键控制转向按空格重置游戏。4.2 避坑记录方向键失效、乱码、速度叠加、食物卡死第一条坑方向键怎么按都没反应。现象是窗口能弹出来蛇也自己会动但按上下左右完全无效。原因通常是窗口没有获得键盘焦点。QWidget 默认不聚焦需要通过setFocusPolicy(Qt::StrongFocus)显式告诉 Qt 这个控件可以接收键盘事件。我当时把这一句写在构造函数里才解决。如果你用的是 QMainWindow还要确保中心窗口也设置了焦点策略。第二条坑源码里写的中文字符串在 Win10 下乱码。现象是标题栏和 Game Over 文字变成一堆“锟斤拷”。原因是 GCC 默认把源码当 UTF-8 处理但 Windows 控制台或某些旧编译器用了 GBK。解决方法是统一用QString::fromUtf8(游戏结束)包裹字符串或者把源文件保存为 UTF-8 编码并在.pro里加一句QMAKE_CXXFLAGS -finput-charsetUTF-8 -fexec-charsetUTF-8。后来我干脆所有界面文案都用英文省心。第三条坑蛇越跑越快甚至一帧走好几格。现象是刚开始速度正常玩了一会儿突然飞一样冲墙。原因是在某处代码里重复执行了m_timer-start()导致同一个 QTimer 被启动了多次。Qt 文档里说start()会使定时器重新计时但如果你的代码在每次吃到食物时都调用timer-start(100)等于把一个 100 毫秒的定时器不断重置并不会叠加速度真正会造成“加速”的是你在gameTick里又写了一个 while 循环或递归调用或者创建了多个 QTimer。排查方法很简单在构造函数里只start()一次后续要加速直接改setInterval不要多次 start。第四条坑食物偶尔生成在墙外或者蛇肚子里。现象是画面上没有红色食物或者食物和蛇身重叠。原因多半是spawnFood里随机范围写错比如用了qrand() % m_gridWidth但实际上 Qt6 的qrand已经废弃或者生成后没有检查是否和蛇身重合。我上面给出的代码同时处理了这两个点用bounded限定范围用contains排除蛇身再加 10000 次尝试的兜底逻辑。如果你自测时发现吃完食物后蛇身没变长多半是prepend之后又无条件pop_back把刚加的头又弹掉了检查条件里的else是否正确。5. 二次开发把简略版升级成可扩展的贪吃蛇框架5.1 加入状态控制暂停、加速、游戏结束面板现在的简略版逻辑全堆在gameTick里想加功能容易越加越乱。我的建议是先把游戏状态枚举化把多个 if 改成状态机。定义enum GameState { Ready, Running, Paused, GameOver }然后让gameTick根据状态分支void SnakeGame::gameTick() { if (m_state ! Running) return; // 原逻辑 }键盘事件里增加暂停处理if (key Qt::Key_P || key Qt::Key_Space) { if (m_state Running) m_state Paused; else if (m_state Paused) m_state Running; }这样暂停后定时器还在跑但每帧都直接返回CPU 占用极低。加速功能就更简单给QTimer::setInterval传一个变量每吃 5 个食物减 5 毫秒下限 50 毫秒。改起来只需要在spawnFood里调用m_timer-setInterval(qMax(50, 100 - m_score / 50 * 5))这类细节我可以放心写进代码。5.2 自定义绘制换肤与障碍物二次开发最常见的是换皮肤。把paintEvent里的颜色提取成成员变量或者用一个QMapQString, QColor存主题色这一级改动不用动逻辑代码。如果想加障碍物就在游戏初始化时固定几个坐标放入QSetQPoint m_obstacles然后在移动和生成食物时把障碍物当成墙处理新头如果撞到障碍物等同 Game Over生成食物时确保不落在障碍物上。再往深一点可以考虑把SnakeGame的绘制部分拆出去单独做一个GameRenderer类让逻辑和渲染彻底解耦。这样你可以在不改变游戏规则的前提下把 QPainter 换成 QOpenGLWidget或者加上平滑动画。不过这些都是后续方向简略版贪吃蛇最可贵的地方就是小到能让你看到所有事情是如何串起来的。从那以后我每次接手新的 Qt 项目都会先花一天时间做一个这种规模的迷你版复刻把事件循环、定时器、绘制这条主线摸熟再去看项目源码心里就不慌了。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑