贪吃蛇/链表实现(C/C++)

        本篇使用C语言实现贪吃蛇小游戏,我们将其分为了三个大部分,第一个部分游戏开始GameStart,游戏运行GameRun,以及游戏结束GameRun。对于整体游戏主要思想是基于链表实现,但若仅仅只有C语言的知识还不够,我们还需要学习控制台的一些相关操作,结合实现贪吃蛇游戏,所以我们先介绍了一些有关Win32 API的知识。

        以下为整体实现的思路,以及对应的代码,在文章的末尾也给出了整体代码以及对应的测试,有需要的读者可以根据目录直接跳到对应的位置。

        另外,这只是一个基础版本的,读者还可在此基础上进行升级,如:

        1.将地图的进行升级,不在仅仅只是一个方框,加大难度;

        2.写一个文件操作,记录历史最高得分记录;

        3.将运行出来的效果加上各种动画,等等。

1.Win32 API介绍

1.1 Win32 API

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

1.2 控制台程序

        平时通过VS2022,或者Windows命令提示符(cmd)运行起来的黑框程序就是控制台程序;

        我们可以通过使用cmd命令来控制控制台窗口的长宽:如下,控制台窗口的大小为30行、100列。

mode con cols=100 lines=30

        如上图所示,可以通过cmd命令来设置控制台窗口的长宽。 

        还可以使用命令设置控制台窗口的名字:

        如果要将以上的控制台窗口执行的命令调用在C语言中,我们可以使用C语言函数中的system来执行,如下: 

#include <stdio.h>
#include <Windows.h>

int main() {
	system("mode con cols=100 lines=30");
	system("title Snake");
	getchar();
	return 0;
}

        如果使用上述的C语言命令无法实现该情况,请先将C语言中的默认终端应用程序 改为:Windows控制台主机,如下:

        (对VS弹出的控制终端,鼠标右键,点击属性,点击终端,找到对应的位置修改即可)。

 1.3 控制台屏幕上的坐标COORD

        COORD是Windows API中定义的一个结构体,表示一个字符在控制台屏幕上的坐标。

        使用该结构体需要头文件 <Windows.h>。

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

COORD pos={ 10, 15 }; //给坐标赋值

        通过以上的坐标结构体,我们可以实现控制一个字符在控制台屏幕的坐标,便于我们在控制台不同的位置打印该字符

1.4 GetStdHandle

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

        即若我们我们需要对某一个打开的控制台程序进行操作,我们就必须通过GetStdHandle这个函数得到对应的控制台标识,获取权限,才可以对中国控制台程序进行操作。

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

1.5 GetConsoleCursorInfo

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

BOOL WINAPI GetConsoleCursorInfo{
	HANDLE				 hConsoleOutput;
	PCONSOLE_CURSOR_INFO lpConsoleCursorInfo;
};

//PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构体的指针,
//该结构体接收有关主机游标的信息

//CONSOLE_CURSOR_INFO 结构体
typedef struct _CONSOLE_CURSOR_INFO {
	DWORD dwSize;
	BOOL  bVisible;
}CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

        对于 CONSOLE_CURSOR_INFO 结构体来说:

         对应其中的dwSize:有光标填充的字符单元格百分比。此值介于1到100之间。光标的外观会变化,范围从填充单元格到单元格底部(消失),通常取值0、25、50、75、100.

        bVisible:游标的可见性,若光标可见,则此成员为TURE,反之为FALSE。

        对于SetConsoleCursorInfo函数来说,就是将设置的CONSOLE_CURSOR_INFO类型变量进行设置,设置指定控制台缓冲区的光标的大小和可见性。如下操作,我们可以将控制台缓冲区的光标隐藏。

int main()
{
	COORD pos = { 40, 10 };
	CONSOLE_CURSOR_INFO cursor_info = {0};

	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	GetConsoleCursorInfo(handle, &cursor_info);
	cursor_info.dwSize = 100;
	cursor_info.bVisible = false;//隐藏
	SetConsoleCursorInfo(handle, &cursor_info);
	getchar();  //用于将程序暂停在此,要不然会一下运行结束
	return 0;
}

        不仅仅可以将光标隐藏,还可以将光标的大小设置为任意位置,读者可以自行操作。

1.6  SetConsoleCursorPosition 

        该函数设置控制台屏幕缓冲区中光标的位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition 函数将光标位置设置到指定位置。所以该函数可以使我们在指定位置打印出我们想要的结果。如下:

int main()
{
	COORD pos = { 10, 5 };
	HANDLE hOutput = NULL;
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置标准输出上光标的位置为pos
	SetConsoleCursorPosition(hOutput, pos);
	printf("haha\n");
	return 0;
}

        使用如上函数,我们就可以在指定的位置打印出我们想要的数据。 为了方便,我们可以封装一个设置光标位置的函数:

void SetPos(short x, short y) {
	COORD pos = { x,y };
	HANDLE hOutput = NULL;
	//获取标准输出的句柄
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置光标位置
	SetConsoleCursorPosition(hOutput, pos);
}

1.7 GetAsyncKeyState

        该函数的作用为:可以将键盘上的每一个健的虚拟键值传递给函数,函数通过返回值,来区分按键的状态。

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

        所以我们可以定义一个宏,来判断虚拟按键VK,是否被按过:

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

        该宏就可以将我们的按键信息传达给程序,便于我们操作贪吃蛇。 

2.贪吃蛇程序的设计

        前面的一些基础知识介绍完之后,我们就需要开始进行正式的贪吃蛇程序的设计了,首先贪吃蛇游戏的板块一共分为三个部分,其中分别为游戏开始前(GameStart)、游戏运行时(GameRun)、游戏结束之后(GameEnd)三个大模块,在这三个大模块中也包含许多小模块,我将以层次图给出,如下:

        上图则为我们大概将要实现的步骤,所以将按照当前思路进行讲解。

# C语言库函数本地化 

        在进行代码编写前,我们需要先对程序进行本地化,因为在C语言中,并不是适用于所有地区的字符,所以如果我们需要使用C语言库中允许打印之外的符号,那么我们需要先对当前的C语言环境进行本地化。

        <locale.h>本地化:<locale.h>提供的函数用于控制C标准库对于不同地区会产生不一样的行为的部分。在标准可以中,依赖于地区部分一共有:数字量的格式、货币量的格式、字符集、日期和时间的表示形式。

        类项:通过修改地区,程序可以改变它的行为来适应世界的不同区域,但是地区的改变可能将会影响库的许多部分,其中一部分可能是我们并不希望修改的,所以C语言中支持针对不同的类项进行修改,如这些宏:LC_COLLATE、LC_CTYPE、LC_MONETARY、LC_NUMERIC、LC_TIME、LC_ALL—针对所有类项修改。

        对于以上的宏的使用,我们需要使用setlocale函数,对于setlocale该函数的使用,如下

//执行C标准库环境
setlocale(LC_ALL, "C");

//将C标准库本地化
setlocale(LC_ALL, " ");

2.1 贪吃蛇数据结构

        蛇身:首先对于贪吃蛇结点的设计,因为贪吃蛇处于一个移动的形式,所以我们在贪吃蛇结构体的设置中需要定义出贪吃蛇的坐标,一级下一个结点的指针。

        整个游戏:然后就是对整个游戏维护的结构体,对于整个游戏维护的结构体中,首先需要对整个蛇进行维护,所以设置一个结构体指针指向蛇头;然后就是对食物的维护,食物同样采用蛇身的结构体;然后是总分和每个食物的分数;还有每一次程序休眠的时间(休眠时间决定贪吃蛇的前进的速度),以及当前蛇头的方向,和蛇的状态。

        代码如下:

//枚举当前的蛇的方向
enum DIRECTION { UP, DOWN, LEFT, RIGHT };
//当前蛇的状态,OK=正常允许,ESC=主动退出,KILL_BY_WALL=被墙杀死,KILL_BY_SELF=咬到自己死亡
enum STATUS { OK, ESC, KILL_BY_WALL, KILL_BY_SELF };

#define WALL L'□'	//打印墙
#define BODY L'●'	//打印蛇身
#define FOOD L'★'	//打印食物
#define POS_X 24	//蛇的初始位置横坐标
#define POS_y 5		//蛇的初始位置纵坐标

//读取键盘信息的宏
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)

typedef struct SnakeNode {
	int x;  //x轴坐标
	int y;	//y轴坐标
	struct SnakeNode* next;
}SnakeNode;

typedef struct Snake {
	SnakeNode* SnakeHead;  //蛇头,用来维护整个蛇身
	SnakeNode* pFood;      //食物,维护食物的指针
	int score;			   //总分,用来计算当前得分
	int foodWeight;		   //食物分数,默认食物的分数
	int SleepTime;		   //每一步的休眠时间
	enum DIRECTION dir;	   //蛇头的方向,默认向右
	enum STATUS status;	   //蛇的状态,当前蛇的状态
}Snake;

2.2 GameStart

2.2.1 欢迎界面

        在打印欢迎界面之前,我们需要将控制台的窗口大小设置为固定大小,并且设置窗口名称,同时获取标准输出句柄,然后将光标隐藏起来,具体操作如下:

void GameStart(Snake* ps) {
	//先设置当前窗口大小
	system("mode con cols=100 lines=30");
	//设置窗口名
	system("title Snake");
	//获取标准输出的句柄
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//隐藏当前光标
	CONSOLE_CURSOR_INFO CursorInfo;  
	GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
	CursorInfo.bVisible = false;			   //隐藏控制台光标
	SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

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

	//打印地图
	CreateMap();

	//初始化蛇
	InitSnake(ps);

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

        该操作都是通过以上对Win32 API介绍的知识。

        然后就是对欢迎界面的设置,对于欢迎界面,我们要将其设置为如下形式:其中欢迎在中间位置,按任意位置继续在界面下端。第二个界面为操作方式讲解,也是位于中间位置。

        设置以上形式的打印,我们只需要调用以上设置位置的函数,然后在打印即可,当然还涉及以下暂停函数和清屏函数,如下:

void SetPos(short x, short y) {
	COORD pos = { x,y };
	HANDLE hOutput = NULL;
	//获取标准输出句柄
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置标准输出上的光标的位置
	SetConsoleCursorPosition(hOutput, 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");
}
2.2.2 打印地图

        对于地图的设计,我们大概设计出下列的地图,大致呈现一个正方形,但是在控制台缓冲区的形式如下,对于一个光标的位置,高度大概是宽度的两倍,所以我们设计出来的宽必须是列的两倍,我们使用打印墙的字符为宽字符(本地字符),刚好占两个字符位,所以我们生成的宽度必须位偶数。

        我们将分别打印墙的上半部分、下半部分、左边和右边,如下:

void CreateMap() {
	int i = 0;
	SetPos(0, 0);
	//打印上面
	for (i = 0; i < 58; i+=2) {
		wprintf(L"%lc", WALL);
	}
	//打印下面
	SetPos(0, 26);
	for (i = 0; i < 58; i += 2) {
		wprintf(L"%lc", WALL);
	}
	//打印左边
	for (i = 1; i < 26; i++) {
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//打印右边
	for (i = 1; i < 26; i++) {
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
}
 2.2.3 初始化蛇

        现在我们需要将蛇初始化,对于蛇的初始化,我们默认将蛇的身体设置为5个结点,每一个结点我们采用链表头插的方式进行插入,从第二十四列第5行开始向右头插。在设置完蛇身之后,我们将蛇给打印出来,然后将蛇的其他信息初始化:食物指针置为NULL,默认方向设置为RIGHT,分数为0,每个食物的分数为10分,默认状态为OK,休眠时间为200ms,如下:

        注:对于蛇身结点的设置,对于x坐标必须为偶数,因为对于以上的墙,假若我们撞左墙或者右墙时刚好横坐标为偶数,便于我们识别撞墙。

void InitSnake(Snake* ps) {
	SnakeNode* cur = NULL;
	for (int i = 0; i < 5; i++) {
		cur = (SnakeNode*)malloc(sizeof(SnakeNode));
		if (cur == NULL) {
			perror("InitSnake malloc:");
			exit(1);
		}
		cur->x = POS_X + 2 * i;
		cur->y = POS_y;
		cur->next = NULL;
		if (ps->SnakeHead == NULL) {
			ps->SnakeHead = cur;
		}
		else {
			cur->next = ps->SnakeHead;
			ps->SnakeHead = cur;
		}
	}
	//打印蛇身
	cur = ps->SnakeHead;
	while (cur) {
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//将其他信息完善
	ps->dir = RIGHT;
	ps->foodWeight = 10;
	ps->pFood = NULL;
	ps->score = 0;
	ps->SleepTime = 200;
	ps->status = OK;

}
2.2.4 创建食物 

        食物创建的结点按照蛇身结点创建,其中仍然必须遵守横坐标为偶数,因为蛇的横坐标为偶数, 吃到食物的时候能更好接上。所以对于食物的生成我们使用rand函数进行生成,但是对于食物的生成有两点需要注意的:

        1.食物的随机生成不能生成在墙外;

        2.食物的随机生成不能生成在蛇的身上。

        最后我们需要将食物打印在屏幕上,实现的代码如下:

void CreateFood(Snake* ps) {
	SnakeNode* cur = ps->SnakeHead;
	int x = 0;
	int y = 0;
	do {
		x = rand() % 53 + 2;
		y = rand() % 24 + 1;
	} while (x % 2 != 0);

	while (cur) {
		if (cur->x == x && cur->y == y) {
			do {
				x = rand() % 53 + 2;
				y = rand() % 24 + 1;
			} while (x % 2 != 0);
		}
		cur = cur->next;
	}
	//创建食物
	SnakeNode* Food = (SnakeNode*)malloc(sizeof(SnakeNode));
	if (Food == NULL) {
		perror("CreateFood malloc:");
		exit(1);
	}
	Food->x = x;
	Food->y = y;
	Food->next = NULL;
	ps->pFood = Food;
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
}

        实现完以上步骤之后,我们打印出来的界面就为: 

2.3 GameRun

        对于GameRun函数的基本框架如下:

void GameRun(Snake* ps) {
	ps->status = OK;
	//打印右侧帮助信息
	PrintHelpInfo();
	do {
		GainKeyInfo(ps);
		if (ps->status == ESC) {
			break;
		}
		SnakeMove(ps);
		Sleep(ps->SleepTime);
	} while (ps->status == OK);
}
2.3.1 打印帮助信息

        打印出来的帮助信息其实就为一些游戏规则和游戏操作,还是按照以上类似的操作进行写代码:

void PrintHelpInfo() {
	SetPos(64, 15);
	printf("不能穿墙,不能咬到自己\n");
	SetPos(64, 16);
	printf("用↑.↓.←.→分别控制蛇的移动.");
	SetPos(64, 17);
	printf("F3:为加速,F4:为减速");
	SetPos(64, 18);
	printf("ESC: 退出游戏、space:暂停游戏.");
	SetPos(64, 20);
	printf("版权@桀桀桀桀桀桀");
}
2.3.2 获取键盘信息

        从键盘获取信息是非常重要的一部分,其中涉及到我们之前定义的宏,在接收到键盘中上、下、左、右的信息时,我们还需要判断当前位置的情况,也就是说,蛇向下移动时不能向上进行掉头,向左移动时不能向右进行掉头……。

        另外,因为是频繁接收到信息,得分和每一个食物所占分数是变化的。所以我们还需要在这一部分把当前得分给打印出来,以及当前每一个食物所占的分数,速度越快,获取的分数越多,速度越慢,获取的分数越少。

        当按下空格健的时候,我们需要将游戏暂停,所以我们需要设计一个死循环,当再一次按下空格键的时候,跳出循环,每一次循环随眠100ms或者200ms。

        获取到F3和F4时,我们需要将对应的休眠时间和每个食物的分数进行调整,每一次加速,休眠时间减少30ms,食物分数增加2,每一次减速,休眠时间增加30ms,食物分数减少2,休眠时间最少为80ms,食物分数最少为2分。

        具体代码如下:

//空格键暂停
void pause() {
	while (1) {
		Sleep(100);
		if (KEY_PRESS(VK_SPACE)) {
			break;
		}
	}
}

void GainKeyInfo(Snake* ps) {
	SetPos(64, 10);    
	printf("得分:%5d", ps->score);
	SetPos(64, 11);
	printf("每个食物得分:%2d", 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_RIGHT) && ps->dir != LEFT) {
		ps->dir = RIGHT;
	}
	else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT) {
		ps->dir = LEFT;
	}
	else if (KEY_PRESS(VK_SPACE)) {
		pause();
	}
	else if (KEY_PRESS(VK_ESCAPE)) {
		ps->status = ESC;
	}
	else if (KEY_PRESS(VK_F3)) {
		if (ps->SleepTime > 80) {
			ps->SleepTime -= 30;
			ps->foodWeight += 2;
		}
	}
	else if (KEY_PRESS(VK_F4)) {
		if (ps->foodWeight > 2) {
			ps->SleepTime += 30;
			ps->foodWeight -= 2;
		}
	}
}
 2.3.3 蛇的移动

        蛇的移动部分属于整个贪吃蛇中最为关键也是最为复杂的一个部分。

        首先我们需要考虑在当前情况下,如果进行转向刚好吃到食物的情况,以及转向之后,不是食物的情况,以及在贪吃蛇每走一步都需要判断是否撞墙或者是否咬到自己。

        转向遇到食物:创建一个结点在蛇要走的下一个位置,若这个结点刚好与食物重叠,那么将创建的结点头插到蛇中,然后将食物的空间删除。然后将整条蛇打印一遍。最后在创建出一个食物。

        转向没有遇到食物:创建一个结点在蛇要走的下一个位置,下一个位置不是食物,将这个结点插入到蛇中,然后将蛇打印出来,但是最后一个结点不能打印,最后一个结点的位置打印两个空格,然后将最后一个结点释放掉。

        撞墙检测:每走完一步都需要将蛇的头结点与墙的位置进行判断,如果出现在墙的位置,那么就撞墙了,将蛇的状态改为KILL_BY_WALL。

        咬到自己检测:每走完一步,都需要将蛇的头结点与身上的各个结点进行对比,若出现横纵坐标都相等的情况,那么说明已经咬到自己了,将蛇的状态改为KILL_BY_SELF。

        实现的代码如下:

//下一个结点是食物
int NextIsFood(SnakeNode* nextNode, Snake* ps) {
	if (nextNode->x == ps->pFood->x && nextNode->y == ps->pFood->y) {
		return 1;
	}
	else {
		return 0;
	}
}

//下一个结点是食物,吃掉食物
void EatFood(SnakeNode* nextNode, Snake* ps) {
	nextNode->next = ps->SnakeHead;
	ps->SnakeHead = nextNode;
	SnakeNode* cur = ps->SnakeHead;
	while (cur) {
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->score += ps->foodWeight;
	free(ps->pFood);
	//创建新食物
	CreateFood(ps);
}

//下一个结点不是食物,不吃食物
void NotEatFood(SnakeNode* nextNode, Snake* ps) {
	nextNode->next = ps->SnakeHead;
	ps->SnakeHead = nextNode;
	SnakeNode* cur = ps->SnakeHead;
	while (cur->next->next) {
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;
}

//检测是否被墙杀死
void KillByWall(Snake* ps) {
	if (ps->SnakeHead->x == 0 ||
		ps->SnakeHead->x == 56 ||
		ps->SnakeHead->y == 0 ||
		ps->SnakeHead->y == 26) {
		ps->status = KILL_BY_WALL;
	}
}

//检测是否咬到自己
void KillBySelf(Snake* ps) {
	SnakeNode* cur = ps->SnakeHead->next;
	while (cur) {
		if (ps->SnakeHead->x == cur->x && ps->SnakeHead->y == cur->y) {
			ps->status = KILL_BY_SELF;
			return;
		}
		cur = cur->next;
	}
}

void SnakeMove(Snake* ps) {
	//创建下一个结点
	SnakeNode* nextNode = (SnakeNode*)malloc(sizeof(SnakeNode));
	if (nextNode == NULL) {
		perror("SnakeMove malloc:");
		exit(1);
	}
	//确定下一个节点的位置
	switch (ps->dir){
	case UP:{
		nextNode->x = ps->SnakeHead->x;
		nextNode->y = ps->SnakeHead->y - 1;
	}
		break;
	case DOWN: {
		nextNode->x = ps->SnakeHead->x;
		nextNode->y = ps->SnakeHead->y + 1;
	}
		break;
	case RIGHT: {
		nextNode->x = ps->SnakeHead->x + 2;
		nextNode->y = ps->SnakeHead->y;
	}
		break;
	case LEFT: {
		nextNode->x = ps->SnakeHead->x - 2;
		nextNode->y = ps->SnakeHead->y;
	}
		break;
	}
	if (NextIsFood(nextNode, ps)) {
		EatFood(nextNode, ps);
	}
	else {
		NotEatFood(nextNode,ps);
	}
	KillByWall(ps);
	KillBySelf(ps);
}

void GameRun(Snake* ps) {
	ps->status = OK;
	//打印右侧帮助信息
	PrintHelpInfo();
	do {
		GainKeyInfo(ps);
		if (ps->status == ESC) {
			break;
		}
		SnakeMove(ps);
		Sleep(ps->SleepTime);
	} while (ps->status == OK);
}

        实现完以上的步骤之后,贪吃蛇代码就已经可以运行起来了,但是我们还是需要对后续进行处理。

2.4 GameEnd

        接下来就是对游戏的善后工作了,我们需要检测游戏是因为什么而结束的,然后在屏幕中间打印出结束的原因,最后我们将蛇的各个结点进行释放。

void GameEnd(Snake* ps) {
	SnakeNode* cur = ps->SnakeHead;
	SetPos(15, 12);
	switch (ps->status){
	case ESC:
		printf("主动退出游戏,正常退出\n");
		break;
	case KILL_BY_SELF:
		printf("很遗憾,你咬到了你自己\n");
		break;
	case KILL_BY_WALL:
		printf("很遗憾,你撞到墙了\n");
		break;
	}
	while (cur) {
		SnakeNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(ps->pFood);
	ps->pFood = NULL;
	ps->SnakeHead = NULL;
	ps = NULL;
}

3.总代码

3.1 snake.h 

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include <stdbool.h>
#include <locale.h>

//枚举当前的蛇的方向
enum DIRECTION { UP, DOWN, LEFT, RIGHT };
//当前蛇的状态,OK=正常允许,ESC=主动退出,KILL_BY_WALL=被墙杀死,KILL_BY_SELF=咬到自己死亡
enum STATUS { OK, ESC, KILL_BY_WALL, KILL_BY_SELF };

#define WALL L'□'	//打印墙
#define BODY L'●'	//打印蛇身
#define FOOD L'★'	//打印食物
#define POS_X 24	//蛇的初始位置横坐标
#define POS_y 5		//蛇的初始位置纵坐标

//读取键盘信息的宏
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)

typedef struct SnakeNode {
	int x;  //x轴坐标
	int y;	//y轴坐标
	struct SnakeNode* next;
}SnakeNode;

typedef struct Snake {
	SnakeNode* SnakeHead;  //蛇头,用来维护整个蛇身
	SnakeNode* pFood;      //食物,维护食物的指针
	int score;			   //总分,用来计算当前得分
	int foodWeight;		   //食物分数,默认食物的分数
	int SleepTime;		   //每一步的休眠时间
	enum DIRECTION dir;	   //蛇头的方向,默认向右
	enum STATUS status;	   //蛇的状态,当前蛇的状态
}Snake;

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

//游戏开始
void GameStart(Snake* ps);

//欢迎界面
void WelcomeToGame();

//打印地图
void CreateMap();

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

//创建食物
void CreateFood(Snake* ps);

//运行游戏
void GameRun(Snake* ps);

//打印右侧帮助信息
void PrintHelpInfo();

//获取键盘信息
void GainKeyInfo(Snake* ps);

//暂停游戏
void pause();

//蛇移动
void SnakeMove(Snake* ps);

//如果下一个位置就是食物
int NextIsFood(SnakeNode* nextNode, Snake* ps);

//吃掉食物
void EatFood(SnakeNode* nextNode, Snake* ps);

//不是食物
void NotEatFood(SnakeNode* nextNode,Snake* ps);
 
//被墙杀死
void KillByWall(Snake* ps);

//被自己杀死
void KillBySelf(Snake* ps);

//游戏结束。处理
void GameEnd(Snake* ps);

3.2 snake.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"

void SetPos(short x, short y) {
	COORD pos = { x,y };
	HANDLE hOutput = NULL;
	//获取标准输出句柄
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置标准输出上的光标的位置
	SetConsoleCursorPosition(hOutput, 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");
}

void CreateMap() {
	int i = 0;
	SetPos(0, 0);
	//打印上面
	for (i = 0; i < 58; i+=2) {
		wprintf(L"%lc", WALL);
	}
	//打印下面
	SetPos(0, 26);
	for (i = 0; i < 58; i += 2) {
		wprintf(L"%lc", WALL);
	}
	//打印左边
	for (i = 1; i < 26; i++) {
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//打印右边
	for (i = 1; i < 26; i++) {
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
}

void InitSnake(Snake* ps) {
	SnakeNode* cur = NULL;
	for (int i = 0; i < 5; i++) {
		cur = (SnakeNode*)malloc(sizeof(SnakeNode));
		if (cur == NULL) {
			perror("InitSnake malloc:");
			exit(1);
		}
		cur->x = POS_X + 2 * i;
		cur->y = POS_y;
		cur->next = NULL;
		if (ps->SnakeHead == NULL) {
			ps->SnakeHead = cur;
		}
		else {
			cur->next = ps->SnakeHead;
			ps->SnakeHead = cur;
		}
	}
	//打印蛇身
	cur = ps->SnakeHead;
	while (cur) {
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//将其他信息完善
	ps->dir = RIGHT;
	ps->foodWeight = 10;
	ps->pFood = NULL;
	ps->score = 0;
	ps->SleepTime = 200;
	ps->status = OK;

}

void CreateFood(Snake* ps) {
	SnakeNode* cur = ps->SnakeHead;
	int x = 0;
	int y = 0;
	do {
		x = rand() % 53 + 2;
		y = rand() % 24 + 1;
	} while (x % 2 != 0);

	while (cur) {
		if (cur->x == x && cur->y == y) {
			do {
				x = rand() % 53 + 2;
				y = rand() % 24 + 1;
			} while (x % 2 != 0);
		}
		cur = cur->next;
	}
	//创建食物
	SnakeNode* Food = (SnakeNode*)malloc(sizeof(SnakeNode));
	if (Food == NULL) {
		perror("CreateFood malloc:");
		exit(1);
	}
	Food->x = x;
	Food->y = y;
	Food->next = NULL;
	ps->pFood = Food;
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
}

void GameStart(Snake* ps) {
	//先设置当前窗口大小
	system("mode con cols=100 lines=30");
	//设置窗口名
	system("title Snake");
	//获取标准输出的句柄
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//隐藏当前光标
	CONSOLE_CURSOR_INFO CursorInfo;  
	GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
	CursorInfo.bVisible = false;			   //隐藏控制台光标
	SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

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

	//打印地图
	CreateMap();

	//初始化蛇
	InitSnake(ps);

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

void PrintHelpInfo() {
	SetPos(64, 15);
	printf("不能穿墙,不能咬到自己\n");
	SetPos(64, 16);
	printf("用↑.↓.←.→分别控制蛇的移动.");
	SetPos(64, 17);
	printf("F3:为加速,F4:为减速");
	SetPos(64, 18);
	printf("ESC: 退出游戏、space:暂停游戏.");
	SetPos(64, 20);
	printf("版权@桀桀桀桀桀桀");
}

void GainKeyInfo(Snake* ps) {
	SetPos(64, 10);
	printf("得分:%5d", ps->score);
	SetPos(64, 11);
	printf("每个食物得分:%2d", 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_RIGHT) && ps->dir != LEFT) {
		ps->dir = RIGHT;
	}
	else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT) {
		ps->dir = LEFT;
	}
	else if (KEY_PRESS(VK_SPACE)) {
		pause();
	}
	else if (KEY_PRESS(VK_ESCAPE)) {
		ps->status = ESC;
	}
	else if (KEY_PRESS(VK_F3)) {
		if (ps->SleepTime > 80) {
			ps->SleepTime -= 30;
			ps->foodWeight += 2;
		}
	}
	else if (KEY_PRESS(VK_F4)) {
		if (ps->foodWeight > 2) {
			ps->SleepTime += 30;
			ps->foodWeight -= 2;
		}
	}
}

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

int NextIsFood(SnakeNode* nextNode, Snake* ps) {
	if (nextNode->x == ps->pFood->x && nextNode->y == ps->pFood->y) {
		return 1;
	}
	else {
		return 0;
	}
}

void EatFood(SnakeNode* nextNode, Snake* ps) {
	nextNode->next = ps->SnakeHead;
	ps->SnakeHead = nextNode;
	SnakeNode* cur = ps->SnakeHead;
	while (cur) {
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->score += ps->foodWeight;
	free(ps->pFood);
	//创建新食物
	CreateFood(ps);
}

void NotEatFood(SnakeNode* nextNode, Snake* ps) {
	nextNode->next = ps->SnakeHead;
	ps->SnakeHead = nextNode;
	SnakeNode* cur = ps->SnakeHead;
	while (cur->next->next) {
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;
}

void KillByWall(Snake* ps) {
	if (ps->SnakeHead->x == 0 ||
		ps->SnakeHead->x == 56 ||
		ps->SnakeHead->y == 0 ||
		ps->SnakeHead->y == 26) {
		ps->status = KILL_BY_WALL;
	}
}

void KillBySelf(Snake* ps) {
	SnakeNode* cur = ps->SnakeHead->next;
	while (cur) {
		if (ps->SnakeHead->x == cur->x && ps->SnakeHead->y == cur->y) {
			ps->status = KILL_BY_SELF;
			return;
		}
		cur = cur->next;
	}
}

void SnakeMove(Snake* ps) {
	//创建下一个结点
	SnakeNode* nextNode = (SnakeNode*)malloc(sizeof(SnakeNode));
	if (nextNode == NULL) {
		perror("SnakeMove malloc:");
		exit(1);
	}
	//确定下一个节点的位置
	switch (ps->dir){
	case UP:{
		nextNode->x = ps->SnakeHead->x;
		nextNode->y = ps->SnakeHead->y - 1;
	}
		break;
	case DOWN: {
		nextNode->x = ps->SnakeHead->x;
		nextNode->y = ps->SnakeHead->y + 1;
	}
		break;
	case RIGHT: {
		nextNode->x = ps->SnakeHead->x + 2;
		nextNode->y = ps->SnakeHead->y;
	}
		break;
	case LEFT: {
		nextNode->x = ps->SnakeHead->x - 2;
		nextNode->y = ps->SnakeHead->y;
	}
		break;
	}
	if (NextIsFood(nextNode, ps)) {
		EatFood(nextNode, ps);
	}
	else {
		NotEatFood(nextNode,ps);
	}
	KillByWall(ps);
	KillBySelf(ps);
}

void GameRun(Snake* ps) {
	ps->status = OK;
	//打印右侧帮助信息
	PrintHelpInfo();
	do {
		GainKeyInfo(ps);
		if (ps->status == ESC) {
			break;
		}
		SnakeMove(ps);
		Sleep(ps->SleepTime);
	} while (ps->status == OK);
}

void GameEnd(Snake* ps) {
	SnakeNode* cur = ps->SnakeHead;
	SetPos(15, 12);
	switch (ps->status){
	case ESC:
		printf("主动退出游戏,正常退出\n");
		break;
	case KILL_BY_SELF:
		printf("很遗憾,你咬到了你自己\n");
		break;
	case KILL_BY_WALL:
		printf("很遗憾,你撞到墙了\n");
		break;
	}
	while (cur) {
		SnakeNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(ps->pFood);
	ps->pFood = NULL;
	ps->SnakeHead = NULL;
	ps = NULL;
}

3.3 test.c

 

#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"

void Test01() {
	int ch = 0;
	do {
		Snake snake = { 0 };
		GameStart(&snake);
		GameRun(&snake);
		GameEnd(&snake);
		SetPos(20, 15);
		printf("再来一局吗?(Y/N)");
		ch = getchar();
		getchar();
	} while (ch == 'Y' || ch == 'y');
}

int main() {
	//修改适配本地中文环境
	setlocale(LC_ALL, "");
	Test01();
	SetPos(0, 27);
	return 0;
}

        至此,整个游戏的编写就结束了,读者还可以对这个代码进行升级,比如:

        1.将地图的进行升级,不在仅仅只是一个方框,加大难度;

        2.写一个文件操作,记录历史最高得分记录;

        3.将运行出来的效果加上各种动画。等等。

4.运行结果

 

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

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

相关文章

2024/1/27 备战蓝桥杯 1-1

目录 求和 0求和 - 蓝桥云课 (lanqiao.cn) 成绩分析 0成绩分析 - 蓝桥云课 (lanqiao.cn) 合法日期 0合法日期 - 蓝桥云课 (lanqiao.cn) 时间加法 0时间加法 - 蓝桥云课 (lanqiao.cn) 扫雷 0扫雷 - 蓝桥云课 (lanqiao.cn) 大写 0大写 - 蓝桥云课 (lanqiao.cn) 标题…

WordPress如何使用SQL实现一键关闭/开启评论功能(已有评论)

WordPress本人就自带评论功能&#xff0c;不过由于种种原因&#xff0c;有些站长不想开启评论功能&#xff0c;那么应该怎么实现一键关闭评论功能或开启评论功能呢&#xff1f;或者针对已有评论功能的文章进行一键关闭或开启评论功能应该怎么操作&#xff1f; 如果你使用的Wor…

第四篇:怎么写express的路由(接口+请求)

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 &#x1f4d8; 引言&#xff1a; &#x1f4…

解密人工智能:探索机器学习奥秘

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;网络奇遇记、数据结构 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;前言一. 机器学习的定义二. 机器学习的发展历程三. 机器学习的原理四. 机器学习的分类…

5.Hive表修改Location,一次讲明白

Hive表修改Loction 一、Hive中修改Location语句二、方案1 删表重建1. 创建表&#xff0c;写错误的Location2. 查看Location3. 删表4. 创建表&#xff0c;写正确的Location5. 查看Location 三、方案2 直接修改Location并恢复数据1.建表&#xff0c;指定错误的Location&#xff0…

有趣的 CSS - 动态圆点水波纹效果

先看效果 整体效果 这个效果使用 css 中 animation 属性&#xff0c;以及搭配伪元素 ::after、::before 来实现两个圆交替变化。 核心代码 html部分代码 <div><label class"dot"></label> </div>label 标签画圆点主体。 css部分代码 .ap…

蓝桥杯---九数组分数

1,2,3 ... 9 这九个数字组成一个分数,其值恰好为1/3,如何组法? 下面的程序实现了该功能,请填写划线部分缺失的代码。 注意,只能填写缺少的部分,不要重复抄写已有代码。不要填写任何多余的文字。

烧录软件(Renesas Flash Programmer)瑞萨RL78G12系列单片机下载工具(E2)的软件配置与硬件链接说明

一、单片机与仿真器连接 E1引脚接线图 RL78系列单片机的GND接仿真器的pin2、pin12、pin14 RL78系列单片机的VDD接仿真器的pin8 RL78系列单片机的Tool0接仿真器的pin5 RL78系列单片机的Reset接仿真器的pin10、pin13 二、确认接线完成后&#xff0c;开始烧录 1、打开RFPV软件…

实时视觉效果制作 -- Resolume Arena 7 中文

Resolume Arena 7是一款专业的实时视觉效果软件&#xff0c;能够为观众带来令人难以置信的视听盛宴。它具备强大的功能和直观的界面设计&#xff0c;使得用户能够轻松地创作、编辑和演示各种视觉效果。无论是在音乐会、派对还是舞台演出中&#xff0c;Resolume Arena 7都能满足…

golang封装业务err(结合iris)

golang封装业务err 我们有时在web开发时&#xff0c;仅凭httpStatus以及msg是不方便维护和体现我们的业务逻辑的。所以就需要封装我们自己的业务错误。 自定义biz_err维护err map&#xff1a;errorResponseMap、errorHttpStatusMap 注意&#xff1a;本文主要以演示为主&#xf…

【ArcGIS微课1000例】0095:横向图例制作案例教程

文章目录 一、加载数据二、高程分级显示三、横向图例四、注意事项一、加载数据 为了便于直观演示,本实验加载一个栅格数据(配套实验数据包中的0095.rar)并进行分级显示,效果如下: 二、高程分级显示 双击dem数据图层,打开栅格数据的【图层属性】对话框,切换到【符号系统…

POJ No.1852 Ants

思路分析 “转向”问题 假设蚂蚁A与蚂蚁B相遇后转向&#xff0c;可以视作A&#xff0c;B交换位置&#xff0c;从而消除转向。 距离问题 最长距离&#xff1a;比较每只蚂蚁距两端的最大距离&#xff0c;取两端中最大值&#xff0c;取一组中最长距离的最大值。 最短距离&…

互联网加竞赛 基于深度学习的中文情感分类 - 卷积神经网络 情感分类 情感分析 情感识别 评论情感分类

文章目录 1 前言2 情感文本分类2.1 参考论文2.2 输入层2.3 第一层卷积层&#xff1a;2.4 池化层&#xff1a;2.5 全连接softmax层&#xff1a;2.6 训练方案 3 实现3.1 sentence部分3.2 filters部分3.3 featuremaps部分3.4 1max部分3.5 concat1max部分3.6 关键代码 4 实现效果4.…

《HTML 简易速速上手小册》第3章:HTML 的列表与表格(2024 最新版)

文章目录 3.1 创建无序和有序列表&#xff08;&#x1f4dd;&#x1f31f;&#x1f44d; 信息的时尚搭配师&#xff09;3.1.1 基础示例&#xff1a;创建一个简单的购物清单3.1.2 案例扩展一&#xff1a;创建一个旅行计划清单3.1.3 案例扩展二&#xff1a;创建一个混合列表 3.2 …

【C++】输入输出、缺省参数、函数重载

目录 C的输入和输出 缺省参数 概念 缺省参数的分类 全缺省参数 半缺省参数 函数重载 概念 C支持函数重载的原理--名字修饰 C的输入和输出 #include<iostream> // std是C标准库的命名空间名&#xff0c;C将标准库的定义实现都放到这个命名空间中 using namespace …

分类预测 | Matlab实现DT决策树多特征分类预测

分类预测 | Matlab实现DT决策树多特征分类预测 目录 分类预测 | Matlab实现DT决策树多特征分类预测分类效果基本描述程序设计参考资料分类效果

详解顺序结构双指针处理算法

&#x1f380;个人主页&#xff1a; https://zhangxiaoshu.blog.csdn.net &#x1f4e2;欢迎大家&#xff1a;关注&#x1f50d;点赞&#x1f44d;评论&#x1f4dd;收藏⭐️&#xff0c;如有错误敬请指正! &#x1f495;未来很长&#xff0c;值得我们全力奔赴更美好的生活&…

计算机网络-编制与调制(基带信号 基带传输 宽度信号 宽度传输 编码 调制 )

文章目录 基带信号与宽带信号编码与调制数字数据编码为数字信号数字数据调制为模拟信号模拟数据编码为数字信号模拟数据调制为模拟信号小结 基带信号与宽带信号 信道上传输的信号除了可以分为数字信号和模拟信号&#xff0c;也可以分为基带信号和宽带信号&#xff0c;只是分类…

数据湖技术之平台建设篇2

数据湖技术之平台建设篇1&#xff0c;主要介绍了湖仓平台建设的前三个主要工作&#xff0c;本次主要继续上次的建设工作介绍&#xff0c;聊一聊一站式湖仓服务平台的相关管理能力建设以及针对小文件的处理。 一. 一站式湖仓服务平台的相关管理能力 主要是将相关能力落地到平台…

【c++】拷贝构造函数

1.概念 在现实生活中&#xff0c;可能存在一个与你一样的自己&#xff0c;我们称其为双胞胎 那在创建对象时&#xff0c;可否创建一个与已存在对象一某一样的新对象呢&#xff1f; 拷贝构造函数&#xff1a;只有单个形参&#xff0c;该形参是对本类类型对象的引用(一般常用c…