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指令
#include<stdio.h>
#include<Windows.h>
int main()
{
	system("mode con cols=30 lines=30");
	system("pause");
	return 0;
}

这是没修改之前
这是修改之后的,这里为什么不是个正方形,后文会讲到
也可以通过命令设置控制台窗口的名字
title命令
#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类型的声明:
typedef struct _COORD 
{
 SHORT X;
 SHORT Y;
} COORD, *PCOORD;

X,Y分别对应横纵坐标

1.4 GetStdHandle

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

返回值其实是指针
int main()
{
	//获得标准输出设备的句柄
	HANDLE houput =  GetStdHandle(STD_OUTPUT_HANDLE);
	return 0;
}

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

1.5 GetConsoleCursorInfo

那么在控制台输出时,光标会不停闪烁,希望将光标去掉

这个函数就是检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
BOOL WINAPI GetConsoleCursorInfo(
 HANDLE hConsoleOutput,
 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标

1.5.1 CONSOLE_CURSOR_INFO

这个结构体,包含有关控制台光标的信息
typedef struct _CONSOLE_CURSOR_INFO {
 DWORD dwSize;
 BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
dwSize,由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完
全填充单元格到单元底部的水平线条。
bVisible,光标的可见性。 如果光标可见,则此成员为 TRUE。
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

设置指定控制台屏幕缓冲区的光标的大小和可见性
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;
}

这里可以看到光标确实变大了
隐藏光标也是可以的
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函数将光标位置设置到指定的位置。
int main()
{
	//获得标准输出设备的句柄
	HANDLE houput = GetStdHandle(STD_OUTPUT_HANDLE);

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


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

光标的位置发生了改变

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

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

虚拟键代码

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

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

将这个过程写成一个宏

#define KEY_PRESS(vk) (GetAsyncKeyState(vk)&1?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;
}

 测试是可行的

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

char* setlocale (int category, const char* locale);
setlocale 函数用于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。
C标准给第⼆个参数仅定义了2种可能取值: "C" (正常模式)和 " " (本地模式)。
在任意程序执行开始,都会默认调用:
setlocale(LC_ALL, "C");
当地区设置为"C"时,库函数按正常方式执行,小数点是⼀个点。
当程序运行起来后想改变地区,就只能显示调用setlocale函数。用" "作为第2个参数,调用
setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。
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
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 欢迎界面与说明界面

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

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 地图的绘制

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

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

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

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

int main()
{
	system("mode con cols=60 lines=30");
	system("pause");
	return 0;
}

这样就是正方形了

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

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

先从上方开始绘制

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

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

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


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

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

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 蛇

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

蛇自身有很多属性

包括

蛇身

蛇的方向

蛇的状态

食物的分数

总分数

蛇的速度

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

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

struct SnakeNode
{
	//坐标
	short X;
	short Y;

	struct SnakeNode* next;
};

为方便书写,进行重命名

typedef struct SnakeNode SNO;

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

这里直接给一个枚举类型

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

蛇的状态无非就是四种

正常

撞墙死

吃自己死

正常退出

也给个枚举类型

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

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

    int food_score;
	int Score;

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

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

这个时间越短速度就越快

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

这样就能描述蛇的速度

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

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

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

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个蛇身类型的节点

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

//申请身体节点
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坐标是可以容纳一个宽字符大小的,就没有特别要求

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

#define INITPOSX 20
#define INITPOSY 10

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

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

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

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

打印出来看下行不行

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

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

符合预期

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

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

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

需要用到rand和srand

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

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

//创建食物
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);
}

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

    snake->direct = RIGHT;//游戏开始时蛇的方向
	snake->Score = 0;// 游戏刚开始时总分数为0
	snake->food_score = 10;//默认状态下单个食物的分数
	snake->food = NULL;//初始食物位置
	snake->state = NORMAL;//初始状态
	snake->sleep_time = 1000;//初始速度 = 10;//初始速度

//蛇身的初始化
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函数中看看效果


void GameStart()
{
	WlecSc();

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

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

void GameStart()
{
	WlecSc();

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

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

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

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 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以及定义的宏

首先理一下逻辑

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

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

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

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

如果没有吃到食物

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

再将尾节点释放掉,置空

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


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

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

将他们封装成函数

//向右移动
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函数,就可以实现移动的效果

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

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

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

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

再按一次就跳出这个循环

else if (KEY_PRESS(VK_SPACE))
	{
		while (1)
		{
			Sleep(1); 
			if (KEY_PRESS(VK_SPACE))
				break;
		}
	}

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

else if (KEY_PRESS(VK_ESCAPE))
	{
		snake->state = END_NORMAL;
	}

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

//蛇的移动
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;
	}
}

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

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

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

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

那么就吃掉食物

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

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

封装成一个函数叫EatFood

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

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

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

//没吃到食物
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);
}

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

//向右移动
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

//撞到墙了

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中即可

//没吃到食物
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

//吃到自己了
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中

//没吃到食物
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);
	}
}

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


//蛇的移动
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;
	}
}
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 游戏结束

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

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

//释放蛇和食物
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;
	}
}

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

    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. 完整代码

//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();
//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);
}
//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;
}
//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);
}
//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();
}

本篇文章到此结束

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

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

相关文章

熟悉Redis吗,那Redis的过期键删除策略是什么

对于Redis&#xff0c;我们业务开发一般都只关心Redis键值对的查询、修改操作&#xff0c;可能因为懒或者只想能用就行&#xff0c;呵呵。很少关心键值对存储在什么地方、键值对过期了会怎么样、Redis有没什么策略处理过期的键、Redis处理过期键又有什么作用&#xff1f;但这些…

《深入Linux内核架构》第4章 进程虚拟内存(1)

目录 4.1 简介 4.2 进程虚拟地址空间 4.2.1 进程地址空间分布 4.2.2 建立布局 本专栏文章将有70篇左右&#xff0c;欢迎关注&#xff0c;订阅后续文章。 第3章讲了两点&#xff1a;物理内存的管理&#xff0c;内核虚拟地址管理。 本章讲&#xff1a;用户进程的虚拟地址空间…

黄金投资怎么判断走势好坏?

投资黄金&#xff0c;就是押注于这一贵金属价格的变动。判断黄金价格的走势好坏&#xff0c;对于投资者来说至关重要。这需要从宏观经济指标、技术分析指标和市场情绪等多元化角度进行综合分析。 宏观经济指标 货币政策&#xff1a;中央银行的货币政策&#xff0c;尤其是利率决…

必应bing国内广告如何开户,怎么收费?

搜索引擎广告作为直接触达潜在客户的有效途径之一&#xff0c;日益受到企业的重视&#xff0c;必应Bing作为全球第二大搜索引擎&#xff0c;在中国市场同样拥有庞大的用户群体&#xff0c;为企业提供了不可忽视的广告投放平台。 一、必应bing国内广告开户流程 1、需求分析与咨…

国产PLC海为如何与电脑通信

前言 这几天接触到了国产海为PLC&#xff0c;做一个记录&#xff01;学习一下&#xff01; 串口联机 步骤 1&#xff1a;使用 USB 转 485 线连接 A8&#xff08;RS485 通讯口&#xff09;和电脑&#xff1b; 步骤 2&#xff1a;打开 Haiwell happy PLC 编程软件&#xff0c…

vcenter7安装nsx

登录控制台 Get services

视频汇聚边缘网关EasyCVR硬件设备无法访问域名,解析失败该如何处理?

安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台部署轻快&#xff0c;可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及支持厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等。视频汇聚融合管理平台EasyCVR既具备传统安防视…

YOLOv9全网最新改进系列:YOLOv9完美融合标准化的注意力模块NAM,高效且轻量级的归一化注意力机制,助力目标检测再上新台阶!

YOLOv9全网最新改进系列&#xff1a;YOLOv9完美融合标准化的注意力模块NAM&#xff0c;高效且轻量级的归一化注意力机制&#xff0c;助力目标检测再上新台阶&#xff01;&#xff01;&#xff01; YOLOv9原文链接戳这里&#xff0c;原文全文翻译请关注B站Ai学术叫叫首er B站全…

细说夜莺监控系统告警自愈机制

虽说监控系统最侧重的功能是指标采集、存储、分析、告警&#xff0c;为了能够快速恢复故障&#xff0c;告警自愈机制也是需要重点投入建设的&#xff0c;所有可以固化为脚本的应急预案都可以使用告警自愈机制来快速驱动。夜莺开源项目从 v7 版本开始内置了告警自愈模块&#xf…

千元投影仪高性价比机型又出新机?大眼橙C1D上市引领市场新潮流

近年来投影仪技术不断更新迭代&#xff0c;家用智能投影仪市场正迎来一场革新风暴。最明显的就是各家品牌都更快地推出自家的投影仪新品&#xff0c;4月底&#xff0c;极米推出了play5&#xff0c;大眼橙推出了c1d&#xff0c;小明推出了newq3pro……都是千元价位的投影仪新品&…

RabbitMQ基础入门

初识MQ 微服务间通讯有同步和异步两种方式&#xff1a; 同步通讯&#xff1a;就像打电话&#xff0c;需要实时响应。 异步通讯&#xff1a;就像发邮件&#xff0c;不需要马上回复。 两种方式各有优劣&#xff0c;打电话可以立即得到响应&#xff0c;但是你却不能跟多个人同…

Linux的目录结构

什么是路径 在Linux系统中&#xff0c;"路径"指的是文件系统中文件或目录的位置。路径可以是绝对的或相对的。 绝对路径&#xff1a;从根目录&#xff08;即 / &#xff09;开始&#xff0c;描述从根目录到目标文件或目录的完整路径。例如&#xff0c;/usr/local/bi…

SWAT模型【建模方法、实例应用、高级进阶技能】实践

第一部分&#xff1a;SWAT模型实践部分 一、SWAT模型及应用介绍 1.1 面源污染概要 1.2 SWAT模型及应用 1.3 SWAT模型原理 1.4 SWAT模型输入文件 1.5 ArcGIS与SWAT关系 二、SWAT模型中GIS必备技术 2.1 GIS软件平台 2.2 ArcGIS10.6安装和注意事项 2.3 ArcGIS入门 2.…

IT外包能在企业上云时提供什么帮助?

在云计算不断发展的背景下&#xff0c;企业对IT部门的要求日益提高&#xff0c;越来越多的企业开始考虑将IT系统迁移到云上。因此&#xff0c;IT外包也成为企业成功上云的重要支持之一。IT外包在企业上云时具体能提供什么帮助&#xff1f;本文将对此进行详细阐述。 业务重心转移…

Linux磁盘逻辑卷LVM丢失

一.原因&#xff1a;服务器异常断电&#xff0c;重启服务器之后&#xff0c;服务所在的磁盘丢失&#xff0c;逻辑卷也不存在。 二.解决方法&#xff1a; 2.1&#xff09;执行以下命令查看lvm配置文件备份内容&#xff1a; more /etc/lvm/backup/datavg01 datavg是之前使…

ubuntu20文件安装和卸载cuda11.6

搜索cuda 11.6 nvidia&#xff0c;进入官网https://developer.nvidia.com/cuda-11-6-0-download-archive 选择linux --> runfile 用安装包安装 wget https://developer.download.nvidia.com/compute/cuda/11.6.0/local_installers/cuda_11.6.0_510.39.01_linux.run sudo s…

【数据分享】2021-2024年我国主要城市逐月轨道交通运营数据

以地铁为代表的轨道交通是大城市居民的主要交通出行方式之一&#xff0c;轨道交通的建设和运营情况也是一个城市发展水平的重要体现。本次我们为大家带来的是2021-2024年我国主要城市的逐月的轨道交通运营数据&#xff01;目前最新数据到2024年2月&#xff0c;数据也会继续更新…

Java类型转换、运算符、流程控制语句你真的懂了吗?

类型转换&#xff1a; 1.数据类型转换之隐式转换&#xff08;表示数据范围从小到大&#xff09; 小的数据类型&#xff0c;和大的数据类型运算&#xff0c;小的会提升为大的之后&#xff0c;再进行运算特殊关注&#xff1a;byte short char 三种数据在运算的时候&#xff0c;不…

OceanBase学习1:分布式数据库与集中式数据库的差异

目录 1. 传统集中式数据库 2. 数据库中间件的分库分表 3. 分布式数据库的基本特点及对比分析 4. OceanBase和传统数据库的对比 5. 小结 1. 传统集中式数据库 优点 成熟稳定:经过近40年的发展&#xff0c;应用到各行各业&#xff0c;产品技术非常成熟稳定行业适配性强:适配…

ElementUI Select选择器多选获取选中对象

html <el-form-item label"账户标签&#xff1a;" prop"tags"><el-selectstyle"width: 500px"value-key"tagId"v-model"form.tags"clearablefilterablemultipleplaceholder"请搜索选择账户标签"><…