【C语言】【数据结构】项目实践——贪吃蛇游戏(超详细)

前言

本篇博客我们来实现一个小游戏项目——贪吃蛇,相信肯定很多人都玩过,那么整个贪吃蛇是怎么实现出来的那,这个项目用到了很多方面的知识:C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等。我们就通过这篇博客一步一步去分析,实现贪吃蛇。

​💓 个人主页:小张同学zkf
⏩ 文章专栏:数据结构   C语言
若有问题 评论区见📝
🎉欢迎大家点赞👍收藏⭐文章

目录

1.游戏背景

2.Win32 API介绍

2.1Win32 API

2.2控制台程序

2.3控制台屏幕上的坐标COORD

2.4GetStdHandle

2.5GetConsoleCursorInfo

2.6CONSOLE_CURSOR_INFO

2.7SetConsoleCursorInfo

2.8SetConsoleCursorPosition

2.9GetAsyncKeyState  

3.贪吃蛇游戏思路 

3.1游戏窗口

3.1.1本地化

3.1.2类项

3.1.3setlocale函数

 3.1.4宽字符的打印

3.1.5地图坐标

 3.2蛇身和食物

3.3数据结构设计  

3.4游戏流程设计

4.核心逻辑分析

4.1游戏主逻辑

4.2 游戏开始(GameStart)

4.2.1打印欢迎界面

4.2.2创建地图 

4.2.3初始化蛇身

4.2.4创建第一个食物

 4.3游戏运行(GameRun)

4.3.1KEY_PRESS

4.3.2PrintHelpInfo

 4.3.3蛇身移动(SnakeMove)

4.3.3.1NextIsFood

4.3.3.2 EatFood

4.3.3.3NoFood

4.3.3.4KillByWall

4.3.3.5KillBySelf

4.4游戏结束

5.参考代码

 


1.游戏背景

贪吃蛇是一款休闲益智类游戏,有PC和手机等多平台版本。既简单又耐玩。该游戏通过控制蛇头方向吃食物,从而使得蛇变得越来越长,贪吃蛇是久负盛名的游戏,它也和俄罗斯方块,扫雷等游戏位列经典游戏的行列。


2.Win32 API介绍

实现贪吃蛇不仅需要C语言与数据结构的一些知识,还要会使用到的一些Win32 API知识,接下来我们就看一下需要用到哪些知识

2.1Win32 API

Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外, 它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数 WIN32 API也就是Microsoft Window32位平台的应用程序编程接口

2.2控制台程序

平常我们运行起来的黑框程序其实就是控制台程序
我们可以使用cmd命令来设置控制台窗口的长宽:设置控制台窗口的大小,30行,100列
mode con cols= 100 lines= 30

 通过这个命令可以把屏幕控制在100列的长度,30行宽度

参考:mode命令

也可以通过命令设置控制台窗口的名字

title 贪吃蛇

参考:title命令

这些能在控制台窗口执行的命令,也可以调用C语言函数system来执行。例如:

# include <stdio.h>
int main ()
{
// 设置控制台窗⼝的⻓
宽:设置控制台窗⼝的⼤⼩, 30 ⾏, 100
system( "mode con cols=100 lines=30" );
// 设置 cmd 窗⼝名称
system( "title 贪吃蛇 " );
return 0 ;
}

2.3控制台屏幕上的坐标COORD

COORD 是Windows API中定义的一个结构体 ,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0) 的原点位于缓冲区的顶部左侧单元格。

COORD类型的声明:

typedef struct _ COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;

给坐标赋值:  

COORD pos = { 10 , 15 };

2.4GetStdHandle

GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个 句柄 (用来标识不同设备的数值),使用这个句柄可以操作设备。

 参考:GetStdHandle

HANDLE GetStdHandle (DWORD nStdHandle);

实例:

HANDLE hOutput = NULL ;
  // 获取标准输出的句柄 ( ⽤来标识不同设备的数值 )
  hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

2.5GetConsoleCursorInfo

这个也是API函数,作用是检索有关指定控制台屏幕缓冲区的光标大小可见性的信息

BOOL WINAPI GetConsoleCursorInfo (
 HANDLE hConsoleOutput,
 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
  );
  PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标(光标)的信息

参考:GetConsoleCursorInfo

实例

HANDLE hOutput = NULL ;
  // 获取标准输出的句柄 ( ⽤来标识不同设备的数值 )
  hOutput = GetStdHandle (STD_OUTPUT_HANDLE);
  CONSOLE_CURSOR_INFO CursorInfo;
  GetConsoleCursorInfo (hOutput, &CursorInfo); // 获取控制台光标信息

2.6CONSOLE_CURSOR_INFO

这个结构体,包含有关控制台光标的信息

typedef struct _ CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
这个结构体的俩成员
dwSize,由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
bVisible,游标的可见性。 如果光标可见,则此成员为TRUE。
CursorInfo.bVisible = false ; // 隐藏控制台光标
参考: CONSOLE_CURSOR_INFO

2.7SetConsoleCursorInfo

设置指定控制台屏幕缓冲区的光标的大小和可见性。

BOOL WINAPI SetConsoleCursorInfo (
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
实例:
HANDLE hOutput = GetStdHandle (STD_OUTPUT_HANDLE);
// 影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo (hOutput, &CursorInfo); // 获取控制台光标信息
CursorInfo.bVisible = false ; // 隐藏控制台光标
SetConsoleCursorInfo (hOutput, &CursorInfo); // 设置控制台光标状态

参考:SetConsoleCursorInfo 


2.8SetConsoleCursorPosition

设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
BOOL WINAPI SetConsoleCursorPosition (
 HANDLE hConsoleOutput,
 COORD pos
  );

参考:SetConsoleCursorPosition 

实例:

COORD pos = { 10 , 5 };
 HANDLE hOutput = NULL ;
  // 获取标准输出的句柄 ( ⽤来标识不同设备的数值 )
 hOutput = GetStdHandle (STD_OUTPUT_HANDLE);
  // 设置标准输出上光标的位置为 pos
  SetConsoleCursorPosition (hOutput, pos);

SetPos: 封装一个设置光标位置的函数

/ 设置光标的坐标
  void SetPos ( short x, short y)
  {
 COORD pos = { x, y };
 HANDLE hOutput = NULL ;
  // 获取标准输出的句柄 ( ⽤来标识不同设备的数值 )
 hOutput = GetStdHandle (STD_OUTPUT_HANDLE);
  // 设置标准输出上光标的位置为 pos
  SetConsoleCursorPosition (hOutput, pos);
  }

2.9GetAsyncKeyState  

这个函数就是获取按键情况,GetAsyncKeyState的函数原型如下:
SHORT GetAsyncKeyState (
  int vKey
  );

参考:GetAsyncKeyState 

将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 的返回值是short类型,在上一次调用 GetAsyncKeyState 函数后,如果
返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起如果最低位被置为1则说明,该按键被按过,否则为0。如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1
对与贪吃蛇来说,按过就行了,所以我们只需要确定键位按过,即判断返回值最低位是否是1
# define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

我们可以定一个宏专门判断最低位是否是1

参考:虚拟键码(Winuser.h) - Win32 apps 

实例:检测数字键
# include <stdio.h>
  # include <windows.h>
int main ()
  {
  while ( 1 )
{
  if (KEY_PRESS( 0x30 ))
 {
  printf ( "0\n" );
 }
  else if (KEY_PRESS( 0x31 ))
 {
  printf ( "1\n" );
 }
  else if (KEY_PRESS( 0x32 ))
 {
  printf ( "2\n" );
 }
  else if (KEY_PRESS( 0x33 ))
 {
printf ( "3\n" );
 }
  else if (KEY_PRESS( 0x34 ))
 {
  printf ( "4\n" );
 }
  else if (KEY_PRESS( 0x35 ))
 {
  printf ( "5\n" );
 }
  else if (KEY_PRESS( 0x36 ))
{
printf ( "6\n" );
}
else if (KEY_PRESS( 0x37 ))
{
printf ( "7\n" );
}
else if (KEY_PRESS( 0x38 ))
{
printf ( "8\n" );
}
else if (KEY_PRESS( 0x39 ))
{
printf ( "9\n" );
}
}
return 0 ;
}

3.贪吃蛇游戏思路 

3.1游戏窗口

这里不得不说一下控制台窗口的一些知识,如果想在控制台的窗口中指定位置输出信息,我们得知道该位置的坐标,所以首先介绍一下控制台窗口的坐标知识。
控制台窗口的坐标如下所示,横向的是X轴,从左向右依次增长,纵向是Y轴,从上到下依次增长。
在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★
普通的字符是占一个字节的,这类宽字符是占用2个字节。
这里再简单说一下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。C语言最初假定字符都是单字节的。但是这些假定并不是在世界的任何地方都适用。
C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (), 在俄语编码中又会代表另一个符号。但是不管怎样,所有这 些编码方式中,0--127表⽰的符号是一样的,不一样的只是128--255的这一段。至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号, 肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 256 x 256 = 65536 个符号。
后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型wchar_t 和宽字符的输入和输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。


3.1.1<locale.h>本地化

<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。
在标准中,依赖地区的部分有以下几项:
数字量的格式
货币量的格式
字符集
期和时间的表示形式

3.1.2类项

通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏, 指定一个类项:
LC_COLLATE:影响字符串比较函数 strcoll() strxfrm()
LC_CTYPE:影响字符处理函数的行为。
LC_MONETARY:影响货币格式。
LC_NUMERIC:影响 printf() 的数字格式。
LC_TIME:影响时间格式 strftime() wcsftime()
LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语言环境

详细介绍:https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/setlocale-wsetlocale?view=msvc-170 


3.1.3setlocale函数

char * setlocale ( int category, const char * locale);
setlocale 函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。
setlocale 的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有的类项。 C标准给第二个参数仅定义了2种可能取值: "C" (正常模式)和 " " (本地模式)。 在任意程序执行开始,都会隐藏式执行调用:
setlocale (LC_ALL, "C" );
当地区设置为"C"时,库函数按正常方式执行,小数点是一个点。 当程序运行起来后想改变地区,就只能显示调用setlocale函数。用" "作为第2个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。 比如: 切换到我们的本地模式后就支持宽字符(汉字)的输出等
setlocale (LC_ALL, " " ); // 切换到本地环境

 3.1.4宽字符的打印

那如果想在屏幕上打印宽字符,怎么打印呢?
宽字符的字面量必须加上前缀“L”,否则 C 语言会把字面量当作窄字符类型处理。前缀“L”在单引
号前面,表示宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前面,表示宽字符串,对应
wprintf() 的占位符为 %ls
# include <stdio.h>
# include <locale.h>
int main () {
setlocale (LC_ALL, "" );
wchar_t ch1 = L' ' ;
wchar_t ch2 = L' ' ;
printf ( "%c%c\n" , 'a' , 'b' );
wprintf ( L"%lc\n" , ch1);
wprintf ( L"%lc\n" , ch2);
return 0 ;
}

这样一些好看的图案就可以在屏幕上打印了,宽字符占两个字节


3.1.5地图坐标

 我们假设实现一个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,如下:

这样我们根据坐标就可以将墙给表示出来了


 3.2蛇身和食物

初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。
注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半儿出现在墙体中,另外一般在墙外的现象,坐标不好对齐!!!
关于食物,就是在墙体内随机生成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。

3.3数据结构设计  

在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们 使用链表存储蛇的信
息,那么蛇的每一节其实就是链表的每个节点 。每个节点只要记录好蛇身节点在地图上的坐标就行,所以蛇节点结构如下:
typedef struct SnakeNode
  {
  int x;
  int y;
  struct SnakeNode * next;
  }SnakeNode, * pSnakeNode;
要管理整条贪吃蛇,我们再封装一个Snake的结构来维护整条贪吃蛇,那我们想象一下,这个蛇有关的信息有哪些:整条蛇指针,食物指针,蛇头方向,游戏状态,总分,每一个食物分,每一步的休眠时间,大概有这些,代码如下:
typedef struct Snake
{
pSnakeNode _pSnake; // 维护整条蛇的指针
pSnakeNode _pFood; // 维护⻝物的指针
enum DIRECTION _Dir; // 蛇头的⽅向 , 默认是向右
enum GAME_STATUS _Status; // 游戏状态
int _Socre; // 游戏当前获得分数
int _foodWeight; // 默认每个⻝物 10
int _SleepTime; // 每⾛⼀步休眠时间
}Snake, * pSnake;

蛇的方向,分为上,下,左,右可以列举,使用枚举 

// ⽅向
enum DIRECTION
{
UP = 1 ,
DOWN,
LEFT,
RIGHT
};

 游戏状态,分为正常运行,撞墙,咬到自己,正常结束可以列举,使用枚举

// 游戏状态
enum GAME_STATUS
{
OK, // 正常运⾏
KILL_BY_WALL, // 撞墙
KILL_BY_SELF, // 咬到⾃⼰
END_NOMAL // 正常结束
};

3.4游戏流程设计


4.核心逻辑分析

4.1游戏主逻辑

首先,程序开始就设置程序支持本地模式,然后进入游戏的主逻辑,有了主逻辑,才能有了明确的方向。
主逻辑分为3个过程: 
游戏开始(GameStart)完成游戏的初始化
游戏运行(GameRun)完成游戏运行逻辑的实现
游戏结束(GameEnd)完成游戏结束的说明,实现资源释放
# include <locale.h>
void test ()
{
int ch = 0 ;
srand (( unsigned int ) time ( NULL ));
do
{
Snake snake = { 0 };
GameStart (&snake);
GameRun (&snake);
GameEnd (&snake);
SetPos ( 20 , 15 );
printf ( " 再来⼀局吗? (Y/N):" );
ch = getchar ();
getchar (); // \n
} while (ch == 'Y' );
SetPos ( 0 , 27 );
}
int main ()
{
// 修改当前地区为本地模式,为了⽀持中⽂宽字符的打印
setlocale (LC_ALL, "" );
// 测试逻辑
test ();
return 0 ;
}

注意:getchar()是获取字符信息,若此刻你没有输出任何字符,相当于运行上的一个暂停。


4.2 游戏开始(GameStart)

这个模块完成游戏的初始化任务:
控制台窗口大小的设置
控制台窗口名字的设置
标光标的隐藏
打印欢迎界面
创建地图
初始化第蛇
创建第一个食物
有了上面的API函数的了解,这些就简单写了
void GameStart (pSnake ps)
{
// 设置控制台窗⼝的⼤⼩, 30 ⾏, 100
//mode DOS 命令
system ( "mode con cols=100 lines=30" );
// 设置 cmd 窗⼝名称
system ( "title 贪吃蛇 " );
// 获取标准输出的句柄 ( ⽤来标识不同设备的数值 )
HANDLE hOutput = GetStdHandle (STD_OUTPUT_HANDLE);
// 影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo (hOutput, &CursorInfo); // 获取控制台光标信息
CursorInfo.bVisible = false ; // 隐藏控制台光标
SetConsoleCursorInfo (hOutput, &CursorInfo); // 设置控制台光标状态
// 打印欢迎界⾯
WelcomeToGame ();
// 打印地图
CreateMap ();
// 初始化蛇
InitSnake (ps);
// 创造第⼀个⻝物
CreateFood (ps);
}


4.2.1打印欢迎界面

 在游戏正式开始之前,做一些功能提醒

void WelcomeToGame ()
{
SetPos ( 40 , 15 );
printf ( " 欢迎来到贪吃蛇⼩游戏 " );
SetPos ( 40 , 25 ); // 让按任意键继续的出现的位置好看点
system ( "pause" );
system ( "cls" );
SetPos ( 25 , 12 );
printf ( " . . . 分别控制蛇的移动, F3 为加速, F4 为减速 \n" );
SetPos ( 25 , 13 );
printf ( " 加速将能得到更⾼的分数。 \n" );
SetPos ( 40 , 25 ); // 让按任意键继续的出现的位置好看点
system ( "pause" );
system ( "cls" );
}

 

 system("pause")这个就是按任意键继续的功能与system("cls")(清空屏幕的功能)一连用就达到了切换页面的效果。


4.2.2创建地图 

 创建地图就是将墙打印出来,因为是宽字符打印,所有使用wprintf函数,打印格式串前使用L

打印地图的关键是要算好坐标,才能在想要的位置打印墙体。
墙体打印的宽字符:
# define WALL L' '

 易错点:就是坐标的计算

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

 创建地图函数CreateMap

void CreateMap ()
{
int i = 0 ;
// (0,0)-(56, 0)
SetPos ( 0 , 0 );
for (i = 0 ; i < 58 ; i += 2 )
{
wprintf ( L"%c" , WALL);
}
// (0,26)-(56, 26)
SetPos ( 0 , 26 );
for (i = 0 ; i < 58 ; i += 2 )
{
wprintf ( L"%c" , WALL);
}
//
//x 0 y 1 开始增⻓
for (i = 1 ; i < 26 ; i++)
{
SetPos ( 0 , i);
wprintf ( L"%c" , WALL);
}
//x 56 y 1 开始增⻓
for (i = 1 ; i < 26 ; i++)
{
SetPos ( 56 , i);
wprintf ( L"%c" , WALL);
}
}

4.2.3初始化蛇身

蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。
创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每一节打印在屏幕上。
蛇的初始位置从 (24,5) 开始。
  再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,每个食物的分数。
游戏状态是:OK
蛇的移动速度:200毫秒
蛇的默认方向:RIGHT
初始成绩:0
每个食物的分数:10
蛇身打印的宽字符:
# define BODY L'
初始化蛇身函数:InitSnake
void InitSnake (pSnake ps)
  {
 pSnakeNode cur = NULL ;
  int i = 0 ;
  // 创建蛇⾝节点,并初始化坐标
  // 头插法
  for (i = 0 ; i < 5 ; i++)
 {
  // 创建蛇⾝的节点
 cur = (pSnakeNode) malloc ( sizeof (SnakeNode));
  if (cur == NULL )
 {
  perror ( "InitSnake()::malloc()" );
  return ;
 }
  // 设置坐标
 cur->next = NULL ;
 cur->x = POS_X + i * 2 ;
 cur->y = POS_Y;
  // 头插法
  if (ps->_pSnake == NULL )
 {
 ps->_pSnake = cur;
 }
  else
 {
 cur->next = ps->_pSnake;
 ps->_pSnake = cur;
 }
 }
  // 打印蛇的⾝体
 cur = ps->_pSnake;
  while (cur)
 {
  SetPos (cur->x, cur->y);
  wprintf ( L"%lc" , BODY);
 cur = cur->next;
 }
  // 初始化贪吃蛇数据
 ps->_SleepTime = 200 ;
 ps->_Socre = 0 ;
 ps->_Status = OK;
 ps->_Dir = RIGHT;
ps->_foodWeight = 10 ;
}
对于这个初始化蛇身的函数,我们要用到单链表的头插,将蛇通过单链表的方式连接到一起,然后一个个遍历,打印宽字符就行了

4.2.4创建第一个食物

先随机生成食物的坐标
       ◦ x坐标必须是2的倍数
       ◦ 物的坐标不能和蛇身每个节点的坐标重复
创建食物节点,打印食物
食物打印的宽字符:
# define FOOD L' '
创建食物的函数:CreateFood
void CreateFood (pSnake ps)
{
int x = 0 ;
int y = 0 ;
again:
// 产⽣的 x 坐标应该是 2 的倍数,这样才可能和蛇头坐标对⻬。
do
{
x = rand () % 53 + 2 ;
y = rand () % 25 + 1 ;
} while (x % 2 != 0 );
pSnakeNode cur = ps->_pSnake; // 获取指向蛇头的指针
// ⻝物不能和蛇⾝冲突
while (cur)
{
if (cur->x == x && cur->y == y)
{
goto again;
}
cur = cur->next;
}
pSnakeNode pFood = (pSnakeNode) malloc ( sizeof (SnakeNode)); // 创建⻝物
if (pFood == NULL )
{
perror ( "CreateFood::malloc()" );
return ;
}
else
{
pFood->x = x;
pFood->y = y;
SetPos (pFood->x, pFood->y);
wprintf ( L"%c" , FOOD);
ps->_pFood = pFood;
}
}

生成食物的函数,用rand随机生成但要注意不要越过墙的坐标范围,而且不能随机生成到蛇身上,也就是说随机坐标要有这俩判断条件,我们这个代码,中间生成的随机值与蛇身重合,就可以用goto语句来重新来一遍循环,将食物节点下一个next值置为空,别忘了,将食物节点,储存到蛇的结构体中


 4.3游戏运行(GameRun)

游戏运行期间,右侧打印帮助信息,提示玩家,坐标开始位置(64, 15)
根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。
需要的虚拟按键的罗列:
上:VK_UP
下:VK_DOWN
左:VK_LEFT
右:VK_RIGHT
空格:VK_SPACE
ESC:VK_ESCAPE
F3:VK_F3
F4:VK_F4

 确定了蛇的方向和速度,蛇就可以移动了。

void GameRun (pSnake ps) 
  {
  // 打印右侧帮助信息
  PrintHelpInfo ();
  do
 {
  SetPos ( 64 , 10 );
  printf ( " 得分: %d " , ps->_Socre);
  printf ( " 每个⻝物得分: %d " , ps->_foodWeight);
  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 (VK_SPACE))
 {
  pause ();
 }
  else if ( KEY_PRESS (VK_ESCAPE))
 {
 ps->_Status = END_NOMAL;
  break ;
 }
  else if ( KEY_PRESS (VK_F3))
 {
  if (ps->_SleepTime >= 80 )
 {
 ps->_SleepTime -= 30 ;
 ps->_foodWeight += 2 ; // ⼀个⻝物分数最⾼是 20
 }
 }
  else if ( KEY_PRESS (VK_F4))
 {
  if (ps->_SleepTime < 320 )
 {
 ps->_SleepTime += 30 ;
ps->_foodWeight -=  2 ; // ⼀个⻝物分数最低是 2
 }
 }
  // 蛇每次⼀定之间要休眠的时间,时间短,蛇移动速度就快
  Sleep (ps->_SleepTime);
  SnakeMove (ps);
} while (ps->_Status == OK);
}

这个函数我们根据虚拟键位值返回的值判断方向,但我们在玩贪吃蛇时,假如蛇方向在上,你不能按下的键,与它方向相反的键你按了不管用,改变不了蛇的状态,在加速减速中,我们通过控制睡眠时间长短,来控制蛇的速度,此外我们还需要确定蛇移动函数,注意:这些信息一定是在游戏正常运行时才能出现的 


4.3.1KEY_PRESS

检测按键状态,我们封装了⼀个宏

# define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)

4.3.2PrintHelpInfo

我们可以在游戏运行时,右边放些提示信息,来方便我们玩游戏

void PrintHelpInfo ()
  {
  // 打印提⽰信息
  SetPos ( 64 , 15 );
  printf ( " 不能穿墙,不能咬到⾃⼰ \n" );
  SetPos ( 64 , 16 );
  printf ( " . . . 分别控制蛇的移动 ." );
  SetPos ( 64 , 17 );
  printf ( "F3 为加速, F4 为减速 \n" );
  SetPos ( 64 , 18 );
  printf ( "ESC :退出游戏 .space :暂停游戏 ." );
  }

 4.3.3蛇身移动(SnakeMove)

上面游戏运行过程中我们不是需要定义一个蛇移动的函数嘛

先创建下一个节点,根据移动方向和蛇头的坐标,蛇移动到下一个位置的坐标。
确定了下一个位置后,看下一个位置是否是食物(NextIsFood),是食物就做吃食物处理
(EatFood),如果不是食物则做前进一步的处理(NoFood)。
蛇身移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上自己蛇身(KillBySelf),从而影响游戏的状态。
那NextIsFood,EatFood,NoFood,KillByWall,KillBySelf函数我们都需要一一定义
void SnakeMove (pSnake ps)
  {
  // 创建下⼀个节点
 pSnakeNode pNextNode = (pSnakeNode) malloc ( sizeof (SnakeNode));
  if (pNextNode == NULL )
 {
  perror ( "SnakeMove()::malloc()" );
  return ;
 }
  // 确定下⼀个节点的坐标,下⼀个节点的坐标根据,蛇头的坐标和⽅向确定
  switch (ps->_Dir)
 {
  case UP:
 {
 pNextNode->x = ps->_pSnake->x;
 pNextNode->y = ps->_pSnake->y - 1 ;
 }
break ;
case DOWN:
{
pNextNode->x = ps->_pSnake->x;
pNextNode->y = ps->_pSnake->y + 1 ;
}
break ;
case LEFT:
{
pNextNode->x = ps->_pSnake->x - 2 ;
pNextNode->y = ps->_pSnake->y;
}
break ;
case RIGHT:
{
pNextNode->x = ps->_pSnake->x + 2 ;
pNextNode->y = ps->_pSnake->y;
}
break ;
}
// 如果下⼀个位置就是⻝物
if ( NextIsFood (pNextNode, ps))
{
EatFood (pNextNode, ps);
}
else // 如果没有⻝物
{
NoFood (pNextNode, ps);
}
KillByWall (ps);
KillBySelf (ps);
}

4.3.3.1NextIsFood

假如下一个坐标是食物,我们需要返回1或0,1就代表是食物,0就代表不是食物,方便If判断

//pSnakeNode psn 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
int NextIsFood (pSnakeNode psn, pSnake ps)
{
return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}

4.3.3.2 EatFood

这个就是吃掉事物的函数,假如下一个坐标是食物,我们吃掉它,我们蛇身需要增长,我们想一下,我们可以直接用食物的节点头插到我们蛇身,成为我们新蛇头节点,这样就可以让蛇身增长了

吃完之后,得需要再打印一遍蛇,吃掉的话,总分就会加食物分,我们不要忘记

//pSnakeNode psn 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
void EatFood (pSnakeNode psn, pSnake ps)
{
// 头插法
psn->next = ps->_pSnake;
ps->_pSnake = psn;
// 打印蛇
pSnakeNode cur = ps->_pSnake;
while (cur)
{
SetPos (cur->x, cur->y);
wprintf ( L"%c" , BODY);
cur = cur->next;
}
ps->_Socre += ps->_foodWeight;
// 释放⻝物节点
free (ps->_pFood);
// 创建新的⻝物
CreateFood (ps);
}


4.3.3.3NoFood
这个函数就是下一步不是食物时,将下一个节点头插入蛇的身体,并将之前蛇身最后一个节点打印为空格,释放掉蛇身的最后一个节点。
易错点: 这里最容易错误的是,释放最后一个结点后,还得将指向在最后一个结点的指针改为NULL, 保证蛇尾打印可以正常结束,不会越界访问。
相当于整体思路:先头插->再打印蛇(不打印最后一个节点)->再打印空格->最后释放掉最后一个节点空间
//pSnakeNode psn 是下⼀个节点的地址
  //pSnake ps 维护蛇的指针
  void NoFood (pSnakeNode psn, pSnake ps)
  {
  // 头插法
 psn->next = ps->_pSnake;
 ps->_pSnake = psn;
  // 打印蛇
 pSnakeNode cur = ps->_pSnake;
 {
  SetPos (cur->x, cur->y);
  wprintf ( L"%c" , BODY);
 cur = cur->next;
 }
  // 最后⼀个位置打印空格,然后释放节点
  SetPos (cur->next->x, cur->next->y);
  printf ( " " );
  free (cur->next);
 cur->next = NULL ;
  }


4.3.3.4KillByWall

 判断蛇头的坐标是否和墙的坐标冲突函数

这个函数目的就在于若蛇头坐标与墙坐标重叠就将游戏状态变为因撞墙而结束

//pSnake ps 维护蛇的指针
int KillByWall (pSnake ps)
{
if ((ps->_pSnake->x == 0 )
|| (ps->_pSnake->x == 56 )
|| (ps->_pSnake->y == 0 )
|| (ps->_pSnake->y == 26 ))
{
ps->_Status = KILL_BY_WALL;
return 1 ;
}
return 0 ;
}


4.3.3.5KillBySelf

 判断蛇头的坐标是否和蛇身体的坐标冲突,若冲突,就将游戏状态变为因撞到蛇身而结束

/pSnake ps 维护蛇的指针
int KillBySelf (pSnake ps)
{
pSnakeNode cur = ps->_pSnake->next;
while (cur)
{
if ((ps->_pSnake->x == cur->x)
&& (ps->_pSnake->y == cur->y))
 ps->_Status = KILL_BY_SELF;
  return 1 ;
 }
 cur = cur->next;
 }
  return 0 ;
  }


4.4游戏结束

 游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点。

void GameEnd (pSnake ps)
  {
 pSnakeNode cur = ps->_pSnake;
  SetPos ( 24 , 12 );
  switch (ps->_Status)
 {
  case END_NOMAL:
  printf ( " 您主动退出游戏 \n" );
  break ;
case KILL_BY_SELF: 
  printf ( " 您撞上⾃⼰了 , 游戏结束 !\n" );
  break ;
  case KILL_BY_WALL:
  printf ( " 您撞墙了 , 游戏结束 !\n" );
  break ;
 }
  // 释放蛇⾝的节点
  while (cur)
 {
 pSnakeNode del = cur;
 cur = cur->next;
  free (del);
 }
}

5.参考代码

test.cpp

#include "Snake.h"
 #include <locale.h>

 void test()
 {
 int ch = 0;
 srand((unsigned int)time(NULL));

 do
 {
 Snake snake = { 0 };
 GameStart(&snake);
 GameRun(&snake);
 GameEnd(&snake);
 SetPos(20, 15);
 printf("再来⼀局吗?(Y/N):");
 ch = getchar();
 getchar();//清理\n

 } while (ch == 'Y' || ch == 'y');
 SetPos(0, 27);
 }
int main()
{
 //修改当前地区为本地模式,为了⽀持中⽂宽字符的打印
 setlocale(LC_ALL, "");
 //测试逻辑
 test();
 return 0;
}
snake.h
#pragma once
#include <windows.h>
#include <time.h>
#include <stdio.h>
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
//⽅向
enum DIRECTION
{
 UP = 1,
 DOWN,
 LEFT,
 RIGHT
};
//游戏状态
enum GAME_STATUS
{
 OK,//正常运⾏
 KILL_BY_WALL,//撞墙
 KILL_BY_SELF,//咬到⾃⼰
 END_NOMAL//正常结束
};
#define WALL L'□'
#define BODY L'●' //★○●◇◆□■
#define FOOD L'★' //★○●◇◆□■
//蛇的初始位置
#define POS_X 24
#define POS_Y 5
//蛇⾝节点
typedef struct SnakeNode
{
 int x;
 int y;
 struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
typedef struct Snake
{
 pSnakeNode _pSnake;//维护整条蛇的指针
 pSnakeNode _pFood;//维护⻝物的指针
 enum DIRECTION _Dir;//蛇头的⽅向默认是向右
 enum GAME_STATUS _Status;//游戏状态
 int _Socre;//当前获得分数
 int _foodWeight;//默认每个⻝物10分
 int _SleepTime;//每⾛⼀步休眠时间
}Snake, * pSnake;
//游戏开始前的初始化
void GameStart(pSnake ps);
//游戏运⾏过程
void GameRun(pSnake ps);
//游戏结束
void GameEnd(pSnake ps);
//设置光标的坐标
void SetPos(short x, short y);
//欢迎界⾯
void WelcomeToGame();
//打印帮助信息
void PrintHelpInfo();
//创建地图
void CreateMap();
//初始化蛇
void InitSnake(pSnake ps);
//创建⻝物
void CreateFood(pSnake ps);
//暂停响应
void pause();
//下⼀个节点是⻝物
int NextIsFood(pSnakeNode psn, pSnake ps);
//吃⻝物
void EatFood(pSnakeNode psn, pSnake ps);
//不吃⻝物
void NoFood(pSnakeNode psn, pSnake ps);
//撞墙检测
int KillByWall(pSnake ps);
//撞⾃⾝检测
int KillBySelf(pSnake ps);
//蛇的移动
void SnakeMove(pSnake ps);
//游戏初始化
void GameStart(pSnake ps);
//游戏运⾏
void GameRun(pSnake ps);
//游戏结束
void GameEnd(pSnake ps);
snake.cpp
#include "Snake.h"
//设置光标的坐标
void SetPos(short x, short y)
{
 COORD pos = { x, y };
 HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
 hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
 //设置标准输出上光标的位置为pos
 SetConsoleCursorPosition(hOutput, pos);
}
void WelcomeToGame()
{
 SetPos(40, 15);
 printf("欢迎来到贪吃蛇⼩游戏");
 SetPos(40, 25);//让按任意键继续的出现的位置好看点
 system("pause");
 system("cls");
 SetPos(25, 12);
 printf("⽤ ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");
 SetPos(25, 13);
 printf("加速将能得到更⾼的分数。\n");
 SetPos(40, 25);//让按任意键继续的出现的位置好看点
 system("pause");
 system("cls");
}
void CreateMap()
{
 int i = 0;
 //上(0,0)-(56, 0)
 SetPos(0, 0);
 for (i = 0; i < 58; i += 2)
 {
 wprintf(L"%c", WALL);
 }
 //下(0,26)-(56, 26)
 SetPos(0, 26);
 for (i = 0; i < 58; i += 2)
 {
 wprintf(L"%c", WALL);
 }
 //左
 //x是0,y从1开始增⻓
 for (i = 1; i < 26; i++)
 {
 SetPos(0, i);
 wprintf(L"%c", WALL);
 }
 //x是56,y从1开始增⻓
 for (i = 1; i < 26; i++)
 {
 SetPos(56, i);
wprintf(L"%c", WALL);
 }
 }


 void InitSnake(pSnake ps)
 {
 pSnakeNode cur = NULL;
 int i = 0;
 //创建蛇⾝节点,并初始化坐标
 //头插法
 for (i = 0; i < 5; i++)
 {
 //创建蛇⾝的节点
 cur = (pSnakeNode)malloc(sizeof(SnakeNode));
 if (cur == NULL)
 {
 perror("InitSnake()::malloc()");
 return;
 }
 //设置坐标
 cur->next = NULL;
 cur->x = POS_X + i * 2;
 cur->y = POS_Y;

 //头插法
 if (ps->_pSnake == NULL)
 {
 ps->_pSnake = cur;
 }
 else
 {
 cur->next = ps->_pSnake;
 ps->_pSnake = cur;
 }
 }

 //打印蛇的⾝体
 cur = ps->_pSnake;
 while (cur)
 {
 SetPos(cur->x, cur->y);
 wprintf(L"%c", BODY);
 cur = cur->next;
 }
//初始化贪吃蛇数据
 ps->_SleepTime = 200;
 ps->_Socre = 0;
 ps->_Status = OK;
 ps->_Dir = RIGHT;
 ps->_foodWeight = 10;
 }



 void CreateFood(pSnake ps)
 {
 int x = 0;
 int y = 0;

 again:
 //产⽣的x坐标应该是2的倍数,这样才可能和蛇头坐标对⻬。
 do
 {
 x = rand() % 53 + 2;
 y = rand() % 25 + 1;
 } while (x % 2 != 0);

 pSnakeNode cur = ps->_pSnake;//获取指向蛇头的指针
 //⻝物不能和蛇⾝冲突
 while (cur)
 {
 if (cur->x == x && cur->y == y)
 {
 goto again;
 }
 cur = cur->next;
 }

 pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); //创建⻝物
 if (pFood == NULL)
 {
 perror("CreateFood::malloc()");
 return;
 }
 else
 {
 pFood->x = x;
 pFood->y = y;
 SetPos(pFood->x, pFood->y);
 wprintf(L"%c", FOOD);
 ps->_pFood = pFood;
}
}
void PrintHelpInfo()
{
 //打印提⽰信息
 SetPos(64, 15);
 printf("不能穿墙,不能咬到⾃⼰\n");
 SetPos(64, 16);
 printf("⽤↑.↓.←.→分别控制蛇的移动.");
 SetPos(64, 17);
 printf("F3 为加速,F4 为减速\n");
 SetPos(64, 18);
 printf("ESC :退出游戏.space:暂停游戏.");
}
void pause()//暂停
{
 while (1)
 {
 Sleep(300);
 if (KEY_PRESS(VK_SPACE))
 {
 break;
 }
 }
}
//pSnakeNode psn 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
int NextIsFood(pSnakeNode psn, pSnake ps)
{
 return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}
//pSnakeNode psn 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
void EatFood(pSnakeNode psn, pSnake ps)
{
 //头插法
 psn->next = ps->_pSnake;
 ps->_pSnake = psn;
 pSnakeNode cur = ps->_pSnake;
 //打印蛇
 while (cur)
{
 SetPos(cur->x, cur->y);
 wprintf(L"%c", BODY);
 cur = cur->next;
 }
 ps->_Socre += ps->_foodWeight;
 free(ps->_pFood);
 CreateFood(ps);
}
//pSnakeNode psn 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
void NoFood(pSnakeNode psn, pSnake ps)
{
 //头插法
 psn->next = ps->_pSnake;
 ps->_pSnake = psn;
 pSnakeNode cur = ps->_pSnake;
 //打印蛇
 while (cur->next->next)
 {
 SetPos(cur->x, cur->y);
 wprintf(L"%c", BODY);
 cur = cur->next;
 }
 //最后⼀个位置打印空格,然后释放节点
 SetPos(cur->next->x, cur->next->y);
 printf(" ");
 free(cur->next);
 cur->next = NULL;
}
//pSnake ps 维护蛇的指针
int KillByWall(pSnake ps)
{
 if ((ps->_pSnake->x == 0)
 || (ps->_pSnake->x == 56)
 || (ps->_pSnake->y == 0)
 || (ps->_pSnake->y == 26))
 {
 ps->_Status = KILL_BY_WALL;
 return 1;
 }
 return 0;
}
//pSnake ps 维护蛇的指针
int KillBySelf(pSnake ps)
{
 pSnakeNode cur = ps->_pSnake->next;
 while (cur)
 {
 if ((ps->_pSnake->x == cur->x)
 && (ps->_pSnake->y == cur->y))
 {
 ps->_Status = KILL_BY_SELF;
 return 1;
 }
 cur = cur->next;
 }
 return 0;
}
void SnakeMove(pSnake ps)
{
 //创建下⼀个节点
 pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
 if (pNextNode == NULL)
 {
 perror("SnakeMove()::malloc()");
 return;
 }
 //确定下⼀个节点的坐标,下⼀个节点的坐标根据,蛇头的坐标和⽅向确定
 switch (ps->_Dir)
 {
 case UP:
 {
 pNextNode->x = ps->_pSnake->x;
 pNextNode->y = ps->_pSnake->y - 1;
 }
 break;
 case DOWN:
 {
 pNextNode->x = ps->_pSnake->x;
 pNextNode->y = ps->_pSnake->y + 1;
 }
 break;
 case LEFT:
 {
 pNextNode->x = ps->_pSnake->x - 2;
 pNextNode->y = ps->_pSnake->y;
}
 break;
 case RIGHT:
 {
 pNextNode->x = ps->_pSnake->x + 2;
 pNextNode->y = ps->_pSnake->y;
 }
 break;
 }
 //如果下⼀个位置就是⻝物
 if (NextIsFood(pNextNode, ps))
 {
 EatFood(pNextNode, ps);
 }
 else//如果没有⻝物
 {
 NoFood(pNextNode, ps);
 }
 KillByWall(ps);
 KillBySelf(ps);
}
void GameStart(pSnake ps)
{
 //设置控制台窗⼝的⼤⼩,30⾏,100列
 //mode 为DOS命令
 system("mode con cols=100 lines=30");
 //设置cmd窗⼝名称
 system("title 贪吃蛇"); 
 //获取标准输出的句柄(⽤来标识不同设备的数值)
 HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
 //影藏光标操作
 CONSOLE_CURSOR_INFO CursorInfo;
 GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
 CursorInfo.bVisible = false; //隐藏控制台光标
 SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
 //打印欢迎界⾯
 WelcomeToGame();
 //打印地图
 CreateMap();
//初始化蛇
 InitSnake(ps);
 //创造第⼀个⻝物
 CreateFood(ps);
}
void GameRun(pSnake ps)
{
 //打印右侧帮助信息
 PrintHelpInfo();
 do
 {
 SetPos(64, 10);
 printf("得分:%d ", ps->_Socre);
 printf("每个⻝物得分:%d分", ps->_foodWeight);
 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(VK_SPACE))
 {
 pause();
 }
 else if (KEY_PRESS(VK_ESCAPE))
 {
 ps->_Status = END_NOMAL;
 break;
 }
 else if (KEY_PRESS(VK_F3))
 {
 if (ps->_SleepTime >= 50)
 {
 ps->_SleepTime -= 30;
 ps->_foodWeight += 2;
}
 }
 else if (KEY_PRESS(VK_F4))
 {
 if (ps->_SleepTime < 350)
 {
 ps->_SleepTime += 30;
 ps->_foodWeight -= 2;
 if (ps->_SleepTime == 350)
 {
 ps->_foodWeight = 1;
 }
 }
 }
 //蛇每次⼀定之间要休眠的时间,时间短,蛇移动速度就快
 Sleep(ps->_SleepTime);
 SnakeMove(ps);
 } while (ps->_Status == OK);
}
void GameEnd(pSnake ps)
{
 pSnakeNode cur = ps->_pSnake;
 SetPos(24, 12);
 switch (ps->_Status)
 {
 case END_NOMAL:
 printf("您主动退出游戏\n");
 break;
 case KILL_BY_SELF:
 printf("您撞上⾃⼰了 ,游戏结束!\n");
 break;
 case KILL_BY_WALL:
 printf("您撞墙了,游戏结束!\n");
 break;
 }
 //释放蛇⾝的节点
 while (cur)
 {
 pSnakeNode del = cur;
 cur = cur->next;
 free(del);
 }
}

结束语

贪吃蛇博客就总结完了,有什么问题,欢迎各位大佬评论,总的来说,结合了C语言知识,数据结构知识,API函数方面有关函数等,只要思路顺清楚还是比较简单的。

OK,感谢观看!!!

​​​​​​​

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

nodejs工具模块学习

util 是一个Node.js 核心模块&#xff0c;提供常用函数的集合&#xff1b; util.inspect(object,[showHidden],[depth],[colors]) 是一个将任意对象转换 为字符串的方法&#xff0c;通常用于调试和错误输出&#xff1b; 如果只有一个参数 object&#xff0c;是要转换的对象&…

网工内推 | 兴业银行总行正编,科技运维部,硕士以上学历

01 兴业银行 招聘岗位&#xff1a;安全渗透专家 职责描述&#xff1a; 1.负责牵头组织本行红蓝对抗、攻防演练等工作&#xff1b; 2.负责牵头制定有效的渗透测试方案&#xff0c;开展对本行防御体系的验证工作&#xff1b; 3.负责牵头组织本行各类应用系统的渗透测试与漏洞扫…

java的Spring XML和注解解析深入理解

正文 熟悉IOC体系结构 要学习Spring源码&#xff0c;我们首先得要找准入口&#xff0c;那这个入口怎么找呢&#xff1f;我们不妨先思考一下&#xff0c;在Spring项目启动时&#xff0c;Spring做了哪些事情。这里我以最原始的xml配置方式来分析&#xff0c;那么在项目启动时&a…

大型网站系统架构演化实例_5.使用反向代理和CDN加速网站响应

1.使用反向代理和CDN加速网站响应 随着网站业务不断发展&#xff0c;用户规模越来越大&#xff0c;由于区域的差别使得网络环境异常复杂&#xff0c;不同地区的用户访问网站时&#xff0c;速度差别也极大。有研究表明&#xff0c;网站访问延迟和用户流失率正相关&#xff0c;网…

二叉检索树(定义、意义、存储数据元素形式),二叉检索树插入方法的图解和实现

1、二叉检索树&#xff1a; &#xff08;1&#xff09;定义 二叉检索树的任意一个结点&#xff0c;设其值为k&#xff0c;则该节点左子树中任意一个结点的值都小于k&#xff1b;该节点右子树中任意一个节点的值都大于或等于k 这里的比较规则可以是针对数字的&#xff0c;也可…

js实现抽奖效果

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>随机抽奖</title> </head> <body>…

synchronized锁升级原理

锁升级过程 jdk1.6之后的优化 synchronized锁有四种状态&#xff0c;无锁&#xff0c;偏向锁&#xff0c;轻量级锁&#xff0c;重量级锁&#xff0c;这几个状态会随着竞争状态逐渐升级&#xff0c;锁可以升级但不能降级&#xff0c;但是偏向锁状态可以被重置为无锁状态。 1、偏…

C++ 类和对象(终篇)

初始化列表 就是给我们每一个成员变量找了一个定义的位置&#xff0c;不然像const这样的成员不好处理 所有的成员能在初始化列表初始化的都在里面初始化 拷贝构造函数和构造函数都允许初始化 构造函数体中的语句只能将其称作为赋初值&#xff0c;而不能称作初始化。 因为初始…

牛客NC314 体育课测验(一)【中等 图,BFS,拓扑排序 Java,Go、PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/1a16c1b2d2674e1fb62ce8439e867f33 核心 图&#xff0c;BFS,拓扑排序&#xff0c;队列参考答案Java import java.util.*;public class Solution {/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修…

Scala 03 —— Scala Puzzle 拓展

Scala 03 —— Scala Puzzle 拓展 文章目录 Scala 03 —— Scala Puzzle 拓展一、占位符二、模式匹配的变量和常量模式三、继承 成员声明的位置结果初始化顺序分析BMember 类BConstructor 类 四、缺省初始值与重载五、Scala的集合操作和集合类型保持一致性第一部分代码解释第二…

浅浅了解一下 LibTorch

&#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ LibTorch 是 PyTorch 提供的一个二进制发行版&#xff0c;包含了所有必要的头文件、库和 CMake 配置文件&#xff0c;便于开发者依赖 PyTorch 开发应用。用户可以从 PyTorch 官网下载包含最新 LibTorch…

【科研】YOLOv8中anchor_points可视化(更新中)

目录 写在前面anchor-point可视化 写在前面 感叹一下&#xff1a;如果GPT能在我刚上大学的时候出来&#xff0c;也许我能学的比现在好太多&#xff0c;毕竟大学有一个比自己优秀太多的人引导着是多么地捷径。 anchor-point可视化

使用免费SSL证书安全吗,怎么获取

许多人可能会有疑问&#xff0c;使用免费的SSL证书真的安全吗&#xff1f;我们又该如何获取它们呢&#xff1f; 让我们简单了解一下什么是SSL证书&#xff1f;SSL证书是一种用于保障网络数据传输安全的小型数据文件。它通过在用户的浏览器与服务器之间建立一个加密的连接&…

常用UI组件

一、文本组件 1.1 概述 Text为文本组件&#xff0c;用于显示文字内容 1.2 参数 Text组件的参数类型为string | Resource Entry Component struct Index {build() {Column({space : 50}) {Text(你好).fontSize(50)}.width(100%).height(100%).justifyContent(FlexAlign.Cent…

使用Docker搭建一主二从的redis集群

文章目录 一、根据基础镜像构建三个docker容器二、构建master机三、配置slave机四、测试 本文使用 主机指代 物理机、 master机指代“一主二从”中的 一主&#xff0c; slave机指代“一主二从”中的 二从 一、根据基础镜像构建三个docker容器 根据本文第一章&#xff08…

Group Query Attention (GQA) 机制详解以及手动实现计算

Group Query Attention (GQA) 机制详解 1. GQA的定义 Grouped-Query Attention (GQA) 是对 Multi-Head Attention (MHA) 和 Multi-Query Attention (MQA) 的扩展。通过提供计算效率和模型表达能力之间的灵活权衡&#xff0c;实现了查询头的分组。GQA将查询头分成了G个组&#…

PE文件注入恶意代码改变代码执行顺序教程

原理&#xff1a;因为PE文件3个段都需要对齐200h&#xff0c;这样就会导致会有很多填充0&#xff0c;在填充0的地方就有机会进行插入恶意代码 在原有的PE文件的基础上修改&#xff0c;文件是我之前发布的一篇博客PE文件构造 主要思路&#xff1a;原来的PE文件中.text的有Mess…

2024面试软件测试,常见的面试题(上)

一、综合素质 1、自我介绍 面试官您好&#xff0c;我叫XXX&#xff0c;一直从事车载软件测试&#xff0c;负责最多的是中控方面。 以下是我的一些优势&#xff1a; 车载的测试流程我是熟练掌握的&#xff0c;且能够独立编写测试用例。 平时BUG提交会使用到Jira&#xff0c;类似…

CSS中的盒子模型

目录 盒子模型介绍 盒子模型组成 盒子边框 边框的基本使用 边框影响盒子大小 盒子内边距 内边距的基本使用 内边距影响盒子大小 内边距不影响盒子大小的情况 盒子外边距 外边距的基本使用 外边距的常见使用 外边距合并问题 相邻块元素垂直外边距的合并 嵌套块元…

Qt - 窗口

目录 1. 前言 2. 菜单栏(QMenuBar) 2.1. 创建菜单栏 2.1.1. 方式一 2.1.2. 方式二 2.2. 在菜单栏中添加菜单和创建菜单项 2.3. 在菜单项之间添加分割线 2.4. 综合示例 3. 工具栏(QToolBar) 3.1. 创建工具栏 3.2. 设置停靠位置 3.2.1. 方式一 3.2.2. 方式二 3.3. 设…