项目实践:贪吃蛇

news2024/11/17 23:37:57

引言

贪吃蛇作为一项经典的游戏,想必大家应该玩过。贪吃蛇所涉及的知识也不是很难,涉及到一些C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32  API等。这里我会介绍贪吃蛇的一些思路。以及源代码也会给大家放到文章末尾。

我们最终的游戏的这样:

在真正的开始制作游戏之前,我们需要先了解一下制作贪吃蛇游戏的预备知识。如果已经知晓了这些预备知识,可以直接跳到"二"。 

一.Win32  API介绍 

1.Win32  API

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

2.控制台程序

平常我们运行起来的黑框程序其实就是控制台程序。我们可以通过cmd命令来控制台窗口的长宽。

#include<stdio.h>
int main()
{
	//设置控制台的窗口的行为30,列为30
	system("mode con cols=30 lines=30");
	//设置控制台的窗口的名字为贪吃蛇
	system("title 贪吃蛇");
	system("pause");
	return 0;
}

呈现出来的窗口为: 

3.控制台屏幕上的坐标COORD

COORD 是Windows API中定义的一个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格。其实就跟我们数学里面学习的坐标差不多,只不过Y轴的正负不一样了:

 COORD类型的结构体声明:

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

我们就可以用它给坐标赋值:

COORD pos = { 10, 15 };

此时的pos代表的就是(10,15)的坐标。

4.GetStdHandle

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

简单的说,我们在炒菜的时候,需要拿着铲子的把手,进行炒菜的动作。相同的,我们在操作设备的时候,也是需要拿着一个“把手”,而这个把手我们称作“句柄”。从而进行对设备的操作。

HANDLE GetStdHandle(DWORD nStdHandle);

 nStdHandle参数有三种,可为其一:

 比如:

HANDLE houtput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值) 
houtput = GetStdHandle(STD_OUTPUT_HANDLE);

HANDLE可以被看作是一个指向资源的指针,其实质上是一个整数值。通过使用HANDLE,程序可以访问和操作操作系统提供的各种资源。

5.GetConsoleCursorInfo

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

语法:

BOOL WINAPI GetConsoleCursorInfo(
 HANDLE hConsoleOutput,
 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

第一个参数hConsoleOutput就是控制台屏幕缓冲区的句柄。

第二个参数lpConsoleCursorInfo是一个指向PCONSOLE_CURSOR_INFO类型的指针。

先看一下实例,后面在介绍什么是PCONSOLE_CURSOR_INFO

HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值) 
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息 

5.1.CONSOLE_CURSOR_INFO

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

typedef struct _CONSOLE_CURSOR_INFO {
 DWORD dwSize;
 BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

dwSize由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元底部的水平线条(比如dwSize的值为25,其实就是占一个单元格的25%)。

bVisible,其实也可以看到,它的类型是BOOL。它就是游标的可见性。如果光标可见,则此成员为TRUE。反之就是FALSE。

举个例子,比如我们在打字的时候,我们的光标就一闪一闪的。那么在控制台上我们就可以修改bVisible的值为false,就可以做到隐藏光标。

就像是上面我定义的一个结构体变量:CONSOLE_CURSOR_INFO CursorInfo;

CursorInfo.bVisible = false; //隐藏控制台光标 

6.SetConsoleCursorInfo

既然我们获得了光标的信息,上面我们也说了我们想修改bVisible的值,那么我们就需要有一个设置,真正的把光标给改变。

语法:

BOOL WINAPI SetConsoleCursorInfo(
 HANDLE hConsoleOutput,
 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

它的结构体的内容跟上面的GetConsoleCursorInfo一样。

所以我们就可以得到一个隐藏或者改变光标占比的操作:

HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//先获得句柄
//影藏光标操作 
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息 
CursorInfo.bVisible = false; //隐藏控制台光标 
SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态 

7.SetConsoleCursorposition

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

拥有了这个函数我们就可以根据坐标随意的在屏幕的任何一个位置打印内容。

语法:

BOOL WINAPI SetConsoleCursorPosition(
 HANDLE hConsoleOutput,
 COORD pos
);

第一个参数依然是句柄,第二个参数就是我们想要光标出现的位置。

看一个实例:

 COORD pos = { 10, 5};
 HANDLE houtput = NULL;
 //获取标准输出的句柄(⽤来标识不同设备的数值) 
 houtput = GetStdHandle(STD_OUTPUT_HANDLE);
 //设置标准输出上光标的位置为pos 
 SetConsoleCursorPosition(houtput, pos);

接下来如果我们想要在控制台上输出内容,就是在我们设置的pos位置开始了。

8.GetAsyncKeyState

这个函数对于实现我们的贪吃蛇的项目十分重要。

语法:

SHORT GetAsyncKeyState(
 int vKey
);

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

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

我们可以建立一个宏,来判断是否按键被按过:

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

关于虚拟键代码有很多:比如从F1到F12的

所有的键盘上的按键都可以用虚拟键代码来代替。

二.贪吃蛇游戏设计

1.地图

在我们开始游戏之前,我们需要有一些提示信息给玩家观看,那么我们就需要在屏幕上打印如下的信息:

 

在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★普通的字符是占⼀个字节的,这类宽字符是占用2个字节。过去C语言并不适合非英语国家(地区)使用。C语言最初假定字符都是单字节的。但是这些假定并不是在世界的任何地方都适用。为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型wchar_t和宽字符的输入和输出函数,加入了头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数(locale.h)。

1.1.setlocale函数

这个函数包含于头文件locale.h中。

它的作用就是修改当前地区

char* setlocale (int category, const char* locale);

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

•LC_COLLATE:影响字符串比较函数strcoll()和strxfrm()。

•LC_CTYPE:影响字符处理函数的⾏为。

•LC_MONETARY:影响货币格式。

•LC_NUMERIC:影响printf()的数字格式。

•LC_TIME:影响时间格式strftime()和wcsftime()。

•LC_ALL :针对所有类项修改,将以上所有类别设置为给定的语言环境。

第二个参数:"C"(正常模式)和""(本地模式)。

注意:用""作为第2个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。

例如:

setlocale(LC_ALL, " ");//切换到本地环境 

1.2.宽字符的打印

既然有了设置为本地模式,那么我们打印宽字符的方式也应该有一些改变了。

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

比如:

这就是宽字符的打印。

1.3.地图坐标 

现在我们知晓了怎么样在屏幕上打印宽字符。我们发现我们在屏幕上打印这些汉字,一些两字节的宽字符的时候,它们的位置是需要我们自己来设置的(上面的Win32 API的6已经介绍了怎么找坐标)。那么我们应该需要知道我们控制台上的坐标是怎么样分布的。

我们假设一个27行58列的棋盘,真正在控制台上的分分布是这样的:

注意观察它们的横坐标和纵坐标的大小关系,差不多两个横坐标的长度才等于一个纵坐标的长度。

 2.蛇身和食物

初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的⼀个坐标处,比如(24,5)处开始出现蛇,连续5个节点。关于食物,就是在墙体内随机生成⼀个坐标,坐标不能和蛇的身体重合,然后打印★。

注意:不论是蛇身还是食物,它们的横坐标都必须是2的倍数,否则可能会出现蛇的一个节点有一半儿出现在墙体中,另外⼀般在墙外的现象,坐标不好对齐。

 3.数据结构设计

现在,我们知道了地图,蛇身,食物的设计。我们可以先大体的思考一下,我们该怎么样维护这条贪吃蛇,这条贪吃蛇的本质是什么?

3.1.贪吃蛇的节点结构

贪吃蛇的本质就是链表,后面我们要进行的贪吃蛇吃食物,实际上就是链表的插入。

typedef struct SnakeNode
{
	int x;//横坐标
	int y;//纵坐标
	struct SnakeNode* next;//下一个节点
}SnakeNode,* pSnakeNode;

 3.2.蛇的方向

enum DIRECTION
{
	UP = 1,//上
	DOWN,//下
	LEFT,//左
	RIGHT//右
};

 3.3.游戏状态

enum GAME_STATUS
{
	OK,//状态正常
	KILL_BY_WALL,//撞到墙
	KILL_BY_SELF,//撞到自己
	END_NORMAL//正常退出
};

3.4.维护贪吃蛇

typedef struct Snake
{
	pSnakeNode _pSnake;//指向蛇头的指针
	pSnakeNode _pFood;//指向食物节点的指针
	enum DIRECTION _dir;//蛇的方向
	enum GAME_STATUS _status;//蛇的状态
	int _food_weight;//食物分数
	int _score;//总分数
	int _sleep_time;//休息时间,时间越短,速度越快
}Snake,*pSnake;

到这里差不多贪吃蛇的前期的准备工作都做完了,后面的“三”我会详细的解释贪吃蛇实现的每一个步骤。

三.贪吃蛇的核心逻辑

在写整个游戏的代码过程中,我们大致分为三步:游戏开始(GameStar):完成游戏的初始化。游戏运行(GameRun):完成游戏运行逻辑的实现。游戏结束(GameEnd):完成游戏结束的说明,实现资源释放。

1.游戏开始(GameStar)

这个过程你,主要就是把给玩家看的东西给展现出来,比如地图的制作,地图上的文字,光标的隐藏,食物,蛇等等。

如下就是我们这个过程需要做的事情:

//游戏开始
void GameStart(pSnake ps)
{
	//把控制台窗口设置为行30,列100,并且改变名称为贪吃蛇
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");
	//获得句柄
	HANDLE houtpot = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo;
    //得到光标信息
	GetConsoleCursorInfo(houtpot, &CursorInfo);
	CursorInfo.bVisible = false;
    //改变光标信息
	SetConsoleCursorInfo(houtpot, &CursorInfo);
	//打印欢迎界面
	WelcomeToGame();
	//打印地图
	CreateMap();
	//初始化蛇
	InitSnake(ps);
	//创造食物
	CreateFood(ps);
}

我们一步一步来做这些事情。

1.1.打印欢迎界面(WelcomeToGame)

为了方便我们的使用,我们把设置光标位置的方法,单独的分装一个函数。

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

然后就是我们欢迎界面的打印:

//打印欢迎界面
void WelcomeToGame()
{
	SetPos(40, 15);//设置光标出现的位置
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(40, 25);//让按任意键继续的出现的位置好看点 
	system("pause");
	system("cls");//清屏
	SetPos(25, 12);
	printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");
	SetPos(25, 13);
	printf("加速将能得到更高的分数。\n");
	SetPos(40, 25);//让按任意键继续的出现的位置好看点 
	system("pause");
	system("cls");
}

1.2.打印地图(CreatMap)

这里其实就是对墙的打印,我们需要用到对宽字符的打印方式。为了好表示我们可以用define定义一下。

#define WALL L'□'

后面依然是考验我们的数学能力,实际也就是数坐标:

上墙的坐标为:(0,0)——(56,0)

下墙的坐标为:(0,,26)——(56,26)

左墙的坐标为:(0,1)——(0,25)

右墙的坐标为:(56,1)到(56,25)

//打印地图
void CreateMap()
{
	int i = 0;
	//上(0,0)-(56, 0) 
	SetPos(0, 0);
	for (i = 0; i < 58; i += 2)//因为宽字符一个占俩,所以要+2
	{
		wprintf(L"%lc", WALL);
	}
	//下(0,26)-(56, 26) 
	SetPos(0, 26);
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//左 
	//x是0,y从1开始增⻓ 
	for (i = 1; i < 26; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//x是56,y从1开始增⻓ 
	for (i = 1; i < 26; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
}

1.3.初始化蛇(InitSnake)

同样的,我们定义一下蛇身:

#define BODY L'●'

我们一开始就让蛇的长度为5,吃掉食物让蛇的身体增长。其实就涉及到了链表,所谓让蛇增长就是让链表的长度增加(这里我们利用头插)。

我们定义一下身刚开始出现的位置:

#define POS_X 24
#define POS_Y 5

然后就是创建蛇身,打印蛇身,初始化其他的数据:

//初始化蛇
void InitSnake(pSnake ps)
{
	int i = 0;
	pSnakeNode cur = NULL;

	for (i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}
		cur->next = NULL;
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;
		//头插法插入链表
		if (ps->_pSnake == NULL) //空链表
		{
			ps->_pSnake = cur;
		}
		else //非空
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}
	cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	//设置贪吃蛇的属性
	ps->_dir = RIGHT;//默认向右
	ps->_score = 0;
	ps->_food_weight = 10;
	ps->_sleep_time = 200;//单位是毫秒
	ps->_status = OK;
}

1.4.创造食物(CreatFood)

我们依然是把食物定义一下:

#define FOOD L'★'

关于食物的创造,我们就需要注意一下随机性了:

//初始化食物的节点
void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;
	//x必须是2的倍数
	//x:2~54
	//y: 1~25
again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);
	//x和y的坐标不能和蛇的身体坐标冲突
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		if (x == cur->x && y == cur->y)
		{
			goto again;
		}
		cur = cur->next;
	}
	//创建食物的节点
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("CreateFood()::malloc()");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;

	SetPos(x, y);//定位位置
	wprintf(L"%lc", FOOD);

	ps->_pFood = pFood; 
}

2.游戏运行(GameRun)

上面我们已经有了蛇身和食物,在这里就是我们要想办法让蛇给动起来,吃食物的过程,加速,减速,食物分数的变化都是在这里实现。

也是为了方便,获取按键信息的时候我们也定义一下

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

之后就是正式的游戏运行:

//游戏运行
void GameRun(pSnake ps)
{
	//打印右侧帮助信息 
	PrintHelpInfo();
	do
	{
		SetPos(64, 10);
		printf("得分:%d ", ps->_Score);
		printf("每个⻝物得分:%d分", ps->_foodWeight);
		if (KEY_PRESS(VK_UP) && ps->_Dir != DOWN)
		{
			ps->_Dir = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && ps->_Dir != UP)
		{
			ps->_Dir = DOWN;
		}
		else if (KEY_PRESS(VK_LEFT) && ps->_Dir != RIGHT)
		{
			ps->_Dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT)
		{
			ps->_Dir = RIGHT;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = END_NOMAL;
			break;
		}
		else if (KEY_PRESS(VK_F3))
		{
			if (ps->_SleepTime >= 80)
			{
				ps->_SleepTime -= 30;
				ps->_foodWeight += 2;//⼀个⻝物分数最⾼是20分 
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			if (ps->_SleepTime < 320)
			{
				ps->_SleepTime += 30;
				ps->_foodWeight -= 2;//⼀个⻝物分数最低是2分 
			}
		}
		//蛇每次一定之间要休眠的时间,时间短,蛇移动速度就快 
		Sleep(ps->_SleepTime);
		SnakeMove(ps);
	} while (ps->_Status == OK);
}

接下来就一一介绍中间涉及到的一些函数 

2.1.打印右侧帮助信息(PrintHelpInfo)

这个就是想在游戏界面的右方提示一下,当前分数什么的:

//打印右侧帮助信息
void PrintHelpInfo()
{
	//打印提⽰信息 
	SetPos(64, 15);
	printf("不能穿墙,不能咬到自己\n");
	SetPos(64, 16);
	printf("用↑.↓.←.→分别控制蛇的移动.");
	SetPos(64, 17);
	printf("F3 为加速,F4 为减速\n");
	SetPos(64, 18);
	printf("ESC :退出游戏.space:暂停游戏.");
}

2.2.暂停响应(pause)

这个函数其实就是在我们暂停游戏之后,我们重新去运行游戏用的:

//暂停响应
void pause()//暂停 
{
	while (1)
	{
		Sleep(300);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

2.3.蛇身移动(SnakeMove)

这个地方所牵扯到的函数有点多,也是整个游戏最核心的地方

//蛇的移动
void SnakeMove(pSnake ps)
{
	//创建下⼀个节点 
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNextNode == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	//确定下⼀个节点的坐标,下⼀个节点的坐标根据蛇头的坐标和方向确定 
	switch (ps->_Dir)
	{
	case UP:
	{
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y = ps->_pSnake->y - 1;//如果是上,横坐标不变,纵坐标减一
	}
	break;
	case DOWN:
	{
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y = ps->_pSnake->y + 1;
	}
	break;
	case LEFT:
	{
		pNextNode->x = ps->_pSnake->x - 2;
		pNextNode->y = ps->_pSnake->y;
	}
	break;
	case RIGHT:
	{
		pNextNode->x = ps->_pSnake->x + 2;
		pNextNode->y = ps->_pSnake->y;
	}
	break;
	}
	//如果下⼀个位置就是⻝物 
	if (NextIsFood(pNextNode, ps))
	{
		EatFood(pNextNode, ps);
	}
	else//如果没有⻝物 
	{
		NoFood(pNextNode, ps);
	}
	KillByWall(ps);
	KillBySelf(ps);
}

这里有牵扯到了五个函数下面一一来介绍

注意:下面提到的psn参数都是蛇要移动到下一个节点的位置。                 

2.3.1.下一个位置是不是食物(NextIsFood)

返回值是int,如果成立就返回1,不成立就返回0.

//判断下一个节点是否有食物
int NextIsFood(pSnakeNode pn, pSnake ps)
{
	return(ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}
2.3.2.吃食物(EatFood)

因为食物的类型跟我们蛇节点的类型是一样的,所以如果有食物的话我们就不需要把蛇的结尾打印成空格。

//蛇的下一个节点有食物
void EatFood(pSnakeNode pn, pSnake ps)
{
	//头插法
	ps->_pFood->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;

	//释放下一个位置的节点
	free(pn);
	pn = NULL;

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

	//重新创建食物
	CreateFood(ps);
}
2.3.3.没有食物(NoFood)

没有食物的情况挺容易出错的,因为我们的循环条件变了。

//蛇的下一个节点没有食物
void NoFood(pSnakeNode pn, pSnake ps)
{
	// 头插法
	pn->next = ps->_pSnake;
	ps->_pSnake = pn;

	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next != NULL)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	//把最后一个结点打印成空格
	SetPos(cur->next->x, cur->next->y);
	printf("  ");

	//释放最后一个结点
	free(cur->next);

	//把倒数第二个节点的地址置为NULL
	cur->next = NULL;
}
2.3.4.撞到墙(KillByWall)
//撞到墙
void KillByWall(pSnake ps)
{
	if ((ps->_pSnake->x == 0)
		|| (ps->_pSnake->x == 56)
		|| (ps->_pSnake->y == 0)
		|| (ps->_pSnake->y == 26))//分别是上下左右墙的边界
	{
		ps->_Status = KILL_BY_WALL;//这里我们把蛇的状态改掉,后面就会跳出循环
		break;
	}

}
2.3.5.撞到自己(KillBySelf)
//撞到自己
void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake->next;
	while (cur)
	{
		if ((ps->_pSnake->x == cur->x)
			&& (ps->_pSnake->y == cur->y))//身体的每一个节点的坐标都不可以与蛇头的坐标相同
		{
			ps->_Status = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}

到这里差不多所有运行需要的代码就结束了。接下来是结束工作。

3.游戏结束(GameEnd)

当游戏的状态不再是OK的时候,游戏就结束了。

//游戏的善后
void GameEnd(pSnake ps)
{
	SetPos(24, 12);
	switch (ps->_status)
	{
	case END_NORMAL:
		wprintf(L"您主动结束游戏\n");
		break;
	case KILL_BY_WALL:
		wprintf(L"您撞到墙上,游戏结束\n");
		break;
	case KILL_BY_SELF:
		wprintf(L"您撞到了自己,游戏结束\n");
		break;
	}

	//释放蛇身的链表

	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
}

四.整体代码分享

snake.h

#pragma once
#include<stdlib.h>
#include<stdio.h>
#include<locale.h>
#include<windows.h>
#include<stdbool.h>
#include<time.h>
#define POS_X 24
#define POS_Y 5

#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define KEY_PRESS(vk)  ((GetAsyncKeyState(vk)&1)?1:0)
//蛇的状态
enum GAME_STATUS
{
	OK,
	KILL_BY_WALL,//撞到墙
	KILL_BY_SELF,//撞到自己
	END_NORMAL//正常退出
};
//蛇的方向
enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

//蛇身的节点类型
typedef struct SnakeNode
{
	//坐标
	int x;
	int y;
	//指向下一个节点的指针
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;

//贪吃蛇的状态
typedef struct Snake
{
	pSnakeNode _pSnake;//指向蛇头的指针
	pSnakeNode _pFood;//指向食物节点的指针
	enum DIRECTION _dir;//蛇的方向
	enum GAME_STATUS _status;//蛇的状态
	int _food_weight;//食物分数
	int _score;//分数
	int _sleep_time;//休息时间,时间越短,速度越快
}Snake,*pSnake;

//函数的声明

//定位光标位置
void SetPos(short x, short y);

//游戏的初始化
void GameStart(pSnake ps);

//欢迎界面的打印
void WelcomeToGame();

//创建地图
void CreateMap();

//初始化蛇身
void InitSnake(pSnake ps);

//创建食物
void CreateFood(pSnake ps);

//游戏运行的逻辑
void GameRun(pSnake ps);

//蛇的移动-走一步
void SnakeMove(pSnake ps);

//判断下一个坐标是否是食物
int NextIsFood(pSnakeNode pn, pSnake ps);

//下一个位置是食物,就吃掉食物
void EatFood(pSnakeNode pn, pSnake ps);

//下一个位置不是食物
void NoFood(pSnakeNode pn, pSnake ps);

//检测蛇是否撞墙
void KillByWall(pSnake ps);

//检测蛇是否撞到自己
void KillBySelf(pSnake ps);

//游戏善后的工作
void GameEnd(pSnake ps);

void test();

snake.c

#include"snack.h"
void SetPos(short x, short y)
{
	//获得标准输出设备的句柄
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//定位光标的位置
	COORD pos = { x,y };
	SetConsoleCursorPosition(houtput, pos);
}



//打印欢迎界面
void WelcomeToGame()
{
	SetPos(40, 14);
	wprintf(L"欢迎来到贪吃蛇游戏\n");
	SetPos(42, 20);
	system("pause");
	system("cls");
	SetPos(40, 14);
	wprintf(L"用↑  ↓  ←  →来控制移动,按F3加速,F4减速\n");
	SetPos(40, 15);
	wprintf(L"加速可以获得更高的分数");
	SetPos(40, 20);
	system("pause");
	system("cls");
}
//打印地图
void CreateMap()
{
	int i = 0;
	//上(0,0)-(56, 0) 
	SetPos(0, 0);
	for (i = 0; i < 58; i += 2)//因为宽字符一个占俩,所以要+2
	{
		wprintf(L"%lc", WALL);
	}
	//下(0,26)-(56, 26) 
	SetPos(0, 26);
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//左 
	//x是0,y从1开始增⻓ 
	for (i = 1; i < 26; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//x是56,y从1开始增⻓ 
	for (i = 1; i < 26; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
}

//初始化蛇
void InitSnake(pSnake ps)
{
	int i = 0;
	pSnakeNode cur = NULL;

	for (i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}
		cur->next = NULL;
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;
		//头插法插入链表
		if (ps->_pSnake == NULL) //空链表
		{
			ps->_pSnake = cur;
		}
		else //非空
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}
	cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	//设置贪吃蛇的属性
	ps->_dir = RIGHT;//默认向右
	ps->_score = 0;
	ps->_food_weight = 10;
	ps->_sleep_time = 200;//单位是毫秒
	ps->_status = OK;
}
//初始化食物的节点
void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;
	//x必须是2的倍数
	//x:2~54
	//y: 1~25
again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);
	//x和y的坐标不能和蛇的身体坐标冲突
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		if (x == cur->x && y == cur->y)
		{
			goto again;
		}
		cur = cur->next;
	}
	//创建食物的节点
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("CreateFood()::malloc()");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;

	SetPos(x, y);//定位位置
	wprintf(L"%lc", FOOD);

	ps->_pFood = pFood;                                                    
}

void GameStart(pSnake ps)
{
	//1.先设置窗口大小,再进行光标隐藏
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");
	HANDLE houtpot = GetStdHandle(STD_OUTPUT_HANDLE);
	//光标隐藏
	CONSOLE_CURSOR_INFO Cursorinfo;
	GetConsoleCursorInfo(houtpot, &Cursorinfo);//获取控制台光标信息
	Cursorinfo.bVisible = false;//隐藏光标
	SetConsoleCursorInfo(houtpot, &Cursorinfo);//设置
	system("pause");

	//2.打印欢迎界面
	WelcomeToGame();
	//3.创建地图
	CreateMap();
	//4.创建蛇
	InitSnake(ps);
	//5.创建食物
	CreateFood(ps);
}






//打印帮助信息
void PrintHelpInfo()
{
	SetPos(64, 14);
	wprintf(L"%ls", L"不能穿墙,不能咬到自己");
	SetPos(64, 15);
	wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动");
	SetPos(64, 16);
	wprintf(L"%ls", L"按F3加速,F4减速");
	SetPos(64, 17);
	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
	SetPos(64, 18);
	wprintf(L"%ls", L"李制作");
}
//蛇的停顿的解除
void Pause()
{
		while (1)
		{
			Sleep(200);
			if (KEY_PRESS(VK_SPACE))
			{
				break;
			}
		}
}
//判断下一个节点是否有食物
int NextIsFood(pSnakeNode pn, pSnake ps)
{
	return(ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}
//蛇的下一个节点有食物
void EatFood(pSnakeNode pn, pSnake ps)
{
	//头插法
	ps->_pFood->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;

	//释放下一个位置的节点
	free(pn);
	pn = NULL;

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

	//重新创建食物
	CreateFood(ps);
}
//蛇的下一个节点没有食物
void NoFood(pSnakeNode pn, pSnake ps)
{
	// 头插法
	pn->next = ps->_pSnake;
	ps->_pSnake = pn;

	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next != NULL)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	//把最后一个结点打印成空格
	SetPos(cur->next->x, cur->next->y);
	printf("  ");

	//释放最后一个结点
	free(cur->next);

	//把倒数第二个节点的地址置为NULL
	cur->next = NULL;
}
//检测蛇是否撞墙
void KillByWall(pSnake ps)
{
	if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 ||
		ps->_pSnake->y == 0 || ps->_pSnake->y == 26)
	{
		ps->_status = KILL_BY_WALL;
	}
}
//检测蛇是否撞到自己
void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake->next;
	while (cur)
	{
		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
		{
			ps->_status = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}
//蛇的移动
void SnakeMove(pSnake ps)
{
	//创建一个结点,表示蛇即将到的下一个节点
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNextNode == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	switch (ps->_dir)
	{
	case UP:
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y = ps->_pSnake->y - 1;
		break;
	case DOWN:
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y = ps->_pSnake->y + 1;
		break;
	case LEFT:
		pNextNode->x = ps->_pSnake->x - 2;
		pNextNode->y = ps->_pSnake->y;
		break;
	case RIGHT:
		pNextNode->x = ps->_pSnake->x + 2;
		pNextNode->y = ps->_pSnake->y;
		break;
	}
	//检测下一个坐标处是否是食物
	if (NextIsFood(pNextNode, ps))
	{
		EatFood(pNextNode, ps);
	}
	else
	{
		NoFood(pNextNode, ps);
	}

	//检测蛇是否撞墙
	KillByWall(ps);
	//检测蛇是否撞到自己
	KillBySelf(ps);
}

void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();
	do
	{
		//打印总分数和食物的分值
		SetPos(64, 10);
		printf("总分数:%d\n", ps->_score);
		SetPos(64, 11);
		printf("当前食物的分数:%2d\n", ps->_food_weight);

		if (KEY_PRESS(VK_UP) && ps->_dir != DOWN)
		{
			ps->_dir = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP)
		{
			ps->_dir = DOWN;
		}
		else if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT)
		{
			ps->_dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT)
		{
			ps->_dir = RIGHT;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			Pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			//正常退出游戏
			ps->_status = END_NORMAL;
		}
		else if (KEY_PRESS(VK_F3))
		{
			//加速
			if (ps->_sleep_time > 60)
			{
				ps->_sleep_time -= 30;
				ps->_food_weight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//减速
			if (ps->_food_weight > 2)
			{
				ps->_sleep_time += 30;
				ps->_food_weight -= 2;
			}
		}

		SnakeMove(ps);//蛇走一步的过程

		Sleep(ps->_sleep_time);

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


//游戏的善后
void GameEnd(pSnake ps)
{
	SetPos(24, 12);
	switch (ps->_status)
	{
	case END_NORMAL:
		wprintf(L"您主动结束游戏\n");
		break;
	case KILL_BY_WALL:
		wprintf(L"您撞到墙上,游戏结束\n");
		break;
	case KILL_BY_SELF:
		wprintf(L"您撞到了自己,游戏结束\n");
		break;
	}

	//释放蛇身的链表

	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
}

test.c

#include"snack.h"
//完成的是游戏的测试逻辑
void test()
{
	int ch = 0;
	do
	{
		system("cls");
		//创建贪吃蛇
		Snake snake = { 0 };
		//初始化游戏
		//1. 打印环境界面
		//2. 功能介绍
		//3. 绘制地图
		//4. 创建蛇
		//5. 创建食物
		//6. 设置游戏的相关信息
		GameStart(&snake);

		//运行游戏
		GameRun(&snake);
		//结束游戏 - 善后工作
		GameEnd(&snake);
		SetPos(20, 15);
		printf("再来一局吗?(Y/N):");
		ch = getchar();
		while (getchar() != '\n');

	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 27);
}
int main()
{
	//先设置适配本地环境
	setlocale(LC_ALL, "");
	//创建随机种子
	srand((unsigned int)time(NULL));
	//调用函数
	test();
	return 0;
}

感谢大家的观看,如有错误,请多多指出

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

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

相关文章

【Ne4j图数据库入门笔记1】图形数据建模初识

1.1 图形建模指南 图形数据建模是用户将任意域描述为节点的连接图以及与属性和标签关系的过程。Neo4j 图数据模型旨在以 Cypher 查询的形式回答问题&#xff0c;并通过组织图数据库的数据结构来解决业务和技术问题。 1.1.1 图形数据模型介绍 图形数据模型通常被称为对白板友…

【Gradle如何安装配置及使用的教程】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

双链表的实现

我们知道链表其实有很多种&#xff0c;什么带头&#xff0c;什么双向啊&#xff0c;我们今天来介绍双向带头循环链表&#xff0c;了解了这个其他种类的链表就很简单了。冲冲冲&#xff01;&#xff01;&#xff01; 链表的简单分类 链表有很多种&#xff0c;什么带头循环链表&…

tcp-learner 数据包分析 20240420

输入输出&#xff1a; 数据包分析&#xff1a; learner和Adapter建立连接。 Learner让Adapter发送RST Adapter没有从SUT抓到任何回复&#xff0c;于是向learner发送timeout learner给adapter发送reset命令&#xff0c;让SUT重置。 这是第一次初始化&#xff0c;由于Adapter和…

Spring Boot后端与Vue前端融合:构建高效旅游管理系统

作者介绍&#xff1a;✌️大厂全栈码农|毕设实战开发&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。 &#x1f345;获取源码联系方式请查看文末&#x1f345; 推荐订阅精彩专栏 &#x1f447;&#x1f3fb; 避免错过下次更新 Springboot项目精选实战案例 更多项目…

【简单讲解下npm常用命令】

&#x1f308;个人主页: 程序员不想敲代码啊 &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共…

【hackmyVM】whitedoor靶机

文章目录 信息收集1.IP地址2.端口探测nmapftp服务 3.访问主页 漏洞利用1.反弹shell2.尝试提权3.base64解密 提权1.切换用户2.john爆破3.切换Gonzalo用户4.vim提权 信息收集 1.IP地址 ┌─[✗]─[userparrot]─[~] └──╼ $fping -ag 192.168.9.0/24 2> /dev/null192.168…

ZYNQ NVME高速存储之EXT4文件系统

前面文章分析了高速存储的各种方案&#xff0c;目前主流的三种存储方案是&#xff0c;pcie switch高速存储方案&#xff0c;zynq高速存储方案&#xff0c;fpga高速存储方案。虽然三种高速存储方案都可以实现高速存储&#xff0c;但是fpga高速存储方案是最烂的&#xff0c;fpga…

Android Studio 新建Android13 代码提示Build Tools revision XX is corrupted无法编译解决

Android Studio 新建Android13 代码提示Build Tools revision XX is corrupted无法编译解决 文章目录 Android Studio 新建Android13 代码提示Build Tools revision XX is corrupted无法编译解决一、前言二、分析解决1、原因分析2、解决方法 三、其他1、Android13 新项目无法编…

什么是时间序列分析

时间序列分析是现代计量经济学的重要内容&#xff0c;广泛应用于经济、商业、社会问题研究中&#xff0c;在指标预测中具有重要地位&#xff0c;是研究统计指标动态特征和周期特征及相关关系的重要方法。 一、基本概念 经济社会现象随着时间的推移留下运行轨迹&#xff0c;按…

随身WiFi真实测评推荐!格行vs新讯随身wifi对比,公认最好的随身WiFi格行随身wifi有什么优势?

在当前移动网络高度发达的时代&#xff0c;随身 WiFi 已成为人们出差、旅行等场景中不可或缺的工具。格行和新讯是目前比较受欢迎的无线随身wifi。本次评测将对比分析这两款产品的区别&#xff0c;做为随身WiFi推荐第一名的格行随身wifi到底有什么优势呢&#xff1f; 品牌对比&…

[阅读笔记15][Orca]Progressive Learning from Complex Explanation Traces of GPT-4

接下来是微软的Orca这篇论文&#xff0c;23年6月挂到了arxiv上。 目前利用大模型输出来训练小模型的研究都是在模仿&#xff0c;它们倾向于学习大模型的风格而不是它们的推理过程&#xff0c;这导致这些小模型的质量不高。Orca是一个有13B参数的小模型&#xff0c;它可以学习到…

C++ 内存分区管理

一、栈区&#xff08;Stack&#xff09; 栈区用来存储函数的参数值、局部变量的值等数据。栈区是自动分配和释放的&#xff0c;函数执行时会在栈区分配空间&#xff0c;函数执行结束时会自动释放这些空间。栈区的数据是连续分配的&#xff0c;由系统自动管理。 注意事项&…

layui框架实战案例(27):弹出二次验证

HTML容器 <button class"layui-btn layui-btn-sm layui-btn-danger" lay-event"delete"><i class"layui-icon layui-icon-delete"></i>批量删除</button>删除封装函数 function delAll(school_id, school_name) {var lo…

牛x之路 - Day1

Day1 微积分之屠龙宝刀&#xff08;武林秘籍&#xff09; 之前的一些东西都在pdf上记得笔记&#xff0c; 没有在这个上面展示一遍&#xff0c;只好学到相关内容的时候再提叙啦&#xff1b;所以其实再写这个小记的时候&#xff0c;我已经看了一半的书&#xff0c;但是不要紧&am…

每日学习笔记:C++ STL算法之移除容器元素

本文API 移除元素 remove(beg, end, value) remove_if(beg, end, op) remove_copy(sourceBeg, sourceEnd, destBeg, value) remove_copy_if(sourceBeg, sourceEnd, destBeg, op) 移除连续重复的元素 unique(beg, end) unique(beg, end, op) unique_copy(sourceBeg, sourceEnd, …

Ribbon 添加快速访问区域

添加快速访问区域挺简单的&#xff0c;实例如下所示&#xff1a; void QtRightFuncDemo::createQuickAccessBar() { RibbonQuickAccessBar* quickAccessBar ribbonBar()->quickAccessBar(); QAction* action quickAccessBar->actionCustomizeButton(); act…

单链表的简单应用

目录 一、顺序表的问题及思考 二、链表的概念及结构 三、单链表的实现 3.1 增 3.1.1 尾插 3.1.2 头插 3.1.3 指定位置前插入 3.1.4 指定位置后插入 3.2 删 3.2.1 尾删 3.2.2 头删 3.2.3 指定位置删除 3.2.4 指定位置后删除 3.2.5 链表的销毁 3.3 查 3.4 改 四…

Python爬虫使用需要注意什么?应用前景如何?

Python爬虫很多人都听说过&#xff0c;它是一种用于从网页上获取信息的程序&#xff0c;它可以自动浏览网页、提取数据并进行处理。技术在使用Python爬虫时需要注意一些重要的事项&#xff0c;同时本文也会跟大家介绍一下爬虫的应用前景。 第一个注意事项就是使用Python爬虫时…

HCIP-OSPF综合实验

一实验拓扑图 二.实验要求 1、R4为ISP&#xff0c;其上只配置IP地址&#xff1b;R4与其他所直连设备间均使用公有IP&#xff1b; 2、R3-R5、R6、R7为MGRE环境&#xff0c;R3为中心站点&#xff1b; 3、整个OSPF环境IP基于172.16.0.0/16划分&#xff1b;除了R12有两个环回&…