C语言贪吃蛇

注 :本文是基于链表实现贪吃蛇游戏

1.Win32 API

本篇文章中实现贪吃蛇会用到一些Win32 API的知识,接下来简单做下介绍

1.1 Win32 API

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

1.2 控制台程序

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

mode指令

cpp 复制代码
#include<stdio.h>
#include<Windows.h>
int main()
{
	system("mode con cols=30 lines=30");
	system("pause");
	return 0;
}


这是没修改之前

这是修改之后的,这里为什么不是个正方形,后文会讲到
也可以通过命令设置控制台窗口的名字
title命令

cpp 复制代码
#include<stdio.h>
#include<Windows.h>
int main()
{
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");
	system("pause");//这里给一个暂停,因为程序结束了就看不出来效果了
	return 0;
}

1.3 控制台屏幕上的坐标COORD

在数学中有坐标的概念,有X轴,y轴等

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

COORD类型的声明:

cpp 复制代码
typedef struct _COORD 
{
 SHORT X;
 SHORT Y;
} COORD, *PCOORD;

X,Y分别对应横纵坐标

1.4 GetStdHandle

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

cpp 复制代码
HANDLE GetStdHandle(DWORD nStdHandle);



返回值其实是指针

cpp 复制代码
int main()
{
	//获得标准输出设备的句柄
	HANDLE houput =  GetStdHandle(STD_OUTPUT_HANDLE);
	return 0;
}

拿到这个句柄,就可以对我们的控制台面板进行一些操作

1.5 GetConsoleCursorInfo

那么在控制台输出时,光标会不停闪烁,希望将光标去掉
这个函数就是检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息

cpp 复制代码
BOOL WINAPI GetConsoleCursorInfo(
 HANDLE hConsoleOutput,
 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标

1.5.1CONSOLE_CURSOR_INFO

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

cpp 复制代码
typedef struct _CONSOLE_CURSOR_INFO {
 DWORD dwSize;
 BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

dwSize,由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完
全填充单元格到单元底部的水平线条。
bVisible,光标的可见性。 如果光标可见,则此成员为 TRUE。

cpp 复制代码
int main()
{
	//获得标准输出设备的句柄
	HANDLE houput =  GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO cursor_info = { 0 };
	GetConsoleCursorInfo(houput, &cursor_info);
	printf("%d\n", cursor_info.dwSize);
	return 0;
}


这里为什么输出的是25

指的就是这里光标占一个字符高度的百分之多少

1.6 SetConsoleCursorInfo

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

cpp 复制代码
int main()
{
	//获得标准输出设备的句柄
	HANDLE houput =  GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO cursor_info = { 0 };
	GetConsoleCursorInfo(houput, &cursor_info);
	printf("%d\n", cursor_info.dwSize);
	cursor_info.dwSize = 50;
	SetConsoleCursorInfo(houput, &cursor_info);
	system("pause");
	return 0;
}


这里可以看到光标确实变大了
隐藏光标也是可以的

cpp 复制代码
int main()
{
	//获得标准输出设备的句柄
	HANDLE houput =  GetStdHandle(STD_OUTPUT_HANDLE);

	//定义一个光标信息的结构体
	CONSOLE_CURSOR_INFO cursor_info = { 0 };

	//获取和houput相关的控制台上的光标信息,存放在cursor_info中
	GetConsoleCursorInfo(houput, &cursor_info);
    printf("%d\n", cursor_info.dwSize);
	
	//修改光标的占比值
	cursor_info.dwSize = 50;
	cursor_info.bVisible = false;

	//设置和houput句柄相关的控制台上的光标信息
	SetConsoleCursorInfo(houput, &cursor_info);
	system("pause");
	return 0;
}

这样就看不见光标了

1.7 SetConsoleCursorPosition

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

cpp 复制代码
int main()
{
	//获得标准输出设备的句柄
	HANDLE houput = GetStdHandle(STD_OUTPUT_HANDLE);

	COORD pos = { 10,20 };
	SetConsoleCursorPosition(houput, pos);


	system("title 贪吃蛇");
	system("pause");
	return 0;
}

光标的位置发生了改变

那么我们将上述操作封装成一个函数SetPos

cpp 复制代码
void Setpos(short X , short Y)
{
    HANDLE houput = GetStdHandle(STD_OUTPUT_HANDLE);

	COORD pos = { X,Y };
	SetConsoleCursorPosition(houput, pos);
}
int main()
{
	//获得标准输出设备的句柄
	

	Setpos(5, 10);
	printf("1");
	
	Setpos(10, 20);
	printf("1");

	Setpos(15, 25);
	printf("1");

	system("title 贪吃蛇");
	getchar();
	return 0;
}

1.8 getAsyncKeyState

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

cpp 复制代码
SHORT GetAsyncKeyState(
 int vKey
);

将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
是short类型,在上⼀次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位
是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说
明,该按键被按过,否则为0。

虚拟键代码

如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低位是否为1.

那我们让GetAsyncKeyState的返回值与1进行&运算就可以判断最低位是不是1了

将这个过程写成一个宏

cpp 复制代码
#define KEY_PRESS(vk) (GetAsyncKeyState(vk)&1?1:0)
cpp 复制代码
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;
}

测试是可行的

1.9 <locale.h>本地化

这里简单的说⼀下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>头文件,其中提供了允许程序员针对

特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。

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

1.9.1 类项

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

1.10 setlocale

cpp 复制代码
char* setlocale (int category, const char* locale);

setlocale 函数用于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。
C标准给第⼆个参数仅定义了2种可能取值: "C" (正常模式)和 " " (本地模式)。
在任意程序执行开始,都会默认调用:

cpp 复制代码
setlocale(LC_ALL, "C");

当地区设置为"C"时,库函数按正常方式执行,小数点是⼀个点。
当程序运行起来后想改变地区,就只能显示调用setlocale函数。用" "作为第2个参数,调用
setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。

cpp 复制代码
int main()
{
	char* p = NULL;
	p = setlocale(LC_ALL, "");
	printf("%s\n", p);
	
	p = setlocale(LC_ALL, "C");
	printf("%s\n", p);
	return 0;
}


比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等

1.10.1 宽字符的打印

那如果想在屏幕上打印宽字符,怎么打印呢?
宽字符的字面量必须加上前缀"L",否则 C 语言会把字⾯量当作窄字符类型处理。前缀"L"在单引
号前面,表示宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前面,表示宽字符串,对应
wprintf() 的占位符为 %ls

cpp 复制代码
int main()
{
	setlocale(LC_ALL, "");
	wchar_t ch1 = L'国';
	char ch2 = 'a';
	char ch3 = 'b';
	printf("%c%c\n", ch2, ch3);
	wprintf(L"%lc\n", ch1);
	return 0;
}

从输出的结果来看,⼀个普通字符占⼀个字符的位置
但是打印⼀个汉字字符,占用2个字符的位置

7. 贪吃蛇的游戏设计

在正式实现贪吃蛇游戏之前,先来看一下最终想要的效果是什么

首先有一个欢迎界面

接下来是操作介绍

再就是游戏界面

那么可以将整个游戏实现的过程分为以下步骤

欢迎界面

说明界面

游戏界面

其中游戏界面又分为

初始化游戏

游戏进行

结束游戏

接下来进行实现

7.1 欢迎界面与说明界面

这里没啥好说的,改变光标位置,让输出的信息出现在我们想要的位置即可

cpp 复制代码
void WlecSc()
{
	SetPos(50,16);
	printf("欢迎来到贪吃蛇游戏\n");
	SetPos(50, 18);
	system("pause");

	SetPos(30, 16);
	printf("使用↑ ↓ ← → 控制蛇的方向,F3为加速,F4为减速,加速将获得更高的分数\n");
	SetPos(35, 17);
	printf("撞到墙体或者吃到自己的身体都会导致死亡,游戏结束\n");
	SetPos(50, 18);
	system("pause");
}

7.2 地图的绘制

在本文中,实现地图墙体的绘制采用的是宽字符□。

那么这里只需要改变光标所在的位置,然后再用循环打印指定数量的□即可

cpp 复制代码
#include<stdio.h>
#include<Windows.h>
int main()
{
	system("mode con cols=30 lines=30");
	system("pause");
	return 0;
}

前文中该代码打印出来的效果是这种

cpp 复制代码
int main()
{
	system("mode con cols=60 lines=30");
	system("pause");
	return 0;
}

这样就是正方形了

所以可以得出结论控制台的坐标关系是2X = Y

这里假如说我们要绘制的棋盘是30*30的一个正方形棋盘

先从上方开始绘制

此时就不需要移动光标,直接绘制即可

cpp 复制代码
int main()
{
	setlocale(LC_ALL, "");
	for (int i = 0; i < 30; i++)
	{
		wprintf(L"%lc",L'□');
	}
	getchar();
	return 0;
}

那么再打印下面和左右的墙体,此时就需要移动光标

cpp 复制代码
int main()
{
	setlocale(LC_ALL, "");
	for (int i = 0; i < 30; i++)
	{
		wprintf(L"%lc",L'□');
	}
	SetPos(0, 29);//将光标移动到(0,29)这个位置
	for (int i = 0; i < 30; i++)
	{
		wprintf(L"%lc", L'□');
	}//下方
	for (int i = 1; i < 29; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", L'□');
	}//左方
	for (int i = 1; i < 29; i++)
	{
		SetPos(58, i);//2*X = Y
		wprintf(L"%lc", L'□');
	}//右方

	getchar();

	return 0;
}

到这里地图墙体就绘制完成了,将他封装为一个函数

cpp 复制代码
void draw()
{
	setlocale(LC_ALL, "");
	for (int i = 0; i < 30; i++)
	{
		wprintf(L"%lc", L'□');
	}
	SetPos(0, 29);//将光标移动到(0,29)这个位置
	for (int i = 0; i < 30; i++)
	{
		wprintf(L"%lc", L'□');
	}//下方
	for (int i = 1; i < 29; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", L'□');
	}//左方
	for (int i = 1; i < 29; i++)
	{
		SetPos(58, i);//2*X = Y
		wprintf(L"%lc", L'□');
	}//右方

	getchar();
}

7.3 蛇与食物

蛇的身体的绘制采用宽字符●

食物绘制采用宽字符▲

在游戏中,蛇可以被分为两种状态

7.3.1 蛇

将蛇的身体用数个●连接起来就组成了一条蛇

蛇自身有很多属性

包括

蛇身

蛇的方向

蛇的状态

食物的分数

总分数

蛇的速度

那么首先就要创建蛇身的结构体类型

这个节点肯定有对应的坐标,以及指向下一个节点的指针

cpp 复制代码
struct SnakeNode
{
	//坐标
	short X;
	short Y;

	struct SnakeNode* next;
};

为方便书写,进行重命名

cpp 复制代码
typedef struct SnakeNode SNO;

蛇的方向无非就是上下左右

这里直接给一个枚举类型

cpp 复制代码
enum Direction
{
	UP = 1,
    DOWN,
    LEFT,
    RIGHT,
};

蛇的状态无非就是四种

正常

撞墙死

吃自己死

正常退出

也给个枚举类型

cpp 复制代码
enum Statement
{
	NORMAL=1,//正常行动
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF,//吃到自己
	END_NORMAL//正常退出
};

单个食物的分数和蛇的总分数直接给整形就好了

cpp 复制代码
    int food_score;
	int Score;

在玩贪吃蛇游戏的时候,贪吃蛇的移动是一闪一闪的

其实就是在移动的时候调用了sleep函数,休息了一段时间

这个时间越短速度就越快

那还是给这个时间一个整形

这样就能描述蛇的速度

将上述属性放到一个蛇结构体里,就创建了蛇

cpp 复制代码
struct Snake
{
	SNO* phead;//指向蛇头的指针
	Direction direct;//方向
	Statement state;//状态
	int food_score;//单个食物分数
	int Score;//总分数
	int sleep_time;//休息时间
};

因为蛇会吃掉食物,那就可以认为,食物与蛇身属于同一类型

cpp 复制代码
struct Snake
{
	SNO* phead;//指向蛇头的指针
	SNO* food;//指向食物的指针
	Direction direct;//方向
	Statement state;//状态
	int food_score;//单个食物分数
	int Score;//总分数
	int sleep_time;//休息时间
};

8 核心逻辑

8.1 游戏主逻辑

程序开始就设置支持本地化,然后进入游戏主逻辑

主逻辑分为三个过程

游戏开始

游戏运行

游戏结束

8.2 游戏开始

定义一个函数为GameStart

这里进行游戏开始需要做的事

游戏开始后,需要进行以下步骤

1.控制台窗口大小的设置

2.控制台窗口名字的设置

3.鼠标光标的隐藏

4.欢迎界面

5.说明界面

6.初始化蛇

7.初始化第一个食物

其中1.2.3.4.5在前文中已经实现了,就不再赘述

这里主要说6.7.

蛇的身体就是由几个蛇身节点组成

那么这里设定一开始蛇身的长度是5个宽字符●组成

这意味着一开始需要动态申请5个蛇身类型的节点

将申请身体节点的过程封装成一个函数

cpp 复制代码
//申请身体节点
SNO* BuyBodyNode(short a , short b)
{
     SNO* newnode = (SNO*)malloc(sizeof(SNO));
	 if (newnode == NULL)
	 {
		 printf("游戏出现错误,请退出后重试\n");
		 exit(1);
	 }
	 newnode->X = a;
	 newnode->Y = b;
	 newnode->next = NULL;
	 return newnode;
}

申请出来的五个节点,需要将他们连接起来,这里就可以用到数据结构链表来进行处理

这里想让蛇一开始是成水平排列,就说明这五个节点中的X坐标是需要移动的,Y坐标是相

等的设定从(20,10)这个位置组成蛇,因为●是宽字符占了两个字节,所以X坐标必须是2的倍

数,而一个Y坐标是可以容纳一个宽字符大小的,就没有特别要求

这里采用宏定义,以便后续想要进行更改的时候只用更改宏定义一处就好了

cpp 复制代码
#define INITPOSX 20
#define INITPOSY 10

给一个循环,申请五个节点,再将他们串起来

这里采取的连接方法是尾插法

但是尾插完成后是让最后一个插入的节点成为头

cpp 复制代码
	snake->phead = NULL;
	for (int i = 0; i < 5; i++)
	{
		SNO* BodyNode = BuyBodyNode(INITPOSX + (i * 2), INITPOSY);
		if (snake->phead == NULL)
		{
			//如果还没有身体就让蛇头指向这个节点
			snake->phead = BodyNode;
		}
		else
		{
			BodyNode->next = snake->phead;
			snake->phead = BodyNode;
		}
	}

打印出来看下行不行

为了方便修改,我们将墙体,蛇身,食物图形都采用宏定义

cpp 复制代码
#define INITPOSX 20
#define INITPOSY 10
#define BODY L'●'
#define FOOD L'▲'
#define WALL L'□'
cpp 复制代码
SNO* cur = snake->phead;
	//蛇身的打印
	while (cur)
	{
		SetPos(cur->X, cur->Y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

符合预期

然后,需要创建第一个食物

将创建食物的过程同样封装成一个函数

食物的生成位置采用随机生成

需要用到rand和srand

需要注意的是生成的随机数中X的坐标也需要满足是2的倍数

且食物不能生成在墙体里面,也不能生成在蛇的身上

cpp 复制代码
//创建食物
void Creatfood(Snake* snake)
{
	SNO* cur = NULL;
again:
	cur = snake->phead;
	srand((unsigned int)time(NULL));
	short a = rand() % 56 + 1;
	short b = rand() % 28 + 1;//食物不能生成在墙体内
	
	while (cur)
	{
		if ((cur->X == a && cur->Y == b) || a % 2 == 1)
		{
			goto again;//生成到蛇的身体上了就再生成一次
		}
		cur = cur->next;
	}
	//坐标合法就打印出来
	SNO* food = BuyBodyNode(a, b);
	snake->food = food;
	Sleep(10);
	SetPos(a, b);
	wprintf(L"%lc", FOOD);
}

在这之后,再将蛇的其他属性初始化一下

cpp 复制代码
    snake->direct = RIGHT;//游戏开始时蛇的方向
	snake->Score = 0;// 游戏刚开始时总分数为0
	snake->food_score = 10;//默认状态下单个食物的分数
	snake->food = NULL;//初始食物位置
	snake->state = NORMAL;//初始状态
	snake->sleep_time = 1000;//初始速度 = 10;//初始速度
cpp 复制代码
//蛇身的初始化
void InitSnake(Snake* snake)
{
	snake->phead = NULL;
	for (int i = 0; i < 5; i++)
	{
		SNO* BodyNode = BuyBodyNode(INITPOSX + (i * 2), INITPOSY);
		if (snake->phead == NULL)
		{
			//如果还没有身体就让蛇头指向这个节点
			snake->phead = BodyNode;
		}
		else
		{
			BodyNode->next = snake->phead;
			snake->phead = BodyNode;
		}
	}
	SNO* cur = snake->phead;
	//蛇身的打印
	while (cur)
	{
		SetPos(cur->X, cur->Y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	snake->direct = RIGHT;//游戏开始时蛇的方向
	snake->Score = 0;// 游戏刚开始时总分数为0
	snake->food_score = 20;//默认状态下单个食物的分数
	snake->food = NULL;//初始食物位置
	snake->state = NORMAL;//初始状态
	snake->sleep_time = 1000;//初始速度
}

再整体封装进GameStart函数中看看效果

cpp 复制代码
void GameStart()
{
	WlecSc();

	Snake* snake = (Snake*)malloc(sizeof(Snake));

	InitSnake(snake);
	Creatfood(snake);
	getchar();
}
cpp 复制代码
void GameStart()
{
	WlecSc();

	Snake* snake = (Snake*)malloc(sizeof(Snake));

	InitSnake(snake);
	Creatfood(snake);
	getchar();
}

此时在游戏进行时,还是希望有帮助信息在屏幕上显示

cpp 复制代码
void HelpInfo(Snake* snake)
{
	SetPos(80, 15);
	printf("当前单个食物分数是:%d\n", snake->food_score);
	SetPos(80, 17);

	printf("按F3加速,按F4减速\n");
	SetPos(80, 19);
	printf("按空格暂停,按ESC退出\n");

	SetPos(80, 21);
	printf("当前的总分数为%d\n", snake->Score);
}
cpp 复制代码
void GameStart()
{
	WlecSc();
	Snake* snake = (Snake*)malloc(sizeof(Snake));
	InitSnake(snake);
	Creatfood(snake);
	HelpInfo(snake);
	getchar();
}
void test()
{
	system("title 贪吃蛇");
	GameStart();

}
int main()
{
	test();
	return 0;
}

那么游戏开始这个阶段就完成了

接下来需要完成的是游戏运行这个阶段

8.3 游戏运行

那么在游戏运行这个阶段,要完成的逻辑是
根据游戏状态检查游戏是否继续,如果是状态是NORMAL,游戏继续,否则游戏结束。
如果游戏继续,就是检测按键情况,确定蛇下⼀步的方向,或者是否加速减速,是否暂停或者退出
游戏。

那么在游戏开始后,玩家需要通过按键来控制蛇的移动,这里就需要用到前文中说到的

getAsyncKeyState以及定义的宏

首先理一下逻辑

蛇肯定不能直接朝着当前运动方向的相反方向运动,这样就会吃到自己

cpp 复制代码
void SnakeMove(Snake* snake)
{
	if (KEY_PRESS(VK_UP) && snake->direct != DOWN)
	{
		snake->direct = UP;
	}
	else if (KEY_PRESS(VK_DOWN) && snake->direct != UP)
	{
		snake->direct = DOWN;
	}
	else if (KEY_PRESS(VK_LEFT) && snake->direct != RIGHT)
	{
		snake->direct = LEFT;
	}
	else if (KEY_PRESS(VK_RIGHT) && snake->direct != LEFT)
	{
		snake->direct = RIGHT;
	}
}

那么在移动蛇其实就是改变节点坐标

这里使蛇走的下一步是再申请一个节点出来,将这个新的节点再与蛇身连接起来

如果没有吃到食物

那么就将尾节点处的图案使用两个空格覆盖掉

再将尾节点释放掉,置空

首先实现初始状态下蛇的移动,即向右移动

cpp 复制代码
void SnakeMove(Snake* snake)
{
	if (KEY_PRESS(VK_UP) && snake->direct != DOWN)
	{
		snake->direct = UP;
	}
	else if (KEY_PRESS(VK_DOWN) && snake->direct != UP)
	{
		snake->direct = DOWN;
	}
	else if (KEY_PRESS(VK_LEFT) && snake->direct != RIGHT)
	{
		snake->direct = LEFT;
	}
	else if (KEY_PRESS(VK_RIGHT) && snake->direct != LEFT)
	{
		snake->direct = RIGHT;
	}
	SNO* cur = snake->phead;
	SNO* newnode = NULL;
	switch (snake->direct)
	{
	case RIGHT:
		newnode = BuyBodyNode(cur->X + 2, cur->Y);
		while (cur->next->next)
		{
			cur = cur->next;
		}
		SetPos(cur->next->X, cur->next->Y);
		printf("  ");
		free(cur->next);
		cur->next = NULL;

		newnode->next = snake->phead;
		snake->phead = newnode;
		SetPos(snake->phead->X, snake->phead->Y);
		wprintf(L"%lc", BODY);
		break;
	case LEFT:
		MoveLeft(snake);
		break;
	case UP:
		MoveUp(snake);
		break;
	case DOWN:
		MoveDown(snake);
		break;
	}
}

类似的,向左向上向下也是上述逻辑

将他们封装成函数

cpp 复制代码
//向右移动
void MoveRight(Snake* snake)
{
	SNO* cur = snake->phead;
	SNO* newnode = NULL;
	newnode = BuyBodyNode(cur->X + 2, cur->Y);
	while (cur->next->next)
	{
		cur = cur->next;
	}
	SetPos(cur->next->X, cur->next->Y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;

	newnode->next = snake->phead;
	snake->phead = newnode;
	SetPos(snake->phead->X, snake->phead->Y);
	wprintf(L"%lc", BODY);
}

//向左移动
void MoveLeft(Snake* snake)
{
	SNO* cur = snake->phead;
	SNO* newnode = NULL;
	newnode = BuyBodyNode(cur->X - 2, cur->Y);
	while (cur->next->next)
	{
		cur = cur->next;
	}
	SetPos(cur->next->X, cur->next->Y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;

	newnode->next = snake->phead;
	snake->phead = newnode;
	SetPos(snake->phead->X, snake->phead->Y);
	wprintf(L"%lc", BODY);
}

//向上移动
void MoveUp(Snake* snake)
{
	SNO* cur = snake->phead;
	SNO* newnode = NULL;
	newnode = BuyBodyNode(cur->X , cur->Y - 1);
	while (cur->next->next)
	{
		cur = cur->next;
	}
	SetPos(cur->next->X, cur->next->Y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;

	newnode->next = snake->phead;
	snake->phead = newnode;
	SetPos(snake->phead->X, snake->phead->Y);
	wprintf(L"%lc", BODY);
}

//向下移动
void MoveDown(Snake* snake)
{
	SNO* cur = snake->phead;
	SNO* newnode = NULL;
	newnode = BuyBodyNode(cur->X , cur->Y + 1);
	while (cur->next->next)
	{
		cur = cur->next;
	}
	SetPos(cur->next->X, cur->next->Y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;

	newnode->next = snake->phead;
	snake->phead = newnode;
	SetPos(snake->phead->X, snake->phead->Y);
	wprintf(L"%lc", BODY);
}

进行移动就GameRun函数中进行,每次打印间隔一个Sleep函数,就可以实现移动的效果

cpp 复制代码
void GameRun(Snake* snake)
{
	do
	{
		SnakeMove(snake);
		Sleep(snake->sleep_time);
	} while (snake->state == NORMAL);
}

然后,要实现蛇的加速 / 减速,其实就是在按下对应按键后让sleep_time减少 / 增加

对应单个食物的分数也要加减

cpp 复制代码
else if (KEY_PRESS(VK_F3))
	{
		if (snake->sleep_time >= 200)
		{
			snake->sleep_time -= 100;
            snake->food_score += 2;
		}
	}
	else if (KEY_PRESS(VK_F4))
	{
		if (snake->sleep_time <= 1500)
		{
			snake->sleep_time += 100;
            snake->food_score -= 2;
		}
	}

空格暂停就是直接让程序休眠

再按一次空格就让程序继续执行

按了一次空格后就死循环的执行Sleep

再按一次就跳出这个循环

cpp 复制代码
else if (KEY_PRESS(VK_SPACE))
	{
		while (1)
		{
			Sleep(1); 
			if (KEY_PRESS(VK_SPACE))
				break;
		}
	}

而按下ESC后,让状态变成END_NORMAL

cpp 复制代码
else if (KEY_PRESS(VK_ESCAPE))
	{
		snake->state = END_NORMAL;
	}

到这里,蛇的移动完成了一部分

cpp 复制代码
//蛇的移动
void SnakeMove(Snake* snake)
{
	if (KEY_PRESS(VK_UP) && snake->direct != DOWN)
	{
		snake->direct = UP;
	}
	else if (KEY_PRESS(VK_DOWN) && snake->direct != UP)
	{
		snake->direct = DOWN;
	}
	else if (KEY_PRESS(VK_LEFT) && snake->direct != RIGHT)
	{
		snake->direct = LEFT;
	}
	else if (KEY_PRESS(VK_RIGHT) && snake->direct != LEFT)
	{
		snake->direct = RIGHT;
	}
	else if (KEY_PRESS(VK_F3))
	{
		if (snake->sleep_time > 100)
		{
			snake->sleep_time -= 100;
		}
	}
	else if (KEY_PRESS(VK_F4))
	{
		if (snake->sleep_time <= 1500)
		{
			snake->sleep_time += 100;
		}
	}
	else if (KEY_PRESS(VK_SPACE))
	{
		while (1)
		{
			Sleep(1); 
			if (KEY_PRESS(VK_SPACE))
				break;
		}
	}
	else if (KEY_PRESS(VK_ESCAPE))
	{
		snake->state = END_NORMAL;
	}
	switch (snake->direct)
	{
	case RIGHT:
		MoveRight(snake);
		break;
	case LEFT:
		MoveLeft(snake);
		break;
	case UP:
		MoveUp(snake);
		break;
	case DOWN:
		MoveDown(snake);
		break;
	}
}

接下来,就是吃食物的实现

首先,蛇每走一步,都要判断下一步是不是食物,给一个函数判断

cpp 复制代码
//判断蛇走的下一步是不是食物
int NextIsFood(SNO* head,SNO* food)
{
	return (head->X == food->X && head->Y == food->Y);
}

如果蛇走的下一步生成的新节点的坐标和食物的坐标相同

那么就吃掉食物

吃掉食物后,蛇的长度会增

这个过程就是将食物的节点挂到蛇的身上同时,不释放尾节点

封装成一个函数叫EatFood

代码直接套用前文中的移动函数,不释放尾节点就好了

cpp 复制代码
void EatFood(Snake* snake, SNO* food)
{
	food->next = snake->phead;//连接起来
	snake->phead = food;
	SetPos(snake->phead->X, snake->phead->Y);
	wprintf(L"%lc", BODY);
	Creatfood(snake);
}

那么没吃到食物就是NotFood

把前文的移动函数中的过程封装在里面即可

cpp 复制代码
//没吃到食物
void NotFood(Snake* snake, SNO* newnode,SNO* cur)
{
	SetPos(cur->next->X, cur->next->Y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;

	newnode->next = snake->phead;//连接起来
	snake->phead = newnode;
	SetPos(snake->phead->X, snake->phead->Y);
	wprintf(L"%lc", BODY);
}

再将前面的移动代码更改一下,使用这两个函数

cpp 复制代码
//向右移动
void MoveRight(Snake* snake)
{
	SNO* cur = snake->phead;
	SNO* newnode = NULL;
	newnode = BuyBodyNode(cur->X + 2, cur->Y);
	while (cur->next->next)
	{
		cur = cur->next;
	}
	
	if (NextIsFood(newnode, snake->food))
	{
		EatFood(snake, snake->food);
		free(newnode);
		newnode = NULL;
	}
	else
	{
		
		NotFood(snake, newnode,cur);
	}
}

//向左移动
void MoveLeft(Snake* snake)
{
	SNO* cur = snake->phead;
	SNO* newnode = NULL;
	newnode = BuyBodyNode(cur->X - 2, cur->Y);
	while (cur->next->next)
	{
		cur = cur->next;
	}
	if (NextIsFood(newnode, snake->food))
	{
		EatFood(snake, snake->food);
		free(newnode);
		newnode = NULL;
	}
	else 
	{
    
		NotFood(snake, newnode,cur);
	}
}

//向上移动
void MoveUp(Snake* snake)
{
	SNO* cur = snake->phead;
	SNO* newnode = NULL;
	newnode = BuyBodyNode(cur->X , cur->Y - 1);

	while (cur->next->next)
	{
		cur = cur->next;
	}

	
	if (NextIsFood(newnode, snake->food))
	{
		EatFood(snake, snake->food);
		free(newnode);
		newnode = NULL;
	}
	else
	{
		
		NotFood(snake, newnode,cur);
	}
}

//向下移动
void MoveDown(Snake* snake)
{
	SNO* cur = snake->phead;
	SNO* newnode = NULL;
	newnode = BuyBodyNode(cur->X , cur->Y + 1);
	while (cur->next->next)
	{
		cur = cur->next;
	}

	if (NextIsFood(newnode, snake->food))
	{
		EatFood(snake, snake->food);
		free(newnode);
		newnode = NULL;
	}
	else
	{	
		NotFood(snake, newnode,cur);
	}
}

接下来来判定是否撞墙

逻辑就是判断走的下一步新生成的节点的X坐标是否等于0或60 Y坐标是否等于0或30

是的话就让蛇的状态为KILL_BY_WALL

cpp 复制代码
//撞到墙了

int HitWall(Snake* snake)
{
	SNO* newnode = snake->phead;
	if (newnode->X == 0 || newnode->Y == 0 || newnode->X == 60 || newnode->Y == 30)
	{
		snake->state = KILL_BY_WALL;
		return 1;
	}
	return 0;
}

通过返回值判断是不是撞墙,然后再将这个函数封装进NotFood中即可

cpp 复制代码
//没吃到食物
void NotFood(Snake* snake, SNO* newnode, SNO* cur)
{
	SetPos(cur->next->X, cur->next->Y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;

	newnode->next = snake->phead;//连接起来
	snake->phead = newnode;
	if (HitWall(snake))
	{
		return;
	}
	else
	{
		SetPos(snake->phead->X, snake->phead->Y);
		wprintf(L"%lc", BODY);
	}
}

迟到自己也封装成一个函数KillSelf

判断走的那一步节点是不是身体的节点就可以了

是的话就让蛇的状态为KILL_BY_SELF

cpp 复制代码
//吃到自己了
int KillSelf(Snake* snake)
{
	SNO* head = snake->phead->next;
	while (head)
	{
		if (snake->phead->X == head->X && snake->phead->Y == head->Y)
		{
			snake->state = KILL_BY_SELF;
			return 1;
		}

		head = head->next;
	}
	return 0;
}

同样的也放进NotFood中

cpp 复制代码
//没吃到食物
void NotFood(Snake* snake, SNO* newnode, SNO* cur)
{
	SetPos(cur->next->X, cur->next->Y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;

	newnode->next = snake->phead;//连接起来
	snake->phead = newnode;
	if (HitWall(snake))
	{
		return;
	}
	else if (KillSelf(snake))
	{
		return;
	}
	else
	{
		SetPos(snake->phead->X, snake->phead->Y);
		wprintf(L"%lc", BODY);
	}
}

那么到这里,游戏运行过程就基本完成了

cpp 复制代码
//蛇的移动
void SnakeMove(Snake* snake)
{
	if (KEY_PRESS(VK_UP) && snake->direct != DOWN)
	{
		snake->direct = UP;
	}
	else if (KEY_PRESS(VK_DOWN) && snake->direct != UP)
	{
		snake->direct = DOWN;
	}
	else if (KEY_PRESS(VK_LEFT) && snake->direct != RIGHT)
	{
		snake->direct = LEFT;
	}
	else if (KEY_PRESS(VK_RIGHT) && snake->direct != LEFT)
	{
		snake->direct = RIGHT;
	}
	else if (KEY_PRESS(VK_F3))
	{
		if (snake->sleep_time > 100)
		{
			snake->sleep_time -= 100;
			snake->food_score += 2;
		}
	}
	else if (KEY_PRESS(VK_F4))
	{
		if (snake->sleep_time < 1500)
		{
			snake->sleep_time += 100;
			snake->food_score -= 2;
		}
	}
	else if (KEY_PRESS(VK_SPACE))
	{
		SetPos(80, 10);
		printf("当前游戏处于暂停状态!\n");
		SetPos(80, 11);
		printf("再按一次空格游戏继续!\n");

		while (1)
		{
			Sleep(1);

			if (KEY_PRESS(VK_SPACE))
			{
				SetPos(80, 10);
				printf("                      ");
				SetPos(80, 11);
				printf("                      ");
				break;
			}

		}
	}
	else if (KEY_PRESS(VK_ESCAPE))
	{
		snake->state = END_NORMAL;
	}
	switch (snake->direct)
	{
	case RIGHT:
		MoveRight(snake);
		break;
	case LEFT:
		MoveLeft(snake);
		break;
	case UP:
		MoveUp(snake);
		break;
	case DOWN:
		MoveDown(snake);
		break;
	}
}
cpp 复制代码
void GameRun(Snake* snake)
{
	do
	{
		Sleep(snake->sleep_time);
		SnakeMove(snake);
		HelpInfo(snake);
	} while (snake->state == NORMAL);
	switch (snake->state)
	{
	case END_NORMAL:
		SetPos(80, 30);
		wprintf(L"您主动退出!\n");
		break;
	case KILL_BY_SELF:
		SetPos(80, 30);
		wprintf(L"您咬到了自己\n");
		break;
	case KILL_BY_WALL:
		SetPos(80, 30);
		wprintf(L"您撞到了墙\n");
		break;
	}
}

8.4 游戏结束

由于采用的是动态申请节点

在游戏结束后,需要将蛇和场上剩下的食物释放掉

cpp 复制代码
//释放蛇和食物
void DestorySnake(Snake* snake)
{
	free(snake->food);
	snake->food = NULL;//释放剩下的食物节点
	SNO* cur = snake->phead;
	while (snake->phead)
	{
		SNO* cur = snake->phead->next;//保存释放节点的下一个节点
		free(snake->phead);//释放蛇身
		snake->phead = NULL;
		snake->phead = cur;
	}
}

结束后还需要问一下是不是要再来一局

cpp 复制代码
    SetPos(80, 35);
	wprintf(L"想要再来一局吗(Y/N)\n");
	SetPos(80, 36);
	CursorShow();
	char ch = getchar();
	getchar();
	if (ch == 'Y')
	{
		system("cls");
		ch = '0';
		goto again;
	}
	else
	{
		system("cls");
		SetPos(50, 16);
		wprintf(L"Exit!\n");
		return;
	}

到这里整个游戏就算是基本完成了

9. 完整代码

cpp 复制代码
//Snake.h
#pragma once
#pragma once
#pragma once
#include<stdio.h>
#include<stdbool.h>
#include<Windows.h>
#include<locale.h>
#include<stdlib.h>
#include<time.h>

#define INITPOSX 20
#define INITPOSY 10
#define BODY L'●'
#define FOOD L'▲'
#define WALL L'□'
#define KEY_PRESS(VK) (GetAsyncKeyState(VK)&1?1:0)

enum Direction
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT,
};

typedef enum Direction Direction;
enum Statement
{
	NORMAL = 1,//正常行动
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF,//吃到自己
	END_NORMAL//主动退出
};

typedef enum Statement Statement;
void draw();//绘制游戏地图

void SetPos(short X, short Y);//定位光标位置

void WlecSc();//欢迎界面

//蛇身结构体
struct SnakeNode
{
	//坐标
	short X;
	short Y;

	struct SnakeNode* next;
};
typedef struct SnakeNode SNO;

//蛇
struct Snake
{
	SNO* phead;//指向蛇头的指针
	SNO* food;//指向食物的指针
	Direction direct;//方向
	Statement state;//状态
	int food_score;//单个食物分数
	int Score;//总分数
	int sleep_time;//休息时间
};
typedef struct Snake Snake;

//隐藏光标
void HiddenCursor();

//帮助信息
void HelpInfo(Snake* snake);

//申请节点
SNO* BuyNode(short a, short b);

//初始化蛇
void InitSnake(Snake* snake);

//创建食物
void Creatfood();

//蛇的移动
void SnakeMove(Snake* snake);

//向上移动
void MoveUp(Snake* snake);

//向下移动
void MoveDown(Snake* snake);

//向右移动
void MoveRight(Snake* snake);

//向左移动
void MoveLeft(Snake* snake);

//判断蛇走的下一步是不是食物
int NextIsFood(SNO* head, SNO* food);

//蛇吃食物
void EatFood(Snake* snake, SNO* food);

//没吃到食物
void NotFood(Snake* snake, SNO* newnode);

//撞到墙了
int HitWall(Snake* snake);

//吃到自己了
int KillSelf(Snake* snake);

//释放蛇和食物
void DestorySnake(Snake* snake);

//显示光标
void CursorShow();
cpp 复制代码
//Start.c
#include"Snake.h"
//地图的绘制
void draw()
{
	SetPos(0, 0);
	setlocale(LC_ALL, "");
	for (int i = 0; i < 30; i++)
	{
		wprintf(L"%lc", L'□');
	}
	SetPos(0, 29);//将光标移动到(0,29)这个位置
	for (int i = 0; i < 30; i++)
	{
		wprintf(L"%lc", L'□');
	}//下方
	for (int i = 1; i < 29; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", L'□');
	}//左方
	for (int i = 1; i < 29; i++)
	{
		SetPos(58, i);//2*X = Y
		wprintf(L"%lc", L'□');
	}//右方
}

//设置光标位置
void SetPos(short X, short Y)
{
	HANDLE houput = GetStdHandle(STD_OUTPUT_HANDLE);

	COORD pos = { X,Y };
	SetConsoleCursorPosition(houput, pos);
}

void HelpInfo(Snake* snake)
{
	SetPos(80, 15);
	printf("当前单个食物分数是:%d\n", snake->food_score);
	SetPos(80, 17);

	printf("按F3加速,按F4减速\n");
	SetPos(80, 19);
	printf("按空格暂停,按ESC退出\n");

	SetPos(80, 21);
	printf("当前的总分数为%d\n", snake->Score);
}

//界面
void WlecSc()
{
	HiddenCursor();
	SetPos(50, 16);
	printf("欢迎来到贪吃蛇游戏\n");
	SetPos(50, 18);
	system("pause");

	SetPos(30, 16);
	printf("使用↑ ↓ ← → 控制蛇的方向,F3为加速,F4为减速,加速将获得更高的分数\n");
	SetPos(35, 17);
	printf("撞到墙体或者吃到自己的身体都会导致死亡,游戏结束\n");
	SetPos(50, 18);
	system("pause");
	system("cls");
	draw();

}

//申请身体节点
SNO* BuyBodyNode(short a, short b)
{
	SNO* newnode = (SNO*)malloc(sizeof(SNO));
	if (newnode == NULL)
	{
		printf("游戏出现错误,请退出后重试\n");
		exit(1);
	}
	newnode->X = a;
	newnode->Y = b;
	newnode->next = NULL;
	return newnode;
}

//创建食物
void Creatfood(Snake* snake)
{
	SNO* cur = NULL;
again:
	cur = snake->phead;
	srand((unsigned int)time(NULL));
	short a = rand() % 56 + 1;
	short b = rand() % 28 + 1;//食物不能生成在墙体内
	
	while (cur)
	{
		if ((cur->X == a && cur->Y == b) || a % 2 == 1)
		{
			goto again;//生成到蛇的身体上了就再生成一次
		}
		cur = cur->next;
	}
	//坐标合法就打印出来
	SNO* food = BuyBodyNode(a, b);
	snake->food = food;
	Sleep(10);
	SetPos(a, b);
	wprintf(L"%lc", FOOD);
}

//蛇身的初始化
void InitSnake(Snake* snake)
{
	snake->phead = NULL;
	for (int i = 0; i < 5; i++)
	{
		SNO* BodyNode = BuyBodyNode(INITPOSX + (i * 2), INITPOSY);
		if (snake->phead == NULL)
		{
			//如果还没有身体就让蛇头指向这个节点
			snake->phead = BodyNode;
		}
		else
		{
			BodyNode->next = snake->phead;
			snake->phead = BodyNode;
		}
	}
	SNO* cur = snake->phead;
	//蛇身的打印
	while (cur)
	{
		SetPos(cur->X, cur->Y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	snake->direct = RIGHT;//游戏开始时蛇的方向
	snake->Score = 0;// 游戏刚开始时总分数为0
	snake->food_score = 10;//默认状态下单个食物的分数
	snake->food = NULL;//初始食物位置
	snake->state = NORMAL;//初始状态
	snake->sleep_time = 1000;//初始速度
}

//隐藏光标
void HiddenCursor()
{
	//获得标准输出设备的句柄
	HANDLE houput = GetStdHandle(STD_OUTPUT_HANDLE);

	//定义一个光标信息的结构体
	CONSOLE_CURSOR_INFO cursor_info = { 0 };
	GetConsoleCursorInfo(houput, &cursor_info);
	cursor_info.bVisible = false;
	SetConsoleCursorInfo(houput, &cursor_info);
}
cpp 复制代码
//Run.c
#include"Snake.h"

//向右移动
void MoveRight(Snake* snake)
{
	SNO* cur = snake->phead;
	SNO* newnode = NULL;
	newnode = BuyBodyNode(cur->X + 2, cur->Y);
	while (cur->next->next)
	{
		cur = cur->next;
	}

	if (NextIsFood(newnode, snake->food))
	{
		EatFood(snake, snake->food);
		free(newnode);
		newnode = NULL;
	}
	else
	{

		NotFood(snake, newnode, cur);
	}
}

//向左移动
void MoveLeft(Snake* snake)
{
	SNO* cur = snake->phead;
	SNO* newnode = NULL;
	newnode = BuyBodyNode(cur->X - 2, cur->Y);
	while (cur->next->next)
	{
		cur = cur->next;
	}
	if (NextIsFood(newnode, snake->food))
	{
		EatFood(snake, snake->food);
		free(newnode);
		newnode = NULL;
	}
	else
	{

		NotFood(snake, newnode, cur);
	}
}

//向上移动
void MoveUp(Snake* snake)
{
	SNO* cur = snake->phead;
	SNO* newnode = NULL;
	newnode = BuyBodyNode(cur->X, cur->Y - 1);

	while (cur->next->next)
	{
		cur = cur->next;
	}


	if (NextIsFood(newnode, snake->food))
	{
		EatFood(snake, snake->food);
		free(newnode);
		newnode = NULL;
	}
	else
	{

		NotFood(snake, newnode, cur);
	}
}

//向下移动
void MoveDown(Snake* snake)
{
	SNO* cur = snake->phead;
	SNO* newnode = NULL;
	newnode = BuyBodyNode(cur->X, cur->Y + 1);
	while (cur->next->next)
	{
		cur = cur->next;
	}

	if (NextIsFood(newnode, snake->food))
	{
		EatFood(snake, snake->food);
		free(newnode);
		newnode = NULL;
	}
	else
	{
		NotFood(snake, newnode, cur);
	}
}

//蛇的移动
void SnakeMove(Snake* snake)
{
	if (KEY_PRESS(VK_UP) && snake->direct != DOWN)
	{
		snake->direct = UP;
	}
	else if (KEY_PRESS(VK_DOWN) && snake->direct != UP)
	{
		snake->direct = DOWN;
	}
	else if (KEY_PRESS(VK_LEFT) && snake->direct != RIGHT)
	{
		snake->direct = LEFT;
	}
	else if (KEY_PRESS(VK_RIGHT) && snake->direct != LEFT)
	{
		snake->direct = RIGHT;
	}
	else if (KEY_PRESS(VK_F3))
	{
		if (snake->sleep_time > 100)
		{
			snake->sleep_time -= 100;
			snake->food_score += 2;
		}
	}
	else if (KEY_PRESS(VK_F4))
	{
		if (snake->sleep_time <= 1500)
		{
			snake->sleep_time += 100;
			snake->Score -= 1;
		}
	}
	else if (KEY_PRESS(VK_SPACE))
	{
		SetPos(80, 10);
		printf("当前游戏处于暂停状态!\n");
		SetPos(80, 11);
		printf("再按一次空格游戏继续!\n");

		while (1)
		{
			Sleep(1);

			if (KEY_PRESS(VK_SPACE))
			{
				SetPos(80, 10);
				printf("                      ");
				SetPos(80, 11);
				printf("                      ");
				break;
			}

		}
	}
	else if (KEY_PRESS(VK_ESCAPE))
	{
		snake->state = END_NORMAL;
	}
	switch (snake->direct)
	{
	case RIGHT:
		MoveRight(snake);
		break;
	case LEFT:
		MoveLeft(snake);
		break;
	case UP:
		MoveUp(snake);
		break;
	case DOWN:
		MoveDown(snake);
		break;
	}
}

//判断蛇走的下一步是不是食物
int NextIsFood(SNO* head, SNO* food)
{
	return (head->X == food->X && head->Y == food->Y);
}

//移动均是生成新的节点
//头插到蛇的身体上
//如果吃到食物就让不释放蛇的最后一个节点
void EatFood(Snake* snake, SNO* food)
{
	food->next = snake->phead;//连接起来
	snake->phead = food;
	SetPos(snake->phead->X, snake->phead->Y);
	wprintf(L"%lc", BODY);
	snake->Score += snake->food_score;
	Creatfood(snake);
}

//没吃到食物
void NotFood(Snake* snake, SNO* newnode, SNO* cur)
{
	SetPos(cur->next->X, cur->next->Y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;

	newnode->next = snake->phead;//连接起来
	snake->phead = newnode;
	if (HitWall(snake))
	{
		return;
	}
	else if (KillSelf(snake))
	{
		return;
	}
	else
	{
		SetPos(snake->phead->X, snake->phead->Y);
		wprintf(L"%lc", BODY);
	}
}


//撞到墙了

int HitWall(Snake* snake)
{
	SNO* newnode = snake->phead;
	if (newnode->X == 0 || newnode->Y == 0 || newnode->X == 60 || newnode->Y == 30)
	{
		snake->state = KILL_BY_WALL;
		return 1;
	}
	return 0;
}

//吃到自己了
int KillSelf(Snake* snake)
{
	SNO* head = snake->phead->next;
	while (head)
	{
		if (snake->phead->X == head->X && snake->phead->Y == head->Y)
		{
			snake->state = KILL_BY_SELF;
			return 1;
		}

		head = head->next;
	}
	return 0;
}
cpp 复制代码
//End.c
#include"Snake.h"

//释放蛇和食物
void DestorySnake(Snake* snake)
{
	free(snake->food);
	snake->food = NULL;//释放剩下的食物节点
	SNO* cur = snake->phead;
	while (snake->phead)
	{
		SNO* cur = snake->phead->next;//保存释放节点的下一个节点
		free(snake->phead);//释放蛇身
		snake->phead = NULL;
		snake->phead = cur;
	}
}

//显示光标
void CursorShow()
{
	//获得标准输出设备的句柄
	HANDLE houput = GetStdHandle(STD_OUTPUT_HANDLE);

	//定义一个光标信息的结构体
	CONSOLE_CURSOR_INFO cursor_info = { 0 };
	GetConsoleCursorInfo(houput, &cursor_info);
	cursor_info.bVisible = true;
	SetConsoleCursorInfo(houput, &cursor_info);
}
cpp 复制代码
//Game.c
#include"Snake.h"
void GameStart(Snake* snake)
{
	WlecSc();
	InitSnake(snake);
	Creatfood(snake);
	HelpInfo(snake);
}

void GameRun(Snake* snake)
{
	do
	{
		Sleep(snake->sleep_time);
		SnakeMove(snake);
		HelpInfo(snake);
	} while (snake->state == NORMAL);
	switch (snake->state)
	{
	case END_NORMAL:
		SetPos(80, 30);
		wprintf(L"您主动退出!\n");
		break;
	case KILL_BY_SELF:
		SetPos(80, 30);
		wprintf(L"您咬到了自己\n");
		break;
	case KILL_BY_WALL:
		SetPos(80, 30);
		wprintf(L"您撞到了墙\n");
		break;
	}
}

void GameEnd(Snake* snake)
{

	DestorySnake(snake);//释放蛇和食物
	SetPos(80, 35);
	wprintf(L"想要再来一局吗(Y/N)\n");
	SetPos(80, 36);
	CursorShow();
}

本篇文章到此结束

相关推荐
小龙在山东1 小时前
Python 包管理工具 uv
windows·python·uv
昏睡红猹1 小时前
我在厂里搞wine的日子
windows·wine
love530love4 小时前
Docker 稳定运行与存储优化全攻略(含可视化指南)
运维·人工智能·windows·docker·容器
1024小神9 小时前
tauri项目在windows上的c盘没有权限写入文件
c语言·开发语言·windows
程序视点18 小时前
Window 10文件拷贝总是卡很久?快来试试这款小工具,榨干硬盘速度!
windows
wuk99818 小时前
基于MATLAB编制的锂离子电池伪二维模型
linux·windows·github
lzb_kkk19 小时前
【C++】C++四种类型转换操作符详解
开发语言·c++·windows·1024程序员节
Paper_Love1 天前
x86-64_windows交叉编译arm_linux程序
arm开发·windows
前端若水1 天前
通过 Windows 共享文件夹 + 手机访问(SMB协议)如何实现
windows·智能手机
超龄超能程序猿2 天前
dnSpy 使用教程
windows·microsoft