C++手写控制台贪吃蛇:从数组到游戏循环的深度拆解
1. 为什么选择C手写贪吃蛇贪吃蛇这个游戏几乎每个学编程的人都见过甚至自己动手写过。但有意思的是大部分人第一次写贪吃蛇用的都是Python的pygame、JavaScript的canvas或者干脆用现成的游戏引擎拖拽几个组件就完事了。真正用C从零手写一个控制台版贪吃蛇的人反而不多。原因很简单——C写贪吃蛇没有现成的图形库帮你处理渲染没有事件循环帮你处理输入甚至连“蛇怎么动”这种最基本的问题都得自己从数组和循环开始想。但恰恰是这种“什么都没有”的环境最能锻炼一个开发者对程序结构的理解。你要自己设计数据结构来存储蛇身自己实现碰撞检测自己处理键盘输入的实时响应自己控制刷新频率让游戏看起来流畅。这些东西在Python里可能三行代码就搞定了但在C里你需要真正理解内存布局、循环控制、时间函数和平台相关的API调用。这篇文章要聊的就是用C实现一个控制台版贪吃蛇的完整过程。不是那种“复制粘贴就能跑”的代码堆砌而是从设计思路到具体实现从数据结构选型到键盘输入处理的深度拆解。适合已经学过C基础语法、想找个项目练手的人也适合那些写过其他语言版本贪吃蛇、想看看C版本有什么不同的人。整个项目不依赖任何第三方图形库只用标准库和Windows平台自带的一些控制台函数代码量控制在300行以内但麻雀虽小五脏俱全。2. 整体设计思路与核心架构拆解2.1 控制台游戏的本质是什么很多人一提到“游戏开发”脑子里浮现的就是Unity、Unreal这些引擎觉得没有图形界面就不算游戏。但实际上控制台游戏的核心逻辑和图形游戏是完全一样的——都是在一个循环里不断处理输入、更新状态、渲染画面。区别只在于渲染的方式图形游戏往显存里写像素控制台游戏往字符缓冲区里写字符。理解这一点很重要因为它决定了你的代码结构。一个标准的游戏循环长这样while (gameRunning) { handleInput(); // 处理玩家输入 updateState(); // 更新游戏逻辑 render(); // 渲染画面 sleepOrWait(); // 控制帧率 }贪吃蛇也不例外。你需要在这个框架下把蛇的移动、食物的生成、碰撞的检测、分数的更新这些逻辑填进去。控制台版的特殊之处在于render()函数不是画像素而是用printf或者cout往终端输出字符handleInput()不是读鼠标事件而是用_kbhit()和_getch()检测键盘按键。2.2 为什么用数组而不是链表存蛇身蛇身的数据结构选择是贪吃蛇实现中最值得聊的一个设计决策。常见的有两种方案链表和数组。链表的好处是插入和删除方便。蛇吃食物的时候在头部加一个节点蛇移动的时候在尾部删一个节点。听起来很自然但实际写起来你会发现链表在控制台游戏里反而更麻烦。因为渲染的时候你需要遍历整个蛇身来画字符链表遍历的缓存局部性差而且每次移动都要动态分配和释放内存对于一个每秒刷新十几次的游戏来说这种开销虽然不大但完全没必要。数组方案更简单粗暴用一个固定大小的数组存蛇身每个节点的坐标数组的第0个元素是蛇头最后一个元素是蛇尾。蛇移动的时候从尾部开始每个节点依次向前复制前一个节点的坐标然后更新蛇头的坐标。吃食物的时候蛇身长度加一不需要额外操作因为数组里本来就有空间。struct Point { int x; int y; }; Point snake[MAX_LENGTH]; // MAX_LENGTH根据地图大小设定 int snakeLength;这种方案的缺点是数组大小固定蛇不能无限增长。但对于贪吃蛇来说地图就那么大蛇最长也不可能超过地图的格子总数所以固定数组完全够用。而且数组的内存是连续的遍历和复制都很快代码写起来也直观。我试过用std::vector来做效果也不错但每次移动都要insert和erase虽然vector的insert在头部是O(n)但对于长度几十的蛇来说性能差异可以忽略不计。不过用固定数组的话你不需要担心迭代器失效的问题代码更稳。2.3 地图和坐标系统的设计控制台的地图本质上就是一个二维字符网格。假设地图是20列宽、15行高那么你需要一个char map[15][20]的二维数组来存储每个格子的状态。格子的状态有三种空、蛇身、食物。但实际写的时候我不建议真的去维护一个完整的二维数组。因为控制台的光标可以自由移动你只需要在每次渲染的时候把蛇身和食物的位置画出来其他位置保持空白就行。这样省去了维护地图数组的麻烦也避免了“蛇移动后忘记清除旧位置”这种常见bug。坐标系统方面控制台的坐标系和数学里的笛卡尔坐标系不一样。控制台的原点在左上角x轴向右递增y轴向下递增。所以蛇向上移动是y-1向下移动是y1向左是x-1向右是x1。这个方向感一定要在脑子里建立清楚否则写移动逻辑的时候很容易搞反。// 方向定义 enum Direction { UP, DOWN, LEFT, RIGHT }; // 根据方向更新蛇头坐标 switch (dir) { case UP: snake[0].y - 1; break; case DOWN: snake[0].y 1; break; case LEFT: snake[0].x - 1; break; case RIGHT: snake[0].x 1; break; }2.4 游戏循环的帧率控制控制台游戏最容易踩的坑就是帧率控制。如果你不加任何延时游戏循环会以CPU的全速运行蛇会瞬间飞出去撞墙。所以必须在每次循环末尾加一个延时。Windows下可以用Sleep()函数参数是毫秒。比如Sleep(100)就是延时100毫秒相当于每秒刷新10次。这个速度对于贪吃蛇来说比较合适蛇的移动速度不会太快也不会太慢。如果你觉得慢可以调到Sleep(80)或者Sleep(50)但再快的话人眼可能就跟不上了。但Sleep()有个问题它是阻塞的。在Sleep期间程序无法响应任何输入。如果你按方向键的时候正好在Sleep按键就会被延迟处理。对于贪吃蛇来说这个问题不算严重因为Sleep时间很短而且游戏本身对输入延迟的要求不高。但如果你想让输入更灵敏可以用非阻塞的输入检测把Sleep拆成多个短延时每次短延时后检查一下有没有按键。// 把100毫秒拆成10个10毫秒每10毫秒检查一次输入 for (int i 0; i 10; i) { if (_kbhit()) { handleInput(); } Sleep(10); }这种写法会让操作手感明显提升尤其是快速转向的时候不会出现“按了没反应”的情况。3. 核心细节解析与实操要点3.1 蛇的移动逻辑从尾部开始复制蛇的移动逻辑是贪吃蛇最核心的部分也是最容易写错的地方。很多人第一反应是“把蛇头往前移一格然后把蛇尾删掉”但具体到代码实现顺序很重要。正确的做法是从蛇尾开始每个节点复制前一个节点的坐标。也就是说snake[i] snake[i-1]从i snakeLength - 1一直循环到i 1。最后再根据方向更新snake[0]蛇头的坐标。// 从尾部开始每个节点移动到前一个节点的位置 for (int i snakeLength - 1; i 0; i--) { snake[i] snake[i - 1]; } // 更新蛇头位置 switch (dir) { case UP: snake[0].y--; break; case DOWN: snake[0].y; break; case LEFT: snake[0].x--; break; case RIGHT: snake[0].x; break; }如果你反过来先更新蛇头再从头部开始复制那么蛇头的旧坐标会被覆盖蛇身的第二节会移动到蛇头的新位置整条蛇就会“缩成一团”。这个坑我踩过当时调试了半天才发现是循环方向搞反了。注意复制循环必须从尾部向头部进行否则会覆盖尚未复制的数据。3.2 食物生成避免生成在蛇身上食物生成看起来简单随机一个坐标就行。但你必须确保食物不会生成在蛇身上否则蛇一移动就会“自动吃到”食物游戏逻辑就乱了。基本的做法是随机生成一个坐标然后遍历蛇身检查这个坐标是否和蛇身的某个节点重合。如果重合就重新生成直到找到一个空位。bool isOnSnake(int x, int y) { for (int i 0; i snakeLength; i) { if (snake[i].x x snake[i].y y) { return true; } } return false; } void generateFood() { do { food.x rand() % MAP_WIDTH; food.y rand() % MAP_HEIGHT; } while (isOnSnake(food.x, food.y)); }这种“随机-检查-重试”的方式在蛇比较短的时候效率很高但如果蛇很长比如占了地图的一半格子那么每次生成食物可能要重试很多次。更优雅的做法是维护一个“空闲格子列表”每次从列表里随机选一个选完就删掉。不过对于20x15的地图来说蛇最长也就300节重试的次数不会太多用简单的方法就够了。还有一个细节食物的坐标范围要和地图边界匹配。如果你的地图是20列15行那么x的范围是0到19y的范围是0到14。用rand() % 20和rand() % 15就能得到正确的范围。3.3 碰撞检测撞墙和撞自己碰撞检测分两种撞墙和撞自己。撞墙的判断很简单蛇头的坐标超出了地图边界。比如snake[0].x 0 || snake[0].x MAP_WIDTH || snake[0].y 0 || snake[0].y MAP_HEIGHT就说明撞墙了游戏结束。撞自己的判断稍微麻烦一点蛇头的坐标和蛇身的某个节点重合。但注意蛇头不能和自己比较所以循环从i 1开始。// 撞墙检测 if (snake[0].x 0 || snake[0].x MAP_WIDTH || snake[0].y 0 || snake[0].y MAP_HEIGHT) { gameOver true; } // 撞自己检测 for (int i 1; i snakeLength; i) { if (snake[0].x snake[i].x snake[0].y snake[i].y) { gameOver true; break; } }这里有个细节如果你在蛇移动之前检测碰撞那么蛇头的新坐标还没更新检测的是旧坐标。正确的顺序是先更新蛇头坐标再检测碰撞。但如果你在蛇移动之后、吃食物之前检测那么蛇头可能会和蛇尾重合——因为蛇尾还没移动。这种情况下蛇头“穿过”蛇尾是合法的不应该判定为撞自己。解决方法是在检测撞自己时忽略蛇尾节点。因为蛇尾在这一帧会移走蛇头移动到蛇尾的位置是安全的。具体来说循环只检查i 1到i snakeLength - 2不检查最后一个节点。// 撞自己检测忽略蛇尾 for (int i 1; i snakeLength - 1; i) { if (snake[0].x snake[i].x snake[0].y snake[i].y) { gameOver true; break; } }这个细节很多人会忽略导致蛇在特定情况下“莫名其妙”地死掉。3.4 键盘输入非阻塞检测与方向过滤控制台下的键盘输入处理Windows平台用_kbhit()和_getch()这对函数。_kbhit()检测是否有按键按下返回非零值表示有_getch()读取按键的字符码不会回显到屏幕上。#include conio.h if (_kbhit()) { char key _getch(); switch (key) { case w: case W: if (dir ! DOWN) dir UP; break; case s: case S: if (dir ! UP) dir DOWN; break; case a: case A: if (dir ! RIGHT) dir LEFT; break; case d: case D: if (dir ! LEFT) dir RIGHT; break; } }方向过滤很重要如果蛇正在向右移动你按了左键蛇不能直接掉头否则会撞到自己。所以每个方向都要检查“新方向是否和当前方向相反”如果是就忽略这次按键。但这里有个更隐蔽的问题如果蛇正在向右移动你快速按了“上”然后马上按“左”在两次按键之间蛇还没有移动那么dir会先变成UP然后变成LEFT。但LEFT和UP不相反所以会被接受。结果蛇会向左移动而蛇身还在右边蛇头就会撞到自己。解决方法是不要直接修改dir而是用一个“待处理方向”变量在每次蛇移动之前才真正应用这个方向。这样即使玩家快速按了多个方向也只有最后一个有效而且方向过滤是基于蛇的实际移动方向而不是按键时的方向。Direction nextDir dir; // 待处理方向 // 输入处理 if (_kbhit()) { char key _getch(); switch (key) { case w: if (dir ! DOWN) nextDir UP; break; case s: if (dir ! UP) nextDir DOWN; break; case a: if (dir ! RIGHT) nextDir LEFT; break; case d: if (dir ! LEFT) nextDir RIGHT; break; } } // 蛇移动之前应用方向 dir nextDir;这个细节在快速操作的时候特别重要能避免很多“莫名其妙撞死”的情况。4. 完整实操流程与核心代码实现4.1 项目结构与环境准备这个项目不需要任何第三方库只需要一个支持C11的编译器和Windows系统因为用到了conio.h和windows.h。如果你用的是Visual Studio直接新建一个空项目添加一个.cpp文件就行。如果你用的是MinGW或者Cygwin也可以编译但要注意链接conio库。项目结构很简单就一个文件snake/ └── main.cpp代码分为几个部分头文件包含、常量定义、全局变量、辅助函数、游戏逻辑函数、主函数。下面我会按这个顺序逐步展开。4.2 头文件与常量定义#include iostream #include conio.h #include windows.h #include cstdlib #include ctime using namespace std; const int MAP_WIDTH 20; const int MAP_HEIGHT 15; const int MAX_LENGTH MAP_WIDTH * MAP_HEIGHT; enum Direction { UP, DOWN, LEFT, RIGHT }; struct Point { int x; int y; };MAP_WIDTH和MAP_HEIGHT决定了游戏区域的大小。20x15是一个比较合适的尺寸既不会太小导致蛇没地方跑也不会太大导致控制台窗口需要滚动。MAX_LENGTH是蛇的最大长度等于地图的格子总数理论上蛇可以填满整个地图。Direction枚举定义了四个方向。Point结构体存储坐标后面蛇身数组和食物都用这个结构体。4.3 全局变量与辅助函数Point snake[MAX_LENGTH]; int snakeLength; Point food; Direction dir; Direction nextDir; int score; bool gameOver; void gotoxy(int x, int y) { COORD coord; coord.X x; coord.Y y; SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord); } void hideCursor() { CONSOLE_CURSOR_INFO cursorInfo; GetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), cursorInfo); cursorInfo.bVisible FALSE; SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), cursorInfo); }gotoxy()是控制台游戏的核心函数它把光标移动到指定坐标这样你就可以在任意位置输出字符而不是只能从上到下、从左到右地打印。hideCursor()隐藏控制台的光标避免光标闪烁影响游戏画面。这两个函数都依赖Windows API所以这个项目只能在Windows下编译运行。如果你想让它在Linux下也能跑需要用ncurses库来替代但那就是另一个话题了。4.4 初始化与渲染void initGame() { snakeLength 3; snake[0] {MAP_WIDTH / 2, MAP_HEIGHT / 2}; snake[1] {MAP_WIDTH / 2 - 1, MAP_HEIGHT / 2}; snake[2] {MAP_WIDTH / 2 - 2, MAP_HEIGHT / 2}; dir RIGHT; nextDir RIGHT; score 0; gameOver false; srand((unsigned)time(nullptr)); generateFood(); } void render() { // 画上边界 gotoxy(0, 0); for (int i 0; i MAP_WIDTH 2; i) cout #; // 画地图主体 for (int y 0; y MAP_HEIGHT; y) { gotoxy(0, y 1); cout #; for (int x 0; x MAP_WIDTH; x) { bool printed false; // 画蛇 for (int i 0; i snakeLength; i) { if (snake[i].x x snake[i].y y) { if (i 0) cout O; // 蛇头 else cout o; // 蛇身 printed true; break; } } // 画食物 if (!printed food.x x food.y y) { cout *; printed true; } if (!printed) cout ; } cout #; } // 画下边界 gotoxy(0, MAP_HEIGHT 1); for (int i 0; i MAP_WIDTH 2; i) cout #; // 显示分数 gotoxy(0, MAP_HEIGHT 2); cout Score: score ; }渲染函数是控制台游戏里最“体力活”的部分。你需要遍历整个地图对每个格子判断是蛇、是食物、还是空白然后输出对应的字符。蛇头用O蛇身用o食物用*边界用#。这里有个性能上的小技巧不要每次渲染都清屏。system(cls)会闪烁得很厉害而且速度慢。正确的做法是用gotoxy()把光标移到每个位置直接覆盖旧的字符。因为地图大小是固定的每个格子的位置也是固定的所以直接覆盖不会留下残影。但要注意如果蛇变短了这个游戏里蛇不会变短但如果你加了什么特殊道具就说不定了旧的位置需要手动清除。最简单的做法是每次渲染前把所有格子都输出一遍空格但那样效率太低。更好的做法是只清除蛇尾移动前的位置。4.5 游戏主循环与逻辑更新void update() { // 应用待处理方向 dir nextDir; // 蛇身移动 for (int i snakeLength - 1; i 0; i--) { snake[i] snake[i - 1]; } // 更新蛇头 switch (dir) { case UP: snake[0].y--; break; case DOWN: snake[0].y; break; case LEFT: snake[0].x--; break; case RIGHT: snake[0].x; break; } // 撞墙检测 if (snake[0].x 0 || snake[0].x MAP_WIDTH || snake[0].y 0 || snake[0].y MAP_HEIGHT) { gameOver true; return; } // 撞自己检测忽略蛇尾 for (int i 1; i snakeLength - 1; i) { if (snake[0].x snake[i].x snake[0].y snake[i].y) { gameOver true; return; } } // 吃食物检测 if (snake[0].x food.x snake[0].y food.y) { snakeLength; score 10; generateFood(); } } int main() { hideCursor(); initGame(); while (!gameOver) { render(); // 非阻塞输入检测拆分成多个短延时 for (int i 0; i 10; i) { if (_kbhit()) { char key _getch(); switch (key) { case w: case W: if (dir ! DOWN) nextDir UP; break; case s: case S: if (dir ! UP) nextDir DOWN; break; case a: case A: if (dir ! RIGHT) nextDir LEFT; break; case d: case D: if (dir ! LEFT) nextDir RIGHT; break; } } Sleep(10); } update(); } gotoxy(0, MAP_HEIGHT 4); cout Game Over! Final Score: score endl; system(pause); return 0; }主循环的结构很清晰渲染、处理输入、更新逻辑、延时。输入处理被拆成了10个10毫秒的小段每段检查一次按键这样既保证了帧率稳定又让输入响应更灵敏。update()函数里蛇身移动、碰撞检测、吃食物检测的顺序很重要。必须先移动蛇头再检测碰撞最后检测吃食物。如果顺序搞反了会出现“蛇头还没动就检测碰撞”或者“吃到食物后蛇身没变长”的问题。4.6 编译与运行如果你用的是Visual Studio直接按F5编译运行就行。如果你用的是命令行可以用g main.cpp -o snake.exe -stdc11然后运行snake.exe。注意这个程序必须在Windows控制台里运行不能在IDE的输出窗口里运行因为IDE的输出窗口不支持gotoxy()和_kbhit()这些控制台函数。运行后你会看到一个20x15的边框中间有一条长度为3的蛇蛇头是O蛇身是o食物是*。用WASD控制方向吃到食物蛇会变长分数加10。撞墙或者撞到自己游戏结束。5. 常见问题与排查技巧实录5.1 蛇移动时画面闪烁怎么办画面闪烁是控制台游戏最常见的问题根本原因是每次渲染都清屏。system(cls)会清空整个控制台缓冲区然后重新输出所有字符这个过程在人眼看来就是闪烁。解决方法是用gotoxy()直接覆盖旧字符不清屏。但这样会有一个新问题如果蛇变短了旧的位置会留下残影。对于贪吃蛇来说蛇只会变长不会变短所以这个问题不存在。但如果你加了什么“缩短蛇身”的道具就需要在蛇尾移动前的位置输出一个空格。还有一个闪烁的来源是光标。控制台的光标默认是闪烁的如果你不隐藏它它会在地图上跳来跳去看起来很不舒服。用hideCursor()把光标隐藏掉就行。5.2 按键没反应或者反应迟钝按键没反应通常是因为Sleep()阻塞了输入检测。如果你在Sleep(100)期间按键程序不会立即响应要等Sleep结束才会处理。解决方法就是把Sleep拆成多个短延时每次短延时后检查一次按键。反应迟钝的另一个原因是方向过滤太严格。比如蛇正在向右移动你按了上键dir变成UP然后你马上按了左键但此时dir已经是UP了左键和UP不相反所以nextDir变成LEFT。但蛇还没移动dir还是RIGHT所以左键实际上被接受了。结果蛇会向左移动而蛇身还在右边蛇头就会撞到自己。解决方法是把方向过滤基于dir而不是nextDir并且只在蛇移动之前才把nextDir应用到dir。这样即使玩家快速按了多个方向也只有最后一个有效而且方向过滤是基于蛇的实际移动方向。5.3 食物生成在蛇身上或者生成在墙外食物生成在蛇身上通常是因为generateFood()函数没有正确检查蛇身。检查一下isOnSnake()函数的循环范围确保它遍历了所有蛇身节点。食物生成在墙外通常是因为随机数的范围搞错了。rand() % MAP_WIDTH得到的是0到MAP_WIDTH - 1正好是地图的x坐标范围。如果你写成了rand() % (MAP_WIDTH 1)就会得到0到MAP_WIDTH其中MAP_WIDTH是墙的位置食物就会生成在墙上。还有一个隐蔽的问题如果你在initGame()里调用generateFood()但此时snakeLength还没设置或者蛇的坐标还没初始化isOnSnake()可能会访问未初始化的内存导致随机行为。确保generateFood()在蛇初始化之后调用。5.4 蛇撞到自己时没有正确检测蛇撞自己的检测最容易忽略的就是蛇尾。如果蛇头移动到蛇尾的位置而蛇尾在这一帧会移走那么蛇头实际上不会撞到自己。所以检测循环应该从i 1到i snakeLength - 2不检查最后一个节点。但如果你在蛇移动之前检测蛇尾还没移走那么蛇头移动到蛇尾的位置就是撞到了。所以检测的时机也很重要。正确的顺序是先移动蛇身和蛇头再检测碰撞。这样蛇尾已经移走了蛇头移动到蛇尾的旧位置是安全的。还有一个特殊情况如果蛇的长度是2蛇头移动到蛇尾的位置实际上蛇头会取代蛇尾的位置蛇身变成1节。这种情况下不应该判定为撞自己。但贪吃蛇的初始长度通常是3而且吃食物只会变长所以长度2的情况不会出现。5.5 常见问题速查表问题现象可能原因解决方法画面闪烁严重每次渲染都清屏用gotoxy覆盖输出不清屏按键没反应Sleep阻塞了输入检测把Sleep拆成多个短延时蛇移动时缩成一团蛇身复制循环方向反了从尾部向头部复制食物生成在蛇身上isOnSnake检查不完整遍历所有蛇身节点食物生成在墙外随机数范围错误用rand() % MAP_WIDTH蛇撞自己检测不准忽略了蛇尾或检测时机不对移动后检测忽略蛇尾快速转向时撞死方向过滤基于nextDir方向过滤基于dir移动前应用nextDir控制台窗口太小地图尺寸超过窗口调整MAP_WIDTH和MAP_HEIGHT5.6 几个提升游戏体验的小技巧第一个技巧是加一个“暂停”功能。按空格键暂停游戏再按一次继续。实现起来很简单在游戏循环里加一个paused标志如果暂停了就不调用update()只处理输入。第二个技巧是加一个“难度递增”。随着分数增加蛇的移动速度逐渐加快。具体做法是动态调整Sleep的时间比如Sleep(100 - score / 50)分数越高Sleep越短蛇移动越快。但要注意设置一个下限比如最小20毫秒否则蛇会快到无法控制。第三个技巧是加一个“最高分记录”。把最高分保存到一个文件里每次游戏结束时更新。这样玩家就有了反复挑战的动力。文件操作可以用fstream很简单。第四个技巧是给蛇加一个“渐变颜色”。控制台的字符颜色可以通过SetConsoleTextAttribute()来设置。蛇头用亮绿色蛇身用暗绿色食物用红色边界用白色。这样画面会好看很多代码改动也不大。void setColor(int color) { SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color); } // 使用示例 setColor(10); // 绿色 cout O; setColor(7); // 恢复默认白色这些技巧都不复杂但能让你的贪吃蛇从“能跑”变成“好玩”。我个人的经验是先把核心逻辑写稳再考虑加这些锦上添花的功能。核心逻辑不稳加再多功能也是白搭。5.7 从控制台到图形界面的扩展思路如果你已经用C写完了控制台版贪吃蛇想进一步挑战图形界面版本有几个方向可以走。第一个方向是用Windows GDI。GDI是Windows自带的图形接口不需要额外安装库。你可以创建一个窗口用Rectangle()画蛇身用Ellipse()画食物用TextOut()显示分数。GDI的优点是零依赖缺点是API比较古老代码量会大很多。第二个方向是用EasyX。EasyX是一个针对C的图形库封装了GDI的常用功能用起来比原生GDI简单很多。画一个矩形只需要fillrectangle()画一个圆只需要fillcircle()。EasyX的缺点是只支持Windows而且需要额外安装。第三个方向是用SFML或者SDL。这两个都是跨平台的图形库功能强大文档丰富。SFML的API设计更现代SDL更底层更灵活。用这两个库的话你的贪吃蛇可以跑在Windows、Linux、macOS上甚至可以做手机版。但不管用哪个图形库游戏的核心逻辑——蛇的移动、碰撞检测、食物生成——都是一样的。控制台版写好了换到图形界面只是把cout换成画图函数把_getch()换成事件处理。所以控制台版是一个很好的起点它让你专注于逻辑本身而不是被图形API的细节分散注意力。我个人在实际操作中的体会是控制台版贪吃蛇虽然看起来简陋但它把游戏开发中最本质的东西——循环、状态、输入、渲染——都暴露出来了。你写完了控制台版再去看那些图形库的教程会发现很多东西都是相通的。而且控制台版的调试比图形版简单得多出了问题直接看输出就行不用去猜是逻辑错了还是渲染错了。所以如果你刚开始学C想找个项目练手贪吃蛇是一个很好的选择。它足够小小到你可以完全理解每一行代码又足够完整完整到你可以从中体会到游戏开发的基本流程。