
1. 从零写一个能跑的控制台贪吃蛇到底难在哪很多人学完 C 语言的struct、指针和malloc再看链表那一章感觉每个字都认识合上书却写不出一个能动的程序。贪吃蛇就是打破这个僵局的好项目它足够小一个main.c就能装下又足够完整把链表的增删遍历、Win32 API 的光标定位和键盘读取全串了一遍。你不需要图形库不需要额外依赖只要一个 Windows 上的编译器VS、MinGW、Dev-C 都行就能做出一个真正能玩的东西。这篇教程面向零基础读者目标很明确产出一个可玩的最小完整版本。蛇身用单向链表管理每个节点存一个屏幕坐标键盘输入用GetAsyncKeyState轮询光标定位用SetConsoleCursorPosition。我会把链表节点结构体、增删遍历骨架、控制台初始化、按键读取、碰撞检测这些关键部分都写成可直接复制的代码并给出编译运行和验证步骤。你跟着敲一遍遇到报错就对照第 5 节的排查清单基本都能解决。需要说明的是本文聚焦在 Windows 控制台环境因为 Win32 API 是 Windows 平台特有的。如果你在别的系统上思路可以借鉴但函数名和头文件要换。下面所有代码都在 Windows 下用gcc和 VS 实测过宽字符打印需要把控制台字体设成能显示中文或方块字的字体否则会看到乱码这一点后面会讲。2. 动手前的准备TaoToken 能帮你做什么写代码的过程中你大概率会遇到两类问题一是某个 Win32 函数的参数记不清二是链表操作写完后不确定逻辑对不对想找个地方快速验证一下思路。这时候一个顺手的模型对话入口会省不少时间。我平时会把 TaoToken 当成一个查文档和理思路的辅助工具它的模型对话入口可以直接问「SetConsoleCursorPosition的第二个参数是什么类型」这类具体问题也能让它帮你逐行解释一段链表插入代码。如果你打算长期写 C 或者做算法练习可以考虑它的 Coding Plan适合需要反复调试、持续对话的场景。接入方式也很简单官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 地址不带 UTM 参数。需要自己写脚本调用的话去控制台拿 Key入口在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各语言的调用示例。这一节不是必须的你完全可以只靠编译器和搜索引擎完成整个项目。但如果你希望有个地方能随时问「这段代码为什么崩了」提前把入口准备好后面排错会顺畅很多。下面进入正题先把链表和 Win32 的骨架搭起来。3. 可复制配置链表节点、控制台初始化与按键读取3.1 链表节点结构体与增删遍历骨架蛇身的本质是一串坐标。蛇每走一步就在头部加一个新坐标尾部删掉一个旧坐标这样视觉上就是整条蛇在移动。用单向链表最合适因为头部插入和尾部删除都是 O(1) 或 O(n) 的简单操作。先定义节点typedef struct SnakeNode { int x; // 列坐标 int y; // 行坐标 struct SnakeNode* next; // 指向下一个节点 } SnakeNode, *pSnakeNode;再定义一个游戏上下文结构体把蛇头指针、食物指针、方向、状态、分数、速度都装进去enum DIRECTION { UP 1, DOWN, LEFT, RIGHT }; enum GAME_STATUS { OK, KILL_BY_WALL, KILL_BY_SELF, END_NORMAL }; typedef struct Snake { pSnakeNode _pSnake; // 指向蛇头 pSnakeNode _pFood; // 指向食物节点 enum DIRECTION _dir; // 当前方向 enum GAME_STATUS _status;// 游戏状态 int _food_weight; // 一个食物的分数 int _score; // 总分数 int _sleep_time; // 每步休眠毫秒数越小越快 } Snake, *pSnake;头插法的骨架初始化蛇身时用得到pSnakeNode cur (pSnakeNode)malloc(sizeof(SnakeNode)); cur-x POS_X 2 * i; cur-y POS_Y; cur-next ps-_pSnake; // 新节点指向原来的头 ps-_pSnake cur; // 头指针移到新节点尾部删除的骨架蛇没吃到食物时用得到pSnakeNode cur ps-_pSnake; while (cur-next-next ! NULL) { cur cur-next; // 停在倒数第二个节点 } free(cur-next); // 释放最后一个节点 cur-next NULL; // 截断链表遍历打印的骨架每次移动后都要重新画蛇pSnakeNode cur ps-_pSnake; while (cur) { SetPos(cur-x, cur-y); wprintf(L%lc, BODY); cur cur-next; }这三个骨架覆盖了链表在贪吃蛇里的全部用法。你只要理解「头插加节点、尾删减节点、遍历重画」这三件事蛇的移动逻辑就通了。3.2 控制台初始化窗口大小、光标隐藏与坐标定位Win32 API 里控制控制台第一步是拿到标准输出设备的句柄。句柄你可以理解成一个「抓手」拿到它才能对屏幕做操作。#include windows.h #include stdio.h #include locale.h HANDLE hOutput GetStdHandle(STD_OUTPUT_HANDLE);设置窗口大小和标题用system执行命令就行system(mode con cols100 lines33); system(title 贪吃蛇);光标一直闪很影响观感把它隐藏掉。注意流程是「先获取、再修改、再设置」不能只改不设CONSOLE_CURSOR_INFO cursor_info { 0 }; GetConsoleCursorInfo(hOutput, cursor_info); cursor_info.bVisible false; SetConsoleCursorInfo(hOutput, cursor_info);坐标定位封装成一个函数后面到处都要用void SetPos(short x, short y) { COORD pos { x, y }; SetConsoleCursorPosition(hOutput, pos); }这里有个容易踩的坑COORD的第一个成员是 X列第二个是 Y行很多人会写反。记住「先列后行」SetPos(38, 14)表示第 38 列、第 14 行。3.3 宽字符打印与本地化设置地图边界用□蛇身用●食物用★这些都是宽字符占两个字节。打印宽字符要用wprintf字面量前面加L#define WALL L□ #define BODY L● #define FOOD L★ wprintf(L%lc, WALL);直接这么写屏幕上可能显示不出来。需要在程序开头调用setlocale切换到本地环境setlocale(LC_ALL, );这行的作用是让程序「入乡随俗」适应操作系统的语言环境这样宽字符才能正确输出。如果你不写这行宽字符可能变成问号或乱码。3.4 按键读取GetAsyncKeyState 与宏封装键盘输入不用scanf因为scanf会阻塞蛇就动不了了。用GetAsyncKeyState轮询它不阻塞随时能查某个键有没有被按过。#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) 0x1) ? 1 : 0)方向键的虚拟键码是VK_UP、VK_DOWN、VK_LEFT、VK_RIGHTW 键是0x57S 键是0x53ESC 是VK_ESCAPE。在游戏主循环里这样用if (KEY_PRESS(VK_UP) ps-_dir ! DOWN) ps-_dir UP; else if (KEY_PRESS(VK_DOWN) ps-_dir ! UP) ps-_dir DOWN; else if (KEY_PRESS(VK_LEFT) ps-_dir ! RIGHT) ps-_dir LEFT; else if (KEY_PRESS(VK_RIGHT) ps-_dir ! LEFT) ps-_dir RIGHT; else if (KEY_PRESS(0x57)) { /* 加速 */ } else if (KEY_PRESS(0x53)) { /* 减速 */ } else if (KEY_PRESS(VK_ESCAPE)) ps-_status END_NORMAL;注意方向判断里加了「不能反向」的条件否则蛇会直接撞到自己。4. 验证请求编译、运行与碰撞检测4.1 编译命令与运行结果把代码保存成snake.c用 gcc 编译gcc snake.c -o snake.exe -lwinmm如果你用的是 VS 开发者命令提示符直接cl snake.c也行。运行snake.exe正常的话你会先看到欢迎界面按任意键后进入游戏屏幕上出现一圈□边框中间一条●组成的蛇还有一个★食物。按方向键蛇会移动吃到食物变长、加分撞墙或撞自己游戏结束。4.2 碰撞检测的验证步骤碰撞检测分两种撞墙和撞自己。撞墙判断蛇头坐标是否等于边界坐标void KillByWall(pSnake ps) { if (ps-_pSnake-x 0 || ps-_pSnake-x 56 || ps-_pSnake-y 0 || ps-_pSnake-y 28) { ps-_status KILL_BY_WALL; } }撞自己则遍历蛇头后面的所有节点看有没有坐标重合void KillBySelf(pSnake ps) { pSnakeNode cur ps-_pSnake-next; while (cur) { if (cur-x ps-_pSnake-x cur-y ps-_pSnake-y) { ps-_status KILL_BY_SELF; break; } cur cur-next; } }验证方法故意让蛇往墙上撞看是否提示「撞到墙上游戏结束」再让蛇转个圈咬到自己看是否提示「撞到了自己」。两个都触发说明碰撞检测正常。4.3 食物生成与吃到食物的验证食物坐标有两个约束X 必须是 2 的倍数且不能和蛇身重合。X 是 2 的倍数是因为宽字符占两列蛇头每次移动 X 变化 2如果食物 X 是奇数蛇头永远到不了。again: do { x rand() % 53 2; y rand() % 28 1; } while (x % 2 ! 0); pSnakeNode cur ps-_pSnake; while (cur) { if (x cur-x y cur-y) goto again; cur cur-next; }验证方法连续吃几个食物观察分数是否按_food_weight增加蛇身是否变长新食物是否出现在空白位置。如果食物出现在蛇身上说明去重逻辑有问题。5. 本篇常见错排查5.1 宽字符显示成乱码或问号最常见的原因是没调用setlocale(LC_ALL, )或者控制台字体不支持。先确认代码里有这行且放在wprintf之前。如果还不行右键控制台标题栏选「属性」把字体改成「点阵字体」或「Consolas」以外的支持中文的字体比如「新宋体」。另外源文件保存时要选 UTF-8 或 GBK 编码和编译器设置一致否则宽字符字面量本身就可能出错。5.2 蛇移动时闪烁严重闪烁是因为每次重画整条蛇。优化方法是只重画变化的部分新蛇头画●旧尾巴画空格。NoFood函数里已经这么做了SetPos(cur-next-x, cur-next-y); printf( );如果你还是觉得闪可以试试双缓冲但控制台程序双缓冲比较麻烦最小版本不用管能玩就行。5.3 蛇撞墙判断不生效检查边界坐标是否和你实际画的地图一致。本文用的是 30 行、58 列的地图所以 X 边界是 0 和 56Y 边界是 0 和 28。如果你改了mode con的列数或画墙的循环次数边界值也要跟着改。另外注意SetPos的参数顺序是「列、行」别写反。5.4 内存泄漏或程序崩溃每次malloc都要有对应的free。游戏结束时必须遍历链表释放所有节点pSnakeNode cur ps-_pSnake; while (cur) { pSnakeNode del cur; cur cur-next; free(del); }如果崩溃发生在SnakeMove里检查pNextNode是否为 NULL以及cur-next-next访问时cur-next是否为空。蛇长度为 1 时尾部删除逻辑要特别小心最小版本建议初始长度设为 4避免边界情况。5.5 按键没反应或方向错乱GetAsyncKeyState的返回值要用 0x1判断「是否被按过」不是判断「是否按下」。如果你写成if (GetAsyncKeyState(VK_UP))会因为高位状态导致误判。另外方向键的虚拟键码别用错VK_UP是上VK_DOWN是下和直觉一致。如果按键完全没反应检查游戏主循环里有没有调用SleepSleep时间太长会导致轮询间隔过大按键被漏掉。6. 继续往下走把最小版本变成你自己的项目到这里一个能跑、能玩、能计分的控制台贪吃蛇就完成了。你手里应该有一个snake.c编译后能正常游戏撞墙和撞自己都有提示食物生成也符合预期。接下来可以做的优化有很多给蛇头换个不同的宽字符让视觉上更容易分辨加一个「穿墙」模式撞墙后从对面出来或者把食物分成不同分值吃一个高分食物加更多分。这些改动都不难核心的链表和 Win32 骨架不用动。如果你在调试过程中遇到想不明白的报错可以把错误信息贴到模型对话里问入口在 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 让它帮你定位问题。需要自己写脚本批量测试或者做自动化验证的话去控制台拿 Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key 在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 管理。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的调用说明。最后提醒一句链表操作里最容易出错的是指针的指向和释放顺序写的时候多画图把每个节点的next指向标清楚比盯着代码看有效得多。贪吃蛇这个项目不大但把链表和 Win32 这两块啃下来后面再写更复杂的控制台程序心里就有底了。