C语言(万字讲解,帮你再现经典游戏) | 贪吃蛇 —— 附完整代码可以直接copy运行游玩

贪吃蛇

引言

在计算机科学的世界里,经典游戏是无法被忽视的一部分。它们不仅是我们对计算机编程技术进行探索和学习的重要工具,也是我们与计算机交互的一种有趣方式。其中,贪吃蛇游戏(Snake Game)无疑是这些经典游戏中的一颗明珠,它简单而充满挑战,一直以来都备受欢迎。

贪吃蛇游戏的核心玩法简单而经典:控制一条蛇在有限的空间内移动,吃掉食物,不断成长,但要避免撞到墙壁或者自己的身体。尽管规则简单,但这个游戏却蕴含了丰富的编程技术和算法挑战。从基本的用户输入处理,到数据结构的应用,再到图形界面的展示,贪吃蛇游戏是一个极好的项目,让我们有机会探索和实践各种计算机科学的基础概念。

在本文中,我们将深入探讨如何使用C语言来实现贪吃蛇游戏。C语言作为一种高效而强大的编程语言,是实现游戏的理想选择。通过这个项目,我们不仅可以加深对C语言的理解,还能够锻炼自己的逻辑思维和问题解决能力。

在接下来的内容中,我们将逐步介绍贪吃蛇游戏的实现过程,从所需要的基本知识到游戏逻辑再到用户界面的设计,带领读者一步步走进这个有趣而充满挑战的编程世界。让我们一起开始这段奇妙的编程之旅吧!

目录

  • 贪吃蛇
    • 引言
    • 1. 游戏效果及基本功能
    • 2. 技术要点
      • 2.0 随机数的生成
        • 2.0.1 rand
        • 2.0.2 srand
        • 2.0.3 time
        • 2.0.4 设置随机数的范围
        • 2.1 Win32 API
        • 2.2 控制台程序
        • 2.3 控制台屏幕上的坐标 `COORD`
        • 2.4 GetStdHandle
        • 2.5 GetConsoleCursorInfo
        • 2.6 CONSOLE_CURSOR_INFO
        • 2.7 SetConsoleCursorInfo
        • 2.8 SetConsoleCursorPosition
        • 2.9 GetAsyncKeyState
        • 3. 贪吃蛇游戏设计和分析
          • 3.1 地图设计
            • 3.1.1 ``本地化
            • 3.1.2 类项
            • 3.1.3 setlocale函数
            • 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.1 蛇身移动`SnakeMove`
                  • 4.3.2 `NextIsFood`
                  • 4.3.3 `EatFood`
                  • 4.3.4 `NoFood`
                  • 4.3.5 `KillByWall`
                  • 4.3.6`KillBySelf`
                  • 4.4 游戏结束(GameEnd)
                  • 5. 参考代码

                    1. 游戏效果及基本功能

                    使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇。

                    游戏画面展示:

                    需要实现的基本功能:

                    1. 贪吃蛇地图控制
                    2. 蛇吃食物的功能(上,下,左,右方向键控制蛇的动作)
                    3. 蛇撞墙死亡
                    4. 蛇撞自身死亡
                    5. 计算得分
                    6. 蛇身加速、减速
                    7. 暂停游戏

                    2. 技术要点

                    要想实现贪吃蛇游戏首先我们需要掌握:

                    C语言随机数的生成、函数、枚举、 结构体、动态内存管理、预处理指令、链表、Win32API 等。

                    这些知识点在我的前几期博客都有介绍。

                    2.0 随机数的生成

                    2.0.1 rand

                    C语言提供了一个函数叫 rand,这函数是可以生成随机数,函数原型如下所示:

                    int rand (void);
                    

                    rand函数会返回一个伪随机数,这个随机数的范围是在0~RAND_MAX之间,这个RAND_MAX的大小是

                    依赖编译器上实现的,但是大部分编译器上是32767。

                    rand函数的使用需要包含一个头文件是:stdlib.h

                    我们现在通过一个示例来了解一下什么是伪随机数。

                    示例

                    #include #include int main()
                    {
                     printf("%d\n", rand());
                     printf("%d\n", rand());
                     printf("%d\n", rand());
                     printf("%d\n", rand());
                     printf("%d\n", rand());
                     return 0;
                    }
                    

                    运行两次之后,看到结果我们会发现,两次产生的随机数序列竟然是一样的,所以说伪随机数不是真正的随机数,是通过某种算法生成的随机数。真正的随机数的是无法预测下一个值是多少的。而rand函数是对一个叫“种子”的基准值进行运算生成的随机数。

                    之所以前面每次运行程序产生的随机数序列是一样的,那是因为rand函数⽣成随机数的默认种子是1。

                    如果要生成不同的随机数,就要让种子是变化的。


                    2.0.2 srand

                    C语言中又提供了一个函数叫 srand,用来初始化随机数的生成器的,srand的原型如下:

                    void srand (unsigned int seed);
                    

                    srand函数是不需要频繁调用的,一次运行的程序中调用一次就够了。

                    程序中在调用rand 函数之前先调用 srand 函数,通过 srand 函数的参数seed来设置rand函数生成随机数的时候的种子,只要种子在变化,每次生成的随机数序列也就变化起来了。

                    因此,我们接下来介绍一个函数time来帮助我们实现“种子随机”。

                    2.0.3 time

                    在程序中我们一般是使用程序运行的时间作为种子,因为时间时刻在发生变化的。

                    在C语言中有一个函数叫 time ,就可以获得这个时间,time函数原型如下:

                    time_t time (time_t* timer);
                    

                    time 函数会返回当前的日历时间,其实返回的是1970年1月1日0时0分0秒到现在程序运行时间之间的差值,单位是秒。返回的类型是time_t类型的,time_t 类型本质上其实就是32位或者64位的整型类型。

                    time函数的参数 timer 如果是非NULL指针的话,函数也会将这个返回的差值放在timer指向的内存中带回去。

                    如果 timer 是NULL,就只返回这个时间的差值。time函数返回的这个时间差也被叫做:时间戳。

                    time函数的时候需要包含头文件:time.h

                    2.0.4 设置随机数的范围

                    如果要生成a~b的随机数,方法如下:

                    a + rand()%(b-a+1)
                    

                    接下来介绍实现贪吃蛇会用到的一些Win32 API知识

                    2.1 Win32 API

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

                    32位平台的应用程序编程接口。

                    2.2 控制台程序

                    平常我们运行起来的黑框程序其实就是控制台程序。

                    如果大家显示的是终端,可以通过这样设置来打开控制台程序。

                    只有这样才能正常运行贪吃蛇游戏!


                    我们可以使用cmd命令来设置控制台窗口的长宽

                    示例 :设置控制台窗口的大小,30行,100列。

                    mode con cols=100 lines=30
                    

                    参考:mode指令

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

                    示例 :命名为贪吃蛇

                    title 贪吃蛇
                    

                    参考: title命令

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

                    示例

                    #includeint 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.4 GetStdHandle

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

                    HANDLE GetStdHandle(DWORD nStdHandle);
                    

                    示例

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

                    STD_OUTPUT_HANDLE 是一个预定义的常量,表示标准输出流。通过调用 GetStdHandle(STD_OUTPUT_HANDLE),就可以获取到标准输出流的句柄,并将其赋值给变量 hOutput,以便后续在程序中使用。


                    2.5 GetConsoleCursorInfo

                    检查有关制定控制台屏幕缓冲区的光标大小和可见性的信息。

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

                    示例

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

                    2.6 CONSOLE_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; //隐藏控制台光标
                      

                      2.7 SetConsoleCursorInfo

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

                      BOOL WINAPI SetConsoleCursorInfo(
                       HANDLE hConsoleOutput,
                       const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
                      );
                      

                      示例

                      HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);// 获得标准输出设备的句柄
                      //隐藏光标操作
                      CONSOLE_CURSOR_INFO CursorInfo; //定义了一个光标信息的结构体变量,名为CursorInfo
                      GetConsoleCursorInfo(hOutput, &CursorInfo);//获取和hOutpot句柄相关的控制台上的光标信息,并存放在CursorInfo
                      CursorInfo.bVisible = false; //隐藏控制台光标
                      SetConsoleCursorInfo(hOutput, &CursorInfo);//设置和hOutpot句柄相关的控制台上的光标状态
                      

                      2.8 SetConsoleCursorPosition

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

                      BOOL WINAPI SetConsoleCursorPosition(
                       HANDLE hConsoleOutput,
                       COORD pos
                      );
                      

                      示例

                       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.9 GetAsyncKeyState

                      获取按键情况,GetAsyncKeyState的函数原型如下:

                      SHORT GetAsyncKeyState(
                       int vKey
                      );
                      

                      将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。

                      GetAsyncKeyState 的返回值是short类型,在上一次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。

                      如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1,来确定。

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

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

                      我们可以参考键码表输入参数来使用GetAsyncKeyState函数判断键是否被按下

                      示例:检测数字键

                      #include #include 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 和宽字符的输入和输出函数,加入了头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。

                      3.1.1 本地化

                      提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。

                      在标准中,依赖地区的部分有以下几项:

                      • 数字量的格式
                      • 货币量的格式
                      • 字符集
                      • 日期和时间的表示形式
                        3.1.2 类项

                        通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏,指定一个类项:

                        • LC_COLLATE:影响字符串比较函数 strcoll()和 strxfrm() 。
                        • LC_CTYPE:影响字符处理函数的行为。
                        • LC_MONETARY:影响货币格式。
                        • LC_NUMERIC:影响 printf() 的数字格式。
                        • LC_TIME:影响时间格式 strftime() 和 wcsftime() 。
                        • LC_ALL :针对所有类项修改,将以上所有类别设置为给定的语言环境。

                          参考: 每个类项的详细说明


                          3.1.3 setlocale函数
                          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 #includeint main() {
                           	setlocale(LC_ALL, "");
                           	wchar_t ch1 = L'●';
                           	wchar_t ch2 = L'⽐';
                           	wchar_t ch3 = L'特';
                           	wchar_t ch4 = L'★';
                           
                           	printf("%c%c\n", 'a', 'b');
                           
                           	wprintf(L"%lc\n", ch1);
                           	wprintf(L"%lc\n", ch2);
                           	wprintf(L"%lc\n", ch3);
                           	wprintf(L"%lc\n", ch4);
                          	return 0;
                          }
                          

                          输出结果为:

                          从输出的结果来看,我们发现⼀个普通字符占一个字符的位置。但是打印一个汉字字符,占用2个字符的位置,那么我们如果要在贪吃蛇中使用宽字符,就得处理好地图上坐标的计算。

                          普通字符和宽字符打印宽度的展示如下:


                          3.1.5 地图坐标

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

                          如下:

                          3.2 蛇身和食物

                          初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。

                          注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半儿出现在墙体中,另外一般在墙外的现象,坐标不好对齐。

                          关于食物,就是在墙体内随机生成一个坐标 (x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。


                          3.3 数据结构设计

                          在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行,所以蛇节点结构如下:

                          //snake.h文件
                          //蛇身的节点类型
                          typedef struct SnakeNode
                          {
                          	//坐标
                          	int x;
                          	int y;
                          	//指向下一个节点的指针
                          	struct SnakeNode* next;
                          }SnakeNode, * pSnakeNode;   //对于这个结构体变量定义一个指针变量pSnakeNode
                          //typedef struct SnakeNode* pSnakeNode;   //这样写也可以
                          

                          要管理整条贪吃蛇,我们再封装一下Snake的结构来维护整条贪吃蛇。

                          //snake.h文件
                          //贪吃蛇
                          typedef struct Snake
                          {
                          	pSnakeNode _pSnake;//指向蛇头的指针
                          	pSnakeNode _pFood;//指向食物节点的指针
                          	enum DIRECTION _dir;//蛇的方向,默认向右
                          	enum GAME_STATUS _status;//游戏的状态
                          	int _food_weight;//一个食物的分数
                          	int _score;      //总成绩
                          	int _sleep_time; //每一步的休眠时间,时间越短,速度越快,时间越长,速度越慢
                          }Snake, * pSnake;
                          

                          蛇的方向可以使用枚举来一一列举。

                          //snake.h文件
                          //蛇的方向
                          enum DIRECTION
                          {
                          	UP = 1,
                          	DOWN,
                          	LEFT,
                          	RIGHT
                          };
                          

                          游戏状态也是通过枚举来一一列举。

                          //snake.h文件
                          //蛇的状态
                          //正常、撞墙、撞到自己、正常退出
                          enum GAME_STATUS
                          {
                          	OK, //正常
                          	KILL_BY_WALL, //撞墙
                          	KILL_BY_SELF, //撞到自己
                          	END_NORMAL //正常退出
                          };
                          

                          3.4 游戏流程设计


                          4. 核心逻辑实现分析

                          4.1 游戏主逻辑

                          程序开始就设置程序支持本模式,然后进入游戏的主逻辑。

                          主逻辑一共分为3个过程:

                          • 游戏开始(GameStart)完成游戏的初始化
                          • 游戏运行(GameRun)完成游戏运行逻辑的实现
                          • 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放
                            //test.c  文件
                            #include //本地化头文件
                            void test()
                            {
                            	int ch = 0;
                            	do
                            	{
                            		system("cls");      		//清空窗口
                            		//创建贪吃蛇
                            		Snake snake = { 0 };
                            		//初始化游戏
                            		//1. 打印环境界面
                            		//2. 功能介绍
                            		//3. 绘制地图
                            		//4. 创建蛇
                            		//5. 创建食物
                            		//6. 设置游戏的相关信息
                            		GameStart(&snake);
                            		//运行游戏
                            		GameRun(&snake);
                            		
                            		//检测是否有按键被按下—— 因为控制台的本身机制,如果在游玩时按上键,控制台会把历史命令显示出来。因此使用系统调用函数来拿走按键
                            		while (_kbhit())
                            		{
                            			// 使用 _getch() 获取按下的键,不阻塞程序
                            			_getch();
                            			// 处理按键事件,可以根据需要进行相应的操作
                            		}
                            		
                            		//结束游戏 - 善后工作
                            		GameEnd(&snake);
                            		//打印是否进入下一局的引导语
                            		SetPos(20, 15);					//定位
                            		printf("再来一局吗?(Y/N):");
                            		ch = getchar();    //获取玩家输入
                            		while (getchar() != '\n');  //防止输入过多无用字符导致程序崩溃
                            	} while (ch=='Y' || ch=='y');
                            	SetPos(0, 27);						//将进程结束提示至于窗口最下方
                            }
                            int main()
                            {
                            	//设置适配本地环境
                            	setlocale(LC_ALL, "");
                            	srand((unsigned int)time(NULL));    //只需初始化一次的随机种子用于随机数
                            	test();
                            	return 0;
                            }
                            

                            4.2 游戏开始(GameStart)

                            这个模块完成游戏的初始化任务:

                            • 控制台窗口大小的设置
                            • 控制台窗口名字的设置
                            • 鼠标光标的隐藏
                            • 打印欢迎界面
                            • 创建地图
                            • 初始化蛇身
                            • 创建第一个食物
                              //snake.c 文件
                              void GameStart(pSnake ps)
                              {
                              	//0. 先设置窗口的大小,再光标隐藏
                              	system("mode con cols=100 lines=30");
                              	
                              	system("title 贪吃蛇");  //设置窗口标题
                              	HANDLE houtput= GetStdHandle(STD_OUTPUT_HANDLE);   //获得句柄
                              	
                              	//影藏光标操作
                              	CONSOLE_CURSOR_INFO CursorInfo;
                              	GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息
                              	CursorInfo.bVisible = false; //隐藏控制台光标
                              	SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态
                              	//1. 打印环境界面和功能介绍
                              	WelcomeToGame();
                              	//2. 绘制地图
                              	CreateMap();
                              	//3. 创建蛇
                              	InitSnake(ps);
                              	//4. 创建食物
                              	CreateFood(ps);
                              }
                              

                              4.2.1 打印欢迎界面

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

                              首先为我们的界面创建做些准备——创建一个位置函数

                              使用这个函数,我们就可以在控制台窗口的任意位置放入我们想要插入的信息。

                              //snake.c文件
                              void SetPos(short x, short y)
                              {
                              	//获得标准输出设备的句柄
                              	HANDLE houtput = NULL;
                              	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
                              	//定位光标的位置
                              	COORD pos = { x, y };
                              	SetConsoleCursorPosition(houtput, pos);
                              }
                              

                              然后在指定位置插入我们的提示信息

                              //snake.c文件
                              void WelcomeToGame()
                              {
                              	SetPos(40, 14);
                              	wprintf(L"欢迎来到贪吃蛇小游戏\n");
                              	
                              	SetPos(42, 20);
                              	system("pause");    //按任意键继续......    按下后会切换页面
                              	system("cls");      //清空上一页面的窗口信息
                              	
                              	SetPos(25, 14);
                              	wprintf(L"用 ↑. ↓ . ← . → 来控制蛇的移动,按F3加速,F4减速\n");
                              	
                              	SetPos(25, 15);
                              	wprintf(L"加速能够得到更高的分数\n");
                              	SetPos(42, 20);
                              	system("pause");    //按任意键继续......    按下后会切换页面
                              	system("cls");		//清空上一页面的窗口信息
                              }
                              

                              4.2.2 创建地图

                              创建地图就是将墙打印出来,因为是宽字符打印,所有使用wprintf函数,打印格式串前使用L。打印地图的关键是要算好坐标,才能在想要的位置打印墙体。

                              墙体打印宽字符:

                              //snake.h文件
                              //在这里我们直接将常用的几个宽字符常量进行定义
                              #define WALL L'□'
                              

                              坐标的计算

                              上:(0,0)到(56,0)

                              下:(0,26)到(56,26)

                              左:(0,1)到(0,25)

                              右:(56,1)到(56,25)

                              创建地图函数CreateMap

                              //snake.c 文件
                              void CreateMap()
                              {
                              	//上
                              	int i = 0;
                              	SetPos(0, 0);	//上(0,0)-(56, 0)
                              	for (i = 0; i < 29; i++)
                              	{
                              		wprintf(L"%lc", WALL);
                              	}
                              	//下
                              	SetPos(0, 26);	//下(0,26)-(56, 26)
                              	for (i = 0; i < 29; i++)
                              	{
                              		wprintf(L"%lc", WALL);
                              	}
                              	//左
                              	for (i = 1; i <= 25; i++)
                              	{
                              		SetPos(0, i);	//x是0,y从1开始增⻓
                              		wprintf(L"%lc", WALL);
                              	}
                              	//右
                              	for (i = 1; i <= 25; i++)
                              	{
                              		SetPos(56, i);	//x是56,y从1开始增⻓
                              		wprintf(L"%lc", WALL);
                              	}
                              }
                              

                              4.2.3 初始化蛇身

                              蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每⼀节打印在屏幕上。

                              • 蛇的初始位置从 (24,5) 开始。

                                再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,每个食物的分数。

                              • 游戏状态是:OK
                              • 蛇的移动速度:200毫秒
                              • 蛇的默认方向:RIGHT
                              • 初始成绩:0
                              • 每个食物的分数:10

                                蛇身打印的宽字符:

                                //snake.h文件
                                //在这里我们直接将常用的几个宽字符常量进行定义
                                #define BODY L'●'
                                

                                初始化蛇身函数:InitSnake

                                //snake.c 文件
                                void InitSnake(pSnake ps)  //初始化蛇身需要将蛇的结构指针传给函数
                                {
                                	int i = 0;
                                	pSnakeNode cur = NULL;
                                	//创建蛇⾝节点,并初始化坐标
                                	//头插法
                                	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 + 2 * i;
                                		cur->y = POS_Y;
                                		//头插法插入链表
                                		if (ps->_pSnake == NULL) //空链表
                                		{
                                			ps->_pSnake = cur;
                                		}
                                		else //非空
                                		{
                                			cur->next = ps->_pSnake;
                                			ps->_pSnake = cur;
                                		}
                                	}
                                	//打印贪吃蛇
                                	cur = ps->_pSnake;  //将蛇头地址给cur
                                	while (cur)
                                	{
                                		SetPos(cur->x, cur->y);	//给定位置
                                		wprintf(L"%lc", BODY);
                                		cur = cur->next;
                                	}
                                	//设置贪吃蛇的属性
                                	ps->_dir = RIGHT;//默认向右
                                	ps->_score = 0;
                                	ps->_food_weight = 10;
                                	ps->_sleep_time = 200;//单位是毫秒,休眠时间是蛇每走一步中间间隔的时间
                                	ps->_status = OK;
                                }
                                

                                4.2.4 创建第一个食物
                                • 先随机生成食物的坐标

                                  ◦ x坐标必须是2的倍数

                                  ◦ 食物的坐标不能和蛇身每个节点的坐标重复

                                • 创建食物节点,打印食物

                                  食物打印的宽字符:

                                  //snake.h文件
                                  //在这里我们直接将常用的几个宽字符常量进行定义
                                  #define FOOD L'★'
                                  

                                  创建食物的函数:CreateFood

                                  //snake.c 文件
                                  void CreateFood(pSnake ps)
                                  {
                                  	int x = 0;
                                  	int y = 0;
                                  	//生成x坐标需是2的倍数
                                  	//x:2~54
                                  	//y: 1~25
                                  	
                                  again:
                                  	do
                                  	{
                                  		x = rand() % 53 + 2; 	//根据合适的坐标范围随机生成食物坐标
                                  		y = rand() % 25 + 1;
                                  	} while (x % 2 != 0);		//判断x坐标是否是2的倍数,不是就进入循环重新生成坐标
                                  	//x和y的坐标不能和蛇的身体坐标冲突
                                  	pSnakeNode cur = ps->_pSnake;	//将指向蛇头的指针赋给cur
                                  	while (cur)
                                  	{
                                  		if (x == cur->x && y == cur->y)	//判断是否和身体坐标发生冲突
                                  		{
                                  			goto again;					//如果冲突,就返回到again标点重新生成坐标
                                  		}
                                  		cur = cur->next;
                                  	}							//到这里随机生成的食物坐标就通过检查了
                                  	//创建食物的节点
                                  	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));  //为食物节点申请空间
                                  	if (pFood == NULL)		//防止空间申请失败
                                  	{
                                  		perror("CreateFood()::malloc()");
                                  		return;
                                  	}
                                  	
                                  	//将生成的坐标赋给食物节点
                                  	pFood->x = x;		
                                  	pFood->y = y;
                                  	pFood->next = NULL;  //让食物节点的next指向空,达到食物节点独立。
                                  	SetPos(x, y);//定位位置
                                  	wprintf(L"%lc", FOOD);
                                  	ps->_pFood = pFood;		//将食物信息赋给食物指针
                                  }
                                  

                                  4.3 游戏运行(GameRun)

                                  游戏运行期间,右侧打印帮助信息提示玩家,坐标(64, 15)。

                                  PrintfHelpInfo

                                  //snake.c 文件
                                  void PrintHelpInfo()
                                  {
                                  	SetPos(64, 5);
                                  	wprintf(L"%ls", L"按空格开始游戏!");
                                  	SetPos(64, 14);
                                  	wprintf(L"%ls", L"不能穿墙,不能咬到自己");
                                  	SetPos(64, 15);
                                  	wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动");
                                  	SetPos(64, 16);
                                  	wprintf(L"%ls", L"按F3加速,F4减速");
                                  	SetPos(64, 17);
                                  	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
                                  	SetPos(64, 18);
                                  	wprintf(L"%ls", L"版权归Jason所有");
                                  }
                                  

                                  根据游戏状态检查游戏是否继续,如果状态是OK,游戏继续,否则游戏结束。

                                  如果游戏继续,就检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。

                                  需要的虚拟按键的罗列:

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

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

                                    //snake.c文件
                                    void GameRun(pSnake ps)
                                    {
                                    	//在窗口右侧打印帮助信息
                                    	PrintHelpInfo();
                                    	do
                                    	{
                                    		//打印总分数和食物的分值
                                    		SetPos(64, 10);
                                    		printf("总分数:%d\n", ps->_score);
                                    		SetPos(64, 11);
                                    		printf("当前食物的分数:%2d\n", ps->_food_weight);
                                    		
                                    		//检测按键是否被按下,以及防止按键冲突
                                    		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_NORMAL;    //将游戏状态设为正常退出
                                    		}
                                    		else if (KEY_PRESS(VK_F3))
                                    		{
                                    			//加速
                                    			if (ps->_sleep_time > 80)    //当休眠时间被缩短到80ms时,速度已经足够快了
                                    			{
                                    				ps->_sleep_time -= 30;   //减少休眠时间
                                    				ps->_food_weight += 2;   //增加食物的分数,最高分是20分
                                    			}
                                    		} 
                                    		else if (KEY_PRESS(VK_F4))
                                    		{
                                    			//减速
                                    			if (ps->_food_weight > 2)  //食物分数已经降到了最低标准
                                    			{
                                    				ps->_sleep_time += 30;  //增加休眠时间
                                    				ps->_food_weight -= 2;  //减少食物的分数
                                    			}
                                    		}
                                    		
                                    		SnakeMove(ps);//蛇走一步的过程的函数
                                    		Sleep(ps->_sleep_time);  //蛇每次到达一定状态,就要进行休眠
                                    	} while (ps->_status==OK);   //只有游戏状态是ok,才会再次进入循环重复进行检测,使游戏进行下去。
                                    }
                                    

                                    封装宏来检测按键状态——低位为1为按下,0为未按下

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

                                    暂停函数Pause

                                    //snake.c文件
                                    void Pause()
                                    {
                                    	while (1)
                                    	{
                                    		Sleep(200);					//暂停200毫秒
                                    		if (KEY_PRESS(VK_SPACE))	//如果没有按下空格,则会反复暂停200毫秒
                                    		{
                                    			break;
                                    		}
                                    	}
                                    }
                                    
                                    4.3.1 蛇身移动SnakeMove

                                    先创建下一个节点,根据移动方向和蛇头的坐标,确定了下一个位置后,看下一个位置是否是食物(NextIsFood),是食物就做吃食物处理(EatFood),如果不是食物则做前进一步的处理(NoFood)。

                                    蛇身移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上自己蛇身(KillBySelf),从而影响游戏的状态。

                                    //snake.c文件
                                    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.2 NextIsFood
                                    //snake.c 文件
                                    //pSnakeNode pn 是下⼀个节点的地址
                                    //pSnake ps 贪吃蛇的指针
                                    int NextIsFood(pSnakeNode pn, pSnake ps)
                                    {
                                    	return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
                                    }
                                    
                                    4.3.3 EatFood
                                    //snake.c 文件
                                    //pSnakeNode pn 是下⼀个节点的地址
                                    //pSnake ps 贪吃蛇的指针
                                    void EatFood(pSnakeNode pn, pSnake ps)
                                    {
                                    	//头插法
                                    	ps->_pFood->next = ps->_pSnake;
                                    	ps->_pSnake = ps->_pFood;
                                    	//释放下一个位置的节点
                                    	free(pn);
                                    	pn = NULL;
                                    	pSnakeNode cur = ps->_pSnake;
                                    	//打印蛇
                                    	while (cur)
                                    	{
                                    		SetPos(cur->x, cur->y);
                                    		wprintf(L"%lc", BODY);
                                    		cur = cur->next;
                                    	}
                                    	ps->_score += ps->_food_weight;  //增加食物获得总分
                                    	//重新创建食物
                                    	CreateFood(ps);
                                    }
                                    
                                    4.3.4 NoFood

                                    将下一个节点头插入蛇的身体,并将之前蛇身最后一个节点打印为空格(遮盖之前的蛇尾,要不然蛇走过的会形成轨迹),释放掉蛇身的最后一个节点。

                                    易错点:这里最容易错误的是,释放最后一个结点后,还得将指向在最后一个结点的指针改为NULL,保证蛇尾打印可以正常结束,不会越界访问。

                                    //snake.c 文件
                                    void NoFood(pSnakeNode pn, pSnake ps)
                                    {
                                    	//头插法
                                    	pn->next = ps->_pSnake;
                                    	ps->_pSnake = pn;
                                    	pSnakeNode cur = ps->_pSnake;
                                    	while (cur->next->next != NULL)
                                    	{
                                    		SetPos(cur->x, cur->y);
                                    		wprintf(L"%lc", BODY);
                                    		cur = cur->next;
                                    	}
                                    	//把最后一个结点打印成空格
                                    	SetPos(cur->next->x, cur->next->y);
                                    	printf("  ");
                                    	//释放最后一个结点
                                    	free(cur->next);
                                    	//把倒数第二个节点的地址置为NULL
                                    	cur->next = NULL;
                                    }
                                    
                                    4.3.5 KillByWall

                                    判断蛇头的坐标是否和墙的坐标发生冲突。

                                    //snake.c 文件
                                    void 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;    //将游戏状态改为:撞墙死
                                    	}
                                    }
                                    
                                    4.3.6KillBySelf
                                    //snake.c 文件
                                    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.4 游戏结束(GameEnd)

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

                                    //snake.c 文件
                                    void GameEnd(pSnake ps)
                                    {
                                    	SetPos(24, 12);
                                    	switch (ps->_status)    //根据最后游戏结束的不同状态打印信息
                                    	{
                                    	case END_NORMAL:
                                    		wprintf(L"您主动结束游戏\n");
                                    		break;
                                    	case KILL_BY_WALL:
                                    		wprintf(L"您撞到墙上,游戏结束\n");
                                    		break;
                                    	case KILL_BY_SELF:
                                    		wprintf(L"您撞到了自己,游戏结束\n");
                                    		break;
                                    	}
                                    	//释放蛇身的链表
                                    	pSnakeNode cur = ps->_pSnake;
                                    	while (cur)
                                    	{
                                    		pSnakeNode del = cur;
                                    		cur = cur->next;
                                    		free(del);
                                    	}
                                    }
                                    

                                    5. 参考代码

                                    snake.h 文件

                                    #pragma once
                                    #include #include #include #include #include #define POS_X 24
                                    #define POS_Y 5
                                    #define WALL L'□'
                                    #define BODY L'●'
                                    #define FOOD L'★'
                                    //类型的声明
                                    //蛇的方向
                                    enum DIRECTION
                                    {
                                    	UP = 1,
                                    	DOWN,
                                    	LEFT,
                                    	RIGHT
                                    };
                                    //蛇的状态
                                    //正常、撞墙、撞到自己、正常退出
                                    enum GAME_STATUS
                                    {
                                    	OK, //正常
                                    	KILL_BY_WALL, //撞墙
                                    	KILL_BY_SELF, //撞到自己
                                    	END_NORMAL //正常退出
                                    };
                                    //蛇身的节点类型
                                    typedef struct SnakeNode
                                    {
                                    	//坐标
                                    	int x;
                                    	int y;
                                    	//指向下一个节点的指针
                                    	struct SnakeNode* next;
                                    }SnakeNode, * pSnakeNode;   //对于这个结构体变量定义一个指针变量pSnakeNode
                                    //typedef struct SnakeNode* pSnakeNode;   //这样写也可以
                                    //贪吃蛇
                                    typedef struct Snake
                                    {
                                    	pSnakeNode _pSnake;//指向蛇头的指针
                                    	pSnakeNode _pFood;//指向食物节点的指针
                                    	enum DIRECTION _dir;//蛇的方向,默认向右
                                    	enum GAME_STATUS _status;//游戏的状态
                                    	int _food_weight;//一个食物的分数
                                    	int _score;      //总成绩
                                    	int _sleep_time; //每一步的休眠时间,时间越短,速度越快,时间越长,速度越慢
                                    }Snake, * pSnake;
                                    //函数的声明
                                    //定位光标位置
                                    void SetPos(short x, short y);
                                    //游戏的初始化
                                    void GameStart(pSnake ps);
                                    //欢迎界面的打印
                                    void WelcomeToGame();
                                    //创建地图
                                    void CreateMap();
                                    void CreateMap();
                                    //初始化蛇身
                                    void InitSnake(pSnake ps);
                                    //创建食物
                                    void CreateFood(pSnake ps);
                                    //游戏运行的逻辑
                                    void GameRun(pSnake ps);
                                    //蛇的移动-走一步
                                    void SnakeMove(pSnake ps);
                                    //判断下一个坐标是否是食物
                                    int NextIsFood(pSnakeNode pn, pSnake ps);
                                    //下一个位置是食物,就吃掉食物
                                    void EatFood(pSnakeNode pn, pSnake ps);
                                    //下一个位置不是食物
                                    void NoFood(pSnakeNode pn, pSnake ps);
                                    //检测蛇是否撞墙
                                    void KillByWall(pSnake ps);
                                    //检测蛇是否撞到自己
                                    void KillBySelf(pSnake ps);
                                    //游戏善后的工作
                                    void GameEnd(pSnake ps);
                                    

                                    snake.c 文件

                                    #define _CRT_SECURE_NO_WARNINGS 1
                                    #include "snake.h"
                                    //GAMESTART
                                    //控制台窗口大小的设置
                                    //控制台窗口名字的设置
                                    //鼠标光标的隐藏
                                    //打印欢迎界面
                                    //创建地图
                                    //初始化蛇身
                                    //创建第一个食物
                                    void GameStart(pSnake ps)
                                    {
                                    	//0. 先设置窗口的大小,再光标隐藏
                                    	system("mode con cols=100 lines=30");
                                    	system("title 贪吃蛇");  //设置窗口标题
                                    	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);   //获得句柄
                                    	//影藏光标操作
                                    	CONSOLE_CURSOR_INFO CursorInfo;
                                    	GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息
                                    	CursorInfo.bVisible = false; //隐藏控制台光标
                                    	SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态
                                    	//1. 打印环境界面和功能介绍
                                    	WelcomeToGame();
                                    	//2. 绘制地图
                                    	CreateMap();
                                    	//3. 创建蛇
                                    	InitSnake(ps);
                                    	//4. 创建食物
                                    	CreateFood(ps);
                                    }
                                    //位置函数
                                    void SetPos(short x, short y)
                                    {
                                    	//获得标准输出设备的句柄
                                    	HANDLE houtput = NULL;
                                    	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
                                    	//定位光标的位置
                                    	COORD pos = { x, y };
                                    	SetConsoleCursorPosition(houtput, pos);
                                    }
                                    //1. 打印环境界面和功能介绍
                                    void WelcomeToGame()
                                    {
                                    	SetPos(40, 14);
                                    	wprintf(L"欢迎来到贪吃蛇小游戏\n");
                                    	SetPos(42, 20);
                                    	system("pause");    //按任意键继续......    按下后会切换页面
                                    	system("cls");      //清空上一页面的窗口信息
                                    	SetPos(25, 14);
                                    	wprintf(L"用 ↑. ↓ . ← . → 来控制蛇的移动,按F3加速,F4减速\n");
                                    	SetPos(25, 15);
                                    	wprintf(L"加速能够得到更高的分数\n");
                                    	SetPos(42, 20);
                                    	system("pause");    //按任意键继续......    按下后会切换页面
                                    	system("cls");		//清空上一页面的窗口信息
                                    }
                                    //2. 绘制地图
                                    void CreateMap()
                                    {
                                    	//上
                                    	int i = 0;
                                    	SetPos(0, 0);	//上(0,0)-(56, 0)
                                    	for (i = 0; i < 58; i+= 2)
                                    	{
                                    		wprintf(L"%lc", WALL);
                                    	}
                                    	//下
                                    	SetPos(0, 26);	//下(0,26)-(56, 26)
                                    	for (i = 0; i < 58; i+=2)
                                    	{
                                    		wprintf(L"%lc", WALL);
                                    	}
                                    	//左
                                    	for (i = 1; i <= 25; i++)
                                    	{
                                    		SetPos(0, i);	//x是0,y从1开始增⻓
                                    		wprintf(L"%lc", WALL);
                                    	}
                                    	//右
                                    	for (i = 1; i <= 25; i++)
                                    	{
                                    		SetPos(56, i);	//x是56,y从1开始增⻓
                                    		wprintf(L"%lc", WALL);
                                    	}
                                    }
                                    //3. 创建蛇
                                    void InitSnake(pSnake ps)  //初始化蛇身需要将蛇的结构指针传给函数
                                    {
                                    	int i = 0;
                                    	pSnakeNode cur = NULL;
                                    	//创建蛇⾝节点,并初始化坐标
                                    	//头插法
                                    	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 + 2 * i;
                                    		cur->y = POS_Y;
                                    		//头插法插入链表
                                    		if (ps->_pSnake == NULL) //空链表
                                    		{
                                    			ps->_pSnake = cur;
                                    		}
                                    		else //非空
                                    		{
                                    			cur->next = ps->_pSnake;
                                    			ps->_pSnake = cur;
                                    		}
                                    	}
                                    	//打印贪吃蛇
                                    	cur = ps->_pSnake;  //将蛇头地址给cur
                                    	while (cur)
                                    	{
                                    		SetPos(cur->x, cur->y);	//给定位置
                                    		wprintf(L"%lc", BODY);
                                    		cur = cur->next;
                                    	}
                                    	//设置贪吃蛇的属性
                                    	ps->_dir = RIGHT;//默认向右
                                    	ps->_score = 0;
                                    	ps->_food_weight = 10;
                                    	ps->_sleep_time = 200;//单位是毫秒,休眠时间是蛇每走一步中间间隔的时间
                                    	ps->_status = OK;
                                    }
                                    //4. 创建食物
                                    void CreateFood(pSnake ps)
                                    {
                                    	int x = 0;
                                    	int y = 0;
                                    	//生成x坐标需是2的倍数
                                    	//x:2~54
                                    	//y: 1~25
                                    again:
                                    	do
                                    	{
                                    		x = rand() % 53 + 2; 	//根据合适的坐标范围随机生成食物坐标
                                    		y = rand() % 25 + 1;
                                    	} while (x % 2 != 0);		//判断x坐标是否是2的倍数,不是就进入循环重新生成坐标
                                    	//x和y的坐标不能和蛇的身体坐标冲突
                                    	pSnakeNode cur = ps->_pSnake;	//将指向蛇头的指针赋给cur
                                    	while (cur)
                                    	{
                                    		if (x == cur->x && y == cur->y)	//判断是否和身体坐标发生冲突
                                    		{
                                    			goto again;					//如果冲突,就返回到again标点重新生成坐标
                                    		}
                                    		cur = cur->next;
                                    	}							//到这里随机生成的食物坐标就通过检查了
                                    	//创建食物的节点
                                    	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));  //为食物节点申请空间
                                    	if (pFood == NULL)		//防止空间申请失败
                                    	{
                                    		perror("CreateFood()::malloc()");
                                    		return;
                                    	}
                                    	//将生成的坐标赋给食物节点
                                    	pFood->x = x;
                                    	pFood->y = y;
                                    	pFood->next = NULL;  //让食物节点的next指向空,达到食物节点独立。
                                    	SetPos(x, y);//定位位置
                                    	wprintf(L"%lc", FOOD);
                                    	ps->_pFood = pFood;		//将食物信息赋给食物指针
                                    }
                                    //GAMERUN
                                    //打印帮助信息
                                    void PrintHelpInfo()
                                    {
                                    	SetPos(64, 5);
                                    	wprintf(L"%ls", L"按空格开始游戏!");
                                    	SetPos(64, 14);
                                    	wprintf(L"%ls", L"不能穿墙,不能咬到自己");
                                    	SetPos(64, 15);
                                    	wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动");
                                    	SetPos(64, 16);
                                    	wprintf(L"%ls", L"按F3加速,F4减速");
                                    	SetPos(64, 17);
                                    	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
                                    	SetPos(64, 18);
                                    	wprintf(L"%ls", L"版权归Jason所有");
                                    }
                                    //封装宏来检测按键状态——低位为1为按下,0为未按下
                                    #define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
                                    //暂停函数
                                    void Pause()
                                    {
                                    	while (1)
                                    	{
                                    		Sleep(200);					//暂停200毫秒
                                    		if (KEY_PRESS(VK_SPACE))	//如果没有按下空格,则会反复暂停200毫秒
                                    		{
                                    			break;
                                    		}
                                    	}
                                    }
                                    //游戏运行
                                    void GameRun(pSnake ps)
                                    {
                                    	//在窗口右侧打印帮助信息
                                    	PrintHelpInfo();
                                    	do
                                    	{
                                    		//打印总分数和食物的分值
                                    		SetPos(64, 10);
                                    		printf("总分数:%d\n", ps->_score);
                                    		SetPos(64, 11);
                                    		printf("当前食物的分数:%2d\n", ps->_food_weight);
                                    		//检测按键是否被按下,以及防止按键冲突
                                    		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_NORMAL;    //将游戏状态设为正常退出
                                    		}
                                    		else if (KEY_PRESS(VK_F3))
                                    		{
                                    			//加速
                                    			if (ps->_sleep_time > 80)    //当休眠时间被缩短到80ms时,速度已经足够快了
                                    			{
                                    				ps->_sleep_time -= 30;   //减少休眠时间
                                    				ps->_food_weight += 2;   //增加食物的分数,最高分是20分
                                    			}
                                    		}
                                    		else if (KEY_PRESS(VK_F4))
                                    		{
                                    			//减速
                                    			if (ps->_food_weight > 2)  //食物分数已经降到了最低标准
                                    			{
                                    				ps->_sleep_time += 30;  //增加休眠时间
                                    				ps->_food_weight -= 2;  //减少食物的分数
                                    			}
                                    		}
                                    		SnakeMove(ps);//蛇走一步的过程的函数
                                    		Sleep(ps->_sleep_time);  //蛇每次到达一定状态,就要进行休眠
                                    	} while (ps->_status == OK);   //只有游戏状态是ok,才会再次进入循环重复进行检测,使游戏进行下去。
                                    }
                                    
                                    //蛇身运动检测
                                    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);
                                    }
                                    //判断下一个坐标是否是食物
                                    //pSnakeNode pn 是下⼀个节点的地址
                                    //pSnake ps 贪吃蛇的指针
                                    int NextIsFood(pSnakeNode pn, pSnake ps)
                                    {
                                    	return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
                                    }
                                    //下一个位置是食物,就吃掉食物
                                    //pSnakeNode pn 是下⼀个节点的地址
                                    //pSnake ps 贪吃蛇的指针
                                    void EatFood(pSnakeNode pn, pSnake ps)
                                    {
                                    	//头插法
                                    	ps->_pFood->next = ps->_pSnake;
                                    	ps->_pSnake = ps->_pFood;
                                    	//释放下一个位置的节点
                                    	free(pn);
                                    	pn = NULL;
                                    	pSnakeNode cur = ps->_pSnake;
                                    	//打印蛇
                                    	while (cur)
                                    	{
                                    		SetPos(cur->x, cur->y);
                                    		wprintf(L"%lc", BODY);
                                    		cur = cur->next;
                                    	}
                                    	ps->_score += ps->_food_weight;  //增加食物获得总分
                                    	//重新创建食物
                                    	CreateFood(ps);
                                    }
                                    //下一个位置不是食物
                                    void NoFood(pSnakeNode pn, pSnake ps)
                                    {
                                    	//头插法
                                    	pn->next = ps->_pSnake;
                                    	ps->_pSnake = pn;
                                    	pSnakeNode cur = ps->_pSnake;
                                    	while (cur->next->next != NULL)
                                    	{
                                    		SetPos(cur->x, cur->y);
                                    		wprintf(L"%lc", BODY);
                                    		cur = cur->next;
                                    	}
                                    	//把最后一个结点打印成空格
                                    	SetPos(cur->next->x, cur->next->y);
                                    	printf("  ");
                                    	//释放最后一个结点
                                    	free(cur->next);
                                    	//把倒数第二个节点的地址置为NULL
                                    	cur->next = NULL;
                                    }
                                    //检测蛇是否撞墙
                                    void 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;    //将游戏状态改为:撞墙死
                                    	}
                                    }
                                    //检测蛇是否撞到自己
                                    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;    //通过函数遍历蛇身所有节点来检测蛇头是否撞到蛇身
                                    	}
                                    }
                                    //游戏善后的工作
                                    void GameEnd(pSnake ps)
                                    {
                                    	SetPos(24, 12);
                                    	switch (ps->_status)    //根据最后游戏结束的不同状态打印信息
                                    	{
                                    	case END_NORMAL:
                                    		wprintf(L"您主动结束游戏\n");
                                    		break;
                                    	case KILL_BY_WALL:
                                    		wprintf(L"您撞到墙上,游戏结束\n");
                                    		break;
                                    	case KILL_BY_SELF:
                                    		wprintf(L"您撞到了自己,游戏结束\n");
                                    		break;
                                    	}
                                    	//释放蛇身的链表
                                    	pSnakeNode cur = ps->_pSnake;
                                    	while (cur)
                                    	{
                                    		pSnakeNode del = cur;
                                    		cur = cur->next;
                                    		free(del);
                                    	}
                                    }
                                    

                                    test.c 文件

                                    #define _CRT_SECURE_NO_WARNINGS 1
                                    #include#include#include #include "snake.h"
                                    #include //本地化头文件
                                    void test()
                                    {
                                    	int ch = 0;
                                    	do
                                    	{
                                    		system("cls");      		//清空窗口
                                    		//创建贪吃蛇
                                    		Snake snake = { 0 };
                                    		//初始化游戏
                                    		//1. 打印环境界面
                                    		//2. 功能介绍
                                    		//3. 绘制地图
                                    		//4. 创建蛇
                                    		//5. 创建食物
                                    		//6. 设置游戏的相关信息
                                    		GameStart(&snake);
                                    		//运行游戏
                                    		GameRun(&snake);
                                    		//检测是否有按键被按下—— 因为控制台的本身机制,如果在游玩时按上键,控制台会把历史命令显示出来。因此使用系统调用函数来拿走按键
                                    		while (_kbhit())
                                    		{
                                    			// 使用 _getch() 获取按下的键,不阻塞程序
                                    			_getch();
                                    			// 处理按键事件,可以根据需要进行相应的操作
                                    		}
                                    		
                                    		//结束游戏 - 善后工作
                                    		GameEnd(&snake);
                                    		//打印是否进入下一局的引导语
                                    		SetPos(20, 15);					//定位
                                    		printf("再来一局吗?(Y/N):");
                                    		ch = getchar();
                                    		while (getchar() != '\n');
                                    	} while (ch == 'Y' || ch == 'y');
                                    	SetPos(0, 27);						//将进程结束提示至于窗口最下方
                                    }
                                    int main()
                                    {
                                    	//设置适配本地环境
                                    	setlocale(LC_ALL, "");
                                    	srand((unsigned int)time(NULL));    //只需初始化一次的随机种子用于随机数
                                    	test();
                                    	return 0;
                                    }