对于实现贪吃蛇游戏的超详细保姆级解析—下 - 教程

news/2025/10/1 19:18:40/文章来源:https://www.cnblogs.com/yxysuanfa/p/19122711

开篇介绍:

hello 大家!

在《对于实现贪吃蛇游戏的超详细保姆级解析 — 上》中,我们全方位地剖析了贪吃蛇的核心玩法规则:深入讲解了蛇的移动逻辑,它并非单个节点的独立移动,而是整体联动,且存在不能反向掉头的限制;清晰阐述了蛇的生长机制,即通过吃食物来实现身体的变长;明确了游戏的失败条件,当蛇头碰撞到边界或者自身身体时,游戏就会结束;还介绍了食物的生成规则,要保证食物是随机出现的,并且不能与蛇身发生重叠。同时,我们系统梳理了实现游戏所需的各类技术工具与知识体系:对于 Win32 API,详细讲解了如何通过 system 命令设置控制台窗口大小与标题,利用 SetConsoleCursorPosition 精准控制光标位置来绘制界面,通过 SetConsoleCursorInfo 隐藏光标提升游戏体验,以及借助 GetAsyncKeyState 获取按键输入实现交互,这些构成了游戏界面与交互的技术基础;在 C 语言国际化特性方面,介绍了宽字符 wchar_t、宽字符串的使用方式(前缀 L、打印函数 wprintf 及对应的占位符 %lc 和 %ls),还有通过 setlocale(LC_ALL, "") 设置本地化环境,确保中文和特殊符号(如  等)能在控制台正常显示,让游戏界面更具表现力;在数据结构与类型设计上,确定采用链表来存储蛇身(每个节点记录坐标信息),并定义了包含蛇头指针、速度、方向、食物、分数、游戏状态等核心信息的 SNAKE 结构体,大大提升了代码的可维护性,同时,运用枚举类型 enum DIRCTION(表示蛇的上、下、左、右方向)和 enum GAMESTATUS(表示游戏正常、撞墙死亡、自身碰撞死亡等状态),替代 “魔法数字”,让代码逻辑更清晰、可读性更强。另外,还给出了游戏主逻辑的代码雏形,通过 test 函数控制游戏的启动→运行→结束→询问重玩的循环流程,main 函数负责初始化本地化环境并启动游戏,为后续具体功能的实现奠定了结构基础。

到了《对于实现贪吃蛇游戏的超详细保姆级解析 — 中》,我们则聚焦贪吃蛇核心功能的初始化代码实现,把之前梳理的技术点和逻辑思路,一步步转化为可运行的代码片段。首先看游戏的整体设计流程,在运行游戏的源文件中,test 函数通过 do - while 循环实现游戏的重复运行,内部定义蛇的结构体并初始化,依次调用 GameStart(游戏启动,初始化游戏状态、蛇的初始位置等)、GameRun(游戏主循环,处理输入、更新蛇的位置、判断碰撞等)、GameEnd(游戏结束,释放资源、显示结束信息等)函数,还会定位光标到指定位置,询问用户是否再来一局;main 函数通过 setlocale(LC_ALL, "") 设置本地化环境,支持中文等宽字符的正常显示,然后执行 test 函数启动游戏流程。接着详细解析了游戏开始函数 GameStart,它完成游戏的初始化任务,包括控制台窗口大小的设置、控制台窗口名字的设置、鼠标光标的隐藏、打印欢迎界面、创建地图、初始化蛇、创建第一个食物。其中,控制台窗口相关设置封装在 setcursorszieandvisible 函数中,通过 system 命令和 Win32 API 实现;打印欢迎界面及功能介绍由 welcomegame 函数完成,利用光标位置控制和命令行操作实现交互;地图创建则是根据设定的坐标范围,通过循环打印宽字符  来绘制上、下、左、右边界;蛇身初始化采用头插法创建包含 5 个节点的链表,保证每个节点 x 坐标为 2 的倍数,并遍历链表打印蛇身符号 ,同时初始化游戏状态、蛇移动速度、默认方向、初始成绩、每个食物的分数等信息;食物创建则通过随机数生成、循环判断和 goto 语句,确保食物 x 坐标为 2 的倍数、在墙体内部且不与蛇身重叠,然后创建食物节点并打印符号 ,关联到游戏信息结构体。

而在这一篇《对于实现贪吃蛇游戏的超详细保姆级解析 — 下》,我们将把研究重点放在游戏运行时的核心逻辑上。会如同拆解精密的机械装置一般,极其细致地剖析 “蛇的移动控制”,看看如何根据按键输入或者预设逻辑,让蛇按照上、下、左、右的方向规则移动,并且保证整体联动且不反向掉头;“碰撞检测” 也是关键部分,既要检测蛇头是否碰撞到地图的边界,又要检测是否碰撞到自身的身体,这涉及到坐标的判断与逻辑处理;还有 “食物获取与蛇身增长” 机制,当蛇头移动到食物位置时,如何让蛇身长度增加,同时重新生成新的食物;以及 “分数计算”,根据吃到食物的数量或者难度,如何准确计算并展示玩家的得分。通过对这些关键机制的深入讲解,让大家透彻了解贪吃蛇是如何真正 “活” 起来的,完整且清晰地呈现游戏的交互过程与玩法逻辑,助力大家彻底掌握贪吃蛇游戏从初始化设置到动态运行的全流程实现细节。

贪吃蛇游戏设计流程

我们再复习一下实现贪吃蛇游戏的流程图。

游戏运行函数的设计:

游戏运行函数(GameRun)是贪吃蛇游戏的核心逻辑载体,负责驱动游戏的动态运行,主要包含以下关键操作:

  • 打印帮助信息:在控制台右侧等合适区域,输出游戏操作指引(如方向键控制移动、加速减速按键、暂停 / 退出方式等),方便玩家了解玩法。
  • 显示得分与蛇身信息:实时打印当前玩家的得分(依据吃到食物的数量计算),以及蛇身的长度等状态信息,让玩家掌握游戏进度。
  • 获取按键输入:通过相关输入函数,持续监听玩家的按键操作(如方向键、功能键等),以此来控制蛇的移动方向、速度变化,或是触发暂停、退出等指令。
  • 蛇的移动处理(SnakeMove
    • 根据蛇头当前坐标和按键确定的方向,计算出蛇头下一个节点的坐标。
    • 判断下一个节点是否是食物:若是,执行 “吃食物” 逻辑(蛇身增长、得分增加、重新生成食物);若不是,蛇整体前移,尾部节点 “删除”(模拟蛇的移动轨迹)。
    • 碰撞检测:同时判断下一个节点是否碰撞到边界(HitWall)或自身蛇身(HitSelf),若发生碰撞,将游戏状态标记为 “结束”。
  • 循环检测游戏状态:持续重复上述 “获取输入 - 处理蛇移动 - 检测状态” 的流程,直到游戏状态变为 “结束”(碰撞边界 / 自身、玩家主动退出等),从而实现游戏的持续交互与运行。

这便是游戏运行函数所需要达到的操作,接下来就让我们来一一解析。

打印帮助信息:

这个就不用我多说了吧,非常简单的一个操作,我们直接调动光标位置,再进行打印即可,我们直接看代码:

//我们要在游戏页面右边打印帮助信息
void printhelpinfo()
{//打印提示信息setcursorpos(75, 10);wprintf(L"%ls",L"不能穿墙,不能咬到自己\n");setcursorpos(75, 11);wprintf(L"用↑.↓.←.→分别控制蛇的移动.");setcursorpos(75, 12);wprintf(L"F3 为加速,F4 为减速\n");setcursorpos(75, 13);wprintf(L"ESC :退出游戏.space:暂停游戏.");setcursorpos(75, 15);wprintf(L"ShineWinsu@版权");
}

就是这么简单。

获取按键情况以及表达食物分数和总分数:

关于如上标题的表达食物分数和总分数,也是和上面打印帮助信息一样简单的操作,所以大家后面看到完整代码就能理解,我在这里也就不过多叙述。

至于获取按键情况,在上的那一篇博客中,我便有所提及到,直接定义宏就行,即如下:

//获取按键按击情况
#define keypress(val) ((GetAsyncKeyState(val)&1)?1:0)//不要有分号

在这里,我们要知道,获取按键情况并进行对应反应的这一部分,是要在gamerun函数中,那么我们又知道,蛇的移动是无间断的,换句话来说就是蛇的移动要一直,除非it die。

再换句话说就是,我们要借助循环去让蛇移动,而这个循环是要在蛇还活着的时候进行,一旦蛇噶了,我们就得这个循环终止,并进行善后操作,那么说到这里,大家肯定也就知道是哪个循环了,没错,又是我们do while循环。

先运行一下,然后判断游戏状态是否为正常。

而在这边,我们还需要知道,我们所创建的这个贪吃蛇游戏有哪些按键有用处,而这些在我们上面的帮助信息中就有所提及,具体如下:

  • 方向键 (上)、(下)、(左)、(右):用于控制贪吃蛇的移动方向,玩家通过按下不同方向键,指挥蛇朝着对应方向行进。
  • F3 键:实现蛇的加速功能,按下该键后,蛇的移动速度会加快,增加游戏的节奏与挑战性。
  • F4 键:起到减速作用,按下后蛇的移动速度会变慢,便于玩家在需要更精细操作时控制蛇的行动。
  • ESC 键:用于退出游戏,当玩家按下此键时,会直接退出当前的贪吃蛇游戏进程。
  • space(空格键):具备暂停游戏的功能,按下空格键后,游戏会暂停运行,再次按下可恢复游戏。

所以,我们也就要对应的进行这些按键的相应判断,不过,我们要先知道这些按键的虚拟键码:

• 上: VK_UP
• 下: VK_DOWN
• 左: VK_LEFT
• 右: VK_RIGHT
• 空格: VK_SPACE
• ESC: VK_ESCAPE
• F3: VK_F3
• F4: VK_F4

所以,我们就可以知道了gamerun函数的一个主体了:

//游戏运行
void gamerun(SNAKE* snake)
{//我们要在游戏页面右边打印帮助信息printhelpinfo();do{//先在右侧打印得分情况setcursorpos(75, 18);printf("得分:%d ", snake->score);printf("每个食物得分:%d分", snake->foodscore);//接着判断用户按键按击情况来进行调整if (keypress(VK_UP) && snake->direction != DOWN)//不能在蛇向下运动的时候又往上{snake->direction = UP;}else if (keypress(VK_DOWN) && snake->direction != UP)//不能在蛇向上运动的时候又往下{snake->direction = DOWN;}else if (keypress(VK_LEFT) && snake->direction != RIGHT)//不能在蛇向右运动的时候又往左{snake->direction = LEFT;}else if (keypress(VK_RIGHT) && snake->direction != LEFT)//不能在蛇向左运动的时候又往右{snake->direction = RIGHT;}else if (keypress(VK_SPACE)){}else if (keypress(VK_ESCAPE)){}else if (keypress(VK_F3)){//进行加速,其实就是将蛇的休息时间减少if (snake->sleeptime > 80)//要有限制速度{}}else if (keypress(VK_F4)){//进行减速,其实就是将蛇的休息时间增多if (snake->foodscore > 0){}}} while (snake->status == NORMAL);//当游戏状态正常时,不断允许经游戏运行代码
}

接下来我们就得知道一下各个按键功能如何实现了

space(空格键):

space(空格键):具备暂停游戏的功能,按下空格键后,游戏会暂停运行,再次按下可恢复游戏,我们知道,空格键要达到如上所述的功能,那么具体要怎么实现呢?

其实不难,我们可以创建一个pasue函数,那我们又知道,暂停的本质其实就是蛇不动了,怎么按都不动,除非我们再按下空格,那大家有没有感觉这很像什么?对,又是循环。

只不过这次是死循环,只要用户不按空格,我们就让程序一直死循环,具体代码如下:

void pause(SNAKE* snake)//暂停函数
{while (1)//直接动用死循环{snake->sleeptime = 200;if (keypress(VK_SPACE))//直到再按下空格才恢复{break;}}
}

具体解释如下:

  1. 死循环实现暂停锁定的深层逻辑
    游戏正常运行时,主循环会按固定频率执行蛇的移动、碰撞检测、画面刷新等操作,形成动态画面。当调用暂停函数时,while(1) 构建的无限循环会 “拦截” 程序执行流 —— 此时主循环中的后续逻辑(如计算蛇的新位置、检测是否碰撞)被暂停函数的死循环阻断,无法继续执行。这种 “拦截” 本质上是通过代码执行顺序的强制停滞,让游戏的所有动态元素(蛇的移动、食物状态等)保持在暂停瞬间的状态,从玩家视角看,画面完全静止,从而实现 “暂停” 的直观效果。

  2. 暂停期间状态维持的细节考量
    蛇的移动间隔时间(sleeptime)直接决定游戏节奏,正常运行时可能因吃到食物等操作发生变化(如加速)。在暂停的死循环内将其强制设为固定值 200,有两层作用:

    • 防止暂停期间其他潜在逻辑(如误触的加速 / 减速按键)意外修改速度参数,确保暂停前后的速度状态一致;
    • 固定的间隔时间也为循环内的按键检测提供稳定的检测频率,避免因速度参数波动导致按键响应不及时或过于灵敏。
      这种设计保证了暂停状态的 “纯粹性”,不会因参数异常影响后续恢复后的游戏体验。
  3. 恢复条件判断的实时性保障
    死循环并非完全 “卡死” 程序,而是在循环内部持续执行 keypress(VK_SPACE) 检测 —— 这是一种非阻塞的按键查询方式,会实时判断空格键是否被按下。当玩家再次按下空格键时,检测结果为真,break 语句立即生效,退出死循环,程序执行流回到暂停函数调用后的位置,主循环得以继续运行,蛇的移动、碰撞检测等逻辑恢复执行,游戏从暂停瞬间的状态无缝衔接至正常运行,实现 “按空格恢复” 的流畅交互。

ESC 键:

实现这个按键的功能就简单多了,我们知道gamerun函数运行调用了do while循环,只有当游戏状态正常的时候,才能继续循环,所以我们系想要退出游戏,只需要在确定玩家按下ESC 键后,直接将游戏信息维护结构体snake中的游戏状态改变为ENDNORMEL即可,这样就会退出循环,也就是退出游戏,具体如下:

else if (keypress(VK_ESCAPE))  // 检测是否按下ESC键
{// 逻辑说明:// 1. 游戏主循环的持续条件是“游戏状态为正常”// 2. 当按下ESC键时,通过修改状态变量终止循环snake->status = ENDNORMAL;  // 将游戏状态设为“正常结束”break;  // 直接退出当前循环,终止游戏运行流程
}

F3 键、F4 键:

我们知道,按下F3 键要实现的功能是实现蛇的加速功能,按下该键后,蛇的移动速度会加快,同时增加食物分数。

那么我们又知道,其实对于蛇的速度,它的关键点就是在于蛇的休息时间,也就是游戏信息维护结构体snake中的sleeptime,为什么呢?因为在gamerun函数的循环中,每当按一次按键,我们都要调用蛇的移动的函数,调动完就Sleep(snake->sleeptime)一下,如此,我们也就可以实现蛇的速度的改变了。

知道了这个之后,我们对于按下F3键后要进行的操作也就知根知底了,那就是把snake->sleeptime减少,并把snake->foodscore增加,但是我们也要注意,不要让速度太快了,比如让snake->sleeptime减到负数去,具体如下:

else if (keypress(VK_F3))  // 检测是否按下F3键(加速功能)
{// 逻辑说明:// 1. 加速原理通过减少蛇的移动间隔时间(sleeptime)实现// 2. 为防止速度过快失控,设置最低间隔限制(80)if (snake->sleeptime > 80)  // 判断当前速度是否可加速(未达最快){snake->sleeptime -= 30;  // 减少休息时间,提升移动速度snake->foodscore += 2;   // 同步提高食物分值,平衡加速带来的难度提升}
}

而F4键与F3一样的道理,大家应该能够明白,我这里就直接上代码:

else if (keypress(VK_F4))  // 检测是否按下F4键(减速功能)
{// 逻辑说明:// 1. 减速原理通过增加蛇的移动间隔时间(sleeptime)实现// 2. 为防止分数异常,设置食物分值下限(>0)if (snake->foodscore > 0)  // 判断当前分数是否可降低(未达最低){snake->sleeptime += 30;  // 增加休息时间,降低移动速度snake->foodscore -= 2;   // 同步降低食物分值,平衡减速带来的难度降低}
}

(上)、(下)、(左)、(右):

这个也很简单,大家应该还记得游戏信息维护结构体snake中的direction,我们就要根据玩家按下的按键去修改游戏信息维护结构体snake中的direction。

只不过在这里大家还要知道一点,那就是:

  1. 不能在蛇向下运动的时候又往上
  2. 不能在蛇向上运动的时候又往下
  3. 不能在蛇向右运动的时候又往左
  4. 不能在蛇向左运动的时候又往右

如此,我们也就可以得到这四个方向键的代码了:

// 方向键控制逻辑:通过按键方向键改变蛇的移动方向,同时限制反向操作
if (keypress(VK_UP) && snake->direction != DOWN)  // 按下上方向键,且当前方向不是向下
{snake->direction = UP;  // 将移动方向改为向上
}
else if (keypress(VK_DOWN) && snake->direction != UP)  // 按下下方向键,且当前方向不是向上
{snake->direction = DOWN;  // 将移动方向改为向下
}
else if (keypress(VK_LEFT) && snake->direction != RIGHT)  // 按下左方向键,且当前方向不是向右
{snake->direction = LEFT;  // 将移动方向改为向左
}
else if (keypress(VK_RIGHT) && snake->direction != LEFT)  // 按下右方向键,且当前方向不是向左
{snake->direction = RIGHT;  // 将移动方向改为向右
}

完整代码:

void pause(SNAKE* snake)//暂停函数
{while (1)//直接动用死循环{snake->sleeptime = 200;if (keypress(VK_SPACE))//直到再按下空格才恢复{break;}}
}
//游戏运行
void gamerun(SNAKE* snake)
{//我们要在游戏页面右边打印帮助信息printhelpinfo();do{//先在右侧打印得分情况setcursorpos(75, 18);printf("得分:%d ", snake->score);printf("每个食物得分:%d分", snake->foodscore);//接着判断用户按键按击情况来进行调整if (keypress(VK_UP) && snake->direction != DOWN)//不能在蛇向下运动的时候又往上{snake->direction = UP;}else if (keypress(VK_DOWN) && snake->direction != UP)//不能在蛇向上运动的时候又往下{snake->direction = DOWN;}else if (keypress(VK_LEFT) && snake->direction != RIGHT)//不能在蛇向右运动的时候又往左{snake->direction = LEFT;}else if (keypress(VK_RIGHT) && snake->direction != LEFT)//不能在蛇向左运动的时候又往右{snake->direction = RIGHT;}else if (keypress(VK_SPACE)){//进行暂停,直到再次按下空格才恢复运行pause(snake);}else if (keypress(VK_ESCAPE)){//由于我们循环的条件便是游戏状态为正常,所以我们只要把状态改变//就不会进入循环snake->status = ENDNORMAL;break;//直接终止}else if (keypress(VK_F3)){//进行加速,其实就是将蛇的休息时间减少if (snake->sleeptime > 80)//要有限制速度{snake->sleeptime -= 30;snake->foodscore += 2;//速度快了,分数也得高起来}}else if (keypress(VK_F4)){//进行减速,其实就是将蛇的休息时间增多if (snake->foodscore > 0){snake->sleeptime += 30;snake->foodscore -= 2;//速度慢了,分数也得低起来}}//接着就要在循环中运行蛇移动的函数//按一次按键就要进行相应的改变snakemove(snake);//移动一次就休眠Sleep(snake->sleeptime);} while (snake->status == NORMAL);//当游戏状态正常时,不断允许经游戏运行代码
}

如此,便是gamerun函数的主体了,我们剩下的就是snakemove函数的操作了。

蛇的移动的函数:

在这里,我们要知道贪吃蛇移动的本质是什么:

贪吃蛇的移动逻辑看似简单,实则是由一系列精准的坐标运算和节点状态管理构成的动态系统,每一步都服务于 “连续运动” 和 “规则约束” 两个核心目标,具体可拆解为以下底层细节:

一、坐标系与运动单位的基础设定

在控制台环境中,贪吃蛇的移动依赖于字符坐标系统,每个节点(包括蛇头、蛇身、食物)都以 “字符位置” 为单位存在:

  • 坐标定义:通常以控制台左上角为原点(0,0),横向为 X 轴(向右递增),纵向为 Y 轴(向下递增)。
  • 移动单位:由于控制台中字符的宽高比约为2:1(横向占 2 个像素宽度,纵向占 1 个像素高度),为了让蛇的移动看起来更 “方正”,X 轴方向的移动步长设为 2(如向右移动时 X+2),Y 轴方向步长设为 1(如向上移动时 Y-1),确保视觉上的移动距离一致。

这种基础设定直接决定了蛇头位移的计算方式,是后续所有运动逻辑的前提。

二、蛇头位移的 “预判 - 执行” 机制

蛇头的移动是整个身体运动的起点,遵循 “先计算目标位置,再执行位移” 的流程:

  1. 方向映射:将枚举的方向(UP/DOWN/LEFT/RIGHT)转化为具体的坐标偏移量:
    • UP → (0, -1)(Y 轴减 1,向上移动)
    • DOWN → (0, +1)(Y 轴加 1,向下移动)
    • LEFT → (-2, 0)(X 轴减 2,向左移动)
    • RIGHT → (+2, 0)(X 轴加 2,向右移动)
  2. 目标坐标计算:用蛇头当前坐标(headX, headY)加上对应方向的偏移量,得到下一刻的目标坐标(newHeadX, newHeadY)
  3. 位移执行:将蛇头的坐标更新为目标坐标,完成 “主动位移”。

这个过程中,蛇头的移动完全独立于身体,仅由当前方向决定,是整个运动的 “第一推动力”。

三、蛇身节点的 “链式记忆 - 传递” 逻辑

蛇身的运动核心是 “后一个节点复制前一个节点的旧位置”,这种传递依赖于对每个节点 “移动前坐标” 的临时存储:

  1. 初始化临时变量:在移动开始前,先记录蛇头当前的坐标(oldHeadX, oldHeadY)—— 这是第一个身体节点的目标位置。
  2. 依次传递位置
    • 第一个身体节点:先记录自己当前的坐标(oldBody1X, oldBody1Y),再将位置更新为(oldHeadX, oldHeadY)
    • 第二个身体节点:先记录自己当前的坐标(oldBody2X, oldBody2Y),再将位置更新为(oldBody1X, oldBody1Y)
    • 以此类推,直到最后一个节点(蛇尾):记录自身旧坐标后,更新为前一个节点的旧坐标。
  3. 完成移动链:经过一轮传递,每个节点都 “跟随” 前一个节点的轨迹移动,形成连续的身体形态。

例如,初始状态为头(10,5)→身1(8,5)→身2(6,5)→尾(4,5)(向右方向):

  • 蛇头先移动到(12,5),临时记录旧头位置(10,5)
  • 身 1 移动到(10,5),临时记录自身旧位置(8,5)
  • 身 2 移动到(8,5),临时记录自身旧位置(6,5)
  • 尾移动到(6,5),完成移动后整体为头(12,5)→身1(10,5)→身2(8,5)→尾(6,5)

这种 “先记录旧位置,再更新新位置” 的机制,确保了每个节点的移动不会相互干扰,是蛇身连贯性的核心保障。

四、“吃食物” 时的特殊处理:长度增长的底层逻辑

当蛇头的目标坐标与食物坐标重合时,移动逻辑会触发 “长度增长” 分支,核心是 “保留蛇尾的旧位置”:

  1. 正常移动(未吃食物):蛇尾在完成位置更新后,其旧位置会被 “清空”(不再绘制),整体长度不变。
  2. 吃到食物时
    • 蛇头照常移动到食物位置,食物被 “消耗”(需要重新生成)。
    • 蛇身节点依然按 “链式传递” 更新位置,但蛇尾的旧位置不被清空,而是作为新增的节点保留。
    • 新增节点的坐标就是蛇尾移动前的旧坐标,通过在链表末尾插入新节点实现长度 + 1。

例如,上述例子中若蛇头(12,5)刚好碰到食物:

  • 身 1、身 2、尾依然按原逻辑移动到(10,5)(8,5)(6,5)
  • 尾的旧位置(4,5)被保留,作为新增的尾节点,整体变为头(12,5)→身1(10,5)→身2(8,5)→尾(6,5)→新尾(4,5),长度从 4 变为 5。

这种设计通过 “是否清空尾节点旧位置” 的简单判断,高效实现了 “吃食物变长” 的规则,避免了复杂的长度计算。

五、移动与画面刷新的同步机制

移动逻辑还需要与画面刷新配合,才能让玩家看到连续的动画效果:

  1. 擦除旧位置:在更新每个节点的坐标前,先在其旧位置绘制 “空白字符”(覆盖原有蛇身符号)。
  2. 绘制新位置:在节点移动到新坐标后,重新绘制蛇头 / 蛇身符号(如表示蛇头,表示蛇身)。
  3. 控制刷新频率:通过sleeptime(移动间隔时间)控制每次移动的时间间隔,间隔越短,移动速度越快,画面越流畅。

这种 “先擦除再绘制” 的机制,确保了画面不会残留旧节点的痕迹,让移动看起来连续且自然。

总结:移动本质的逻辑闭环

贪吃蛇的移动是 **“坐标运算(蛇头)→ 位置传递(蛇身)→ 状态判断(是否吃食物)→ 画面同步(刷新)”** 的完整闭环:

  • 蛇头的主动位移提供运动源动力;
  • 蛇身的链式传递保证形态连贯性;
  • 食物检测决定长度是否增长;
  • 画面刷新让运动可视化。

这四个环节相互配合,最终形成了玩家眼中 “蛇身连续移动、吃食物变长” 的经典效果,每个细节都服务于 “模拟生物运动” 的核心目标。

大家对于上述要好好理解,这是关键

所以实现蛇的移动的关键就在于我们要找到当蛇移动后,蛇头会碰到的下一个坐标(也算是节点),那么根据这幅图:

我们知道,蛇头将要到的下一个节点是:

  • UP → (0, -1)(Y 轴减 1,向上移动)
  • DOWN → (0, +1)(Y 轴加 1,向下移动)
  • LEFT → (-2, 0)(X 轴减 2,向左移动)
  • RIGHT → (+2, 0)(X 轴加 2,向右移动)

而在进行判断之前,我们依然要创建一个蛇身节点用来存放蛇头将要到的下一个节点,然后我们就要分情况进行讨论,当蛇的方向是UP时怎么怎么样,是DOWN的时候怎么怎么样,而这些方向也同样是在我们存储游戏信息的结构体snake中,那么针对这些情况,我们就需要switch语句去进行分别计算蛇头在某个方向下碰到的下一个节点的坐标,这个大家应该也不陌生,详细如下:

// 计算蛇头移动后的下一个位置
// 本质是创建一个临时节点,用于存储蛇头即将到达的坐标
snakenode* pnext = (snakenode*)malloc(sizeof(snakenode));
// 根据当前蛇的移动方向,计算下一个节点(蛇头新位置)的坐标
switch (snake->direction)
{case UP:  // 向上移动时pnext->x = snake->pheadsnake->x;  // X坐标不变pnext->y = snake->pheadsnake->y - 1;  // Y坐标减1(向上偏移1个单位)break;case DOWN:  // 向下移动时pnext->x = snake->pheadsnake->x;  // X坐标不变pnext->y = snake->pheadsnake->y + 1;  // Y坐标加1(向下偏移1个单位)break;case LEFT:  // 向左移动时pnext->x = snake->pheadsnake->x - 2;  // X坐标减2(向左偏移1个字符宽度)pnext->y = snake->pheadsnake->y;  // Y坐标不变break;case RIGHT:  // 向右移动时pnext->x = snake->pheadsnake->x + 2;  // X坐标加2(向右偏移1个字符宽度)pnext->y = snake->pheadsnake->y;  // Y坐标不变break;
}

如此之后,我们便存储了,可以看到,我们的坐标都是通过解引用snake结构体中的蛇头指针,然后去确定的,这也说明了snake的重要性以及蛇头指针和头插的必要性。

知道了蛇头要到的下一个节点之后,我们就要进行判断是不是吃到食物了,如过吃到了食物,就要运行吃到食物的函数,没吃到的话,就要运行没吃到食物的函数。

那么我们要怎么判断是否吃到了食物呢?很简单,就看我们食物的节点(要记得我们生成了食物之后,可是把食物这个节点(包括坐标)传到snake结构体中)的坐标与蛇头要碰到的下一个节点坐标是否一样,一样就代表吃到了食物,具体如下:

//检测贪吃蛇移动前方一个格子是否为食物
int checkfood(snakenode* pnext, SNAKE* snake)
{// 需确保蛇头下一个位置(pnext)与食物位置(snake->food)完全匹配return (snake->food->x == pnext->x && snake->food->y == pnext->y);
}

然后最后蛇的移动的过程中,我们还要进行是否撞到墙和是否撞到自己身体的函数,毕竟在蛇的移动的函数中,我们除了要判断是否吃到食物,肯定还要判断是否撞到了什么。

所以我们也就可以得到蛇的移动的总代码了:

//运行蛇移动的函数
void snakemove(SNAKE* snake)
{//我们想要让蛇移动,就得先知道蛇按用户指定方向移动后蛇头碰到的下一个节点//其本质也算是一个节点,所以也可以用蛇身结构体snakenode* pnext = (snakenode*)malloc(sizeof(snakenode));//使用switch循环,去判断此时蛇是什么状态//其实也就是判断要往哪个方向进行移动switch (snake->direction){//得先知道蛇按用户指定方向移动后蛇头碰到的下一个节点case UP:pnext->x = snake->pheadsnake->x;pnext->y = snake->pheadsnake->y - 1;break;case DOWN:pnext->x = snake->pheadsnake->x;pnext->y = snake->pheadsnake->y + 1;break;case LEFT:pnext->x = snake->pheadsnake->x - 2;pnext->y = snake->pheadsnake->y;break;case RIGHT:pnext->x = snake->pheadsnake->x + 2;pnext->y = snake->pheadsnake->y;break;}//其实贪吃蛇移动的原理便是贪吃蛇蛇身这个链表会将蛇头往前移动碰到的格子//其实这个格子也算是一个节点,会把这个格子加入到贪吃蛇蛇身的链表中//如果吃到了食物,就把这个格子(节点)头插进蛇身链表中//然后我们再去把这个蛇身链表进行遍历打印//这边需要注意,我们的创建蛇身时打印的蛇身只是最开始的情况//后面玩家控制是要不断更新不断打印的//所以我们会用到do while循环//如果没吃到食物,但是为了有移动的感觉//也会把这个格子(节点)加入到贪吃蛇蛇身的链表中//但是会相应的去将加入到贪吃蛇蛇身的链表中的最后一个节点删除掉//就是要让最后一个节点去打印空格,毕竟我们地图就是空的//然后我们再去把这个蛇身链表进行遍历打印//接着我们要判断蛇是否吃到了食物if (checkfood(pnext, snake))//吃到了食物{//运行吃到食物的函数//我们要把蛇头碰到的下一个格子(节点)也传进去eatfood(pnext, snake);}else//没吃到食物{//运行没吃到食物的函数//我们要把蛇头碰到的下一个格子(节点)也传进去nofood(pnext, snake);}//接下来我们还要进行碰墙判断以及撞自己判断//碰墙判断//本质上要看蛇头是否有到墙killbywall(snake);//撞自己判断//本质上要看蛇头有没有碰到自身节点中的数据killbyself(snake);
}

接下来我们就要对吃到食物的函数、没吃到食物的函数以及是否撞到墙和是否撞到自己身体的函数的分析。

吃到食物的函数:

我们经过上文已经知道了蛇的移动的本质(贪吃蛇移动的核心逻辑:蛇头向前移动时,会将碰到的格子(此处为食物所在位置)作为新节点加入蛇身链表,吃到食物时,这个新节点会被保留,实现蛇身长度增加),那么对于蛇如果吃到了食物,就把这个格子(节点)(其实也算是蛇头要碰到的下一个坐标(节点))头插进蛇身链表中,然后我们再去把这个蛇身链表进行遍历打印,这边注意依旧是要头插哦,所以步骤也是很简单,即如下:

  1. 准备新节点:为蛇头增长创建载体
    先动态分配一个新的蛇身节点(snakenext),用于存储蛇头即将移动到的位置(即食物所在坐标)。这一步的作用是为蛇身增长提供 “物理实体”,确保新增的长度有对应的节点记录坐标。若内存分配失败(snakenext == NULL),则通过错误提示终止程序,避免后续操作出错。

  2. 复制坐标:关联新节点与食物位置
    将预判的蛇头下一个位置(pnextxy坐标,也就是食物所在坐标)复制到新节点snakenext中。这一步让新节点精准对应食物的位置,保证蛇头 “正好吃到食物” 的视觉效果。

  3. 头插链表:实现蛇身增长
    通过 “头插法” 将新节点snakenext加入蛇身链表 —— 让新节点的next指针指向原蛇头(snake->pheadsnake),再将蛇头指针更新为新节点。此时新节点成为新的蛇头,原蛇头变为第一个身体节点,蛇身长度增加 1,完成 “吃食物变长” 的核心逻辑。

  4. 重新绘制:同步更新视觉效果
    遍历整个蛇身链表(从新蛇头开始),通过setcursorpos移动光标到每个节点的坐标,并用wprintf打印蛇身符号()。这一步是为了让玩家看到蛇身增长后的新形态,确保视觉效果与数据结构的变化同步,避免画面残留旧蛇身。

  5. 更新分数:反馈游戏进度
    将当前食物的分值(snake->foodscore)累加到总分(snake->score)中。这一步将 “吃到食物” 的操作转化为分数奖励,符合游戏 “积累分数” 的核心目标,同时为后续可能的难度调整(如分数越高速度越快)提供数据基础。

  6. 清理旧食物:释放资源
    释放原食物节点(snake->food)的内存并将指针置空,避免内存泄漏。这是因为原食物已被 “吃掉”,其数据不再需要,必须及时清理以保证程序的内存管理规范。

  7. 生成新食物:维持游戏循环
    调用createfood函数重新生成一个新的食物(确保位置在地图内且不与蛇身重叠)。这一步保证游戏能持续进行 —— 新食物的出现为玩家提供了下一个目标,维持 “移动 - 吃食物 - 增长” 的循环机制。

这里大家可一定要记得再次生成新事食物,毕竟我们之前的生成食物的函数是只生成一次的,要不断调用才不断生成,以下是完整代码:

// 处理贪吃蛇吃到食物的函数
// 参数:
//   pnext:蛇头即将移动到的位置(即食物所在坐标的节点)
//   snake:指向蛇整体数据结构的指针,包含蛇身链表、分数、食物信息等
void eatfood(snakenode* pnext, SNAKE* snake)
{// 贪吃蛇移动的核心逻辑:// 蛇头向前移动时,会将碰到的格子(此处为食物所在位置)作为新节点加入蛇身链表// 吃到食物时,这个新节点会被保留,实现蛇身长度增加// 1. 创建一个新的蛇身节点,用于存储食物所在位置(蛇头即将到达的位置)snakenode* snakenext = (snakenode*)malloc(sizeof(snakenode));// 检查内存分配是否成功if (snakenext == NULL){perror("why malloc false:");  // 输出内存分配失败的错误信息exit(1);  // 终止程序,避免后续使用空指针导致崩溃}// 2. 将食物所在坐标(蛇头下一个位置)复制到新节点中// 确保新节点与食物位置完全重合,实现"吃到食物"的坐标匹配snakenext->x = pnext->x;  // 复制X坐标snakenext->y = pnext->y;  // 复制Y坐标// 3. 通过头插法将新节点加入蛇身链表,实现蛇身增长// 头插法步骤://   a. 新节点的next指针指向原蛇头(连接原有蛇身)//   b. 将蛇的头指针更新为新节点(新节点成为新蛇头)snakenext->next = snake->pheadsnake;snake->pheadsnake = snakenext;// 4. 重新绘制整个蛇身,同步视觉效果// 遍历更新后的蛇身链表,在每个节点的坐标位置重新绘制蛇身符号snakenode* current = snake->pheadsnake;  // 从新蛇头开始遍历while (current != NULL)  // 遍历到链表尾部(NULL)时结束{setcursorpos(current->x, current->y);  // 移动光标到当前节点坐标wprintf(L"%lc", L'●');  // 打印蛇身符号(●),覆盖原食物符号current = current->next;  // 移动到下一个节点}// 5. 更新总分:将当前食物的分值累加到总分中// 食物分值(foodscore)可能随游戏难度/速度变化,体现"难度越高奖励越多"的设计snake->score += snake->foodscore;// 6. 清理旧食物资源,避免内存泄漏free(snake->food);  // 释放原食物节点占用的内存snake->food = NULL;  // 将食物指针置空,防止野指针错误// 7. 生成新的食物,维持游戏循环// createfood函数会在地图内随机生成新食物,且确保不与蛇身重叠createfood(snake);
}

没吃到食物的函数:

其实这个函数反而要比吃到食物的函数复杂,我们知道:贪吃蛇移动的核心逻辑:蛇头向前移动时,会将碰到的格子作为新节点加入蛇身链表,吃到食物时,这个新节点会被保留,实现蛇身长度增加,如果没吃到食物,依然要将这个节点加入并进行打印,只不过原本蛇身链表的最后一个节点就要删除了,也就是要蛇身的蛇尾进行删除,由原本的打印身体变为打印空白(两个空格哦(宽字符))。

所以,我们也就能知道没吃到食物函数的大致实现方式了,依旧是把蛇头要到的下一个坐标(节点)头插进蛇身链表,当作贪吃蛇的新蛇头。

之后呢,我们就要把从这个新的头结点到原链表的倒二个节点都进行打印蛇身,这里要知道这个节点的特征就是它的next的next是为NULL,这也是我们遍历新链表的循环终止条件。

接着,我们就要把原蛇身链表的最后一个节点给它free掉,只不过在free之前,我们要记得在那个节点所在的坐标打印上空白,那么这是为什么呢?

原因如下:

  1. 消除视觉残留,模拟 “移动离开” 效果
    蛇身节点在画面上以特定符号(如)显示,尾节点的坐标原本属于蛇身的一部分。当蛇移动时,尾节点会离开原位置 —— 若不打印空白,原位置的会残留,导致画面上出现 “蛇尾未移动” 的残影,破坏 “整体移动” 的视觉连贯性。打印空白(宽字符空格)能精准覆盖原尾节点的符号,让玩家直观感受到蛇 “离开了” 这个位置。

  2. 与蛇身增长逻辑形成对称,保证画面一致性
    吃到食物时,新节点加入后会通过遍历打印新蛇身(包含新增节点);未吃到食物时,虽然尾节点被删除,但新节点已作为新蛇头加入 —— 此时必须通过 “擦除尾节点 + 打印新蛇身” 的组合操作,与 “增长逻辑” 形成视觉上的对称。若只删除尾节点而不擦除,画面上蛇的长度会看起来没有变化(实际已移动),造成 “数据长度不变但视觉长度多一节” 的矛盾。

  3. 符合控制台绘图的底层特性
    控制台环境中,字符一旦绘制就会一直存在,直到被新字符覆盖。蛇的移动本质是 “旧位置擦除 + 新位置绘制” 的循环,尾节点的原位置不再属于蛇身,必须主动用空白覆盖,否则会保留在画面中。这是控制台绘图的 “惰性更新” 特性决定的 —— 程序必须显式处理 “删除” 的视觉表现,而非自动清除。

这里大家这一步可一定要牢记,是重中之重,演示图就不给大家看了,大家有兴趣可以自己尝试一下。

而且呢,我们还要记得在把原链表的倒二个节点的next指针置为NULL,不难可能会发生空指针访问(即被free的那个节点被访问)。

那么,经过上述,我们也就可以知道完整代码了:

//没吃到食物的函数
void nofood(snakenode* pnext, SNAKE* snake)
{//如果没吃到食物,但是为了有移动的感觉//也会把这个格子(节点)加入到贪吃蛇蛇身的链表中//但是会相应的去将加入到贪吃蛇蛇身的链表中的最后一个节点删除掉//就是要让最后一个节点去打印空格,毕竟我们地图就是空的//然后我们再去把这个蛇身链表进行遍历打印//先创建蛇身节点snakenode* snakenext = (snakenode*)malloc(sizeof(snakenode));if (snakenext == NULL){perror("why malloc false:");exit(1);}snakenext->x = pnext->x;snakenext->y = pnext->y;//接着将这个格子(节点)头插进蛇身链表中snakenext->next = snake->pheadsnake;snake->pheadsnake = snakenext;//然后我们要找到蛇身链表的倒二个节点//我们要将新的头节点到倒二个节点的蛇身都打印出来//这个节点的特征就是它的next的next是为NULLsnakenode* temp = snake->pheadsnake;while (temp->next->next != NULL){//移动光标setcursorpos(temp->x, temp->y);wprintf(L"%lc", L'●');temp = temp->next;}//出来循环之后,temp就变为蛇身链表的倒二个节点snakenode* last = temp->next;//last即为蛇身链表的最后一个节点//我们要对原链表的最后一个节点的位置打印空格setcursorpos(last->x, last->y);printf("  ");//打印两个空格//然后再把这个节点释放掉free(last);last = NULL;//但是我们也要记得去将链表的倒二个节点的next指针置为空,//避免链表进行最后一个节点的访问temp->next = NULL;
}

so so easy。

判断是否撞到墙的函数:

这个函数相对于上面两个函数,可就又简单了很多,判断逻辑也很简单,我们只需要判断蛇头(是蛇头,不是蛇头的下一个节点哦),它的坐标是不是和墙的坐标一样即可(x,y都要和墙对比哦,有一个符合,就要die哦),很简单。

上:(0,0)到(56,0)
下:(0,26)到(56,26)
左:(0,1)到(0,25)
右:(56,1)到(56,25)

具体代码如下:

//碰墙判断
int killbywall(SNAKE* snake)
{//上:(0, 0)到(56, 0)//下:(0, 26)到(56, 26)//左:(0, 1)到(0, 25)//右:(56, 1)到(56, 25)//本质上要看蛇头是否有到墙if (snake->pheadsnake->x == 0 || snake->pheadsnake->y == 0 || snake->pheadsnake->x == 56 || snake->pheadsnake->y == 26){snake->status = DIEBYWALL;//改变游戏状态,退出gamerun的do while循环return 1;}return 0;
}

判断是否撞到自己身体的函数:

这一个函数,稍微复杂一丢丢,但是也不难,却也有一个小细节需要注意。

我们这个函数的主要思路就是遍历蛇身链表,看看蛇头的坐标有没有和蛇身的坐标一模一样的,有就代表撞到自己了。

但是细节就是,我们的遍历要从蛇身链表的头节点的下一个节点开始,不难从头结点就开始的话,很明显就会判断蛇头的坐标有没有和蛇身的坐标一模一样。这个点大家要注意。

当判断为撞到自己了,就改变游戏状态,下面是完整代码:

//撞自己判断
int killbyself(SNAKE* snake)
{//这边要注意,我们要从蛇头的下一个节点进行判断//不难在下面循环中,会直接gameoversnakenode* current = snake->pheadsnake->next;//本质上要看蛇头有没有碰到自身节点中的数据while (current != NULL){if (snake->pheadsnake->x == current->x && snake->pheadsnake->y == current->y){snake->status = DIEBYSELF;//改变游戏状态,退出gamerun的do while循环return 1;}current = current->next;}return 0;
}

游戏结束代码:

恭喜大家,我们终于到了贪吃蛇游戏的最后一个函数了,就是游戏结束函数,因为我们肯定要进行游戏的善后工作,比如告诉玩家游戏因什么而结束,然后把我们的蛇身链表全部释放掉,避免内存泄露。

告诉玩家游戏因什么而结束就通过我们存储在snake结构体中的游戏状态来进行判断,毕竟我们是有进行相应的改变的。

而释放节点,也是我们老生常谈的一个东西了,这里我就不再多余讲述。

完整代码如下:

//游戏结束
void gameend(SNAKE* snake)
{//在游戏结束的环节中,我们要判断用户因什么而结束游戏//并进行表达setcursorpos(24, 12);switch (snake->status){case ENDNORMAL:printf("您主动退出游戏\n");break;case DIEBYSELF:printf("您撞上自己了 ,游戏结束!\n");break;case DIEBYWALL:printf("您撞墙了,游戏结束!\n");break;}//然后我们要将蛇身链表的节点都释放掉snakenode* current= snake->pheadsnake;while (current){//保存节点,不难直接释放current会导致程序出错snakenode* temp = current;current = current->next;//我们释放临时变量free(temp);}
}

到此,我们对贪吃蛇游戏的各个功能、各个函数就都分析了,下面就给出贪吃蛇游戏的完整代码,大家可以进行参考,写作不易,望大家多多支持,感谢诸位!!!

贪吃蛇游戏的头文件:

#pragma once
#define posx 24
#define posy 5
//创建贪吃蛇的蛇身
//经过观察,其实不难发现,贪吃蛇本身就是一个链表
//所以我们要先在头文件中,创建贪吃蛇蛇身
struct snakenode
{//注意:蛇的每个节点的x坐标必须是2个倍数,//否则可能会出现蛇的一个节点有一半儿出现在墙体中,//另外一般在墙外的现象,坐标不好对齐int x;int y;//以上是蛇身的坐标struct snakenode* next;
};
//将结构体重命名
typedef struct snakenode snakenode;
//贪吃蛇的信息大概为这些:
//1.找到贪吃蛇的头
//2.蛇的速度
//3.蛇的方向
//4.食物
//5.食物的分数
//6.总分数
//7.贪吃蛇(游戏)的状态
//对于上面这些信息,我们一个一个创建变量显然太麻烦
//为了利于我们的维护,我们可以把这些变量全部丢在一个结构体中,
//便于我们维护这些信息
//贪吃蛇
//对于方向,一共就4个方向,所以我们为了便利维护这些方向
//我们直接将它们列举出来
enum DIRECTION
{UP=1,//要用逗号分隔DOWN,RIGHT,LEFT
};
//同样,对于游戏的状态,也有四种
//正常 撞墙结束 撞到自己结束 正常结束
//我们同样可以使用枚举
enum GAMESTATUS
{NORMAL,DIEBYWALL,DIEBYSELF,ENDNORMAL
};
struct snake
{//贪吃蛇的信息大概为这些://1.找到贪吃蛇的头//2.蛇的速度//3.蛇的方向//4.食物//5.食物的分数//6.总分数//7.贪吃蛇(游戏)的状态snakenode* pheadsnake;//表示贪吃蛇的蛇头snakenode* food;//指向食物的指针,因为食物其实也是个坐标//所以为了方便,我们也可以借用贪吃蛇蛇头的结构体int sleeptime;//对于贪吃蛇而言,其休息的时间越短,那就代表其的速度越快//这就需要我们的Sleep函数int foodscore;//食物的分数,也是整型int score;//总分数,依旧是整型enum DIRECTION direction;//方向enum GAMESTATUS status;//游戏状态
};
typedef struct snake SNAKE;
//初始化游戏
//0.先设置窗口的大小,再光标隐藏
//1.打印欢迎界面
//2.功能介绍
//3.绘制地图
//4.创建蛇
//5.创建食物
//6.设置游戏的相关信息,而这个就在我们上面创建的SNAKE snake = { 0 };
//又由于要传址调用,所以我们要传地址
void gamestart(SNAKE* snake);
//修改指定窗口的大小以及光标的大小以及可见性的函数
void setcursorszieandvisible();
//修改指定窗口光标所在的位置的函数
void setcursorpos(short int x, short int y);
//1.打印欢迎界面
//2.功能介绍
void welcomegame();
//3.绘制地图
void createmap();
//4.创建蛇
//因为我们要创建蛇,那么也就肯定需要用到我们的蛇的信息结构体
void initsnake(SNAKE* snake);
//5.创建食物
void createfood(SNAKE* snake);
//获取按键按击情况
#define keypress(val) ((GetAsyncKeyState(val)&1)?1:0)//不要有分号
//游戏运行
void gamerun(SNAKE* snake);
//我们要在游戏页面右边打印帮助信息
void printhelpinfo();
//运行蛇移动的函数
void snakemove(SNAKE* snake);
//检测贪吃蛇移动前方一个格子是否为食物
int checkfood(snakenode* pnext, SNAKE* snake);
//吃到食物的函数
void eatfood(snakenode* pnext, SNAKE* snake);
//没吃到食物的函数
void nofood(snakenode* pnext, SNAKE* snake);
//碰墙判断
//本质上要看蛇头是否有到墙
int killbywall(SNAKE* snake);
//撞自己判断
//本质上要看蛇头有没有碰到自身节点中的数据
int killbyself(SNAKE* snake);
//游戏结束
void gameend(SNAKE* snake);
//贪吃蛇移动的核心逻辑:蛇头向前移动时,会将碰到的格子作为新节点加入蛇身链表,
//吃到食物时,这个新节点会被保留,实现蛇身长度增加,
//如果没吃到食物,依然要将这个节点加入并进行打印,
//只不过原本蛇身链表的最后一个节点就要删除了,
//也就是要蛇身的蛇尾进行删除,
//由原本的打印身体变为打印空白(两个空格哦(宽字符))
//要牢记,其实贪吃蛇到的下一个坐标,就算是一个节点
//我们要把坐标当作节点,节点当作坐标
//可以看图分析

贪吃蛇游戏的源文件:

#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "snake.h"
//修改指定窗口的大小以及光标的大小以及可见性的函数
void setcursorszieandvisible()
{//修改窗口大小system("mode con cols=100 lines=30");//修改名字system("title 贪吃蛇");//我们要获取是哪个窗口HANDLE houtput = NULL;//是指针类型,可以直接初始化为NULLhoutput = GetStdHandle(STD_OUTPUT_HANDLE);//知道了是哪个窗口之后,我们就可以先去获取指定窗口的光标信息//而这就需要CONSOLE_CURSOR_INFO结构体去储存定窗口的光标信息CONSOLE_CURSOR_INFO cursorinfo = { 0 };//同样创建该结构体变量//再借用GetConsoleCursorInfo函数去知道指定窗口光标的信息GetConsoleCursorInfo(houtput, &cursorinfo);//接下来我们就可以设置指定窗口光标的信息了cursorinfo.bVisible = false;//使用false记得要包含stdbool头文件哦//再借用SetConsoleCursorInfo函数修改即可SetConsoleCursorInfo(houtput, &cursorinfo);
}
//修改指定窗口光标所在的位置的函数
void setcursorpos(short int x,short int y)
{COORD position = { x,y };//我们要获取是哪个窗口HANDLE houtput = NULL;//是指针类型,可以直接初始化为NULLhoutput = GetStdHandle(STD_OUTPUT_HANDLE);//接下来我们就可以借助SetConsoleCursorPosition函数去修改指定窗口的光标位置了SetConsoleCursorPosition(houtput, position);
}
//1.打印欢迎界面
//2.功能介绍
void  welcomegame()
{//欢迎界面setcursorpos(50, 14);wprintf(L"欢迎来到贪吃蛇小游戏~");setcursorpos(52, 20);system("pause");//会显示请按任意键继续//接下来就要打印一些操作指导system("cls");//先清除上一个打印setcursorpos(35, 14);wprintf(L"用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");setcursorpos(52, 20);system("pause");//会显示请按任意键继续system("cls");//记得清空
}
//3.绘制地图
void createmap()
{//我们要以□为地图边界,且要打印上下左右//易错点:就是坐标的计算//上:(0, 0)到(56, 0)//下:(0, 26)到(56, 26)//左:(0, 1)到(0, 25)//右:(56, 1)到(56, 25)//上://因为在上面我们动过光标位置了,所以在这里我们要重新定位光标位置setcursorpos(0, 0);//因为我们要打印多个,所以要借助循环来进行//“□” 是宽字符,在控制台中占 2 列宽度(普通字符占 1 列),//因此横向绘制时,循环步长设为 i += 2,确保边界连续不重叠。//上 / 下边界的横向范围是 0~56(列),共 56 / 2 + 1 = 29 个 “□”,//刚好填满横向空间。for (int i = 0; i < 58; i += 2)//横向绘制时,循环步长设为 i += 2,确保边界连续不重叠。{wprintf(L"%lc ",L'□');//为了对齐,我们在打印一个符号后就打印一个空格}//下://对最下列的打印,我们就得再次移动光标位置setcursorpos(0, 26);for (int i = 0; i <58; i += 2){wprintf(L"%lc ", L'□');}//左://这个时候我们就要打印竖着的墙了//不能连续横着打印//所以我们就得借助光标移动去打印for (int i = 1; i <= 25; i++)//因为是移动光标,所以不用i+2{setcursorpos(0, i);wprintf(L"%lc", L'□');}//右://与左边边界的打印类似for (int i = 1; i <= 25; i++)//因为是移动光标,所以不用i+2{setcursorpos(56, i);wprintf(L"%lc", L'□');}
}
//4.创建蛇
//因为我们要创建蛇,那么也就肯定需要用到我们的蛇的信息结构体
void initsnake(SNAKE* snake)
{//我们要创建蛇身,那么就需要蛇身结构体//所以我们要先创建蛇身结构体变量snakenode* current = NULL;//蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。//创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,//将蛇的每一节打印在屏幕上。//• 蛇的初始位置从(24,5)开始//每后一节都要横坐标加2//所以我们要借助循环去创建链表,并不断头插for (int i = 0; i < 5; i++){current = (snakenode*)malloc(sizeof(snakenode));if (current == NULL){perror("申请空间失败原因:");exit(1);}//初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,//比如(24, 5)处开始出现蛇,连续5个节点。//注意:蛇的每个节点的x坐标必须是2个倍数,//否则可能会出现蛇的一个节点有一半儿出现在墙体中,//另外一半在墙外的现象,坐标不好对齐。//关于食物,就是在墙体内随机生成一个坐标(x坐标必须是2的倍数),//坐标不能和蛇的身体重合,然后打印★。//由于我们老是写24、5太麻烦,所以我们直接将其定义为posx和posy//在头文件中定义哦current->x = posx + i * 2;current->y = posy;current->next = NULL;//接下来我们就要进行头插://在贪吃蛇初始化中选择头插法创建链表(蛇身),核心原因与蛇的移动逻辑和数据结构特性直接相关://1. 符合蛇的移动规律:新节点永远在头部生成//蛇的移动逻辑是:头部先向目标方向移动一格,后续身体节段依次跟随前一节的位置。//当蛇初始创建时,我们需要让第一节(头部)位于最前端,后续节段依次排列在后方(例如初始位置(24, 5)为头部,第二节(26, 5)、第三节(28, 5)…… 依次向后)。//头插法的特性是:每次新节点都插入到链表头部。//第一次循环:创建第一个节点(头部)(24, 5),链表为[头(24, 5)]//第二次循环:创建第二个节点(26, 5),插入到头部前方,链表变为[节点2(26, 5) → 头(24, 5)]//直到第五次循环后,链表实际顺序为[节点5(32, 5) → 节点4(30, 5) → ... → 头(24, 5)]//此时链表的首节点(phead)是蛇的尾部,尾节点是蛇的头部,恰好符合 “头部在前、尾部在后” 的物理排列,与后续移动时 “新节点从头部生成” 的逻辑完全匹配。////2. 简化移动时的节点操作//蛇移动时,核心操作是://在头部前方新增一个节点(按当前方向移动后的位置);//若未吃到食物,则删除尾部节点(保持长度不变)。//头插法创建的链表,使得 “新增头部节点” 可以直接通过头插实现(new_node->next = phead),而 “删除尾部节点” 只需遍历到链表末尾释放即可。//如果用尾插法,新增头部节点反而需要遍历到头部再插入,操作更繁琐。////3. 初始化效率更高//初始化 5 节蛇身时,头插法每次插入新节点只需修改指针(current->next = snake->pheadsnake; snake->pheadsnake = current; ),时间复杂度为O(1)。//若用尾插法,每次需要遍历到链表末尾才能插入,时间复杂度为O(n),虽然 5 节差异不大,但体现了对数据结构效率的考量。//在这里,我们要知道,我们创建的蛇身的链表其实就是我们//关于蛇的信息的结构体中的蛇的头部,即snake->pheadsnakeif (snake->pheadsnake == NULL)//如果是空链表的话{snake->pheadsnake = current;//直接头插}else//不是空链表的情况下,我们就进行头插{current->next = snake->pheadsnake;snake->pheadsnake = current;}//此时链表的首节点(snake->pheadsnake)是蛇的尾部(即节点5(32, 5)),尾节点是蛇的头部//链表实际顺序为[节点5(32, 5) → 节点4(30, 5) → ... → 头(24, 5)]}//接下来我们就可以开始打印蛇身了//让光标处于蛇身链表中节点所指定的位置current = snake->pheadsnake;while (current != NULL){setcursorpos(current->x, current->y);wprintf(L"%lc",L'●');current = current->next;}//而后我们要设置蛇的一些初始情况//这些初始情况也是在snake结构体变量中//• 游戏状态是:NORMAL//• 蛇的移动速度:200毫秒//• 蛇的默认方向:RIGHT//• 初始成绩:0//• 每个食物的分数:10snake->foodscore = 10;//一个食物的分数为10snake->score = 0;//刚开始总分为0snake->status = NORMAL;//刚开始游戏状态肯定为正常snake->sleeptime = 200;//刚开始休息时间为200mssnake->direction = RIGHT;//刚开始运动方向为右//食物的设置,我们要单独一个函数
}
//5.创建食物
//• 先随机生成食物的坐标
//◦ x坐标必须是2的倍数
//◦ 食物的坐标得在墙体内部
//◦ 食物的坐标不能和蛇身每个节点的坐标重复
//• 创建食物节点,打印食物
void createfood(SNAKE* snake)
{//先初始化食物的x,y坐标,后面要调用rand函数去随机值int x = 0;int y = 0;//◦ x坐标必须是2的倍数//所以我们要调用do while循环,先随机生成//再去判断x是否为2的倍数//不是就再随机生成//是就退出循环
again:do{//◦ 食物的坐标得在墙体内部//所以我们要%x = rand() % 52 + 2;y = rand() % 24 + 1;} while (x % 2 != 0);//x不是偶数的时候就一直循环//◦ 食物的坐标不能和蛇身每个节点的坐标重复//为了避免,所以我们就要遍历蛇身节点坐标//如果有随机生成了相同的,就用goto语句去再随机生成snakenode* current = snake->pheadsnake;while (current != NULL){if (current->x == x && current->y == y){goto again;}current = current->next;}//出来之后,我们就可以创建食物节点了//并将光标移动到随机生成的坐标,打印食物snakenode* pfood = (snakenode*)malloc(sizeof(snakenode));if (pfood == NULL)//申请失败{perror("申请食物链表失败:");exit(1);}else//申请成功{//将随机生成的坐标传进食节点中pfood->x = x;pfood->y = y;//移动光标setcursorpos(pfood->x, pfood->y);//打印食物wprintf(L"%lc",L'★');//再把我们申请的食物节点传回维护贪吃蛇信息的结构体中snake->food = pfood;}
}
//初始化游戏
//0.先设置窗口的大小,再光标隐藏
//1.打印欢迎界面
//2.功能介绍
//3.绘制地图
//4.创建蛇
//5.创建食物
//6.设置游戏的相关信息,而这个就在我们上面创建的SNAKE snake = { 0 };
//又由于要传址调用,所以我们要传地址
void gamestart(SNAKE* snake)
{//0.先设置窗口的大小,再光标隐藏setcursorszieandvisible();//直接调用即可//1.打印欢迎界面//2.功能介绍welcomegame();//3.绘制地图createmap();//4.创建蛇//因为我们要创建蛇,那么也就肯定需要用到我们的蛇的信息结构体initsnake(snake);//5.创建食物createfood(snake);//6.设置游戏的相关信息,而这个就在我们上面创建的SNAKE snake = { 0 };//这个就在我们的创建蛇的函数中,已经有了//我们这里就不在多余
}
//我们要在游戏页面右边打印帮助信息
void printhelpinfo()
{//打印提示信息setcursorpos(75, 10);wprintf(L"%ls",L"不能穿墙,不能咬到自己\n");setcursorpos(75, 11);wprintf(L"用↑.↓.←.→分别控制蛇的移动.");setcursorpos(75, 12);wprintf(L"F3 为加速,F4 为减速\n");setcursorpos(75, 13);wprintf(L"ESC :退出游戏.space:暂停游戏.");setcursorpos(75, 15);wprintf(L"ShineWinsu@版权");
}
void pause(SNAKE* snake)//暂停函数
{while (1)//直接动用死循环{snake->sleeptime = 200;if (keypress(VK_SPACE))//直到再按下空格才恢复{break;}}
}
//检测贪吃蛇移动前方一个格子是否为食物
int checkfood(snakenode* pnext, SNAKE* snake)
{// 需确保蛇头下一个位置(pnext)与食物位置(snake->food)完全匹配return (snake->food->x == pnext->x && snake->food->y == pnext->y);
}
//吃到食物的函数
void eatfood(snakenode* pnext,SNAKE* snake)
{//其实贪吃蛇移动的原理便是贪吃蛇蛇身这个链表会将蛇头往前移动碰到的格子//其实这个格子也算是一个节点,会把这个格子加入到贪吃蛇蛇身的链表中//如果吃到了食物,就把这个格子(节点)头插进蛇身链表中//然后我们再去把这个蛇身链表进行遍历打印//先创建蛇身节点snakenode* snakenext = (snakenode*)malloc(sizeof(snakenode));if (snakenext == NULL){perror("why malloc false:");exit(1);}snakenext->x = pnext->x;snakenext->y = pnext->y;//接着将这个格子(节点)头插进蛇身链表中snakenext->next = snake->pheadsnake;snake->pheadsnake = snakenext;//接着我们就要控制光标去再次把这个蛇身打印出来snakenode* current = snake->pheadsnake;while (current != NULL){//移动光标setcursorpos(current->x, current->y);wprintf(L"%lc", L'●');current = current->next;}//因为吃掉了食物,那么总分也要对应增加snake->score += snake->foodscore;//然后我们得去把之前的那个食物节点给释放掉//并再次生成食物free(snake->food);snake->food = NULL;createfood(snake);
}
//没吃到食物的函数
void nofood(snakenode* pnext, SNAKE* snake)
{//如果没吃到食物,但是为了有移动的感觉//也会把这个格子(节点)加入到贪吃蛇蛇身的链表中//但是会相应的去将加入到贪吃蛇蛇身的链表中的最后一个节点删除掉//就是要让最后一个节点去打印空格,毕竟我们地图就是空的//然后我们再去把这个蛇身链表进行遍历打印//先创建蛇身节点snakenode* snakenext = (snakenode*)malloc(sizeof(snakenode));if (snakenext == NULL){perror("why malloc false:");exit(1);}snakenext->x = pnext->x;snakenext->y = pnext->y;//接着将这个格子(节点)头插进蛇身链表中snakenext->next = snake->pheadsnake;snake->pheadsnake = snakenext;//然后我们要找到蛇身链表的倒二个节点//我们要将新的头节点到倒二个节点的蛇身都打印出来//这个节点的特征就是它的next的next是为NULLsnakenode* temp = snake->pheadsnake;while (temp->next->next != NULL){//移动光标setcursorpos(temp->x, temp->y);wprintf(L"%lc", L'●');temp = temp->next;}//出来循环之后,temp就变为蛇身链表的倒二个节点snakenode* last = temp->next;//last即为蛇身链表的最后一个节点//我们要对原链表的最后一个节点的位置打印空格setcursorpos(last->x, last->y);printf("  ");//打印两个空格//然后再把这个节点释放掉free(last);last = NULL;//但是我们也要记得去将链表的倒二个节点的next指针置为空,//避免链表进行最后一个节点的访问temp->next = NULL;
}
//碰墙判断
int killbywall(SNAKE* snake)
{//上:(0, 0)到(56, 0)//下:(0, 26)到(56, 26)//左:(0, 1)到(0, 25)//右:(56, 1)到(56, 25)//本质上要看蛇头是否有到墙if (snake->pheadsnake->x == 0 || snake->pheadsnake->y == 0 || snake->pheadsnake->x == 56 || snake->pheadsnake->y == 26){snake->status = DIEBYWALL;return 1;}return 0;
}
//撞自己判断
int killbyself(SNAKE* snake)
{//这边要注意,我们要从蛇头的下一个节点进行判断//不难在下面循环中,会直接gameoversnakenode* current = snake->pheadsnake->next;//本质上要看蛇头有没有碰到自身节点中的数据while (current != NULL){if (snake->pheadsnake->x == current->x && snake->pheadsnake->y == current->y){snake->status = DIEBYSELF;return 1;}current = current->next;}return 0;
}
//运行蛇移动的函数
void snakemove(SNAKE* snake)
{//我们想要让蛇移动,就得先知道蛇按用户指定方向移动后蛇头碰到的下一个节点//其本质也算是一个节点,所以也可以用蛇身结构体snakenode* pnext = (snakenode*)malloc(sizeof(snakenode));//使用switch循环,去判断此时蛇是什么状态//其实也就是判断要往哪个方向进行移动switch (snake->direction){//得先知道蛇按用户指定方向移动后蛇头碰到的下一个节点case UP:pnext->x = snake->pheadsnake->x;pnext->y = snake->pheadsnake->y - 1;break;case DOWN:pnext->x = snake->pheadsnake->x;pnext->y = snake->pheadsnake->y + 1;break;case LEFT:pnext->x = snake->pheadsnake->x - 2;pnext->y = snake->pheadsnake->y;break;case RIGHT:pnext->x = snake->pheadsnake->x + 2;pnext->y = snake->pheadsnake->y;break;}//其实贪吃蛇移动的原理便是贪吃蛇蛇身这个链表会将蛇头往前移动碰到的格子//其实这个格子也算是一个节点,会把这个格子加入到贪吃蛇蛇身的链表中//如果吃到了食物,就把这个格子(节点)头插进蛇身链表中//然后我们再去把这个蛇身链表进行遍历打印//这边需要注意,我们的创建蛇身时打印的蛇身只是最开始的情况//后面玩家控制是要不断更新不断打印的//所以我们会用到do while循环//如果没吃到食物,但是为了有移动的感觉//也会把这个格子(节点)加入到贪吃蛇蛇身的链表中//但是会相应的去将加入到贪吃蛇蛇身的链表中的最后一个节点删除掉//就是要让最后一个节点去打印空格,毕竟我们地图就是空的//然后我们再去把这个蛇身链表进行遍历打印//接着我们要判断蛇是否吃到了食物if (checkfood(pnext, snake))//吃到了食物{//运行吃到食物的函数//我们要把蛇头碰到的下一个格子(节点)也传进去eatfood(pnext, snake);}else//没吃到食物{//运行没吃到食物的函数//我们要把蛇头碰到的下一个格子(节点)也传进去nofood(pnext, snake);}//接下来我们还要进行碰墙判断以及撞自己判断//碰墙判断//本质上要看蛇头是否有到墙killbywall(snake);//撞自己判断//本质上要看蛇头有没有碰到自身节点中的数据killbyself(snake);
}
//游戏运行
void gamerun(SNAKE* snake)
{//游戏运行期间,右侧打印帮助信息,提示玩家,坐标开始位置(64, 15)//根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。//如果游戏继续,就是检测按键情况,确定蛇下一步的方向,//或者是否加速减速,是否暂停或者退出游戏。//需要的虚拟按键的罗列://• 上: VK_UP//• 下: VK_DOWN//• 左: VK_LEFT//• 右: VK_RIGHT//• 空格: VK_SPACE//• ESC: VK_ESCAPE//• F3: VK_F3//• F4: VK_F4//我们要在游戏页面右边打印帮助信息printhelpinfo();do{//先在右侧打印得分情况setcursorpos(75, 18);printf("得分:%d ", snake->score);printf("每个食物得分:%d分", snake->foodscore);//接着判断用户按键按击情况来进行调整if (keypress(VK_UP) && snake->direction != DOWN)//不能在蛇向下运动的时候又往上{snake->direction = UP;}else if (keypress(VK_DOWN) && snake->direction != UP)//不能在蛇向上运动的时候又往下{snake->direction = DOWN;}else if (keypress(VK_LEFT) && snake->direction != RIGHT)//不能在蛇向右运动的时候又往左{snake->direction = LEFT;}else if (keypress(VK_RIGHT) && snake->direction != LEFT)//不能在蛇向左运动的时候又往右{snake->direction = RIGHT;}else if (keypress(VK_SPACE)){//进行暂停,直到再次按下空格才恢复运行pause(snake);}else if (keypress(VK_ESCAPE)){//由于我们循环的条件便是游戏状态为正常,所以我们只要把状态改变//就不会进入循环snake->status = ENDNORMAL;break;//直接终止}else if (keypress(VK_F3)){//进行加速,其实就是将蛇的休息时间减少if (snake->sleeptime > 80)//要有限制速度{snake->sleeptime -= 30;snake->foodscore += 2;//速度快了,分数也得高起来}}else if (keypress(VK_F4)){//进行减速,其实就是将蛇的休息时间增多if (snake->foodscore > 0){snake->sleeptime += 30;snake->foodscore -= 2;//速度慢了,分数也得低起来}}//接着就要在循环中运行蛇移动的函数//按一次按键就要进行相应的改变snakemove(snake);//移动一次就休眠Sleep(snake->sleeptime);} while (snake->status == NORMAL);//当游戏状态正常时,不断允许经游戏运行代码
}
//游戏结束
void gameend(SNAKE* snake)
{//在游戏结束的环节中,我们要判断用户因什么而结束游戏//并进行表达setcursorpos(24, 12);switch (snake->status){case ENDNORMAL:printf("您主动退出游戏\n");break;case DIEBYSELF:printf("您撞上自己了 ,游戏结束!\n");break;case DIEBYWALL:printf("您撞墙了,游戏结束!\n");break;}//然后我们要将蛇身链表的节点都释放掉snakenode* current= snake->pheadsnake;while (current){//保存节点,不难直接释放current会导致程序出错snakenode* temp = current;current = current->next;//我们释放临时变量free(temp);}
}

贪吃蛇游戏的测试源文件:

#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "snake.h"
void test()
{char ch = 0;srand((unsigned int)time(NULL));//用于随机生成食物坐标do{SNAKE snake = { 0 };gamestart(&snake);gamerun(&snake);gameend(&snake);setcursorpos(20, 15);printf("再来一局吗?(Y/N):");scanf("%c", &ch);// 清理输入缓冲区while (getchar() != '\n');} while (ch == 'Y' || ch == 'y');setcursorpos(0, 27);
}
int main()
{//修改当前地区为本地模式,为了支持中文宽字符的打印setlocale(LC_ALL, "");//测试逻辑test();return 0;
}

结语:

当我们的指尖离开键盘,屏幕上的贪吃蛇仍在按照既定逻辑游走 —— 这个由代码构筑的小小世界,恰似编程学习的缩影:每一行指令都是对规则的定义,每一次循环都是对规律的践行,每一次交互都是人与机器的对话。从最初构思 “蛇如何动起来” 的朦胧想法,到用 “头插新节点、尾删旧节点” 的逻辑让蛇身流畅游走;从纠结 “坐标计算为何 X 轴步长是 2” 的细节,到理解控制台字符 “宽高不等” 的视觉特性;从困惑 “为何要单独写释放节点的函数”,到明白 “内存泄漏是程序健壮性的隐形杀手”—— 这趟贪吃蛇的构建之旅,我们收获的不仅是一个可运行的程序,更是一套 “将抽象需求转化为具体代码” 的思维框架。

那些曾让我们反复调试的细节,如今都成了理解编程本质的注脚。你是否记得,当发现 “蛇身移动的核心是数据传递”—— 后一节点复制前一节点的旧位置,前一节点接收更前节点的旧位置 —— 时,那种对 “联动逻辑” 的透彻领悟?是否注意到,“禁止反向移动” 的规则,不过是在按键检测时增加了 “新方向与当前方向是否相反” 的判断,用简单逻辑规避了不合理操作?又是否曾为 “食物生成时要避开蛇身” 而煞费苦心,直到用循环遍历蛇身每个节点做碰撞检测,才真正理解 “遍历算法” 在实际场景中的应用价值?这些细节背后,是编程的底层逻辑:复杂问题拆解为简单步骤,特殊情况转化为条件判断,动态变化依托于数据结构。

这个小小的贪吃蛇,实则是程序设计原则的生动教材。我们用struct封装蛇的坐标、长度、状态,是在实践 “信息隐藏” 的模块化思想,让数据与操作形成有机整体;用enum定义方向与游戏状态,是在追求 “代码自解释性”,让数字背后的业务含义一目了然;用函数拆分初始化、运行、结束三个阶段,是在践行 “单一职责” 原则,让每个模块专注于特定功能;而选择链表而非数组存储蛇身,则展现了 “数据结构适配需求” 的重要性 —— 当蛇身长度动态变化时,链表的插入删除优势远胜数组。甚至连 “先计算蛇头新位置→检测是否碰撞→再执行移动或结束” 的步骤设计,都在诠释 “预判优先于补救” 的工程思维。

当你能轻松解释 “蛇身增长的本质是‘暂停一次尾节点删除’”,能清晰看透 “暂停功能是通过阻塞循环等待按键信号实现的”,能深刻理解 “游戏主循环是‘输入 - 处理 - 输出’模式的经典应用”—— 你就已经从 “代码的模仿者” 成长为 “逻辑的创造者”。这个过程中,我们掌握的不仅是 “贪吃蛇的实现技巧”,更是 “问题解决的通用方法”:从明确核心需求(移动、生长、死亡),到设计数据模型(节点结构、蛇结构体),再到编写控制逻辑(移动算法、碰撞检测、循环驱动),最后优化用户体验(速度调节、视觉反馈、资源回收),每一步都是 “抽象建模→具象编码→测试迭代” 的完整实践。

游戏有终结,但编程的探索永无止境。当你尝试为蛇头添加不同皮肤,为食物设置随机分值,甚至实现穿过墙壁的 “传送门” 功能时 —— 这些基于基础框架的创新,正是编程创造力的起点。贪吃蛇的真正价值,在于它像一面 “逻辑放大镜”:让我们看清 “变量赋值” 如何改变状态,“条件判断” 如何决定分支,“循环结构” 如何驱动流程,“函数调用” 如何组织代码。这些最基础的编程元素,在贪吃蛇的世界里变得具体而有温度。

或许未来某天,在面对百万行级别的大型项目时,你会突然想起贪吃蛇的实现细节:想起链表节点 “前趋后继” 的关系如何保证数据一致性,想起状态机模式(游戏状态切换)如何管理复杂流程,想起资源释放时 “遍历清理” 的严谨态度如何避免内存泄漏。这些看似微小的知识点,终将沉淀为驾驭复杂系统的核心能力。

屏幕上的贪吃蛇可能会停止移动,但代码赋予我们的逻辑思维与创造能力,将永远在成长。就像那只不断追逐食物的小蛇,我们对编程世界的探索,也永远在路上。

最后,依旧是那句话,诸君共勉!!!!!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/924186.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

撕裂的乡土:在人性荒原上寻找微光

我从未将故乡湘源涂抹成田园牧歌的幻境。这座深藏福建武夷山脉褶皱中的村庄,海拔八百米,森林如墨染,云雾终年缠绕山腰,溪涧清可见底。现常住人口仅五十余人,仅为80年代的十分之一人口,时间在这里仿佛凝滞,唯山风…

2025蔬菜配送服务公司 TOP 企业推荐排行榜,深圳、宝安、光明、松岗、东莞、长安、虎门、沙田、厚街、大岭山蔬菜配送推荐

引言​ 在当今社会,蔬菜配送行业作为连接农产品生产与消费的重要纽带,其发展态势备受关注。然而,该行业目前存在着诸多问题。一方面,部分配送公司在食材新鲜度保障上存在不足,由于缺乏有效的冷链物流技术和管理手…

2025液压缸TOP企业品牌推荐排行榜!抓斗、伺服、大吨位、车辆、工程、拉杆、冶金、重载、港机液压缸推荐

引言在液压装备领域,液压缸作为重要的动力传递元件,其品质与性能直接影响着众多行业的生产效率与运行安全。当前,市场上液压缸品牌数量众多,产品质量参差不齐,技术水平也存在较大差异。部分品牌为追求短期利益,在…

2025 年破胶机厂家品牌推荐榜单白皮书,多规格型号 610/710/810、大型、自动型、低温环保、节能省电、自动打块、轮胎破胶机公司推荐

引言​ 在废旧橡胶回收再利用产业蒸蒸日上的今天,破胶机作为不可或缺的关键设备,其性能优劣与质量高低,直接关系到企业的生产效率和最终产品品质。不过,当前破胶机市场呈现出一番复杂景象:制造商数量繁杂,产品质…

乱七八糟的国庆做题记录

模拟赛T1 题面 赛时糖了,写了个会t的状压还不会处理下界 题面中的限制可以转为: 对于任意合法集合 1.必须包含n的每个质因数的最大次方 2.至少出现一对不同质因数 严肃发现质因子数目比logn还要小的多,可以爆搜 直接…

2025 年健身器材品牌 TOP 推荐排行榜,室内 / 健身房 / 体育 / 运动 / 家用 / 商用 / 单位 / 家庭 / 有氧 / 力量健身器材推荐

引言在当今健身行业蓬勃发展的背景下,健身器材市场呈现出蓬勃生机,但同时也面临着诸多问题。市场上健身器材品牌众多,产品质量参差不齐,部分品牌为追求利润,在材料选择和工艺制作上偷工减料,导致产品可靠性和耐用…

网站注册价格福田欧辉校车

分布式文件系统 SpringBootFastDFSVue.js【四】 八、文件的下载和删除功能8.1.FastDFSClient.java8.2.FileServerController.java8.3.Vue的fast.js8.4.fastdfsimg.vue8.5.效果 九、总结endl 八、文件的下载和删除功能 8.1.FastDFSClient.java Slf4j public class FastDFSClie…

详细介绍:给贾维斯加“手势控制”:从原理到落地,打造多模态交互的本地智能助

详细介绍:给贾维斯加“手势控制”:从原理到落地,打造多模态交互的本地智能助pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-f…

完整教程:学术论文 Word 样式规范

完整教程:学术论文 Word 样式规范pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco&…

完整教程:QT示例 使用QTcpSocket和QTcpServer类实现TCP的自定义消息头、消息体通信示例

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

企业网站建设方案论文自己做网站用花钱吗

1、什么是接口mock 主要是针对单元测试的应用&#xff0c;它可以很方便的解除单元测试中各种依赖&#xff0c;大大的降低了编写单元测试的难度 2、什么是mock server 正常情况下&#xff1a;测试客户端——测试——> 被测系统 ——依赖——>外部服务依赖 在被测系统和…

东莞网站忧化wordpress素锦模板

今天没有早八&#xff0c;八点之钱起床了&#xff0c;上午背了半小时的单词&#xff0c;然后就在写top100&#xff0c;目前中等和简单写了30题&#xff0c;基本上都没有看题解。我自己也整理下&#xff0c;每一题的思路&#xff0c;这样子&#xff0c;也会让我至少拥有做模板题…

温州网站建设设计公司网络营销推广的力度

前言 在管理端会遇到多分类时&#xff0c;要求有层次展示出来&#xff0c;并且每个分类有额外的操作。例如&#xff1a;添加分类、编辑分类、删除、拖到分类等。 下面将会记录这样的一个需求实习过程。 了解需求 分类展示按层级展示分类根据特定的参数展示可以操作的按钮&a…

【c++】深入理解string类(3):典型OJ题 - 指南

【c++】深入理解string类(3):典型OJ题 - 指南pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", …

取印度孟买指数(SENSEX)实时行情API对接指南

获取印度孟买指数(SENSEX)实时行情API对接指南本文详细介绍如何通过API获取印度孟买敏感30指数(SENSEX)的实时行情数据,包含多种数据获取方式和代码示例概述 印度孟买敏感30指数(SENSEX)是印度孟买证券交易所的主要股…

网站推广存在的问题wordpress on.7主题

背景&#xff1a; 纯虚类(抽象类) 是只至少拥有一个纯虚函数的类&#xff0c;这种类可以有成员变量&#xff0c;但是不能进行单独的实例化(new&#xff0c;局部变量&#xff0c;智能指针构造等等)。其根本原因是由于纯虚类提供了未实现的成员函数&#xff0c;所以编译器无法知…

京东物流网站建设特点潜江58同城

CSS进阶 目标&#xff1a;掌握复合选择器作用和写法&#xff1b;使用background属性添加背景效果 01-复合选择器 定义&#xff1a;由两个或多个基础选择器&#xff0c;通过不同的方式组合而成。 作用&#xff1a;更准确、更高效的选择目标元素&#xff08;标签&#xff09;。…

企业建站网站认证企业的网站推广意义

目录 一、配置接口的全球单播地址 二、配置接口本地链路地址 三、配置接口任播地址 四、配置接口PMTU 配置静态PMTU&#xff1a; 配置动态PMTU&#xff1a; 五、接口配置IPV6地址示例&#xff1a; 一、配置接口的全球单播地址 全球单播地址类似于IPv4公网地址&#xff0…

网站流量推广网站1996年推广

前言 之前文章简单介绍了如何运行ginvue的前后端分离开源项目&#xff0c;该项目是学习了Gin实践教程后结合vue-element-admin写的&#xff0c;该教程讲得很详细&#xff0c;适合入门Gin。本篇文章将介绍ginvue的前后端分离开源项目中如何使用gin-jwt对API进行权限验证。 安装g…

2025青海视频号运营优质公司推荐榜:专业服务与创新策略口碑

2025氧化镁优质厂家权威推荐榜:品质卓越与技术实力深度解析 一、行业背景 氧化镁作为一种重要的无机化工产品,在众多领域都有着广泛的应用。它具有高熔点、高硬度、良好的化学稳定性等特性,被广泛应用于耐火材料、橡…