贪吃蛇(1.5w字、手把手教你实现)

贪吃蛇

前言:

贪吃蛇的实现,至少需要下面的知识:

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

有关Win32 API的知识会讲解的。

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

知识准备

我们选择使用VS2022来实现。


Win32 API

Windows API 又叫做 API 函数,是 Windows 的核心,我们可以在 Windows 操作系统里做技术开发,通过调用这个服务中心的各种服务(API函数),达到开启视窗、绘制图形、使用周边设备等目的。由于服务对象是应用程序,所以便称之为 Application Programming Interface ,简称 API。

Win32 API 也就是Microsoft Windows 32位平台的应用编程接口。


控制台设置

了解到这,我们知道了 Win32 API 的服务对象应用程序,其中,控制台程序也在服务范围内,我们将在控制台程序上实现。

我们平时运行起来的黑色框框就是控制台程序

界面大小设置

既然这样的一个黑色框框就是我们的游戏界面,那我们要考虑对游戏界面大小进行设置,我们需要使用cmd命令来实现控制控制台窗口的大小。

使用cmd命令需要包含头文件windows.h

system("mode con cols=100 lines=30"); //100是列数,30是行数

如果没有办法设置大小,我们要检查一下:

  • 运行显示出控制台界面,在边框右击鼠标,点开属性

  • 在属性里找到如下界面,一点要选择控制台主机,选择终端将无法实现。

    在这里插入图片描述

  • 每个人的初始状态可能不一样,只要将终端设置成控制台主机即可。

界面标题设置

如下语句:

system("title 贪吃蛇");

我们运行控制台可能会发现左上角的标题并没有修改。
在这里插入图片描述

其实不是的,那是因为程序结束了,我们只需要在标题命令后加上暂停命令

system("pause");

在这里插入图片描述

操作到这里,我们好像并没有使用到Win 32 API的知识,我们继续看。

我们发现,我们运行时某个位置总有光标闪烁,我们在游戏中不希望看到这个,设置这一点,我们就需要用到Win32 API


GetStdHandle

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

通俗地讲,我们要对控制台进行操作,我们首先得拿到操作它的信息,就像一个"手柄",有了它就可以操作控制台了,我们对控制台光标的设置需要句柄。

HANDLE GetStdHandle(DWORD nStdHandle);

看到这个函数的参数我们可能有点小懵,HANDLEDWORD其实是重命名得到的。

  • HANDLE

    在这里插入图片描述

  • DWORD

    在这里插入图片描述

  • 参数是DWORD类型我们不必关心,对于GetStdHandle函数,我们可以传入三个值:STD_INPUT_HANDLESTD_OUTPUT_HANDLESTD_ERROR_HANDLE,就我们贪吃蛇而言,我们需要传入STD_OUTPUT_HANDLE,拿到控制台的句柄。

    在这里插入图片描述

  • 返回值是HANDLE,我们便可以创建一个HANDLE类型的变量接收函数返回值(因为后面会用到)。

具体操作如下:

HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);

GetConsoleCursorInfo

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

BOOL WINAPI GetConsoleCursorInfo(HANDLE hConsoleOutput, PCONSOLE_CURSOR_INFO lpConsoleCursorInfo);
  • PCONSOLE_CURSOR_INFO是指向CONSOLE_CURSOR_INFO结构体的指针,这个结构体中存放的就是缓冲区光标信息。

    typedef struct _CONSOLE_CURSOR_INFO
    {
        DWORD dwSize;//光标的大小,值0~100,50指的就是占一个高度的50%。
        BOOL bVisible;//光标的显示与否,默认为true
    }CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
    
  • 对于返回值我们不必探讨,也不会用到。

  • 使用方法:我们自行创建一个CONSOLE_CURSOR_INFO结构体,将它的地址传到第二个参数位置,将之前拿到的控制台的句柄传到第一个参数位置。最终句柄对应的控制台的光标信息就存放在了我们自己创建的CONSOLE_CURSOR_INFO类型的结构体中。

具体操作如下:

	//拿到句柄
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//获取句柄对应的控制台的光标信息
	CONSOLE_CURSOR_INFO cursorinfo = { 0 };
	GetConsoleCursorInfo(houtput, &cursorinfo);

SetConsoleCursorInfo

我们自己尝试也能发现,仅仅获取到光标信息没有效果,我们需要SetConsoleCursorInfo函数来设置光标信息。

BOOL WINAPI SetConsoleCursorInfo(HANDLE hConsoleOutput, const CONSOLE_CURSOR_INFO* lpConsoleCursorInfo);
  • 参数1就是句柄,参数2是一个CONSOLE_CURSOR_INFO类型的指针变量,将从传入的第二个参数中找到待设置的光标信息。
  • 返回值仍然不需要关心。

具体操作如下:

	//拿到句柄
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//获取句柄对应的控制台的光标信息
	CONSOLE_CURSOR_INFO cursorinfo = { 0 };
	GetConsoleCursorInfo(houtput, &cursorinfo);

	//设置光标信息
	cursorinfo.bVisible = false;
	SetConsoleCursorInfo(houtput, &cursorinfo);

语句后加上暂停命令观察效果:

在这里插入图片描述

设置成功!光标成功隐藏!


SetConsoleCursorPosition

设置指定句柄对应控制台屏幕缓冲区中的光标位置。

我们观察实现好的游戏界面,需要在屏幕中心位置打印欢迎信息以及在其他各个位置打印不同的信息,这就需要我们定位光标的位置,这样我们就能实现在控制台窗口的不同位置打印信息。

BOOL WINAPI SetConsoleCursorPosition(HANDLE hConsoleOutput, COORD pos);
  • COORD是Windows API 中定义的一个结构体,表示一个字符在控制台屏幕缓冲区上的坐标,坐标系的原点(0,0)位于缓冲区的顶部左侧单元格。关于缓冲区坐标的认识会在后面进行单独讨论。

    typedef struct _COORD
    {
        SHORT X;
        SHORT Y;
    }COORD, *PCOORD;
    
  • 第一个参数就是要定位的光标所在的控制台的句柄,将对传入的句柄对应的控制台的光标进行定位。

  • 对于返回值不需要关心,我们不需要接收返回值。

具体操作如下:

//拿到句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);

//创建坐标结构体
COORD pos = { 40, 14 };
SetConsoleCursorPosition(houtput, pos);
printf("欢迎来到贪吃蛇小游戏!");
getchar();//用于防止程序结束,观察定位后的打印效果

在这里插入图片描述

打印位置符合我们的预期!

由于后期游戏实现要经常定位光标,我们不妨将定位光标的逻辑单独分装成一个函数,命名为SetPos

void SetPos(short x, short y)
{
    HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
    COORD pos = { x, y };
    SetConsoleCursorPosition(houtput, pos);
}

这一点是为后期服务的,我们记得这个分装函数就可以。


GetAsyncKeyState

我们知道,游戏运行起来后,我们需要按键对贪吃蛇进行操作,那么程序怎么知道我们按了什么键呢?

GetAsyncKeyState函数就可以帮我们做到这一点,其作用就是获取按键情况

SHORT GetAsyncKeyState(int vKey);
  • 我们需要将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。

    虚拟键码

    每一个键都对应一个键码,键码表过长我们给出一段:

在这里插入图片描述

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

我们的贪吃蛇游戏只需要判断是否按过键,所以我们使用时只需要判断返回值的最低位的值即可,怎么判断呢?

我们将 返回值 & 1(GetAsyncKeyState() & 1) 即可,1的二进制序列就是最低为为1,其余位上全是0,执行运算时,最低位比较,当返回值最低位为1,那么计算结束后最后一位值就是1;反之,如果返回值最低位为0,那么计算结束后最后一位的值就是0。

游戏执行过程中,我们肯定要不断地判断某个键是否被按过,那么我们可以考虑将它包装一下,对于这样单语句,我们考虑使用宏来实现代码的简化。

#define KEY_PRESS(vk) ((GetAsyncKeyState(vk) & 1) ? 1 : 0)

定义宏后,编译器在预处理阶段就会执行宏替换,演示如下:

	while(1) 
	{
		if (KEY_PRESS(0X30))//替换成:if(((GetAsyncKeyState(0x30) & 1) ? 1 : 0))
		printf("0\n");
	}

在这里插入图片描述

这是一个死循环,我们每按一次0键,就会打印一个0。

窗口坐标

前面我们介绍了函数SetConsoleCursorPosition函数以及COORD结构体。

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

那么我们怎样设置COORD的两个成员变量的值?我们得先介绍一下控制台窗口坐标的知识。对于我们的窗口坐标,有以下图:

在这里插入图片描述

将每个坐标的位置看作小格子,我们发现,每个小格子的高长大于宽长,并且左右两个小格子的宽度才等于一个小格子的高度。

我们使用cmd指令验证一下:

system("mode con cols=30 lines=30");

在这里插入图片描述

我们发现,当横纵坐标的值相等时,我们的窗口并不是一个正方形。

当我们调整横轴的值为纵轴的两倍时,我们的窗口变成了正方形

在这里插入图片描述

了解了这些,我们就能更好地使用GetConsoleCursorPosition函数来定位坐标了。

本地化

观察游戏界面,我们需要打印出墙体等地图要素,基于游戏界面的美观性,我们肯定希望墙体是一个正方形。不过,我们平时的普通字符都是占一个字节的,只占1个小格子,一个小格子不是正方形,所以我们引入宽字符,它们占用两个字节

怎样才能打印出宽字符呢?

我们必须进行本地化,切换到我们的本地模式,这样就支持宽字符(汉字)的输出。

<locale.h>本地化

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

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

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

通俗地说,每个地区本地化后的上述项的形式不同。

类项

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

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

大家初步了解就好,后面会使用就可以。

setlocale

setlocale函数包含在头文件<locale.h>

用于修改当前地区,可以针对任意类项修改。

char* setlocale(int category, const char* locale);
  • 第一个参数就是前面列举的类项中的一个,可以针对单个类项,也可以选择传入LC_ALL来影响全部的类项。

    我们观察:

    在这里插入图片描述

  • 第二个参数仅有两种可能取值:"C"(正常模式) 和""(本地模式)

其实,在任意程序执行开始,都会隐藏调用:

setlocale(LC_ALL, "C");

所以,我们可以通过以下语句实现本地化:

setlocale(LC_ALL, "");

切换到我们的本地模式后就支持宽字符的输出等。

wprintf

wprintf用于在屏幕上打印宽字符。

  • 宽字符的字面量必须加上前缀"L",否则C语言会将字面量当作窄字符类型处理。
  • 对应wprintf打印宽字符的占位符为%lc,打印宽字符串的占位符为%ls

另外,wchar_t是宽字符类型。


我们对本地化和宽字符的打印进行演示:

//首先我们先不本地化
setlocale(LC_ALL, "C");
wchar_t a = L'★';
wchar_t b = L'□';
wchar_t c = L'●';
wprintf(L"%lc", a);
wprintf(L"%lc", b);
wprintf(L"%lc", c);

在这里插入图片描述

打印的是问号,这是不本地化的结果。

//本地化
setlocale(LC_ALL, "");
wchar_t a = L'★';
wchar_t b = L'□';
wchar_t c = L'●';
wprintf(L"%lc", a);
wprintf(L"%lc", b);
wprintf(L"%lc", c);

在这里插入图片描述

本地化后打印成功!


随机数的生成

我们知道,贪吃蛇每次吃掉食物,新食物都将刷新到地图的随机位置,位置又可以用坐标表示,坐标用数字表示,那么我们需要了解随机数的生成的知识。

rand

rand函数包含在头文件<stdlib.h>

生成随机数,返回一个伪随机数

int rand(void);
  • rand函数返回一个伪随机数,范围为0~RAND_MAX

为什么说是伪随机数呢?

我们第一次启动程序:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    printf("%d\n", rand());
    printf("%d\n", rand());
    printf("%d\n", rand());
    return 0;
}

在这里插入图片描述

我们再次启动程序:

在这里插入图片描述

我们发现,两次启动程序打印的随机数相等,证明了rand函数生成的随机数是伪随机的

试想一下,如果我们仅依赖rand函数来实现贪吃蛇游戏的随机数生成,那么,我们每次运行游戏程序时,食物的生成位置和次序都是相同的,游戏就失去了随机性。

伪随机数不是真正的随机数,它是通过某种算法生成的随机数,真正的随机数是无法预测下个值是多少的。

rand函数其实是对一个叫 “种子” 的基准值进行运算生成的随机数,之所以前面每次运行程序生成的随机数序列是一样的,那是因为rand函数生成随机数的默认种子是1

了解到这,我们容易理解:如果要生成不同的随机数,就要让种子是变化的

解决办法为:在调用rand前要先调用srand函数。

srand

设置随机数的生成器(种子),包含在头文件stdlib.h中。

void srand(unsigned int seed);

srand函数传入种子,前面提到,种子默认为1,那么不难推断,如果我们给srand函数传入1,再调用rand函数,得到的数值应该与我们之前的相同,且多次运行结果不变。

#include <stdio.h>
#include <stdlib.h>
int main()
{
    srand(1);
    printf("%d\n", rand());
    printf("%d\n", rand());
    printf("%d\n", rand());
    return 0;
}

在这里插入图片描述

推断成立!我们得到一点:srand函数能够将 “种子” 设置成传入的参数的值。

那么,如果每次运行程序给srand函数传入不同的 “种子”,rand函数就会生成 "真"随机数!

这里我们可能有一个疑问,我们要生成随机数,我们还得有一个随机的数作为"种子",是不是矛盾了?

其实,传给srand函数的数值是一个变化的值就可以,我们要清楚,随机变化是不完全同的。

那么我们怎么得到变化的值呢?

我们通常用时间戳这一变化的值来作srand函数的参数。

如图是一个时间戳的在线转换工具:

在这里插入图片描述

所以,我们只要把时间戳传给srand函数,就能根据变化的值设置不同的"种子",让rand函数能够生成"真"随机数。

C语言提供一个函数time,可以获得时间戳,它包含在头文件time.h中。

time_t time(time_t* timer);
  • time_t:实际上是long long类型的:

    在这里插入图片描述

  • 返回的值通常表示自 1970 年 1 月 1 日 00:00 UTC 以来的秒数(即当前 unix 时间戳)。

  • 函数的参数是一个指针类型,我们在使用时只需要传入NULL即可。

具体操作如下:

    srand((unsigned int)time(NULL));

NULL记住就可以,由于srand函数的参数是unsigned int,所以我们要对time函数的返回值执行强转。

这条语句设置一次就可以了,不需要在每次使用rand函数时设置。


了解到这,我们就可以实现打印"真"随机数了:

#include <stdio.h>
#include <time.h>

int main()
{
    srand((unsigned int)time(NULL));
    printf("%d\n", rand());
    printf("%d\n", rand());
    printf("%d\n", rand());
    return 0;
}

在这里插入图片描述

在这里插入图片描述

我们发现:两次运行程序打印的结果不同了~

总结一下:

生成真随机数做到以下几点:

  • 包含头文件stdlib.hsrand函数) 和 time.htime.h函数)。
  • 语句 srand((unsigned int)time(NULL));设置不断变化的种子,这条语句仅需调用一次。
  • 调用rand函数生成随机数即可。

关于随机数的生成,还有一些使用的技巧

比如,我们想生成 0~99之间的随机数:(只需要让rand函数的返回值% 100即可,余数0 ~ 99)

#include <stdio.h>
#include <time.h>

int main()
{
    srand((unsigned int)time(NULL));
    printf("%d\n", rand() % 100);
    printf("%d\n", rand() % 100);
    printf("%d\n", rand() % 100);
    return 0;
}

在这里插入图片描述

如果我们想要生成1 ~ 100之间的随机数呢?(前面生成0 ~ 99的随机数,我们将得到的随机数加1即可生成实际1~100的随机数)

#include <stdio.h>
#include <time.h>

int main()
{
    srand((unsigned int)time(NULL));
    printf("%d\n", rand() % 100 + 1);
    printf("%d\n", rand() % 100 + 1);
    printf("%d\n", rand() % 100 + 1);
    return 0;
}

至此,我们的知识准备部分就到这里了。接下来我们将一步一步地实现游戏逻辑。


游戏逻辑

我们的贪吃蛇是三文件实现的,比如分别取名为:Snake.hSnake.ctest.c

Snake.h存放需要引用的头文件、#define定义、关键函数的声明、各种结构体的声明。

Snake.c关键函数的定义存放于此

test.c游戏测试、主函数位于此。

而我们将游戏逻辑分装成3个函数,每个函数实现不同的游戏逻辑。

游戏总的逻辑分为三大部分(三个函数),分别是GameStartGameRunGameEnd

  • GameStart函数负责:

    1. 游戏窗口设置,光标隐藏
    2. 打印游戏欢迎界面和游戏功能界面WelcomeToGame
    3. 绘制游戏地图CreateMap
    4. 创建蛇InitSnake
    5. 创建食物CreateFood
  • GameRun

    在游戏进行期间的根据按键情况来执行相关操作,这些操作导致的分数、贪吃蛇的状态的变化。

  • GameEnd

    游戏结束后,进行善后处理,包括释放空间。


GameStart

上面已经交代了我们希望GameStart函数实现的功能,接下来我们一步步实现。

SetWindow

我们将第一条功能分装到SetWindow函数中,这一子函数做到:

  • 设置界面大小和标题
  • 隐藏控制台光标

首先是窗口大小设置和标题设置,这一点因人而异,使用cmd命令调整即可。

	//设置界面大小和标题
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

值得注意的是,窗口数值大小会影响后面的游戏逻辑实现,也就是说,这里的数据与后面的数据是对应的,改前或后都会导致游戏出现问题。(我们这里选择100, 30)

其次是隐藏控制台光标,运用前面介绍过的GetStdHandleGetConsoleCursorInfoSetConsoleCursorInfo函数即可。

	//拿到控制台的句柄
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//隐藏光标
	CONSOLE_CURSOR_INFO consorinfo = { 0 };
	GetConsoleCursorInfo(houtput, &consorinfo);
	consorinfo.bVisible = false;
	SetConsoleCursorInfo(houtput, &consorinfo);

最终SetWindow函数的代码:

void SetWindow()
{
	//设置界面大小和标题
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//拿到控制台的句柄
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//隐藏光标
	CONSOLE_CURSOR_INFO consorinfo = { 0 };
	GetConsoleCursorInfo(houtput, &consorinfo);
	consorinfo.bVisible = false;
	SetConsoleCursorInfo(houtput, &consorinfo);
}

WelcomToGame

设置好界面后,我们就可以:

  • 打印游戏欢迎界面
  • 打印游戏功能界面

我们希望在屏幕中间打印欢迎信息,用到SetConsoleCursorPosition函数来定位光标位置,从而实现指定位置打印。

基于之前设置的 100 × 30 窗口大小 ,我们设置COORD结构体的成员变量为40、14。

HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { 40, 14 };
SetConsoleCursorPosition(houtput, pos);

这里考虑到一点,我们要频繁地定位光标位置来打印我们想要的信息,所以,我们将定位光标的逻辑分装成一个函数。

void SetPos(short x, short y)
{
    HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
    COORD pos = { x, y };
    SetConsoleCursorPosition(houtput, pos);
}

这样我们就可以很方便地定位光标位置了,如此,我们可以实现打印界面的工作了。

功能界面在欢迎界面之后,采用system("pause");命令来实现暂停,值得注意的是,暂停时界面会打印一句话:请按任意键继续…
我们希望这一句话也在正确的位置,所以暂停前要定位光标,别忘记system("cls");清理屏幕。

void WelcomeToGame()
{
	//欢迎界面
	SetPos(40, 14);
	wprintf(L"欢迎来到贪吃蛇小游戏!");
	SetPos(40, 20);
	system("pause");
	system("cls");

	//功能界面
	SetPos(35, 14);
	wprintf(L"%ls", L"使用 ↑ . ↓ . ← . → 来控制贪吃蛇的移动");
	SetPos(35, 15);
	wprintf(L"%ls", L"按 F3 加速,按 F4 减速");
	SetPos(35, 16);
	wprintf(L"%ls", L"按 space 暂停,按 ESC 退出游戏");
	SetPos(35, 20);
	system("pause");
	system("cls");
}

CreateMap

功能界面后就是游戏运行界面了,这个界面首先要有地图,我们完成CreateMap函数来打印地图

地图就是我们的墙体,我们决定用 来作为墙体。我们可以用#define定义一个常量墙,以便后续换墙的符号。(放在Snake.h文件中)

#define WALL L'□'
//定义成这样的格式方便后面使用wprintf

我们整个界面是一个矩形,我们希望墙围出一块区域并位于界面左侧。

在这里插入图片描述

我们前面设置的窗口大小为 100 × 30 ,按照上图(可以自己画图估计,多次修改参数以达到目的),我们希望内场地(不包括墙体)大小为:27 × 25 ,就如图红框所示。

在这里插入图片描述

怎样打印一个 27 × 25 的内场呢?

内场地大小意味着上下墙的 数为 29 个,左右墙体的 数为 27

我们先打印较为简单的位于上、下的横向墙体。

上墙体的打印很简单,下墙体的打印需要考虑内场地大小,关键是纵坐标的确定,打印时要使用之前分装的SetPos函数定位光标到正确位置。(按照我们的参数,我们设置纵坐标为26,然后开始打印墙体)。

因为坐标从0开始,场地的左墙长为27,且内场地左右长度为25,所以纵坐标为0、26的位置就是始终墙体的位置纵坐标。

	for (int i = 0; i < 29; ++i)
	{
		wprintf(L"%lc", WALL);
	}
	SetPos(0, 26);
	for (int i = 0; i < 29; ++i)
	{
		wprintf(L"%lc", WALL);
	}

在这里插入图片描述

我们再打印左右墙体,左右墙体各打印 25 即可,这里的打印讲究一点,每次循环都要重新定位,代码如下:

	for (int i = 1; i < 26; ++i)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}

	for (int i = 1; i < 26; ++i)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}

两块代码块合并得到我们的CreateMap函数:

void CreateMap()
{
	
	for (int i = 0; i < 29; ++i)
	{
		wprintf(L"%lc", WALL);
	}
	SetPos(0, 26);
	for (int i = 0; i < 29; ++i)
	{
		wprintf(L"%lc", WALL);
	}

	for (int i = 1; i < 26; ++i)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}

	for (int i = 1; i < 26; ++i)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
}

我们使用暂停命令观察一下:

在这里插入图片描述


InitSnake

打印完地图,我们该处理我们的主角——贪吃蛇了!

蛇的每个结点的符号用#define来定义:

#define BODY L'●'

我们要怎么表示我们的蛇呢?

我们选择使用链表来表示,它的身体就是一个一个的结点,开始时打印在地图中的某个指定位置。

既然要用链表表示,那么我们得先有链表结点的声明:(放在Snake.h中)

我们清楚,链表结点的指针域里存放下一个结点的地址,那么我们的数据域存放什么来指示不同的结点?

前面提到,初始时蛇是被打印在场地上的,每个 ● 就是一个结点,所以我们决定在数据域中存放坐标位置。

typedef struct SnakeNode
{
    struct SnakeNode* next;
    int x;
    int y;
}SnakeNode, *pSnakeNode;

我们可以用一个头指针来找到我们的贪吃蛇,那么我们的贪吃蛇的属性只有这些吗?

不止这些,前面没有提具体的游戏功能,不过,我们的功能界面已经写了,里面指示了一些贪吃蛇的属性:

在这里插入图片描述

从中我们可以提取出贪吃蛇的一些属性:当前方向速度

通过我们的运行界面读取一些信息:

在这里插入图片描述

我们发现,游戏还有一些参数:每个食物的分数总分数贪吃蛇当前的状态

我们再考虑,初始时食物也是被打印到场地的,所以我们是否可以使用蛇的身体结点来表示食物? 当然可以!

这样的话,我们选择把当前食物的位置也看作贪吃蛇的属性。

对于贪吃蛇所有的属性,我们再次定义一个结构体,它就可以全面表示我们的贪吃蛇了:

typedef struct Snake
{
    pSnakeNode _pSnake;//指向蛇头
    pSnakeNode _pFood;//指向食物
    int _score;//总分数
    int _foodweight;//每个食物的分数
    ......
}Snake, *pSnake;

我们发现,对于方向速度当前状态的表示不够明确。

对于速度

其实,当我们运行游戏时,我们会发现贪吃蛇会一闪一闪,走一步一闪,一闪就是一次打印,每一步的间隔可以用来间接表示速度,每走一步停一会,停的越久,速度越慢;相反,速度越块。

我们可以使用Sleep()来实现 “停顿” 的效果,它包含在头文件windows.h中。

void Sleep(DWORD ms);

Sleep函数可以使程序进入休眠状态,休眠状态持续时间取决于传入的参数是多少,参数类型是DWORD类型,就是unsigned long类型。在这里插入图片描述

单位是:毫秒

比如:

Sleep(200);//程序将会休眠200毫秒

所以,传给Sleep函数的参数,可以作为速度的标识量。

对于状态方向

我们用枚举的知识:

enum DIRECTION
{
    UP,
    DOWN,
    RIGHT,
    LEFT
};

enum STATE
{
    OK,//存活
    KILL_BY_WALL,//撞墙结束
    KILL-BY_SELF,//吃到自己结束
    NORMAL_END//正常ESC结束
};

到这我们就可以补充完我们的蛇结构体了:

typedef struct Snake
{
    pSnakeNode _pSnake;//指向蛇头
    pSnakeNode _pFood;//指向食物
    int _score;//总分数
    int _foodweight;//每个食物的分数
    enum DIRECTION dir;
    enum STATE state;
    int sleep_time;
}Snake, *pSnake;

前面的工作做完后,我们尝试使用链表的知识构建一个贪吃蛇,将它打印到界面指定位置(我们选择如下图红框附近):

我们规定(因人而异):

  • 贪吃蛇初始蛇身结点有5个
  • 贪吃蛇初始方向向右

在这里插入图片描述


我们考虑一个问题:贪吃蛇的维护只在某个子函数里面吗?如果我们在某个子函数里创建好贪吃蛇,别的函数需要怎么办?

所以,我们选择在GameStart函数调用前定义一个蛇的结构体,后续的GameRunGameEnd函数需要贪吃蛇,我们传这个贪吃蛇结构体的地址即可。

因此,我们的GameStart函数的声明就完成了:

void GameStart(pSnake ps);

void InitSnake(pSnake ps);

以上就是我们的初始化蛇的子函数的声明,接下来我们完成它。

我们将初始化蛇的函数的逻辑分成两大部分:

  • 创建蛇
  • 打印蛇
  • 初始化蛇的其他属性
  • (食物的处理相对复杂,我们用一个独立的函数处理)

创建蛇:

创建蛇就是创建一个链表,每个结点存储一个坐标,打印时利用链表依次访问结点的坐标数据来定位并打印。

我们创建链表采用头插法,这意味着最先创建出来的结点将会处于链表的最末尾,反映到界面上就是:

在这里插入图片描述

所以我们在给蛇身结点赋值时,最开始的结点应该被赋上最靠左的坐标信息。

对于坐标信息,我们要考虑一点:由于蛇身由 ● 打印,是宽字符,占两个字节,所以我们在赋值横坐标时,坐标递增的步长为 2

另外,为了增加游戏的可玩性,我们决定用#define来定义开始坐标的信息,它的值不固定。

#define POS_X 26
#define POS_Y 5

我们给出这一部分逻辑的代码:

	pSnakeNode cur = NULL;
	for (int i = 0; i < 5; i++)//规定起始5个结点,所以循环创建5个结点
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));//接收申请的空间
		if (!cur)
		{
			perror("InitSnake()::malloc()");
			exit(-1);
		}
         
        //初始化蛇身结点的信息
		cur->next = NULL;
		cur->x = POS_X + i * 2;
		cur->y = POS_Y;
        //连接
		if (ps->_pSnake == NULL)//空蛇
		{
			ps->_pSnake = cur;
		}
		else//已经有蛇身结点了
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}

打印蛇:

从蛇头结点遍历打印到屏幕上:

找到结点存储的坐标信息后,定位光标再打印。

	cur = ps->_pSnake;//找到蛇头
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

初始化蛇的其他属性:

	ps->_foodweight = 10;//每个食物的分数
	ps->_score = 0;//总分初始为0
	ps->_sleep_time = 200;//初始休眠时间为200ms
	ps->_dir = RIGHT;//初始方向朝右
	ps->_state = OK;//初始状态存活

合并三块代码,完成InitSnake函数:

void InitSnake(pSnake ps)
{
    //构建蛇
	pSnakeNode cur = NULL;
	for (int i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (!cur)
		{
			perror("InitSnake()::malloc()");
			exit(-1);
		}

		cur->next = NULL;
		cur->x = POS_X + i * 2;
		cur->y = POS_Y;
		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = cur;
		}
		else
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}
	//打印蛇
	cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
    //初始化蛇的其他属性
	ps->_foodweight = 10;
	ps->_score = 0;
	ps->_sleep_time = 200;
	ps->_dir = RIGHT;
	ps->_state = OK;
}

CreateFood

初始场地还需要食物,对于食物,我们用 ★ 代表它,与墙体符号和蛇身体符号一样,我们使用#define来定义一下:

#define FOOD L'★'

食物刷新的位置满足:

  • 在内场地内

  • 不与贪吃蛇位置重合

  • 满足上述两个条件后,食物的横坐标必须是 2 的倍数,否则就会出现食物嵌在墙体的情况

在这里插入图片描述

由于我们要满足食物不与贪吃蛇的身体重合,所以我们必须知道贪吃蛇的每个身体结点的坐标信息。因此,我们需要贪吃蛇结构体:

得到CreateFood函数的声明:

void CreateFood(pSnake ps);

如何确定食物坐标生成范围,取决于你的场地大小,我们创建的场地的内场是 27 × 25(方块数),所以我们横坐标的生成范围为 2 ~ 54;纵坐标的生成范围为 1 ~ 25

运用随机数生成的知识(链表知识要熟练):

void CreateFood(pSnake ps)
{
	int x = 0;//食物横坐标
	int y = 0;//食物纵坐标

again:
    //生成内场范围内的随机坐标
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);//如果横坐标不是2的倍数,重新生成

	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		if (cur->x == x && cur->y == y)//如果和蛇身的结点的坐标重复,重新生成
		{
			goto again;
		}
		cur = cur->next;
	}
	pSnakeNode pfood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (!pfood)
	{
		perror("CreateFood()::malloc()");
		exit(-1);
	}
	pfood->x = x;
	pfood->y = y;
	pfood->next = NULL;
	SetPos(x, y);//定位光标
	wprintf(L"%lc", FOOD);
	ps->_pFood = pfood;//初始化食物
}

最后光标停留的位置在食物位置的后面,不方便观察,所以我们要想观察,需要重新定位光标,以免影响效果演示。

在这里插入图片描述

由于右侧的帮助信息中有总分数这一运行时变化项,所以我们将它归类在接下来的GameRun函数中。

我们将当前代码写过的代码合并在一起,多文件均展示一下:(后续每一大部分完成后都会将之前所有代码进行演示)

//Snake.h


#pragma once

#include <stdio.h>
#include <windows.h>
#include <stdbool.h>
#include <locale.h>
#include <stdlib.h>
#include <time.h>

#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define POS_X 26
#define POS_Y 5

enum DIRECTION
{
	UP,
	DOWN,
	LEFT,
	RIGHT
};

enum STATE
{
	OK = 1,
	KILL_BY_WALL,
	KILL_BY_SELF,
	NORMAL_END
};


//蛇身和食物的结点
typedef struct SnakeNode
{
	short x;
	short y;
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;


//蛇的代表结点
typedef struct Snake
{
	pSnakeNode _pSnake;
	pSnakeNode _pFood;
	enum DIRECTION _dir;
	enum STATE _state;
	int _sleep_time;
	int _score;
	int _foodweight;
}Snake, * pSnake;



//游戏逻辑接口

void GameStart(pSnake ps);

void SetPos(short x, short y);
void SetWindow();
void WelcomeToGame();
void CreateMap();
void InitSnake(pSnake ps);
void CreateFood(pSnake ps);
#include "Snake.h"


void SetPos(short x, short y)
{
	//拿句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD pos = { x, y };
	SetConsoleCursorPosition(houtput, pos);
}


void SetWindow()
{
	//设置界面大小和标题
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//拿到控制台的句柄
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//隐藏光标
	CONSOLE_CURSOR_INFO consorinfo = { 0 };
	GetConsoleCursorInfo(houtput, &consorinfo);
	consorinfo.bVisible = false;
	SetConsoleCursorInfo(houtput, &consorinfo);
}



void WelcomeToGame()
{
	//欢迎界面
	SetPos(40, 14);
	wprintf(L"欢迎来到贪吃蛇小游戏!");
	SetPos(40, 20);
	system("pause");
	system("cls");

	//功能界面
	SetPos(35, 14);
	wprintf(L"%ls", L"使用 ↑ . ↓ . ← . → 来控制贪吃蛇的移动");
	SetPos(35, 15);
	wprintf(L"%ls", L"按 F3 加速,按 F4 减速");
	SetPos(35, 16);
	wprintf(L"%ls", L"按 space 暂停,按 ESC 退出游戏");
	SetPos(35, 20);
	system("pause");
	system("cls");
}



void CreateMap()
{
	
	for (int i = 0; i < 29; ++i)
	{
		wprintf(L"%lc", WALL);
	}
	SetPos(0, 26);
	for (int i = 0; i < 29; ++i)
	{
		wprintf(L"%lc", WALL);
	}

	for (int i = 1; i < 26; ++i)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}

	for (int i = 1; i < 26; ++i)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
}


void InitSnake(pSnake ps)
{

	pSnakeNode cur = NULL;
	for (int i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (!cur)
		{
			perror("InitSnake()::malloc()");
			exit(-1);
		}

		cur->next = NULL;
		cur->x = POS_X + i * 2;
		cur->y = POS_Y;
		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = cur;
		}
		else
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}
	//打印蛇
	cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->_foodweight = 10;
	ps->_score = 0;
	ps->_sleep_time = 200;
	ps->_dir = RIGHT;
	ps->_state = OK;
}


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

again:

	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);

	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}
	pSnakeNode pfood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (!pfood)
	{
		perror("CreateFood()::malloc()");
		exit(-1);
	}
	pfood->x = x;
	pfood->y = y;
	pfood->next = NULL;
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	ps->_pFood = pfood;
}



void GameStart(pSnake ps)
{
	//游戏窗口设置,光标隐藏
	SetWindow();

	//创建欢迎界面和功能界面
	WelcomeToGame();

	//地图的打印
	CreateMap();

	//初始化蛇
	InitSnake(ps);

	//创建食物
	CreateFood(ps);
}
#include "Snake.h"


void test()
{
	//创建蛇
	Snake snake = { 0 };

	//游戏开始
	GameStart(&snake);

}

int main()
{
	//本地化
	setlocale(LC_ALL, "");
    //设置随机种子
	srand((unsigned int)time(NULL));

    //暂时用来测试
	test();
	return 0;
}

至此,游戏的第一大部分就完成了,接下来是GameRun函数的实现。


GameRun

我们梳理一下游戏运行时的操作逻辑:

程序需要不断读取我们的按键,以此执行相关操作:

  • ↑ ↓ ← →:行进方向
  • F3F4:加速、减速
  • space:暂停游戏
  • ESC:正常退出游戏

贪吃蛇将一直存活(状态是OK),除非:

  • 撞到墙体
  • 撞到自己
  • 按ESC正常退出

PrintHelpInfo

在此之前,我们要打印我们的帮助信息界面:(这部分是不变的)

void PrintHelpInfo()
{
	//定位到指定打印位置
	SetPos(60, 13);
	wprintf(L"%ls", L"不能传墙,不能咬到自己");
	SetPos(60, 14);
	wprintf(L"%ls", L"使用 ↑ . ↓ . ← . → 控制蛇的移动");
	SetPos(60, 15);
	wprintf(L"%ls", L"按 F3 加速,按 F4 减速");
	SetPos(60, 16);
	wprintf(L"%ls", L"按 space 暂停,按 ESC 退出游戏");
	SetPos(60, 18);
	wprintf(L"%ls", L"@四角小裤儿儿制作");
}

打印完我们的帮助信息后,我们就需要开始实现我们的主要逻辑了。

我们先写一个大体框架:

void GameRun(pSnake ps)
{
	//打印帮助界面
	PrintHelpInfo();

	//游戏运行逻辑
	do
	{
		//打印当前面板分数
		SetPos(60, 10);
		printf("总分数:%d\n", ps->_score);
		SetPos(60, 11);
		printf("当前食物的分数:%2d\n", ps->_foodweight);//%2d是避免覆盖不全
        
		//判断按键
		if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT)//控制左移
		{

		}
		else if (KEY_PRESS(VK_UP) && ps->_dir != DOWN)//控制上移
		{

		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT)//控制右移
		{

		}
		else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP)//控制下移
		{

		}
		else if (KEY_PRESS(VK_F3))//F3加速
		{

		}
		else if (KEY_PRESS(VK_F4))//F4减速
		{

		}
		else if (KEY_PRESS(VK_SPACE))//暂停
		{
            Pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))//主动退出游戏
		{

		}

		SnakeMove(ps);//贪吃蛇的移动

		Sleep(ps->_sleep_time);//速度体现在这里

	} while (ps->_state == OK);//游戏结束标志:蛇的状态不是OK
    
}

这里要特别注意

  • 贪吃蛇的转向不能和当前方向相反
  • 贪吃蛇将一直移动除非状态不是OK
  • 由于PrintHelpInfo打印的帮助信息是固定的,所以我们一定要在游戏逻辑里实现总分数和每个食物分数的变化,只需要每次覆盖掉之前的打印信息即可
  • 代码待补充部分将会分装很多子函数,比如已经写在框架里的SnakeMove函数,后面我们会一步步讲解。

理解了上述框架代码后,我们开始补充:

对于方向分支,我们只需要将方向(ps->_dir)改变即可,以其中一块为例,其余的方向调整类似:

		if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT)//控制左移
		{
            ps->_dir = LEFT;//改变方向为左
		}

对于加速/减速分支,我们需要改变ps->_sleep_time的数值即可,不过我们得考虑一些注意事项:

  • 减速/加速的上限
  • 减速/加速后每个食物的分数要随之减少/增加

我们把那块代码单独拿过来演示(具体数值因人而异):

		else if (KEY_PRESS(VK_F3))//F3加速
		{
			if (ps->_sleep_time > 80)
			{
				ps->_sleep_time -= 30;
				ps->_foodweight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))//F4减速
		{
             if (ps->_foodweight > 2)
			{
				ps->_sleep_time += 30;
				ps->_foodweight -= 2;
			}
		}
Pause

对于暂停分支,我们分装一个函数Pause实现即可

逻辑:当玩家选择暂停时,将调用暂停函数,用死循环实现,无限休眠,除非用户再次按下空格键才可以退出暂停状态。

void Pause()
{
    while (1)
    {
        Sleep(100);
        if(KEY_PRESS(VK_SPACE))
        {
            break;
        }
    }
}

对于ESC分支,我们要改变贪吃蛇的状态即可

		else if (KEY_PRESS(VK_ESCAPE))//主动退出游戏
		{
			ps->_state = NORMAL_END;
		}

综合第一次补充后的代码:

void GameRun(pSnake ps)
{
	//打印帮助界面
	PrintHelpInfo();

	//游戏运行逻辑
	do
	{
		//更新面板分数
		SetPos(60, 10);
		printf("总分数:%d\n", ps->_score);
		SetPos(60, 11);
		printf("当前食物的分数:%2d\n", ps->_foodweight);
        
		//判断按键
		if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT)//控制左移
		{
			ps->_dir = LEFT;
		}
		else if (KEY_PRESS(VK_UP) && ps->_dir != DOWN)//控制上移
		{
			ps->_dir = UP;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT)//控制右移
		{
			ps->_dir = RIGHT;
		}
		else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP)//控制下移
		{
			ps->_dir = DOWN;
		}
		else if (KEY_PRESS(VK_F3))//F3加速
		{
			if (ps->_sleep_time > 80)
			{
				ps->_sleep_time -= 30;
				ps->_foodweight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))//F4减速
		{
			if (ps->_foodweight > 2)
			{
				ps->_sleep_time += 30;
				ps->_foodweight -= 2;
			}
		}
		else if (KEY_PRESS(VK_SPACE))//暂停
		{
			Pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))//主动退出游戏
		{
			ps->_state = NORMAL_END;
		}

		SnakeMove(ps);

		Sleep(ps->_sleep_time);

	} while (ps->_state == OK);


}
SnakeMove

接下来我们实现SnakeMove函数,这个函数是贪吃蛇移动一步的逻辑核心:

移动时一定要先清楚下一步的位置。

贪吃蛇的每一步都惊险万分,我们需要判断它的下一步是:

  1. 食物
  2. 墙体
  3. 自己
  4. 普通的位置(什么也没有)

我们这样考虑,创建一个蛇身结点,它将存储贪吃蛇下一步的坐标信息,这要根据贪吃蛇此时的方向来判断

所以我们先将这一块的逻辑实现:

注意:

  • 当蛇的方向是左边或右边,此时它的下一个位置的横坐标的变化量应该是2
  • 自己在在给下一个位置结点赋值的时候一定要注意x与x、y与y相对应,小裤儿就在这里栽了个跟头,最后发现case的每一个情况都是给nextpos->x赋的值。
void SnakeMove(pSnake ps)
{
	pSnakeNode nextpos = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (nextpos == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	switch (ps->_dir)
	{
	case LEFT:
		nextpos->x = ps->_pSnake->x - 2;//x
		nextpos->y = ps->_pSnake->y;//y
		break;
	case RIGHT:
		nextpos->x = ps->_pSnake->x + 2//x
		nextpos->y = ps->_pSnake->y;//y
		break;
	case UP:
		nextpos->x = ps->_pSnake->x;//x
		nextpos->y = ps->_pSnake->y - 1;//y
		break;
	case DOWN:
		nextpos->x = ps->_pSnake->x;//x
		nextpos->y = ps->_pSnake->y + 1;//y
		break;
	}
	nextpos->next = NULL;
	//…………
}

到这里,我们就得到了贪吃蛇的下一步的位置,接下来分情况讨论:

EatFood

下一步是食物

由于下一个位置结点、蛇身结点、食物结点的结构体类型相同,我们可以:

  • 将食物结点或者下一个位置结点头插到蛇链表,释放另一个,别忘了更新蛇的头指针
  • 吃到食物后,我们需要更新总分数ps->_score
  • 在屏幕上打印蛇的的结点,模拟蛇移动、吃食物、长度增加的过程
  • 重新创建食物CreateFood

主逻辑放在EatFood函数中:

void EatFood(pSnake ps, pSnakeNode nextpos)
{
	//头插并更新头指针,释放
	ps->_pFood->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;
	free(nextpos);
	nextpos = NULL;
	//分数增加
	ps->_score += ps->_foodweight;

	//重新打印
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	//重新创建食物
	CreateFood(ps);
}

已经判断下一个位置是食物,才会调用EatFood函数,这里想表达的是,要有一个 if 判断:

	if (nextpos->x == ps->_pFood->x && nextpos->y == ps->_pFood->y)
	{
		EatFood(ps, nextpos);
	}
NoFood

下一个位置不是食物

按照上面的处理风格,我们要:

  • 将下一个位置的结点头插到蛇链表中
  • 将蛇尾部的结点释放掉,注意,释放前要在尾结点的位置打印两个空格以覆盖掉之前打印在屏幕上的结点符号(由于一个蛇身体的符号 ● 占两个横坐标,所以打印两个空格)
  • 重新打印蛇,模拟蛇前进了一格

我们将这里的逻辑在NoFood函数中实现:

void NoFood(pSnake ps, pSnakeNode nextpos)
{
	//释放尾结点(需要拿到它的前一个结点),覆盖之前的打印
	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next)
	{
		cur = cur->next;
	}
	//停下的时候,cur指向尾结点的前一个结点
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;

	//让下一个位置的结点头插到蛇链表中
	nextpos->next = ps->_pSnake;
	ps->_pSnake = nextpos;

	//重新打印
	cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
}

这种实现的逻辑顺序与前面的相差较大,我们给出第二种更加简洁的写法:

void NoFood(pSnake ps, pSnakeNode nextpos)
{
	//头插法
	nextpos->next = ps->_pSnake;
	ps->_pSnake = nextpos;

	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;
}

我们知道,蛇的下一个位置的情况不能仅仅分成有无食物两种情况处理,如果下一个位置不是食物,那么它的下一个位置可能是墙体,可能是它自己的身体。

前面有无食物的函数使得蛇已经移动了,食物不可能生成在蛇位置和墙的位置,所以EatFood函数移动蛇后,蛇的头结点不可能与蛇的身体和墙体重复;而如果下一个位置不是食物,则贪吃蛇移动后的头结点可能与墙体或身体重合,这时候就需要特别处理了。

KillByWall

头结点与墙体重合

我们分装函数KillByWall实现。

KillByWall函数需要首先判断此时头结点是否与墙体重合,重合则将贪吃蛇的状态变为KILL_BY_WALL

注意这里的边界判断取决于自己设置的场地大小。

void KillByWall(pSnake ps)
{
	if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26)
	{
		ps->_state = KILL_BY_WALL;
	}
}
KillBySelf

头结点与身体重合

我们分装函数KillBySelf实现。

一样的思路,KillBySelf判断是否重合,重合则将贪吃蛇的状态设置为KILL_BY_SELF

void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake->next;
	while (cur)
	{
		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
		{
			ps->_state = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}

至此,游戏的第二大部分就完成了,我们给出此时三个文件的代码,方便读者宏观地掌握。

//Snake.h



#pragma once

#include <stdio.h>
#include <windows.h>
#include <stdbool.h>
#include <locale.h>
#include <stdlib.h>
#include <time.h>

#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define POS_X 26
#define POS_Y 5

#define KEY_PRESS(vk) ((GetAsyncKeyState(vk) & 1) ? 1 : 0)

enum DIRECTION
{
	UP,
	DOWN,
	LEFT,
	RIGHT
};

enum STATE
{
	OK = 1,
	KILL_BY_WALL,
	KILL_BY_SELF,
	NORMAL_END
};


//蛇的结点
typedef struct SnakeNode
{
	short x;
	short y;
	struct SnakeNode* next;
}SnakeNode, *pSnakeNode;


//蛇的属性
typedef struct Snake
{
	pSnakeNode _pSnake;
	pSnakeNode _pFood;
	enum DIRECTION _dir;
	enum STATE _state;
	int _sleep_time;
	int _score;
	int _foodweight;
}Snake, *pSnake;



//游戏逻辑接口


//第一部分-初始化开始前准备
void GameStart(pSnake ps);

void SetPos(short x, short y);
void SetWindow();
void WelcomeToGame();
void CreateMap();
void InitSnake(pSnake ps);
void CreateFood(pSnake ps);




//第二部分-游戏运行逻辑
void GameRun(pSnake ps);


void PrintHelpInfo();
void Pause();
void SnakeMove(pSnake ps);
void EatFood(pSnake ps, pSnakeNode nextpos);
void NoFood(pSnake ps, pSnakeNode nextpos);
void KillByWall(pSnake ps);
void KillBySelf(pSnake ps);


//Snake.c



#include "Snake.h"

void SetPos(short x, short y)
{
	//拿句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD pos = { x, y };
	SetConsoleCursorPosition(houtput, pos);
}


void SetWindow()
{
	//设置界面大小和标题
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//拿到控制台的句柄
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//隐藏光标
	CONSOLE_CURSOR_INFO consorinfo = { 0 };
	GetConsoleCursorInfo(houtput, &consorinfo);
	consorinfo.bVisible = false;
	SetConsoleCursorInfo(houtput, &consorinfo);
}


void WelcomeToGame()
{
	//欢迎界面
	SetPos(40, 14);
	wprintf(L"欢迎来到贪吃蛇小游戏!");
	SetPos(40, 20);
	system("pause");
	system("cls");

	//功能界面
	SetPos(35, 14);
	wprintf(L"%ls", L"使用 ↑ . ↓ . ← . → 来控制贪吃蛇的移动");
	SetPos(35, 15);
	wprintf(L"%ls", L"按 F3 加速,按 F4 减速");
	SetPos(35, 16);
	wprintf(L"%ls", L"按 space 暂停,按 ESC 退出游戏");
	SetPos(35, 20);
	system("pause");
	system("cls");
}


void CreateMap()
{
	
	for (int i = 0; i < 29; ++i)
	{
		wprintf(L"%lc", WALL);
	}
	SetPos(0, 26);
	for (int i = 0; i < 29; ++i)
	{
		wprintf(L"%lc", WALL);
	}

	for (int i = 1; i < 26; ++i)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}

	for (int i = 1; i < 26; ++i)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
}


void InitSnake(pSnake ps)
{

	pSnakeNode cur = NULL;
	for (int i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (!cur)
		{
			perror("InitSnake()::malloc()");
			exit(-1);
		}

		cur->next = NULL;
		cur->x = POS_X + i * 2;
		cur->y = POS_Y;
		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = cur;
		}
		else
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}
	//打印蛇
	cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->_foodweight = 10;
	ps->_score = 0;
	ps->_sleep_time = 200;
	ps->_dir = RIGHT;
	ps->_state = OK;
}


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

again:

	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);

	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}
	pSnakeNode pfood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (!pfood)
	{
		perror("CreateFood()::malloc()");
		exit(-1);
	}
	pfood->x = x;
	pfood->y = y;
	pfood->next = NULL;
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	ps->_pFood = pfood;
}


void GameStart(pSnake ps)
{
	//游戏窗口设置,光标隐藏
	SetWindow();

	//创建欢迎界面和功能界面
	WelcomeToGame();

	//地图的打印
	CreateMap();

	//初始化蛇
	InitSnake(ps);

	//创建食物
	CreateFood(ps);
}

//

void PrintHelpInfo()
{
	//定位到指定打印位置
	SetPos(60, 13);
	wprintf(L"%ls", L"不能传墙,不能咬到自己");
	SetPos(60, 14);
	wprintf(L"%ls", L"使用 ↑ . ↓ . ← . → 控制蛇的移动");
	SetPos(60, 15);
	wprintf(L"%ls", L"按 F3 加速,按 F4 减速");
	SetPos(60, 16);
	wprintf(L"%ls", L"按 space 暂停,按 ESC 退出游戏");
	SetPos(60, 18);
	wprintf(L"%ls", L"@四角小裤儿儿制作");
}


void Pause()
{
	while (1)
	{
		Sleep(100);
		if (KEY_PRESS(VK_SPACE))
			break;
	}
}


void EatFood(pSnake ps, pSnakeNode nextpos)
{
	//头插并更新头指针,释放
	ps->_pFood->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;
	free(nextpos);
	nextpos = NULL;
	//分数增加
	ps->_score += ps->_foodweight;

	//重新打印
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	//重新创建食物
	CreateFood(ps);
}


void NoFood(pSnake ps, pSnakeNode nextpos)
{
	//释放尾结点(需要拿到它的前一个结点),覆盖之前的打印
	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next)
	{
		cur = cur->next;
	}
	//停下的时候,cur指向尾结点的前一个结点
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;

	//让下一个位置的结点头插到蛇链表中
	nextpos->next = ps->_pSnake;
	ps->_pSnake = nextpos;

	//重新打印
	cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
}


void KillByWall(pSnake ps)
{
	if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26)
	{
		ps->_state = 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->_state = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}


void SnakeMove(pSnake ps)
{
	pSnakeNode nextpos = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (nextpos == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	switch (ps->_dir)
	{
	case LEFT:
		nextpos->x = ps->_pSnake->x - 2;
		nextpos->y = ps->_pSnake->y;
		break;
	case RIGHT:
		nextpos->x = ps->_pSnake->x + 2;
		nextpos->y = ps->_pSnake->y;
		break;
	case UP:
		nextpos->x = ps->_pSnake->x;
		nextpos->y = ps->_pSnake->y - 1;
		break;
	case DOWN:
		nextpos->x = ps->_pSnake->x;
		nextpos->y = ps->_pSnake->y + 1;
		break;
	}
	nextpos->next = NULL;

	//如果下一个位置是食物
	if (nextpos->x == ps->_pFood->x && nextpos->y == ps->_pFood->y)
	{
		EatFood(ps, nextpos);
	}
	else
	{
		//不是食物
		NoFood(ps, nextpos);
	}

	//如果此时头结点与墙体重合
	KillByWall(ps);

	KillBySelf(ps);
	
}


void GameRun(pSnake ps)
{
	//打印帮助界面
	PrintHelpInfo();

	//游戏运行逻辑
	do
	{

		//更新面板分数
		SetPos(60, 10);
		printf("总分数:%d\n", ps->_score);
		SetPos(60, 11);
		printf("当前食物的分数:%2d\n", ps->_foodweight);
        
		//判断按键
		if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT)//控制左移
		{
			ps->_dir = LEFT;
		}
		else if (KEY_PRESS(VK_UP) && ps->_dir != DOWN)//控制上移
		{
			ps->_dir = UP;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT)//控制右移
		{
			ps->_dir = RIGHT;
		}
		else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP)//控制下移
		{
			ps->_dir = DOWN;
		}
		else if (KEY_PRESS(VK_F3))//F3加速
		{
			if (ps->_sleep_time > 80)
			{
				ps->_sleep_time -= 30;
				ps->_foodweight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))//F4减速
		{
			if (ps->_foodweight > 2)
			{
				ps->_sleep_time += 30;
				ps->_foodweight -= 2;
			}
		}
		else if (KEY_PRESS(VK_SPACE))//暂停
		{
			Pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))//主动退出游戏
		{
			ps->_state = NORMAL_END;
		}
         
        //蛇的移动
		SnakeMove(ps);

		Sleep(ps->_sleep_time);

	} while (ps->_state == OK);

}
//test.c



#include "Snake.h"

void test()
{
	//创建蛇
	Snake snake = { 0 };

	//游戏开始
	GameStart(&snake);

	//运行游戏
	GameRun(&snake);

}

int main()
{
	//本地化
	setlocale(LC_ALL, "");
    
    //设置随机种子
	srand((unsigned int)time(NULL));

	test();
	return 0;
}

至此,游戏的第二大部分就完成了,接下来就是游戏的最后一大部分GameEnd函数

GameEnd

进行到这,我们需要对游戏做好善后工作:

  • 根据贪吃蛇此时的状态在屏幕指定位置打印相应的语句来标明游戏结束的原因。
  • 释放动态开辟的空间
PrintEndInfo

根据贪吃蛇的状态打印相应的信息:

void PrintEndInfo(pSnake ps)
{
	switch (ps->_state)
	{
	case NORMAL_END:
		SetPos(10, 15);
		wprintf(L"%ls", L"游戏正常结束");
		break;
	case KILL_BY_WALL:
		SetPos(10, 15);
		wprintf(L"%ls", L"您撞墙了,游戏结束");
		break;
	case KILL_BY_SELF:
		SetPos(10, 15);
		wprintf(L"%ls", L"您撞到自己了,游戏结束");
		break;
	}
}
FreeSpace

释放食物结点、释放蛇链表结点:

void FreeSpace(pSnake ps)
{
	//释放食物结点
	free(ps->_pFood);
 
	//释放蛇结点
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode next = cur->next;
		free(cur);
		cur = next;
	}
}

GameEnd函数较为简单,实现完成:

void GameEnd(pSnake ps)
{
	//打印结束信息
	PrintEndInfo(ps);

	//释放空间
	FreeSpace(ps);
}

主函数的优化

至此,游戏主要逻辑实现完毕,现在我们要做的就是将test.c函数优化完善。

  • 使用do-while循环实现重玩的功能
  • 根据用户输入决定是否重来,考虑到getchar函数会读取\n,我们要清理缓冲区的内容。
#include "Snake.h"



int main()
{
	//本地化
	setlocale(LC_ALL, "");

	//设置随机种子
	srand((unsigned int)time(NULL));

	char ch = 0;
	do
	{
		//创建蛇
		Snake snake = { 0 };

		//游戏开始
		GameStart(&snake);

		//运行游戏
		GameRun(&snake);

		//游戏结束
		GameEnd(&snake);

		//游戏重来
		SetPos(10, 16);
		wprintf(L"%ls", L"想要再来一局吗?请选择Y(是)/N(否)");
		ch = getchar();
		while (getchar() != '\n');//清理缓冲区内容


	} while (ch == 'y' || ch == 'Y');

	return 0;
}

终于实现完毕了,我们看代码和演示:

看累的你赶紧去来一把吧!

代码及演示

代码
//Snake.h



#pragma once

#include <stdio.h>
#include <windows.h>
#include <stdbool.h>
#include <locale.h>
#include <stdlib.h>
#include <time.h>

#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define POS_X 26
#define POS_Y 5

#define KEY_PRESS(vk) ((GetAsyncKeyState(vk) & 1) ? 1 : 0)

enum DIRECTION
{
	UP,
	DOWN,
	LEFT,
	RIGHT
};

enum STATE
{
	OK = 1,
	KILL_BY_WALL,
	KILL_BY_SELF,
	NORMAL_END
};


//蛇的结点
typedef struct SnakeNode
{
	short x;
	short y;
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;


//蛇的属性
typedef struct Snake
{
	pSnakeNode _pSnake;
	pSnakeNode _pFood;
	enum DIRECTION _dir;
	enum STATE _state;
	int _sleep_time;
	int _score;
	int _foodweight;
}Snake, * pSnake;



//游戏逻辑接口


//第一部分-初始化开始前准备
void GameStart(pSnake ps);

void SetPos(short x, short y);
void SetWindow();
void WelcomeToGame();
void CreateMap();
void InitSnake(pSnake ps);
void CreateFood(pSnake ps);




//第二部分-游戏运行逻辑
void GameRun(pSnake ps);


void PrintHelpInfo();
void Pause();
void SnakeMove(pSnake ps);
void EatFood(pSnake ps, pSnakeNode nextpos);
void NoFood(pSnake ps, pSnakeNode nextpos);
void KillByWall(pSnake ps);
void KillBySelf(pSnake ps);




//第三部分-游戏结束的善后工作


void GameEnd(pSnake ps);
void PrintEndInfo(pSnake ps);
void FreeSpace(pSnake ps);
//Snake.c



#include "Snake.h"

void SetPos(short x, short y)
{
	//拿句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD pos = { x, y };
	SetConsoleCursorPosition(houtput, pos);
}


void SetWindow()
{
	//设置界面大小和标题
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//拿到控制台的句柄
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//隐藏光标
	CONSOLE_CURSOR_INFO consorinfo = { 0 };
	GetConsoleCursorInfo(houtput, &consorinfo);
	consorinfo.bVisible = false;
	SetConsoleCursorInfo(houtput, &consorinfo);
}


void WelcomeToGame()
{
	//欢迎界面
	SetPos(40, 14);
	wprintf(L"欢迎来到贪吃蛇小游戏!");
	SetPos(40, 20);
	system("pause");
	system("cls");

	//功能界面
	SetPos(35, 14);
	wprintf(L"%ls", L"使用 ↑ . ↓ . ← . → 来控制贪吃蛇的移动");
	SetPos(35, 15);
	wprintf(L"%ls", L"按 F3 加速,按 F4 减速");
	SetPos(35, 16);
	wprintf(L"%ls", L"按 space 暂停,按 ESC 退出游戏");
	SetPos(35, 20);
	system("pause");
	system("cls");
}


void CreateMap()
{

	for (int i = 0; i < 29; ++i)
	{
		wprintf(L"%lc", WALL);
	}
	SetPos(0, 26);
	for (int i = 0; i < 29; ++i)
	{
		wprintf(L"%lc", WALL);
	}

	for (int i = 1; i < 26; ++i)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}

	for (int i = 1; i < 26; ++i)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
}


void InitSnake(pSnake ps)
{

	pSnakeNode cur = NULL;
	for (int i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (!cur)
		{
			perror("InitSnake()::malloc()");
			exit(-1);
		}

		cur->next = NULL;
		cur->x = POS_X + i * 2;
		cur->y = POS_Y;
		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = cur;
		}
		else
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}
	//打印蛇
	cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->_foodweight = 10;
	ps->_score = 0;
	ps->_sleep_time = 200;
	ps->_dir = RIGHT;
	ps->_state = OK;
}


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

again:

	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);

	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}
	pSnakeNode pfood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (!pfood)
	{
		perror("CreateFood()::malloc()");
		exit(-1);
	}
	pfood->x = x;
	pfood->y = y;
	pfood->next = NULL;
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	ps->_pFood = pfood;
}


void GameStart(pSnake ps)
{
	//游戏窗口设置,光标隐藏
	SetWindow();

	//创建欢迎界面和功能界面
	WelcomeToGame();

	//地图的打印
	CreateMap();

	//初始化蛇
	InitSnake(ps);

	//创建食物
	CreateFood(ps);
}


void PrintHelpInfo()
{
	//定位到指定打印位置
	SetPos(60, 13);
	wprintf(L"%ls", L"不能传墙,不能咬到自己");
	SetPos(60, 14);
	wprintf(L"%ls", L"使用 ↑ . ↓ . ← . → 控制蛇的移动");
	SetPos(60, 15);
	wprintf(L"%ls", L"按 F3 加速,按 F4 减速");
	SetPos(60, 16);
	wprintf(L"%ls", L"按 space 暂停,按 ESC 退出游戏");
	SetPos(60, 18);
	wprintf(L"%ls", L"@四角小裤儿儿制作");
}


void Pause()
{
	while (1)
	{
		Sleep(100);
		if (KEY_PRESS(VK_SPACE))
			break;
	}
}


void EatFood(pSnake ps, pSnakeNode nextpos)
{
	//头插并更新头指针,释放
	ps->_pFood->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;
	free(nextpos);
	nextpos = NULL;
	//分数增加
	ps->_score += ps->_foodweight;

	//重新打印
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	//重新创建食物
	CreateFood(ps);
}


void NoFood(pSnake ps, pSnakeNode nextpos)
{
	//释放尾结点(需要拿到它的前一个结点),覆盖之前的打印
	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next)
	{
		cur = cur->next;
	}
	//停下的时候,cur指向尾结点的前一个结点
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;

	//让下一个位置的结点头插到蛇链表中
	nextpos->next = ps->_pSnake;
	ps->_pSnake = nextpos;

	//重新打印
	cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
}


void KillByWall(pSnake ps)
{
	if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26)
	{
		ps->_state = 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->_state = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}


void SnakeMove(pSnake ps)
{
	pSnakeNode nextpos = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (nextpos == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	switch (ps->_dir)
	{
	case LEFT:
		nextpos->x = ps->_pSnake->x - 2;
		nextpos->y = ps->_pSnake->y;
		break;
	case RIGHT:
		nextpos->x = ps->_pSnake->x + 2;
		nextpos->y = ps->_pSnake->y;
		break;
	case UP:
		nextpos->x = ps->_pSnake->x;
		nextpos->y = ps->_pSnake->y - 1;
		break;
	case DOWN:
		nextpos->x = ps->_pSnake->x;
		nextpos->y = ps->_pSnake->y + 1;
		break;
	}
	nextpos->next = NULL;

	//如果下一个位置是食物
	if (nextpos->x == ps->_pFood->x && nextpos->y == ps->_pFood->y)
	{
		EatFood(ps, nextpos);
	}
	else
	{
		//不是食物
		NoFood(ps, nextpos);
	}

	//如果此时头结点与墙体重合
	KillByWall(ps);

	KillBySelf(ps);

}


void GameRun(pSnake ps)
{
	//打印帮助界面
	PrintHelpInfo();

	//游戏运行逻辑
	do
	{

		//更新面板分数
		SetPos(60, 10);
		printf("总分数:%d\n", ps->_score);
		SetPos(60, 11);
		printf("当前食物的分数:%2d\n", ps->_foodweight);

		//判断按键
		if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT)//控制左移
		{
			ps->_dir = LEFT;
		}
		else if (KEY_PRESS(VK_UP) && ps->_dir != DOWN)//控制上移
		{
			ps->_dir = UP;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT)//控制右移
		{
			ps->_dir = RIGHT;
		}
		else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP)//控制下移
		{
			ps->_dir = DOWN;
		}
		else if (KEY_PRESS(VK_F3))//F3加速
		{
			if (ps->_sleep_time > 80)
			{
				ps->_sleep_time -= 30;
				ps->_foodweight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))//F4减速
		{
			if (ps->_foodweight > 2)
			{
				ps->_sleep_time += 30;
				ps->_foodweight -= 2;
			}
		}
		else if (KEY_PRESS(VK_SPACE))//暂停
		{
			Pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))//主动退出游戏
		{
			ps->_state = NORMAL_END;
		}

		//蛇的移动
		SnakeMove(ps);

		Sleep(ps->_sleep_time);

	} while (ps->_state == OK);

}

void PrintEndInfo(pSnake ps)
{
	switch (ps->_state)
	{
	case NORMAL_END:
		SetPos(10, 15);
		wprintf(L"%ls", L"游戏正常结束");
		break;
	case KILL_BY_WALL:
		SetPos(10, 15);
		wprintf(L"%ls", L"您撞墙了,游戏结束");
		break;
	case KILL_BY_SELF:
		SetPos(10, 15);
		wprintf(L"%ls", L"您撞到自己了,游戏结束");
		break;
	}
}

void FreeSpace(pSnake ps)
{
	//释放食物结点
	free(ps->_pFood);

	//释放蛇结点
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode next = cur->next;
		free(cur);
		cur = next;
	}
}


void GameEnd(pSnake ps)
{
	//打印结束信息
	PrintEndInfo(ps);

	//释放空间
	FreeSpace(ps);
}
//test.c


#include "Snake.h"



int main()
{
	//本地化
	setlocale(LC_ALL, "");

	//设置随机种子
	srand((unsigned int)time(NULL));

	char ch = 0;
	do
	{
		//创建蛇
		Snake snake = { 0 };

		//游戏开始
		GameStart(&snake);

		//运行游戏
		GameRun(&snake);

		//游戏结束
		GameEnd(&snake);

		//游戏重来
		SetPos(10, 16);
		wprintf(L"%ls", L"想要再来一局吗?请选择Y(是)/N(否)");
		ch = getchar();
		while (getchar() != '\n');


	} while (ch == 'y' || ch == 'Y');

	return 0;
}

演示

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


多动手,多思考,多进步!

  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值