C语言贪吃蛇

news2024/11/24 7:41:12

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

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.coloradmin.cn/o/1652944.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vcenter7安装nsx

登录控制台 Get services

【噪声学习】噪声标签的鲁棒点云分割

Robust Point Cloud Segmentation with Noisy Annotations 事实上,与二维图像标注[1]、[2]相比,三维数据的干净标签更难获得。这主要是因为1)需要标注的点数通常非常庞大,例如在 ScanNetV2 [3] 中标注一个典型的室内场景时,需要标注百万量级的点数;2)标注过程本身更加复…

【4089】基于小程序实现的互动打卡系统

作者主页&#xff1a;Java码库 主营内容&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app等设计与开发。 收藏点赞不迷路 关注作者有好处 文末获取源码 技术选型 【后端】&#xff1a;Java 【框架】&#xff1a;spring…

Windows安装RabbitMQ教程(附安装包)

需要两个安装包 Erlang 安装包: https://download.csdn.net/download/Brevity6/89274663 (自己从官网下载也可以) RabbitMQ Windows 安装包&#xff1a; https://download.csdn.net/download/Brevity6/89274667 (自己从官网下载也可以) Erlang安装 Erlang安装傻瓜式下一…

Redis 之 布隆过滤器 与 布谷鸟过滤器

大家都知道,在计算机中IO一直是一个瓶颈,很多框架以及技术甚至硬件都是为了降低IO操作而生,今天聊一聊过滤器,先说一个场景: 我们业务后端涉及数据库,当请求消息查询某些信息时,可能先检查缓存中是否有相关信息,有的话返回,如果没有的话可能就要去数据库里面查询,这时候有一个…

新书速览|Rust编程与项目实战

掌握Rust编程基础和开发方法&#xff0c;实战网络编程、图像和游戏开发、数据分析项目 本书内容 Rust是一门系统编程语言&#xff0c;专注于安全&#xff0c;尤其是并发安全&#xff0c;它也是支持函数式、命令式以及泛型等编程范式的多范式语言。标准Rust在语法和性能上和标准…

小麦穗检测数据集VOC+YOLO格式6508张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;6508 标注数量(xml文件个数)&#xff1a;6508 标注数量(txt文件个数)&#xff1a;6508 标注…

IP定位技术在解决“薅羊毛”问题中扮演着关键角色

IP定位技术在解决被“薅羊毛”问题中扮演着关键角色。所谓“薅羊毛”&#xff0c;通常指的是在网络平台上&#xff0c;通过不正当手段获取优惠、奖励或利润的行为。这种行为不仅损害了平台的经济利益&#xff0c;也破坏了公平竞争的市场环境。IP定位技术通过提供IP地址的地理位…

多核DSP并行计算跨平台通信解决方案

并行计算的核心是计算节点以及节点间的通信与协调机制。OpenMP虽然给开发者提供了极易上手的增量式开发方式&#xff0c;但是OpenMP在与复杂架构的MCSDK结合后&#xff0c;工具与代码产生了大量不可调试的黑盒子&#xff0c;更是决定了它不能用于关键任务领域&#xff0c;如军工…

测评方式揭秘:自养号测评与真人测评的利与弊

在当今电商行业飞速发展的背景下&#xff0c;不少卖家为了提升产品销量和积累良好评价&#xff0c;采取了真人测评和自养号测评两种策略。然而&#xff0c;这两种测评方式的具体运作机制和效果差异&#xff0c;许多卖家可能并未深入了解。接下来&#xff0c;我们将深入挖掘真人…

Visual Studio C++ 2019进行安装

Visual Studio C 2019进行下载安装 链接&#xff1a;https://my.visualstudio.com/Downloads?qvisual%20studio%202017&wt.mc_idomsftvscom~older-downloads

流量分析利器arkime的学习之路(三)---结合Suricata攻击检测

1、基础 Arkime安装部分参考《流量分析利器arkime的学习之路&#xff08;一&#xff09;—安装部署》 在此基础上安装suricata软件并配置。 2、安装suricata yum install suricate 可能依赖的文件包括libyaml&#xff0c;PyYAML&#xff0c;这些可能在之前安装arkime或者其他…

在全志H616核桃派开发板上实现超声波传感器测距

前言​ 超声波传感器是一款测量距离的传感器。其原理是利用声波在遇到障碍物反射接收结合声波在空气中传播的速度计算的得出。在测量、避障小车&#xff0c;无人驾驶等领域都有相关应用。 实验目的​ 通过python编程实现超声波传感器测距。 实验讲解​ 下图是一款市面上常…

5V升8.4V2A升压恒压WT3231

5V升8.4V2A升压恒压WT3231 WT3231 是一种高性能直流-直流&#xff08;DC-DC&#xff09;转换器&#xff0c;集成了能够承受10A电流和26mΩ低导通电阻的功率MOSFET。该转换器能提供高达12V的稳定输出电压&#xff0c;并具有固定600KHz开关频率&#xff0c;使得小型外部电感和电…

UV胶具有哪些特点和优势

1. 快速固化&#xff1a;UV胶在紫外线照射下能够迅速固化&#xff0c;固化时间通常在几秒钟到几分钟之间&#xff0c;大大提高了生产效率。 2. 高粘接强度&#xff1a;UV胶固化后&#xff0c;具有较高的粘接强度&#xff0c;能够在各种材料上实现可靠的粘接&#xff0c;提供持…

盘点四种计算数组中元素值为1的个数的方法

目录 一、引言 二、方法一&#xff1a;基础循环遍历 三、方法二&#xff1a;列表推导式 四、方法三&#xff1a;使用内置函数sum和生成器表达式 五、方法四&#xff1a;使用NumPy库 六、性能比较 七、性能结果分析与讨论 八、最佳实践 九、总结 一、引言 在编程和数…

ModuleNotFoundError: No module named ‘PyQt5‘

运行python程序的时候报错&#xff1a;ModuleNotFoundError: No module named ‘PyQt5‘ 这是因为没有安装pyqt5依赖包导致的&#xff0c;安装一下即可解决该问题。 安装依赖 pip install PyQt5 -i https://pypi.tuna.tsinghua.edu.cn/simple 这里是使用的清华镜像源进行安装…

uniapp 如何修改 IPA 文件信息页的本地化语言

实现效果&#xff1a; 最终会对应到苹果商店的语言&#xff1a; 例如微信的语言就有多个&#xff1a; 操作&#xff1a; 在 mainfest.json 源码视图中加入&#xff1a; 具体对应的语言key值可以参考Xcode中的语言代码 这个取决于打包后的 lproj 文件 将后缀ipa改成zip打开即…

人工智能_大模型052_模型微调012_模型训练结果对比_模型训练过程梳理---人工智能工作笔记0187

前面我们训练以后,然后 可以看到训练以后的结果, 可以用自己训练后的情况和这个对比一下. 看看效果 然后我们来看如果我们自己要训练一个模型,对模型进行微调,那么过程是怎么样的? ## 十二、数据准备与处理 ### 12.1、数据采集 - 自然来源(如业务日志):真实数据 - W…

当前端Vue3为插槽添加单击事件但会立即执行的代码错误

在你的代码中&#xff0c;你希望根据传入的 record 对象动态生成表格操作的配置数组&#xff0c;并且每个操作都有对应的点击事件处理函数。然而&#xff0c;你的代码中存在一些常见的问题需要解决。让我们逐步来看一下&#xff1a; 问题分析 1. 函数调用问题&#xff1a; …