☰
Dev-C++贪吃蛇:C++控制台游戏主循环与碰撞检测实战
2026/10/28 2:17:21 网站建设 项目流程

简介:DevC++贪吃蛇小游戏是一份面向C++入门者、课程设计学生与控制台游戏学习者的完整项目案例,借助Dev-C++工程演示经典贪吃蛇从输入控制到游戏结束的完整流程。压缩包共9个文件、约112KB,以cpp主程序、头文件、Dev-C++工程配置文件和可执行文件为主,导入IDE后即可查看源码、编译运行并对照调试。目前已有11544人学习下载。项目内容涵盖蛇身与食物位置维护、方向键实时控制、难度调速、计时暂停、碰撞检测、得分与重置等环节,也涉及非阻塞输入、清屏、随机食物生成以及控制台游戏循环的基本组织。读者可据此理解数组、循环、条件判断和屏幕刷新在小型游戏中的组织方式,适合作为C++课程实践、期末小项目或控制台游戏开发入门参考,也能为后续扩展功能与排错提供清晰蓝本。

1. 用 Dev-C++ 写贪吃蛇:这个“老掉牙”的组合为什么还值得下载

期末前一周,一个学弟抱着装了 Dev-C++ 的笔记本找我,说要交 C++ 课程设计,题目就是贪吃蛇。程序能跑,但一按方向键就撞死自己,他改了两天没找到原因。我看十分钟就发现问题:他把方向键读到的值直接写进了当前方向变量。这类毛病在控制台版贪吃蛇里特别集中——代码不到三百行,坑全堆在输入、碰撞和刷新这几个点上。

这份 DevC++ 贪吃蛇小游戏资源,路子就是“能编译、能通关、每一行都讲得清”:纯 C++ 加 Windows 控制台 API,不依赖图形库,Dev-C++ 打开.dev工程按 F11 就能出画面。适合刚学完数组、循环、结构体,要交课程设计的人;也适合手上缺一个干净小项目,想把“输入—更新—渲染”这条主线一次走通的从业者。下面按拆包、跑通、拆函数、避坑、调手感的顺序讲。

2. 拆包:四个模块与蛇身数据结构的选型理由

2.1 一份能编译的 .dev 工程该包含哪些文件

拿到压缩包先别急着双击 exe。控制台小游戏的工程结构通常很固定,先确认下面这几类文件在不在,缺哪一类就决定了你要“补”还是“重写”:

文件类型典型文件名作用缺失后果
工程文件贪吃蛇.devDev-C++ 的工程描述,记录源文件列表和编译参数只能手动新建工程再逐个添加源文件
主源文件main.cpp入口、主循环、初始化无法编译
头文件snake.h/game.h常量、结构体、函数声明单文件版可以没有,多文件版必须有
实现文件game.cpp移动、碰撞、绘制等函数体多文件版缺它会在链接期报 undefined reference
编译中间件*.o、Makefile.win增量编译产物建议直接删掉重新编译,见 5.1

单文件版和多文件版没有优劣,只看你的交付场景:课程设计要交报告、要讲模块划分,多文件更像样;自己练手、想快速改参数,单文件改起来更省事。我一般会先把*.o和旧 exe 全删掉,再在 Dev-C++ 里执行一次“全部重新编译”,避免旧目标文件混进来导致“明明改了代码却没生效”这种血泪问题。

打开工程后确认两件事:工具 → 编译选项里编译器套件是不是 Windows 目标(TDM-GCC 一类),以及“编译时加入以下命令”里有没有-std=c++11。资源里如果用了nullptr、auto、范围 for,缺这个参数会直接报'nullptr' was not declared in this scope,很多人以为是代码坏了。

2.2 四个模块的职责边界

控制台贪吃蛇看着简单,本质还是一个游戏循环,模块切不干净,后面加个加速功能都能把碰撞判定带崩。合格的划分是四块:

  • 输入模块:非阻塞读键,只负责把按键翻译成“期望方向”,不修改任何游戏状态。
  • 更新模块:按当前方向推进一格,做碰撞判定,处理吃食物和增长,改的是数据结构。
  • 渲染模块:把状态画到屏幕,只读状态,绝不产生随机数、绝不改蛇身。
  • 节奏模块:控制一帧走多快,也就是帧间隔和步进累加。

为什么非要这么切?因为控制台版最容易出的 bug,是“渲染时顺手改了状态”。比如有人在画蛇的函数里调rand()生成食物,结果每帧都重新随机一次,食物在屏幕上乱跳;还有人在碰撞判定里顺手把蛇头推进去了,撞墙之后蛇头停在墙里,下一帧再从墙外冒出来,看起来像穿墙。把“改状态”和“画状态”分开,这些玄学问题会少一大半。

2.3 蛇身用数组还是链表:先看你要不要“增长”

这是选型上唯一值得犹豫的地方。蛇的形态是“头进尾出”,天然就是一个双端队列:

#include <deque> std::deque<Point> snake; // front 是蛇头,back 是蛇尾 // 每走一步: snake.push_front(nextHead); // 头进,O(1) if (!willGrow) snake.pop_back(); // 没吃到食物才尾出,O(1)

deque的好处是长度自适应,不用预先估一个上限;push_front和pop_back都是常数时间,控制台版每帧也就操作几十次,性能开销肉眼不可见。缺点是内存不连续,如果你想做“回放”“存档”“把整条蛇序列化写文件”,还得遍历拷贝一遍。

数组方案适合固定上限的场景,写法是环形缓冲:

const int MAX_LEN = MAP_W * MAP_H; // 最极端情况:蛇占满整张图 Point body[MAX_LEN]; int headIdx = 0; // body[headIdx] 是蛇头 int tailIdx = 0; // body[tailIdx] 是蛇尾 int len = 3; inline Point& seg(int i) { return body[(tailIdx + i) % MAX_LEN]; } // i=len-1 即蛇头

环形数组的好处是零动态分配、内存连续,存盘时一个fwrite(body, sizeof(Point), len, fp)就完事;坏处是索引写错就是越界,而且MAX_LEN必须和地图尺寸严格对齐,改了地图忘了改上限,蛇一长就踩内存。我的建议是:练习和课程设计用deque,要做回放、要交“数据结构课程”的作业再用环形数组,那是另一个评分点。

不管选哪种,蛇身只存坐标,不要存字符。有人在结构体里加一个char ch表示这一节画什么,结果换皮肤的时候整片代码都要改。

2.4 坐标系、方向枚举与边界常量:一次定死

坐标约定不统一是后期最烦的坑。控制台光标坐标是(列, 行),左上角(0,0);很多人写游戏逻辑时用(x, y),画的时候忘了换算,蛇就整体偏移一格。常见做法是把地图四周留出墙壁,可活动区域从1开始:

#pragma once const int MAP_W = 24; // 可活动区域宽度(不含左右墙) const int MAP_H = 18; // 可活动区域高度(不含上下墙) // 蛇头坐标合法范围:x ∈ [1, MAP_W],y ∈ [1, MAP_H] // 墙画在 x=0、x=MAP_W+1、y=0、y=MAP_H+1 上 enum Dir { DIR_UP = 0, DIR_DOWN, DIR_LEFT, DIR_RIGHT }; struct Point { int x, y; }; // 方向增量表:用查表代替 switch,少写四行还不会漏分支 const int DX[4] = { 0, 0, -1, 1 }; const int DY[4] = { -1, 1, 0, 0 };

这样定死之后,撞墙判定就退化成两次比较,不用去扫地图数组;方向反向了也有现成办法:

inline bool isOpposite(Dir a, Dir b) { return (a == DIR_UP && b == DIR_DOWN) || (a == DIR_DOWN && b == DIR_UP) || (a == DIR_LEFT && b == DIR_RIGHT) || (a == DIR_RIGHT && b == DIR_LEFT); }

提示:MAP_W建议取偶数。蛇身如果用[]、##这类双列字符绘制,一列字符对应两列光标,宽度取偶数时墙和蛇身才能对齐,否则右边框会参差不齐。

3. 在 Dev-C++ 里跑通第一版:编码、主循环与吃食物

3.1 新建工程与文件编码,先把乱码掐死在源头

Dev-C++ 的默认编码在不同版本里不一致,源文件存 UTF-8 而控制台按 GBK(代码页 936)解释,菜单里的中文就会变成一堆问号或方块。两种做法,选一种坚持到底:

  • 方案一(省事):源文件统一保存为 ANSI/GBK,控制台默认代码页就是 936,直接printf("贪吃蛇")正常显示。
  • 方案二(通用):源文件存 UTF-8,程序启动第一行加SetConsoleOutputCP(65001);,把控制台输出代码页切成 UTF-8。

方案二有个副作用:切成 65001 之后,某些老版本控制台对_getch()的中文输入回显不正常,但贪吃蛇只读方向键,不受影响。我一般用方案二,因为源文件进 Git 不会因为编码差异产生大段 diff。

#include <windows.h> #include <cstdio> int main() { SetConsoleOutputCP(65001); // 控制台按 UTF-8 解释输出 SetConsoleTitleA("Snake - DevC++"); // 标题栏用 A 版 API,避免宽字符问题 printf("按方向键开始,ESC 退出\n"); // ... return 0; }

这里有个容易忽略的点:SetConsoleTitle在UNICODE宏开启时会展开成宽字符版本,而 Dev-C++ 默认工程不加-DUNICODE,为了两边都能编过,显式写SetConsoleTitleA。同类问题还有MessageBox,课程设计里少用为妙。

新建工程时选“Console Application”,语言选 C++,把main.cpp和头文件加进工程。别用“新建源文件”然后单独编译,那样工程里没有文件列表,后面加第二个.cpp会链接失败。

3.2 主循环骨架:输入、更新、渲染三段式

所有控制台小游戏的主循环都是同一个骨架,差别只在“更新”里干什么:

int main() { srand((unsigned)time(NULL)); // 播种只做一次,放在循环外面 hideCursor(); // 隐藏光标,见 4.1 GameState gs; initGame(gs); drawBorder(); render(gs); int frameDelay = 150; // 帧间隔,毫秒 bool running = true; while (running) { Dir want; if (readInput(want)) { // 非阻塞,没按键就返回 false if (want == DIR_NONE) running = false; // ESC else applyDir(gs, want); } if (!step(gs)) { // 走一步,返回 false 表示撞了 running = false; break; } render(gs); // 只重绘变化的格子,见 5.3 Sleep(frameDelay); // 放循环末尾,控制节奏 } showResult(gs); return 0; }

几个参数值得说清楚。frameDelay是一次循环的“睡眠时间”,不是精确帧率,因为step、render本身也要花时间,实际帧间隔是Sleep 时长 + 逻辑耗时,这也是为什么第 6 章要换成固定步长累加。srand必须放在循环外,放里面且时间种子精度只有秒级,同一秒内重启两次地图布局完全一样,测随机性的时候会以为随机函数坏了。

applyDir和step分开是有意为之:按键只是登记“想往哪走”,真正转向发生在下一次step里。这个设计直接消灭了“一帧内连按导致反向自撞”的问题,具体见 4.4。

3.3 食物生成:随机不能落在蛇身上

最朴素的写法是rand() % MAP_W + 1,问题有两个:可能生成在蛇身上,以及蛇快占满地图时死循环。

Point spawnFood(const std::deque<Point>& snake) { Point f; int guard = 0; do { f.x = rand() % MAP_W + 1; f.y = rand() % MAP_H + 1; ++guard; } while (onSnake(snake, f) && guard < 2000); // 后悔药:防死循环 return f; } bool onSnake(const std::deque<Point>& s, Point p) { for (size_t i = 0; i < s.size(); ++i) if (s[i].x == p.x && s[i].y == p.y) return true; return false; }

guard计数器是很多教程版本没有的。当蛇长到接近满图时,随机命中的空白格概率极低,do-while会卡在这几行里,表现出来就是“游戏突然卡死”。加个上限,超过就退化成顺序扫描找第一个空格,逻辑上更稳。

另一个细节:rand() % n有轻微的取模偏差,低编号的格子被选中的概率高一点点。控制台版完全不用管,但如果你要写“食物出现概率”的统计测试,记得用(int)((double)rand() / RAND_MAX * n)。

3.4 计分、加速与结束判定

吃到食物的处理集中在onEat里,别散落在各处:

struct GameState { std::deque<Point> snake; Point food; Dir dir; int score; int eaten; // 已吃数量,和分数分开记,方便调参 bool grow; // 本帧是否要长一节 }; void onEat(GameState& gs, int& frameDelay) { gs.score += 10; gs.eaten += 1; gs.grow = true; if (gs.eaten % 5 == 0 && frameDelay > 60) frameDelay -= 10; // 每 5 个食物提速 10ms,下限 60ms gs.food = spawnFood(gs.snake); }

frameDelay的下限必须有。不加下限,玩到三四十个食物之后Sleep(0)甚至负数,Sleep传负数会被当成一个极大值(实际是按INFINITE处理),表现就是“吃得越多游戏越卡”,非常反直觉。加速的粒度和下限按手感调,150ms 起步、60ms 封顶是我试下来比较舒服的一组。

结束判定要有区分度:撞墙、撞自己、按 ESC 退出,三种情况的结算文案不一样,玩家才知道自己死在哪。课程设计答辩时这点很加分,因为它说明你想过状态机。

4. 关键函数拆解:绘制、移动与碰撞判定顺序

4.1 gotoxy 定位绘制与隐藏光标

控制台没有“画布”,只有光标位置。想让蛇动起来且不闪屏,必须能精确地把光标挪到某一格重画,而不是每帧system("cls")全清。

#include <windows.h> void gotoxy(int col, int row) { COORD pos = { (SHORT)col, (SHORT)row }; SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos); } void hideCursor() { HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO info; GetConsoleCursorInfo(h, &info); info.bVisible = FALSE; // 光标乱闪很难看,先关掉 SetConsoleCursorInfo(h, &info); }

COORD里的 X 是列、Y 是行,和数学坐标系的 y 轴方向相反,这一点在“蛇向上走”时最容易写反:向上是y - 1,不是y + 1。

隐藏光标不纯粹是美观问题。光标停在屏幕上会跟着gotoxy一起跳,如果正好停在蛇头那一格,看起来像蛇头多了一个方块,截图贴进报告里非常显眼。

注意:SetConsoleCursorPosition的坐标原点包含控制台窗口的缓冲区原点。如果你的控制台窗口设置了“快速编辑模式”,鼠标点一下就会暂停输出,游戏看起来像卡死。跑之前右键标题栏 → 属性 → 取消勾选“快速编辑模式”,这个坑我踩过不止一次。

4.2 头进尾出的移动实现

移动是整个游戏里唯一修改蛇身的地方,写法直接决定碰撞判定对不对:

bool step(GameState& gs) { Point next = gs.snake.front(); next.x += DX[gs.dir]; next.y += DY[gs.dir]; // 1) 撞墙:坐标越界 if (next.x < 1 || next.x > MAP_W || next.y < 1 || next.y > MAP_H) return false; // 2) 撞自己:先算这帧会不会长,决定尾巴那格算不算“安全通道” bool willGrow = (next.x == gs.food.x && next.y == gs.food.y); size_t checkLen = willGrow ? gs.snake.size() : gs.snake.size() - 1; for (size_t i = 0; i < checkLen; ++i) { if (gs.snake[i].x == next.x && gs.snake[i].y == next.y) return false; } // 3) 全部通过才真正改数据 gs.snake.push_front(next); if (willGrow) onEat(gs, gs.frameDelay); else gs.snake.pop_back(); return true; }

第 2 步的checkLen是核心。蛇每走一步尾巴会离开一格,所以“头撞到尾巴当前所在的格子”实际上是安全的——下一帧尾巴就不在那了。如果循环写满snake.size(),会出现“蛇紧贴着自己尾巴拐弯就死”的诡异现象,而且只在蛇比较长的时候偶发,非常难查。

willGrow也影响这一步:吃到食物这一帧尾巴不动,所以尾巴那一格不能当成安全通道,必须完整检查。

4.3 三种碰撞的判定顺序不能乱

顺序错了会出现“明明撞墙了却先判自撞”“死亡位置比实际晚一格”这类问题。正确顺序是:边界 → 自身 → 食物,原因是边界判定不需要遍历,先做能省一次循环;自身判定要在真正插入蛇头之前做;食物判定要放在自身判定之后,因为它决定checkLen。

判定时机依赖常见写错方式
撞墙计算完新头坐标立即做MAP_W、MAP_H写成> MAP_W + 1,蛇头会先穿进墙里一格
撞自身插入蛇头之前蛇身队列、willGrow循环写满全长度,贴着尾巴转弯被误判
吃食物自身判定通过、插入之前食物坐标放在插入之后,导致蛇头已经算进蛇身再比对,永远撞不到

还有个隐藏顺序问题:如果先push_front再判撞,撞墙的那次也会把蛇头推进队列,死亡动画里蛇头会突出在墙外。视觉上的“穿墙”多半就是这么来的,不是渲染的锅。

4.4 方向键读取与转向缓冲

控制台下方向键返回两个字节:前缀0或224,再加一个扫描码。只读一次会拿到前缀,扫描码留在缓冲区里,下一次读又读到它,于是“按一下方向键,蛇连转两次”。

#include <conio.h> // 返回 false 表示没按键;want 为 DIR_NONE 表示 ESC bool readInput(Dir& want) { if (!_kbhit()) return false; // 非阻塞,没按键立即返回 int k = _getch(); if (k == 0 || k == 224) { // 双字节前缀 int code = _getch(); // 第二次读才是真正的方向键码 switch (code) { case 72: want = DIR_UP; break; case 80: want = DIR_DOWN; break; case 75: want = DIR_LEFT; break; case 77: want = DIR_RIGHT; break; default: return false; } return true; } if (k == 27) { want = DIR_NONE; return true; } // ESC return false; }

关键的转向缓冲这样写:

void applyDir(GameState& gs, Dir want) { // 只和“已生效的方向”比较,不能和“本帧刚登记的方向”比 if (want == gs.dir || isOpposite(want, gs.dir)) return; gs.dir = want; }

为什么不能直接gs.dir = want?玩家手快的时候,在一帧之内可能连按“上”再按“左”。当前方向是“右”,第一次比较“左”和“右”是反向,被拒绝,没问题;但如果第一次按的是“上”,gs.dir立刻变成“上”,第二次按“下”时比较的是“上”和“下”,同样会被拒——看起来没问题,可一旦配上“速度越快按键越容易堆积”,就会出现比较基准被污染的情况,两条方向在缓冲里来回覆盖,最后蛇往奇怪的方向拐,玩家叫你玄学。把输入缓冲成pendingDir、在step里统一生效,是最省心的做法。

_kbhit和_getch在 MinGW 的conio.h里带下划线前缀,如果你的编译器只声明了带下划线的版本,直接写kbhit()会报未定义,见 5.1。

5. 避坑与排查:Dev-C++ 下最容易翻车的五个点

5.1 现象:编译报conio.h: No such file or directory或_kbhit未定义

现象:代码在别人机器上能编,在你的 Dev-C++ 里报找不到头文件,或者[Error] '_kbhit' was not declared in this scope。

原因:三种可能。一是工程指向的编译器套件不是 Windows 目标(比如误配了某个不带 console 支持的套件),conio.h是 Windows 控制台扩展而不是 C++ 标准头,非 Windows 目标下本来就没有;二是头文件里只暴露了带下划线的版本,而代码里写的是老式kbhit();三是把conio.h和<windows.h>的包含顺序写反,某些老版本头文件里宏冲突,把声明吃掉了。

解决:先看工具 → 编译选项 → 编译器设定,确认选的是 Windows 目标套件;然后把调用统一成_kbhit()、_getch(),或者加一行#define kbhit _kbhit做兼容;包含顺序固定为#include <windows.h>在前、#include <conio.h>在后。如果还不行,把conio.h换成#include <windows.h>之后直接用GetAsyncKeyState查键,牺牲一点可读性换编译通过。

5.2 现象:中文显示成乱码、问号或方块

现象:菜单和结算文案里的中文全变成锟斤拷或者一排?,但英文和数字正常。

原因:源文件编码和控制台输出代码页不一致。Dev-C++ 默认保存编码在不同版本里不统一,源文件存成 UTF-8 而控制台按 936 解释,双字节被拆开重新组合就是乱码;反过来存 GBK 但程序里调了SetConsoleOutputCP(65001),同样会乱。

解决:二选一并坚持到底。要么源文件存 ANSI/GBK、程序里不碰代码页;要么源文件存 UTF-8、main第一行调SetConsoleOutputCP(65001)。改完编码后必须“全部重新编译”,只按 F9 编译单文件有时不会刷新。另外别用system("chcp 65001")代替 API,它会额外弹一行提示,还会把原来的代码页改掉。

5.3 现象:蛇一动整个屏幕就闪,或者留下残影

现象:蛇每走一步,屏幕明显抖一下;或者蛇走过去之后原来的位置没擦干净,拖出一条越来越长的影子。

原因:每帧system("cls")全屏清空再全画,控制台的清屏是整屏重绘,闪是必然的;残影则是没有擦除尾巴离开的那一格,只画了新头。

解决:改成局部刷新,只擦尾巴、画新头,边界和食物画一次就不再动。

void renderStep(const GameState& gs, Point oldTail) { // 宽度 2 列,因为蛇身用双列字符绘制 gotoxy(oldTail.x * 2 - 1, oldTail.y); printf(" "); // 擦尾 gotoxy(gs.snake.front().x * 2 - 1, gs.snake.front().y); printf("[]"); // 画头 }

坐标乘 2 是因为一个汉字或[]占两列光标,不乘的话蛇身宽度和墙壁对不上,越走越偏。擦除用两个空格而不是空字符串,空格能覆盖掉原字符,空字符串什么都不做。

5.4 现象:按方向键没反应,或者一按就撞死自己

现象:程序跑起来后方向键完全无效,只能用 WASD;或者方向键能用,但快速连按两次就会自撞。

原因:没反应多半是只读了一次_getch(),拿到的是前缀字节,扫描码留在缓冲区里,后面的读取全部错位;自撞则是在输入处理里直接修改了当前方向,导致反向判定失效(见 4.4)。还有一种情况:在 Dev-C++ 内部按 F11 运行时,控制台窗口没有获得焦点,按键被 IDE 截走了。

解决:读键必须成对读,前缀判断写if (k == 0 || k == 224);方向先用pendingDir缓存,在step里生效;测试时编译完直接双击 exe 跑,别在 IDE 里测按键手感。另外Sleep时间太长时按键会在缓冲区堆积,可以用while (_kbhit())把缓冲读空、只保留最后一次有效转向。

5.5 现象:分数高了以后蛇穿墙、瞬移或者直接崩溃

现象:吃了十几个食物之后,蛇从右边出去又从左边冒出来,或者吃到某个位置程序直接闪退,没有任何报错。

原因:两处。一是撞墙判定写成了next.x > MAP_W + 1,多放了一格,蛇头进了墙里还没判死;二是用环形数组存蛇身时索引算错,MAX_LEN和地图尺寸不同步,蛇一长就越界写内存,表现为随机崩溃。加速逻辑也可能是一帧走多步、碰撞只判一次,看起来就像瞬移。

解决:把边界判断抽成一个函数inMap(Point),所有地方调用同一份,不要复制粘贴;用环形数组时在seg()里加assert(i >= 0 && i < len),Debug 版本跑一遍就能定位;加速只调Sleep的时长,不要在一帧里循环调用多次step,需要一帧多步就用第 6 章的固定步长累加,每步都完整做一次碰撞判定。

6. 进阶:把手感和可维护性一起调出来

6.1 固定步长累加:速度不再靠猜 Sleep

Sleep(frameDelay)的问题是帧间隔不精确:渲染耗时、系统调度都会叠加进去,加速之后体感变化不均匀。常见做法是把“逻辑步进”和“渲染”解耦,用时间累加器驱动:

int stepMs = 120; // 逻辑步长 int acc = 0; DWORD last = GetTickCount(); while (running) { DWORD now = GetTickCount(); acc += (int)(now - last); // 32 位计数 49 天回绕,游戏时长内无影响 last = now; Dir want; if (readInput(want)) { /* 处理按键 */ } while (acc >= stepMs) { // 该走几步就走几步,每步都判碰撞 acc -= stepMs; if (!step(gs)) { running = false; break; } } render(gs); Sleep(1); // 让出 CPU,别空转跑满一个核 }

这样调速度只改stepMs,改到 60 也不会出现“一帧走两步却没判撞”的漏洞,因为每一小步都过了完整的step。Sleep(1)别省,省了就是 100% 占用一个核,笔记本风扇会告诉你。

6.2 逻辑与渲染分层:以后换图形界面不用重写

想从控制台迁到图形界面,最痛的不是画图,是发现逻辑和printf搅在一起。提前把状态独立出来:

struct GameState { // 只放数据,不放任何 IO std::deque<Point> snake; Point food; Dir dir; int score; bool alive; }; void update(GameState& s, Dir input, int& stepMs); // 不碰 printf / gotoxy void render(const GameState& s); // 不碰 rand / 不改数据

分层之后,render换成 GDI 的Rectangle、或者换成任何一个绘图库,逻辑层一行不用动。附带一个好处:输入可以录制。把每帧的Dir存成数组,配一个固定种子,就能完整复现“吃到第 37 个食物时撞死”的过程,比盯着屏幕猜快得多。

6.3 五分钟自测表

改完代码别急着交,按这张表跑一遍,能挡掉八成返工:

测试项操作期望结果
边界贴着四面墙各走一次撞墙立即结束,蛇头不越出边框
贴尾转弯蛇长 ≥ 5 后贴着尾巴拐弯不判死,正常通过
反向键向右走时按左无反应,蛇继续向右
连按转向一帧内连按上、左只生效一次,不自撞
食物重叠吃到只剩 2 格空白不卡死,食物落在空白格
加速下限连吃 40 个食物帧间隔不低于下限,不出现负数 Sleep
退出ESC、撞墙、撞自己各一次三种结算文案正确,程序正常退出

最后说个自己的习惯。刚学那会儿我总觉得这种两百多行的小游戏“跑起来就行”,结果每次交作业前改个加速功能,都要重新调半天碰撞。后来我强制自己每改一处逻辑,就把 6.3 那张表从头跑一遍,尤其是贴尾转弯和连按转向这两项,它们从来不报错,只在你演示的时候翻车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询