当前位置: 首页 > news >正文

C语言项目实践——贪吃蛇

手把手教兄弟们如何写贪吃蛇

目录

一、游戏背景

二、课程目标

三、课程定位

四、技术要点

五、相关Win32API介绍

5.1 Win32API

5.2 控制台程序(Console)

5.3 控制台屏幕上的坐标COORD

5.4 GetStdHandle

5.5 GetConsoleCursorInfo 

5.6 SetConsoleCursorInfo

5.7 SetConsoleCursorPosition

5.8 GetAsyncKetState

六、贪吃蛇游戏设计与分析

6.1 地图

6.1.1  本地化

6.1.2 类项

6.1.3 setlocale函数

6.1.4 宽字符打印

6.1.5 地图坐标

6.2 蛇身和食物 

6.3 数据结构设计

6.4 游戏流程设计 

七、核心逻辑实现分析

7.1 游戏主逻辑

7.2 GameStart函数的实现

7.2.1 控制台窗口大小和标题的设置

7.2.2 鼠标光标的隐藏

7.2.3 打印欢迎界面

7.2.4 创建地图

7.2.5  初始化蛇

7.2.6 创建第一个食物

7.3 GameRun函数的实现

7.3.1 打印游戏的帮助信息

 7.3.2 打印分数 

​7.3.3 检测按键

7.3.4 蛇移动SnakeMove

7.3.4.1 判断下一个节点是不是食物NextIsFood

7.3.4.2 下一个节点是食物EatFood

7.3.4.3 下一个节点不是食物NoFood

7.3.4.4  检测蛇是否撞墙

7.3.4.5  检测蛇是否撞到自己

7.3.4.6 SnakeMove的整体代码

7.4 GameEnd函数的实现

7.4.1 判断是哪一种情况导致游戏结束

7.4.2 释放蛇身的链表

八、问题和后续

九、代码

9.1 snakke.c部分

9.2 snake.h部分

9.3 test.c部分


一、游戏背景

贪吃蛇是久负盛名的游戏,它也和俄罗斯方块,扫雷等游戏位列经典游戏的行列。

在编程语言的教学中,我们以贪吃蛇为例,从设计到代码实现来提升学生的编程能力和逻辑能力。

二、课程目标

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

实现基本的功能:

  • 贪吃蛇地图绘制
  • 蛇吃物的功能 (上、下、左、右向键控制蛇的动作)
  • 蛇撞墙死亡
  • 蛇撞自身死亡
  • 计算得分
  • 蛇加速、减速
  • 暂停游戏

    三、课程定位

    • 对编程的兴趣
    • 对C语语法做⼀个基本的巩固。
    • 对游戏开发有兴趣的同学做⼀个启发。
    • 适合:C语⾔学完的同学,有⼀定的代码能,初步接触数据结构中的链表

    四、技术要点

    C语函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等。

    五、相关Win32API介绍

    本次实现贪吃蛇会使到的⼀些Win32 API知识,接下来我们就学习⼀下。

    5.1 Win32API

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

    5.2 控制台程序(Console)

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

    我们可以使用cmd命令来设置控制台窗口的长宽:设置控制台窗口的大小,30行,100列

    mode  con cols=100 lines=30

    我们按win+r键输出cmd,打开控制台程序,然后输出上面的控制台命令,我们的控制台窗口就会变成100列和30行。如下所示:

    我们这个控制台的颜色为黑色,可以右键点开控制台数据来进行改变背景颜色为黑色。如下所示:

    我们在序号1位置右键点开属性,然后到序号2点击颜色, 到序号3选择屏幕背景,然后选择序号4的灰色,最后点击确认,然后再次打开控制台,控制台程序就是灰色的了。改成灰色之后,控制台程序可能上面看不到任何的字体,这是因为屏幕背景颜色是灰色,屏幕字体也是灰色,所以我们要将屏幕字体改成黑色,最后点击确认。如下所示:

    最后我们的控制台就变成这样了:

     因为我们是做贪吃蛇的小游戏,我们不想要让控制台显示命令提示符,如下所示:

    我们可以使用 title 贪吃蛇来改变标题,如下所示:

     这些都是在控制台执行的命令,我们如何在VS上使用这些命令呢?

    答案是:我们可以使用C语言函数system函数来执行,使用system函数需要引用头文件<stdlib.h>如下所示:

    #include<stdio.h>
    #include<stdlib.h>
    int main() {//设置控制台相关程序system("mode con cols=30 lines=30");system("title 贪吃蛇");return 0;
    }

    我们运行上述代码,控制台程序就会变成30行30列,标题变成贪吃蛇,但是我们运行时候会发现,控制台程序是30行30列,但是标题不是贪吃蛇,这是为什么呢?这是因为我们运行代码后,程序已经结束了,所以就不会显示贪吃蛇了,我们可以在return前加入getchar()或者system("pause")来暂停程序,不让程序结束。如下所示:

    #include<stdio.h>
    #include<stdlib.h>
    int main() {//设置控制台相关程序system("mode con cols=100 lines=30");system("title 贪吃蛇");system("pause");return 0;
    }

    运行如下所示: 

    5.3 控制台屏幕上的坐标COORD

    把基础的背景设置完成之后,我们需要在控制台屏幕上输出贪吃蛇的信息,食物信息,和字幕信息,这个时候我们就需要使用到坐标的概念了,控制台屏幕上每一个小方框都有一个坐标,如下所示:

    控制台屏幕坐标轴如下所示:

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

    COORD类型的声明如下所示:

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

    使用这个COORD类型要包含头文件windows.h。

    假设我们现在要找下面这个点的信息,就可以定义一个COORD类型的变量来代表这个位置

    COORD pos={30,6}   //假设这个点的坐标是(30,6);

    5.4 GetStdHandle

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

    例如:这个贪吃蛇需要在控制台上打印一些信息,我们就需要拿到这个控制台程序的句柄,有了这个句柄,就可以来操作这个控制台程序了。

    函数原型如下:

    HANDLE  GetStdHandle(DWORD nStdHandle);

    使用这个程序需要一个参数,参数有如下三种选择:

    我们需要使用屏幕信息,所以就需要传第二个参数。

    同时这个函数也有一个返回值,返回的是这个屏幕的句柄,有了句柄,我们就可以来操作这个屏幕了。

    实例如下:

    #include<windows.h>
    int main() {//获得标准输出设备的句柄HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//需要引用头文件windows.hreturn 0;
    }

    5.5 GetConsoleCursorInfo 

    在我们使用system("pause")或者getchar()后,我们需要输入任意一个字符继续,运行控制台程序后,有一个光标会一直闪烁,会导致游戏的视觉不太好,所以就需要使用这个函数GetConsoleCursorInfo来设置这个光标的信息。

    GetConsoleCursorInfo函数就是检索有关指定控制台屏幕缓冲区的光标大学和可见性信息。

    函数原型如下:

    BOOL WINAPI GetConsoleCursorInfo (
            HANDLE hConsoleOutput,
            PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
    );

    这个函数有两个参数

    第一个参数就是HANDLE,就是要给这个函数一个控制台输出窗口的句柄。表示获取哪一个控制台程序的光标信息

    第二次参数是PCONSOLE_CURSOR_INFO ,它是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标(光标的信息)。这个结构如下所示:

    typedef struct _CONSOLE_CURSOR_INFO {

            DWORD dwSize;  //由光标填充的字符单位百分比

            BOOL  bVisible;//光标的可见性,如果光标可见,值为TRUE。

    } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

    实例如下:

    #include<stdio.h>
    #include<stdlib.h>
    #include<windows.h>
    int main() {//获得标准输出设备的句柄HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//需要引用头文件windows.h//创建CONSOLE_CURSOR_INFO类型的变量CONSOLE_CURSOR_INFO cursor_info = { 0 };//使用GetConsoleCursorInfo函数来获取屏幕的光标信息。GetConsoleCursorInfo(houtput, &cursor_info);//打印光标信息printf("%d\n", cursor_info.dwSize);system("pause");return 0;
    }

    输出如下所示:

    这个输出25是指,这个光标占整个字符单元格高度的25%。 

     我们使用以下代码来改变这个光标的信息:

    #include<stdio.h>
    #include<stdlib.h>
    #include<windows.h>
    int main() {//获得标准输出设备的句柄HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//需要引用头文件windows.h//创建CONSOLE_CURSOR_INFO类型的变量CONSOLE_CURSOR_INFO cursor_info = { 0 };//使用GetConsoleCursorInfo函数来获取屏幕的光标信息。GetConsoleCursorInfo(houtput, &cursor_info);cursor_info.dwSize = 50;//打印光标信息printf("%d\n", cursor_info.dwSize);system("pause");return 0;
    }

    我们运行后发现这个光标的高度占比是50%,但是实际上我们在控制台上的光标高度还是25%,如下所示:

    因此我们就需要使用5.6的SetConsoleCursorInfo函数来设置光标信息。

    5.6 SetConsoleCursorInfo

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

    函数原型如下所示:

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

    第一个参数还是句柄,表示我们要设置哪一个控制台屏幕的光标信息。

    第二个参数还是指向 CONSOLE_CURSOR_INFO 结构的指针。

    这两个参数与GetConsoleCursorInfo函数的参数一样。

    实例如下所示:

    #include<stdio.h>
    #include<stdlib.h>
    #include<windows.h>
    int main() {//获得标准输出设备的句柄HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//需要引用头文件windows.h//创建CONSOLE_CURSOR_INFO类型的变量CONSOLE_CURSOR_INFO cursor_info = { 0 };//使用GetConsoleCursorInfo函数来获取屏幕的光标信息。GetConsoleCursorInfo(houtput, &cursor_info);//修改光标的百分比高度cursor_info.dwSize = 50;//设置光标信息SetConsoleCursorInfo(houtput, &cursor_info);//打印光标信息printf("%d\n", cursor_info.dwSize);system("pause");return 0;
    }

    运行代码如下所示:

    我们可以发现,这个光标的百分比高度从25%到50%了。

    我们在控制台中并不想显示这个光标信息,我们就可以修改cursor_info结构体的第二个变量,将其改为false;

    如下所示:

    #include<stdio.h>
    #include<stdlib.h>
    #include<windows.h>
    #include<stdbool.h>
    int main() {//获得标准输出设备的句柄HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//需要引用头文件windows.h//创建CONSOLE_CURSOR_INFO类型的变量CONSOLE_CURSOR_INFO cursor_info = { 0 };//使用GetConsoleCursorInfo函数来获取屏幕的光标信息。GetConsoleCursorInfo(houtput, &cursor_info);//修改光标可见度cursor_info.bVisible = false;//使用false需要包含头文件<stdbool.h>//设置光标信息SetConsoleCursorInfo(houtput, &cursor_info);system("pause");return 0;
    }

    运行结果如下所示:

    我们可以发现那个光标看不见了。 

    5.7 SetConsoleCursorPosition

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

    函数原型如下所:

    BOOL WINAPI SetConsoleCursorPosition (
            HANDLE hConsoleOutput,
            COORD pos
    );

    第一个参数是句柄,表示要操作哪个控制台。

    第二个参数是COORD类型的结构体。

    实例如下:

    #include<stdio.h>
    #include<stdlib.h>
    #include<windows.h>
    int main() {//获取标准输出设备的句柄HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//定义光标的位置COORD pos = { 10,6 };SetConsoleCursorPosition(houtput, pos);system("pause");return 0;
    }

    运行结果如下:

     我们可以发现光标的信息改变了。坐标(10,6)位置是从“请”开始的,然后依次向后打印。

    在后续打印实物,蛇和中文信息,会经常重新定位光标的位置。所以我们可以将定位光标位置封装成一个函数。如下所示:

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

    5.8 GetAsyncKetState

    获取按键情况,我们会使用上下左右等键来控制蛇的移动,我们就可以使用这个函数来获取我们按了哪一个键。

    GetAsyncKetState函数原型如下:

    SHORT GetAsyncKeyState ( int vKey);

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

    虚拟键值链接如下:Virtual-Key 代码 (Winuser.h) - Win32 apps | Microsoft Learn

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

    如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1.我们可以来用define来定义以下宏,如果该键被按过,返回1,否则返回0。

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

    实例:检测数字键

    #include<stdio.h>
    #include<windows.h>
    #define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
    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;
    }

     如果我们按哪一个数字键,该数字键就会被打印到屏幕上。

    六、贪吃蛇游戏设计与分析

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

    6.1.1 <locale.h>本地化

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

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

    1.         数字量的格式
      1.         货币量的格式
        1.         字符集
          1.         日期和时间的表示形式

    6.1.2 类项

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

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

    6.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, " ");//切换到本地环境

    6.1.4 宽字符打印

    那如果想在屏幕上打印宽字符,怎么打印呢?

    宽字符的字面量必须加上前缀“L”,否则 C 语言会把字面量当作窄字符类型处理。前缀“L”在单引号前面,表⽰宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前面,表⽰宽字符串,对应wprintf() 的占位符为 %ls

    实例:宽字符的打印

    #include<stdio.h>
    #include<locale.h>
    int main() {setlocale(LC_ALL, "");//切换本地环境char a = 'a';char b = 'b';printf("%c%c", a, b);wchar_t wc1 = L'比';wchar_t wc2 = L'特';wprintf(L"\n%lc\n%lc\n", wc1,wc2);wprintf(L"%lc\n", L'●');return 0;
    }

    输出结果如下所示:

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

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

    6.1.5 地图坐标

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

    6.2 蛇身和食物 

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

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

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

    6.3 数据结构设计

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

    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;

    蛇的方向可以一 一列举出来,所以我们选择枚举类型,如下所示:

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

    同样的,游戏的状态也可以一 一列举出来,也可以使用枚举类型,如下所示:

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

    6.4 游戏流程设计 

     

    七、核心逻辑实现分析

    我们设置三个文件

    test.c:游戏的测试

    snack.c:游戏的实现

    snack.h:游戏的函数声明和类型声明

    7.1 游戏主逻辑

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

    主逻辑分为3个过程:

            游戏开始(GameStart)完成游戏的初始化

            游戏运行(GameRun)完成游戏运行逻辑的实现 

            游戏结束(GameEnd)完成游戏结束的说明,实现资源释放

    首先设置适配本地环境,如下所示:

     set_locale(LC_ALL,""); //需要引用头文件#include<locale.h>

     三个框架如下所示:

    #include"snack.h"//完成游戏的测试逻辑
    void test() {//创建贪吃蛇Snake snake = { 0 };//初始化游戏:// 1打印环境界面,2功能介绍,3绘制地图,4创建蛇,5创建食物,6设置游戏相关信息GameStart(&snake);//运行游戏GameRun(&snake);//结束游戏--善后工作GameEnd(&snake);
    }
    int main() {//设置适配本地环境set_locale(LC_ALL, "");//需要引用头文件locale.htest();return 0;
    }

    7.2 GameStart函数的实现

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

    •         1:控制台窗口大小和标题的设置       
      •         2:鼠标光标的隐藏
        •         3:打印欢迎界面
          •         4:创建地图
            •         5:初始化蛇
              •         6:创建第⼀个食物
              • 7.2.1 控制台窗口大小和标题的设置

              • 	//1:控制台窗口大小和标题的设置system("mode con cols=100 lines=30");//使用system,需要包含头文件<stdlib.h>system("title 贪吃蛇");

                7.2.2 鼠标光标的隐藏

              • //2:鼠标光标的隐藏
                HANDLE houtput=GetStdHandle(STD_OUTPUT_HANDLE);//使用这个函数需要包含头文件<windows.h>
                CONSOLE_CURSOR_INFO CursorInfo;
                GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息
                CursorInfo.bVisible = false;//隐藏控制台光标,使用false需要包含头文件<stdbool.h>
                SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态

                7.2.3 打印欢迎界面

              • 我们写一个WelcomeToGame的函数,来实现起始三张地图的打印:
              • 首先我们要在控制台屏幕中间打字,就需要重新定位光标,我们写一个SetPos函数来重新定位光标,如下所示:
              • void SetPos(short x, short y) {//获得标准输出设备的句柄HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//使用这个函数需要包含头文件<windows.h>//定位光标位置COORD pos = { x,y }; //使用这个COORD类型要包含头文件windows.h。SetConsoleCursorPosition(houtput, pos);//使用这个函数需要包含头文件<windows.h>
                }

                第一个欢迎界面,代码如下:

              • //打印欢迎界面
                void WelcomeToGame() {//第一个界面的打印SetPos(40, 14);printf("欢迎来到贪吃蛇小游戏\n");SetPos(42, 20);//第二个界面的打印}
              • 我们运行代码之后,如下所示:

              • 这个输出表示程序已经运行结束了,我们不想要程序结束, 所以我们在在后面加上一个system(“pause”),如下所示:

              • //打印欢迎界面
                void WelcomeToGame() {//第一个界面的打印SetPos(40, 14);printf("欢迎来到贪吃蛇小游戏\n");SetPos(42, 20);system("pause");//第二个界面的打印}

                运行如下所示:

              • 第一个界面已经成功的输出了,那我们现在如何打印第二张界面呢?

              • 代码如下所示:

              • //打印欢迎界面
                void WelcomeToGame() {//第一个界面的打印SetPos(40, 14);printf("欢迎来到贪吃蛇小游戏\n");SetPos(42, 20);system("pause");//第二个界面的打印system("cls");//清空屏幕SetPos(25, 14);wprintf(L"用↑.↓.←.→来控制蛇的移动,按F3加速,F4减速\n");SetPos(35, 15);wprintf(L"加速能得到更高的分数\n");SetPos(37, 17);system("pause");system("cls");
                }
              • 7.2.4 创建地图

              • 使用CreateMap函数来创建地图。我们实现⼀个棋盘27行,58列的棋盘。如下所示:
              • 我们创建上和下墙体的时候,由于光标是默认左向右的,所以我们只需要定位一次光标就可以了,但是我们创建左和右墙体时候,需要光标向下移动,所以我们每一次打印墙体都需要重新定位,如下所示:
                //4:创建地图,实现⼀个棋盘27⾏,58列的棋盘
                void CreateMap() {// 上//SetPos(0,0),可以不用写,因为光标默认从(0,0)开始的int i = 0;for (i = 0; i < 29; i++) {wprintf(L"%lc", WALL);}// 下SetPos(0, 26);for (i = 0; i < 29; i++) {wprintf(L"%lc", WALL);}// 左for (i = 1; i < 26; i++) {SetPos(0, i);wprintf(L"%lc", WALL);}// 右for (i = 1; i < 26; i++) {SetPos(56, i);wprintf(L"%lc", WALL);}getchar();//不想让程序结束,写到后面可以删掉,也可能后面实现其他函数的时候,加在后面
                }

                WALL是我们使用#define定义的一个常量,此外我们还定义的其他的两个常量如下所示:

              • #define WALL L'□'
                #define BODY L'●'
                #define FOOD L'★'

              • 运行结果如下:棋盘成功的创建了

    7.2.5  初始化蛇

    蛇最开始长度为5节,每节对应链表的⼀个节点,蛇身的每⼀个节点都有自己的坐标。

    创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每⼀节打印在屏幕上。

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

    如下所示:

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

    •         游戏状态是:OK
      •         蛇的移动速度:200毫秒
        •         蛇的默认方向:RIGHT
          •         初始成绩:0
            •         每个食物的分数:10
          • 我们使用InitSnake函数来完成上面任务。代码如下,同时将CreateMap函数中的getchar()放到InitSnake函数后面.
          • //初始化蛇
            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;while (cur) {SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//设置贪吃蛇的属性ps->_Dir = RIGHT;//方向默认向右ps->_foodWeight = 10;ps->_SleepTime = 200;ps->_Socre = 0;ps->_Status = OK;getchar();
            }

    运行结果如下:

    7.2.6 创建第一个食物

    我们使用CreateFood函数来创建第一个食物,我们的食物也是一个SnakeNode类型的节点,食物的位置必须在棋盘内部,不能是贪吃蛇的节点的位置,同时食物的x轴坐标必须是2的倍数,然后打印食物。使用rand函数来生成x,y。如下所示:

    同时将InitSnake函数中的getchar()放到CreateFood函数后面.

    //6:创建第⼀个食物
    void CreateFood(pSnake ps) {int x = 0;//x的取值范围2到54int y = 0;//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;while (cur) {if (x == cur->x && y == cur->y) {goto 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;//打印食物SetPos(x, y);wprintf(L"%lc", FOOD);ps->_pFood = pfood;getchar();}

    运行如下所示:

    7.3 GameRun函数的实现

    游戏运行期间,右侧打印帮助信息,提升玩家。

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

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

    7.3.1 打印游戏的帮助信息

    使用PrinthelpInfo函数完成此操作,将CreateFood函数中的getchar()放到PrintHelpInfo函数后面。如下所示:

    //打印帮助信息
    void PrintHelpInfo() {SetPos(64, 14);printf("不能穿墙,不能咬到自己");SetPos(64, 15);printf("用↑.↓.←.→来控制蛇的移动\n");SetPos(64, 16);printf("按F3加速,F4减速");SetPos(64, 17);printf("按ESC退出游戏,按空格暂停游戏");getchar();
    }

    运行如下所示:

     7.3.2 打印分数 

    如下所示:

    void GameRun(pSnake ps) {//打印帮助信息PrintHelpInfo();do{//打印总分数和食物的分值SetPos(64, 10);printf("总分数:%d", ps->_Socre);SetPos(64, 11);printf("当前食物的分数:%d", ps->_foodWeight);} while (ps->_Status==OK);}

    运行如下所示:

    7.3.3 检测按键

    检测按键状态,我们封装了一个宏,如下所示:

    # define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
    需要的虚拟按键的罗列:
    • 上: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);SetPos(64, 11);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_NORMAL;}else if (KEY_PRESS(VK_F3)) {//加速//每按一次休眠减少30ms,不能无限加速,加一次速,食物加2分if (ps->_SleepTime > 80) {ps->_SleepTime -= 30;ps->_foodWeight += 2;}}else if(KEY_PRESS(VK_F4)){//减速//每按一次休眠增加30ms,不能无限减速,减一次速,食物减2分if (ps->_foodWeight > 2) {ps->_SleepTime += 30;ps->_foodWeight -= 2;}}//贪吃蛇走一步} while (ps->_Status==OK);}

    7.3.4 蛇移动SnakeMove

    蛇移动需要确定方向,根据方向来确定下一步走哪去,并且还要确定下一步是不是食物,如果是食物就吃掉食物,并且生成新的食物,更新分数,如果不是食物就需要重新调整蛇的位置,还要检测蛇走一步会不会撞墙,会不会撞到自己。

    为了方便操作,我们把蛇的下一步位置存起来,定义一个新的节点,如果下一步不是食物,就把这个节点头插到蛇节点中并且把最后一个节点释放,切记还要在这个蛇尾打印一个空字符。如果是食物就把食物这个节点插入蛇。

    创建新节点代码如下:

    //蛇移动
    void SnackMove(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;}}
    7.3.4.1 判断下一个节点是不是食物NextIsFood

    如果下一步不是食物,返回0。

    如果下一步是食物,返回1。

    代码如下所示:

    //判断下一个坐标是不是食物
    int NextIsFood(pSnakeNode pn, pSnake ps) {return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
    }
    7.3.4.2 下一个节点是食物EatFood

    如果下一个节点是食物,我们就使用头插法将这个节点插入到贪吃蛇。将下一个节点位置信息插入蛇之后,就可以释放下一个节点了,蛇吃点食物之后,蛇会变长,再打印一下蛇。然后吃到食物会加分数。最后在重新创建食物;

    代码如下所示:

    //下一个位置是食物,吃掉食物
    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->_Socre = ps->_Socre + ps->_foodWeight;//重新创建新的食物CreateFood(ps);
    }
    7.3.4.3 下一个节点不是食物NoFood

    当下一个节点不是食物时,我们同样的将下一个节点头插到蛇中,然后释放最后一个节点,并且在最后一个位置打印空字符“ ”,如果不打印空字符,蛇走一步就会变长,尾巴没有处理。

    代码如下:

    //下一个位置不是食物
    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);cur->next = NULL;}
    7.3.4.4  检测蛇是否撞墙

    当蛇移动一次后,可能会撞墙,我们写一个KillByWall函数来检测蛇是否撞墙。

    代码如下:

    //检测蛇是否撞墙
    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;}
    }
    7.3.4.5  检测蛇是否撞到自己

    当蛇移动一次后,也有可能会撞到自己,我们写一个KillBySelf函数来检测蛇是否撞到自己。

    代码如下:

    //检测蛇是否撞到自己
    void 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;break;}cur = cur->next;}
    }
    
    7.3.4.6 SnakeMove的整体代码
    //蛇移动
    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);}

    7.4 GameEnd函数的实现

    当蛇撞墙,撞到自己或者主动选择ESC退出,游戏就会结束,结束之后我们就需要释放贪吃蛇的信息。

    7.4.1 判断是哪一种情况导致游戏结束

    我们首先使用一个switch来判断是哪一种情况导致游戏结束,代码如下:

    //结束游戏
    void GameEnd(pSnake ps) {SetPos(24,12);switch (ps->_Status) {case END_NORMAL:printf("你主动结束游戏\n");break;case KILL_BY_WALL:printf("你撞到墙上,游戏结束\n");break;case KILL_BY_SELF:printf("你撞到了自己,游戏结束\n");break;}//释放蛇身的链表
    }

    运行结果如下:

    游戏结束之后,下面的信息显示的不是很好,我们在游戏结束后,使用SetPos再次地位光标到(0,27) .如下所示

    7.4.2 释放蛇身的链表

    代码如下

    	//释放蛇身的链表pSnakeNode cur = ps->_pSnake;while (cur) {pSnakeNode del = cur;cur = cur->next;free(del);}

    八、问题和后续

    当我们运行游戏的时候,按F4来进行减速,食物的分数会出错,这是因为当两位数变成一位数时,那个位置的信息0并没与消去,所以会出现那样的情况。如下所示:

    解决这个问题很简单,当我们打印食物分数的时候,以%2d的方式打印就可以了,如下所示:

    printf("当前食物的分数:%2d", ps->_foodWeight); 

    当这一句游戏结束之后,可还想要继续玩,代码如下所示: 

    //完成游戏的测试逻辑
    void test() {int ch = 0;do {//创建贪吃蛇Snake snake = { 0 };//初始化游戏:// 1打印环境界面,2功能介绍,3绘制地图,4创建蛇,5创建食物,6设置游戏相关信息GameStart(&snake);//运行游戏GameRun(&snake);//结束游戏--善后工作GameEnd(&snake);SetPos(20, 15);printf("再来一局吗?(Y/N):");ch = getchar();} while (ch=='y'||ch=='Y');SetPos(0, 27);
    }

    当我们写出上述代码之后,ch将会得到我们输入的字符,但是我们不仅仅只是输入了一个y或者n,还要一个空格,这个空格会导致第二次ch得到一个空格导致游戏结束,那现在我们怎么办呢?

    我们可以在ch得到字符y之后,再次使用getchar()把这个空格读走。 

    但是另外一个问题又来了,当我们第一次输入y,游戏重新开始,第二次游戏结束的时候,第一次输入的y会留在屏幕上,如下所示:

    如何解决这个问题呢?我们可以在游戏开始之前使用system("cls");来清空屏幕,下一次就不会出现这种情况了。

    如果我们在输入的时候卡了,连续输入了几个y,会导致游戏结束后,下一次继续开始,而没有等我们自己输入,那这个问题怎么解决呢?

    我们可以使用一个while循环来读取第一个字符后面的字符,如下所示:

    void test() {int ch = 0;do {system("cls");//创建贪吃蛇Snake snake = { 0 };//初始化游戏:// 1打印环境界面,2功能介绍,3绘制地图,4创建蛇,5创建食物,6设置游戏相关信息GameStart(&snake);//运行游戏GameRun(&snake);//结束游戏--善后工作GameEnd(&snake);SetPos(20, 15);printf("再来一局吗?(Y/N):");ch = getchar();while (getchar() != '\n');} while (ch=='y'||ch=='Y');SetPos(0, 27);
    }

    九、代码

    9.1 snakke.c部分

    #pragma once
    #include<locale.h>
    #include<stdio.h>
    #include<windows.h>
    #include<stdbool.h>
    #include<stdlib.h>
    #include<time.h>//类型的声明
    #define WALL L'□'
    #define BODY L'●'
    #define FOOD L'★'
    #define POS_X 24
    #define POS_Y  5#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_NORMAL
    };
    //蛇身的节点类型
    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 WelcomeToGame();
    //定位光标
    void SetPos(short x,short y);//4:创建地图
    void CreateMap();
    //5: 初始化蛇
    void InitSnake(pSnake ps);
    //6:创建第⼀个食物
    void CreateFood(pSnake ps);//游戏运行
    void GameRun(pSnake ps);
    //打印帮助信息
    void PrintHelpInfo();//蛇移动
    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);

    9.2 snake.h部分

    #define _CRT_SECURE_NO_WARNINGS 1
    #include"snack.h"
    //定位光标
    void SetPos(short x, short y) {//获得标准输出设备的句柄HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//使用这个函数需要包含头文件<windows.h>//定位光标位置COORD pos = { x,y }; //使用这个COORD类型要包含头文件windows.h。SetConsoleCursorPosition(houtput, pos);//使用这个函数需要包含头文件<windows.h>
    }
    //打印欢迎界面
    void WelcomeToGame() {//第一个界面的打印SetPos(40, 14);printf("欢迎来到贪吃蛇小游戏\n");SetPos(42, 20);system("pause");//第二个界面的打印system("cls");//清空屏幕SetPos(25, 14);wprintf(L"用↑.↓.←.→来控制蛇的移动,按F3加速,F4减速\n");SetPos(35, 15);wprintf(L"加速能得到更高的分数\n");SetPos(37, 17);system("pause");system("cls");
    }
    //4:创建地图,实现⼀个棋盘27⾏,58列的棋盘
    void CreateMap() {// 上//SetPos(0,0),可以不用写,因为光标默认从(0,0)开始的int i = 0;for (i = 0; i < 29; i++) {wprintf(L"%lc", WALL);}// 下SetPos(0, 26);for (i = 0; i < 29; i++) {wprintf(L"%lc", WALL);}// 左for (i = 1; i < 26; i++) {SetPos(0, i);wprintf(L"%lc", WALL);}// 右for (i = 1; i < 26; i++) {SetPos(56, i);wprintf(L"%lc", WALL);}
    }
    //初始化蛇
    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;while (cur) {SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//设置贪吃蛇的属性ps->_Dir = RIGHT;//方向默认向右ps->_foodWeight = 10;ps->_SleepTime = 200;ps->_Socre = 0;ps->_Status = OK;}
    //6:创建第⼀个食物
    void CreateFood(pSnake ps) {int x = 0;//x的取值范围2到54int y = 0;//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;while (cur) {if (x == cur->x && y == cur->y) {goto 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;//打印食物SetPos(x, y);wprintf(L"%lc", FOOD);ps->_pFood = pfood;//getchar();}
    void GameStart(pSnake ps) {//1:控制台窗口大小和标题的设置system("mode con cols=100 lines=30");//使用system,需要包含头文件<stdlib.h>system("title 贪吃蛇");//2:鼠标光标的隐藏HANDLE houtput=GetStdHandle(STD_OUTPUT_HANDLE);//使用这个函数需要包含头文件<windows.h>CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息CursorInfo.bVisible = false;//隐藏控制台光标,使用false需要包含头文件<stdbool.h>SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态//3:打印欢迎界面WelcomeToGame();//4:创建地图CreateMap();//5:初始化蛇InitSnake(ps);//6:创建第⼀个食物CreateFood(ps);//SetPos(0, 27);//system("pause");
    }//打印帮助信息
    void PrintHelpInfo() {SetPos(64, 14);printf("不能穿墙,不能咬到自己");SetPos(64, 15);printf("用↑.↓.←.→来控制蛇的移动\n");SetPos(64, 16);printf("按F3加速,F4减速");SetPos(64, 17);printf("按ESC退出游戏,按空格暂停游戏");
    }
    void Pause() {while (1) {Sleep(200);if (KEY_PRESS(VK_SPACE)){break;}}
    }
    //判断下一个坐标是不是食物
    int NextIsFood(pSnakeNode pn, pSnake ps) {return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
    }
    //下一个位置是食物,吃掉食物
    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->_Socre = ps->_Socre + ps->_foodWeight;//重新创建新的食物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);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 (ps->_pSnake->x == cur->x && ps->_pSnake->y == cur->y) {ps->_Status = KILL_BY_SELF;break;}cur = cur->next;}
    }
    //蛇移动
    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 GameRun(pSnake ps) {//打印帮助信息PrintHelpInfo(); do{//打印总分数和食物的分值SetPos(64, 10);printf("总分数:%d", ps->_Socre);SetPos(64, 11);printf("当前食物的分数:%2d", 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_NORMAL;}else if (KEY_PRESS(VK_F3)) {//加速//每按一次休眠减少30ms,不能无限加速,加一次速,食物加2分if (ps->_SleepTime > 80) {ps->_SleepTime -= 30;ps->_foodWeight += 2;}}else if(KEY_PRESS(VK_F4)){//减速//每按一次休眠增加30ms,不能无限减速,减一次速,食物减2分if (ps->_foodWeight > 2) {ps->_SleepTime += 30;ps->_foodWeight -= 2;}}//贪吃蛇走一步SnakeMove(ps);//走一步休息一下Sleep(ps->_SleepTime);} while (ps->_Status==OK);
    }//结束游戏
    void GameEnd(pSnake ps) {SetPos(24,12);switch (ps->_Status) {case END_NORMAL:printf("你主动结束游戏\n");break;case KILL_BY_WALL:printf("你撞到墙上,游戏结束\n");break;case KILL_BY_SELF:printf("你撞到了自己,游戏结束\n");break;}//释放蛇身的链表pSnakeNode cur = ps->_pSnake;while (cur) {pSnakeNode del = cur;cur = cur->next;free(del);}
    }

    9.3 test.c部分

    #define _CRT_SECURE_NO_WARNINGS 1
    #include"snack.h"//完成游戏的测试逻辑
    void test() {int ch = 0;do {system("cls");//创建贪吃蛇Snake snake = { 0 };//初始化游戏:// 1打印环境界面,2功能介绍,3绘制地图,4创建蛇,5创建食物,6设置游戏相关信息GameStart(&snake);//运行游戏GameRun(&snake);//结束游戏--善后工作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, "");//需要引用头文件locale.hsrand((unsigned int)time(NULL));test();return 0;
    }

    http://www.lryc.cn/news/572252.html

    相关文章:

  • Python Redis 简介
  • Day05_数据结构总结Z(手写)
  • 设计模式精讲 Day 7:桥接模式(Bridge Pattern)
  • 68、数据访问-crud实验-删除用户完成
  • 优化TCP/IP协议栈与网络层
  • 十年年化50%+的策略如何进化?兼容机器学习流程的量化策略开发,附python代码
  • WOOT BD活动背后的策略与操作
  • openKylin适配RISC-V高性能服务器芯片,携手睿思芯科共拓智算新蓝海
  • Linux head 命令
  • 软件项目管理(第4版)部分课后题答案
  • 腾讯云TCCP认证考试报名 - TDSQL数据库交付运维高级工程师(MySQL版)
  • 【设计模式】用观察者模式对比事件订阅(相机举例)
  • 智能混合检索DeepSearch
  • 《二叉搜索树》
  • Git版本控制详细资料
  • Postman 的 Jenkins 管理 - 自动构建
  • ABP VNext + MongoDB 数据存储:多模型支持与 NoSQL 扩展
  • 【深度学习】生成对抗网络(GANs)深度解析:从理论到实践的革命性生成模型
  • 理想树获沙利文认证,赢得中学教辅图书市场认可
  • java Class类反射getDeclaredMethod() 和 getMethod()的区别
  • Linux中的阻塞信号与信号原理
  • Linux 并发编程:从线程池到单例模式的深度实践
  • 用 STM32 HAL/LL + Arduino 混合编程
  • 硬件-DAY04(ds18b20、ARM内核)
  • Python打卡:Day31
  • 矩阵置零C++
  • Linux:信号和线程
  • 如何在 Pop!_OS 或 Ubuntu Linux 上安装 Dash to Dock
  • 设备巡检系统小程序ThinkPHP+UniApp
  • 中科米堆全自动三维光学测量航空部件尺寸测量分析