尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

C++控制台贪吃蛇:DevC++环境下的完整实现与避坑指南

C++控制台贪吃蛇:DevC++环境下的完整实现与避坑指南 简介面向C初学者的DevC贪吃蛇小游戏完整源码包基于控制台界面实现经典玩法涵盖蛇身数据结构、键盘方向控制、随机食物生成、碰撞检测、难度调节与暂停计时等核心逻辑适合练习结构体、数组、循环与分支语句的综合应用。资源共9个文件以cpp源文件、h头文件、dev工程文件、exe可执行程序及编译中间文件等类型为主压缩包仅112KB结构紧凑便于快速上手。项目在CSDN已有11539人浏览学习代码模块划分明确可直接运行体验游戏效果也可逐段阅读源码理解控制台游戏从初始化到结束的完整流程同时涉及随机数、计时器和非阻塞输入等实用技巧对于想入门C小游戏开发的读者是性价比很高的参考资料。1. 项目定位一台旧电脑、一个DevC半小时跑起贪吃蛇如果你手里只有一台装着 DevC 的 Windows 电脑想找一个能立刻编译运行、还能看清代码逻辑的 C 小项目贪吃蛇几乎是最合适的起点。这个工程就是一个完全基于控制台的贪吃蛇实现没有图形库、没有外部依赖只用到系统自带的 windows.h 和 conio.h。它提供完整的游戏循环方向键控制蛇头移动吃一个食物长一节并加 10 分撞墙或咬到自己游戏结束。适合刚学完变量、数组、循环想看看“程序是怎么变成游戏”的人也适合当课程设计或作业的起步模板。和那些动不动就要安装图形库的版本相比这份实现最核心的价值在于把环境配置成本砍到接近零——新建一个源文件粘贴进去在 DevC 里按 F11 就能看到蛇动起来。2. 技术栈与架构控制台贪吃蛇是怎么组织起来的2.1 控制台还是图形库先接受这个“土”方案一提到做贪吃蛇很多人第一反应是用 graphics.h 画一个彩色界面。但坦率地说在 DevC 里配 graphics.h 是一件很玄学的事情新版本 MinGW 编译器和老的 Borland 图形库并不直接兼容你需要去网上找 bgi.h、libbgi.a把库文件放进指定目录稍有不慎就是一堆 undefined reference。我一般会把第一步压成“让程序跑起来”所以控制台方案更合适。这个项目选择用 Windows 控制台窗口当作画布用 SetConsoleCursorPosition 定位光标用 std::cout 输出字符。蛇身用字符 O 表示食物用 F 表示空地用 . 表示。这样做的优势很明显头文件全部来自系统自带不存在第三方库配置问题。控制台程序的调试和日志打印天然方便逻辑和渲染可以分开。渲染逻辑只有 20x20 个字符输出压力极小性能足够。想复现这个环境操作步骤只有三步打开 DevC文件→新建→源文件保存为 main.cpp在工具→编译选项里确认编译器是 TDM-GCC 4.9.2 或更新版本按 F11 编译运行。如果代码里用到了 Sleep 和 SetConsoleCursorPosition编译器会自动链接 Windows API不需要额外引入 lib 文件。2.2 文件与模块划分一个 main.cpp 怎么塞下整个游戏贪吃蛇的控制台版通常不会切成多个文件因为全部逻辑只有三个大块初始化、游戏循环、收尾。游戏循环内部再拆成输入、更新、渲染三件事。以这份资源的代码组织方式来看入口是 main()它依次调用 initGame() 完成开局然后进入 while(true) 循环循环里先 handleInput() 处理键盘如果状态是运行中则 update() 移动蛇并检测碰撞最后 render() 把新画面画到控制台Sleep(speed) 控制帧间隔。状态变成 GAME_OVER 后退出循环打印最终分数。函数职责initGame()清空蛇身、重置方向/分数/速度、生成第一个食物handleInput()读取方向键、空格、R键并做反向保护update()移动蛇头、增长蛇身、检测撞墙和撞自己render()隐藏光标、定位到原点、输出整个画面和分数main()初始化后进入主循环统一调度如果你有扩充需求比如加音效、加关卡建议先把这些函数拆到 snake.h / snake.cpp / game.cpp 里而不是全部堆在 main.cpp。但在当前阶段单文件反而更容易读代码从上到下就是执行顺序调试时不用跳来跳去。2.3 核心数据结构为什么蛇身用 deque 而不是 vector游戏里最重要的数据是蛇身每一段都有一个 x、y 坐标。如果用数组长度不好确定如果用 vector头部插入新节点会很慢。这里使用了一个很合适的容器deque。#include deque #include vector enum Direction { DIR_UP, DIR_DOWN, DIR_LEFT, DIR_RIGHT }; enum GameState { STATE_RUNNING, STATE_PAUSED, STATE_OVER }; struct Point { int x; int y; Point(int nx 0, int ny 0) : x(nx), y(ny) {} }; using Snake std::dequePoint; Snake snake; Point food; int score 0; int speed 150; Direction dir DIR_RIGHT; GameState state STATE_RUNNING; const int MAP_WIDTH 20; const int MAP_HEIGHT 20;代码说明Point 是一个坐标对构造函数带默认值坐标从 0 开始控制台左上角是 (0,0)。deque 和 vector 都是容器但 deque 头部插入和删除也是常数时间。贪吃蛇移动时蛇头新增一节、蛇尾移除一节正好对应 push_front 和 pop_back这两个操作在 deque 上都是 O(1)。vector 没有 push_front只能 insert是 O(n)蛇长到几十节后会有明显延迟。Direction 枚举把方向语义化switch 时可读性更好也方便后面扩展斜向移动。speed 是每次 Sleep 的毫秒数数值越小蛇越快做难度等级时改这个值就行。MAP_WIDTH 和 MAP_HEIGHT 限定了游戏区域render() 里靠这两个常量控制打印行列。如果对比一下容器选型容器头部插入头部删除随机访问适合场景dequeO(1)O(1)O(1)贪吃蛇运动vectorO(n)O(n)O(1)只做尾部增删的栈场景2.4 状态机暂停、运行、结束如何切换一个只有 RUNNING 状态的小游戏按暂停键时只能靠一个 flag 临时撑住。但如果要加“暂停中”和“游戏结束后重玩”状态机是更稳的做法。这份资源定义了三态 GameStateSTATE_RUNNING 正常移动STATE_PAUSED 跳过 updateSTATE_OVER 退出主循环。代码处理方式很简单void handleStateChange(int key) { if (key ) { if (state STATE_RUNNING) { state STATE_PAUSED; } else if (state STATE_PAUSED) { state STATE_RUNNING; } } else if (key r || key R) { if (state STATE_OVER) { initGame(); } } }这里有一段常见误区用state !state来切换暂停在两态模型下确实有效但一旦加入 STATE_OVER取反就会把结束状态切回运行逻辑就乱了。显式判断当前值再转移状态再多也不会失控。在 handleInput() 里先调用 handleStateChange再决定是否读取方向键这样暂停时方向键不会改变蛇的方向重玩后所有状态都重置。设计状态机的原则是“每个按键只负责一个状态转移”不要把空格键在 RUNNING 和 PAUSED 之间来回写 if那样后续加菜单、加通关状态时一定翻车。3. 核心模块拆解从按键到画面刷新的完整链路3.1 初始化把地图、蛇和食物摆好初始化函数在每次开局和重玩时被调用它要做的事非常固定清空蛇身、把蛇放到地图中央、方向设为右、分数清零、生成第一个食物。下面是完整逻辑#define _WIN32_WINNT 0x0500 #include windows.h #include conio.h #include time.h #include deque #include iostream #include algorithm HANDLE hConsole GetStdHandle(STD_OUTPUT_HANDLE); void initGame() { snake.clear(); snake.push_back(Point(MAP_WIDTH / 2, MAP_HEIGHT / 2)); snake.push_back(Point(MAP_WIDTH / 2 - 1, MAP_HEIGHT / 2)); snake.push_back(Point(MAP_WIDTH / 2 - 2, MAP_HEIGHT / 2)); dir DIR_RIGHT; score 0; speed 150; state STATE_RUNNING; srand((unsigned)time(NULL)); spawnFood(); }代码说明snake.clear() 会释放上一局留下的所有节点保证重玩时不会残留旧坐标。初始蛇身三段头部在中心中间和尾部在左边方向 DIR_RIGHT 正好对应开局不会立刻撞到自己。srand(time(NULL)) 用当前时间做随机种子否则每次启动食物位置都一样。spawnFood() 生成食物具体实现放在移动模块里讲。hConsole 是全局句柄后面渲染和隐藏光标都要用它。注意_WIN32_WINNT 0x0500要写在所有头文件之前。在旧版 MinGW 中缺少这个宏会让 SetConsoleCursorPosition 声明不完整导致编译报错。3.2 输入处理方向键的扫描码和反向保护控制台程序里键盘输入由 conio.h 库提供。_kbhit() 检查键盘缓冲区是否有字符_getch() 读取一个字符但不回显。方向键是扩展键第一次 _getch() 会返回 224 或 0第二次返回真正的扫描码。处理代码void handleInput() { if (!_kbhit()) return; int key _getch(); if (key ) { handleStateChange( ); } else if (key 224 || key 0) { key _getch(); Direction newDir dir; switch (key) { case 72: newDir DIR_UP; break; case 80: newDir DIR_DOWN; break; case 75: newDir DIR_LEFT; break; case 77: newDir DIR_RIGHT; break; } if (!((dir DIR_UP newDir DIR_DOWN) || (dir DIR_DOWN newDir DIR_UP) || (dir DIR_LEFT newDir DIR_RIGHT) || (dir DIR_RIGHT newDir DIR_LEFT))) { dir newDir; } } }代码说明224 和 0 都可能作为扩展码取决于键盘和输入法所以两者都要判断。扫描码 72/80/75/77 对应上下左右这是 Windows 控制台的通用编码。反向保护看起来条件很长本质是“新方向和当前方向不能正好相反”。没有这个保护蛇头会瞬间钻进相邻一格的身体游戏直接结束。这里有个常见误写把新方向直接赋给 dir不做反向判断或者只判断一次。更好的习惯是先用 newDir 存下用户意图确认合法后再赋值。这样调试时打印 newDir 和 dir 的对比一眼就能看出问题。3.3 移动与碰撞贪吃的核心逻辑update() 是每次刷新时真正让蛇动起来的函数它分三步计算新蛇头、判断撞墙和吃食物、判断撞自己。void update() { Point newHead snake.front(); switch (dir) { case DIR_UP: newHead.y--; break; case DIR_DOWN: newHead.y; break; case DIR_LEFT: newHead.x--; break; case DIR_RIGHT: newHead.x; break; } if (newHead.x 0 || newHead.x MAP_WIDTH || newHead.y 0 || newHead.y MAP_HEIGHT) { state STATE_OVER; return; } if (newHead food) { snake.push_front(newHead); score 10; spawnFood(); } else { snake.push_front(newHead); snake.pop_back(); } for (auto it snake.begin() 1; it ! snake.end(); it) { if (*it newHead) { state STATE_OVER; return; } } }代码说明newHead 先用当前蛇头坐标初始化按方向加减一个单位产生新的头部位置。越界判断是“移动后立刻判断”撞墙的蛇不会出现在画面上。如果 newHead 和 food 坐标相同执行 push_front 增加一节分数加 10重新生成食物否则 push_front 加头、pop_back 去尾蛇身长度不变。最后从 snake.begin() 1 开始遍历因为第一个节点就是 newHead 本身自己等于自己没有意义。一旦发现新头和身体某段重叠判定结束。还有一个容易被忽略的细节如果吃到食物且食物恰好生成在蛇尾newHead 等于旧蛇尾而旧蛇尾在吃食物时不会被 pop_back所以碰撞检测会捕获这种情况判定游戏结束。这个行为是符合预期的。spawnFood() 的常见写法是 do-while 循环生成一个随机点如果该点在蛇身上就重新选void spawnFood() { do { food.x rand() % MAP_WIDTH; food.y rand() % MAP_HEIGHT; } while (std::find(snake.begin(), snake.end(), food) ! snake.end()); }为了让 std::find 能比较 Point需要给 Point 重载 运算符bool operator(const Point a, const Point b) { return a.x b.x a.y b.y; }这个运算符不写在游戏循环里但少了它spawnFood 根本编译不过。这也是“几十行的小项目编译错误却藏在细节里”的典型例子。3.4 渲染用光标定位替代清屏控制台游戏最影响体验的往往是闪烁。system(cls) 每次清屏再重绘屏幕上会出现明显的闪烁和残影。这个项目的渲染方法把光标定位到左上角然后逐字符覆盖整个画面。void render() { CONSOLE_CURSOR_INFO cursorInfo; cursorInfo.bVisible FALSE; cursorInfo.dwSize 1; SetConsoleCursorInfo(hConsole, cursorInfo); COORD origin {0, 0}; SetConsoleCursorPosition(hConsole, origin); for (int y 0; y MAP_HEIGHT; y) { for (int x 0; x MAP_WIDTH; x) { if (food.x x food.y y) { std::cout F; } else { bool isBody false; for (const auto p : snake) { if (p.x x p.y y) { isBody true; break; } } std::cout (isBody ? O : .); } } std::cout \n; } std::cout Score: score Speed: speed std::endl; }代码说明cursorInfo.bVisible 设为 FALSE 可以在游戏期间隐藏光标这对视觉影响很大强烈建议保留。SetConsoleCursorPosition 把光标移到 (0,0)后续 cout 的内容从左上角开始覆盖旧画面。判断顺序是食物优先、蛇身其次、空地兜底。spawnFood 已经保证了食物不会和蛇重合所以先判断谁都可以。末尾输出 Score 和 Speed每次覆盖刷新。注意输出行尾部可能因为上一次内容更长而残留可以在每行末尾补空格这点在避坑章节会展开。如果你觉得控制台颜色单调可以再加 SetConsoleTextAttribute 给蛇身、食物、分数分别上色SetConsoleTextAttribute(hConsole, 10); // 绿色蛇身 SetConsoleTextAttribute(hConsole, 12); // 红色食物 SetConsoleTextAttribute(hConsole, 15); // 白色文字颜色不是必须的但它是从“能玩”到“像样”的第一级台阶。3.5 主循环把刷新频率和游戏逻辑串起来main() 里做的事并不多初始化后进入一个循环直到游戏结束int main() { hConsole GetStdHandle(STD_OUTPUT_HANDLE); initGame(); while (true) { handleInput(); if (state STATE_RUNNING) { update(); } render(); if (state STATE_OVER) { break; } Sleep(speed); } std::cout \nGame Over! Final Score: score std::endl; return 0; }代码说明handleInput() 只在有键盘输入时改变方向或状态没有输入就什么都不做。只有 RUNNING 状态才执行 update()PAUSED 状态画面会停在原地。render() 无论什么状态都执行这样暂停时还能看到当前画面结束前也能看到最后一下。Sleep(speed) 放在 render 之后、下一帧输入之前目的是控制帧率。sleep 放在 update 之前也可以但放在渲染后能让画面保持时间更稳定视觉上更流畅。到这里整个游戏从初始化到刷新的链路就完整了键盘输入 → 更新方向 → 移动蛇 → 碰撞检测 → 渲染画面 → 等待下一帧。这六步是几乎所有控制台小游戏的通用骨架。4. 避坑指南DevC环境下的经典翻车现场4.1 方向键按下没反应第一次_getch吃掉了扫描码现象游戏运行后蛇一直按初始方向走按上下左右键完全没有反应但空格键和普通字母键能正常触发。原因Windows 控制台把方向键编码为两个字节第一个字节是 224 或 0表明这是扩展键第二个字节才是方向扫描码。很多初版代码只写了一次 _getch于是读到了 224switch 没有匹配方向不变。调试时按住上键打印第一次读取的值会得到 224 而不是 72这就是标准诊断信号。解决按下面的模式处理输入if (_kbhit()) { int key _getch(); if (key 224 || key 0) { key _getch(); // 此时 key 才是方向键扫描码 } }在 DevC 自带的 TDM-GCC 环境下这个方案能稳定响应四个方向。注意不能只判断 224有些键盘和输入法组合下第一次返回的是 0漏掉 0 同样会失灵。另外不要在主循环里连续调用 _getch 期待拿到方向码那会因为第二次读取时机不对而把正常字符吃掉。4.2 屏幕闪烁严重system(cls)是元凶现象蛇移动起来屏幕一直闪烁偶尔还能看到上一帧的蛇尾巴残影玩几分钟眼睛就酸。原因system(cls) 清屏时控制台先抹掉全部字符然后重新绘制清屏和重绘之间的空档肉眼可见。再加上 20x20 地图加状态栏一次刷屏要执行几百次 cout闪烁自然明显。解决第一步把 system(cls) 改成 SetConsoleCursorPosition 定位到 (0,0)然后覆盖写。第二步隐藏光标CONSOLE_CURSOR_INFO cursorInfo; cursorInfo.bVisible FALSE; cursorInfo.dwSize 1; SetConsoleCursorInfo(hConsole, cursorInfo);第三步如果覆盖写仍有尾部残影说明当前行的字符比上一帧短解决方式是在每行末尾补空格。比如在 render() 的内层循环结束后先输出一段占位空格再换行std::cout \n;空格数量等于 MAP_WIDTH 即可。实测把这三步都做了以后闪烁基本消失。如果机器配置太差还可以略微调大 speed 或减小地图尺寸让输出量降下来。4.3 编译报错Sleep、SetConsoleCursorPosition找不到声明现象在 DevC 按 F11编译报错 Sleep was not declared in this scope或者 SetConsoleCursorPosition 未声明代码看起来正确头文件也包含了。原因常见情况有两个。第一忘了包含 windows.hSleep 和 SetConsoleCursorPosition 都声明在里面。第二MinGW 的部分版本需要定义 _WIN32_WINNT 宏才能暴露较新的窗口函数声明。此外如果代码把 Sleep 写成了小写 sleep标准库和 Windows 都没有这个名字也会报错。解决在源文件最前面加上宏定义再按顺序包含头文件#define _WIN32_WINNT 0x0500 #include windows.h #include conio.h #include time.h #include deque #include iostream #include algorithm这种“先宏后头文件”的顺序必须保持宏定义不能放在 windows.h 后面。如果仍然报错检查 DevC 的编译器设置确认在工具→编译选项里选中的不是老旧的 TDM-GCC 4.8 以下版本或者改用 std::this_thread::sleep_for 代替 Sleep但那样要额外包含 thread 头文件而且在控制台程序里没必要。4.4 蛇自己咬自己判断错误碰撞检查的时机和起点现象蛇明明已经贴身绕过自己画面显示身体交叉却没有判定结束另一种情况是蛇刚反向移动还没碰到身体就提前结束了。原因反向控制没做好是主要原因。当前方向向右玩家按左蛇头的新位置就是原来的脖子一个转身直接钻进身体如果没有反向保护就会立刻结束。另一个问题是碰撞检查从蛇头开始遍历新蛇头必然等于第一个节点于是“自己撞自己”永远成立游戏一开始就结束。解决在 handleInput 里加方向相反判断禁止原地掉头。碰撞检查时用auto it snake.begin() 1跳过第一个节点。判断时机放在 push_front 之后这时新蛇头已经在容器里遍历除第一个节点外的所有节点正好能判断是否咬到自己。这里有更细的注意点没吃到食物时push_front 后紧接着 pop_back旧尾巴会被移除所以新蛇头跑到旧尾巴位置是允许的不应该误判吃到食物时尾巴保留这时新蛇头到达旧尾巴位置就真的是咬到自己。因此正确顺序是“先移动和增长再检查碰撞”而不是先检查再增长。把这个顺序理清楚能避免大半的边界 bug。4.5 输出中文乱码要不要在源码里写中文现象游戏里用 std::cout得分: 编译运行后显示成乱码或问号。原因DevC 5.11 的编辑器默认用 ANSI 编码保存源文件而 Windows 控制台的代码页可能是 936 或 437两边的字符集不一致中文就会被解释成乱码。加上 MinGW 的宽字符支持比较别扭这个坑很难通过配置彻底解决。解决最省事的做法是全部用英文输出比如 Score 和 Game Over。如果一定要中文可以在 main 开头调用 SetConsoleOutputCP(65001)或者把源文件另存为 UTF-8 with BOM但后者在 DevC 里经常不识别。我的建议是这类控制台小项目直接英文输出把中文留给界面升级成图形化以后再做。5. 二次开发把基础版改造成能拿去交作业的程度5.1 先补暂停和重新开始现在游戏结束时直接退出体验有点突兀。我一般会先加一个“按 R 重玩”在 STATE_OVER 状态下主循环不立即 break而是继续监听键盘如果按下 R 就调用 initGame() 并 continue。改动量很小只需要把 main 里的 break 换成对 state 的再次判断或者拆出一个结束界面。暂停在状态机里已经预留把 handleStateChange 里空格键的代码补上提示文字即可。5.2 把成绩写进文件做一个简单排行榜游戏结束时把 score 追加写入 score.txt下一局开始前读取最高分并显示。这段代码不复杂#include fstream #include string int readHighScore() { std::ifstream in(score.txt); int maxScore 0, val; while (in val) { if (val maxScore) maxScore val; } return maxScore; } void saveScore(int s) { std::ofstream out(score.txt, std::ios::app); out s \n; out.close(); }这段代码在 initGame 前调用 readHighScore 初始化最高分在游戏结束时调用 saveScore。文件路径是相对路径也就是 exe 所在目录配置文件不会丢。5.3 速度曲线和界面颜色让 speed 随分数下降每吃 5 个食物 speed 减 10最小到 50这样游戏不会一直匀速到无聊。用 SetConsoleTextAttribute 给蛇身、食物、边界分别上色再在地图四周加一圈边界字符 #视觉上更像一个完整游戏。改动这些点时最重要的一件事是保持状态机清晰每次新增功能先画出新一轮的状态转移表再改主循环。这个习惯是从做贪吃蛇开始的后来做其他小游戏也沿用。几百行的控制台程序状态不清改到暂停和重玩的时候一定会翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表