数据结构:贪吃蛇详解

目录

一.地图的设计

1.字符与坐标:

2.本地化(头文件):

3.类项:

4.setlocale函数:

(1)函数原型:

(2)使用:

5.宽字符的打印:

(1)宽字符是什么:

(2)打印:

6.地图的实现和打印:

(1)具体代码与预期实现效果:

(2)解释:

二.游戏逻辑与大体框架

1.游戏逻辑与大体框架:

2.游戏初步实现(GameStart函数):

(1)欢迎界面与地图的打印:

 (2)蛇身与食物的介绍:

3.蛇身的初始化:

(1)结构体的创建:

(2)蛇的初始化(主要是结点间的链接):

4.食物的创建与初始化:

三.游戏运行逻辑(GameRun函数)的具体实现

1.打印右侧提示信息( PrintHelpInfo函数):

2.游戏状态的实现:

(1)食物的分析:

(2)蛇每走一步的分析:

(3)汇总:

四.游戏收尾(GameEnd函数)


我以过客之名,祝你前程似锦

一.地图的设计

在详细讲贪吃蛇的游戏设计之前,先说一下这里有些特别需要注意的几个点:

1.字符与坐标:

(1)控制台窗口的⼀些知识,如果想在控制台的窗⼝中指定位置输出信息,我们得知道 该位置的坐标,所以⾸先介绍⼀下控制台窗⼝的坐标知识。 控制台窗⼝的坐标如下所⽰,横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增⻓

(2)关于字符:在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★ 普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节

//这⾥再简单的讲⼀下C语⾔的国际化特性相关的知识(粗略看看就行),过去C语⾔并不适合⾮英语国家(地区)使⽤。 C语⾔最初假定字符都是但自己的。但是这些假定并不是在世界的任何地⽅都适⽤。C语⾔字符默认是采⽤ASCII编码的,ASCII字符集采⽤的是单字节编码,且只使⽤了单字节中的低7 位,最⾼位是没有使⽤的,可表⽰为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语 国家中,128个字符是基本够⽤的,但是,在其他国家语⾔中,⽐如,在法语中,字母上方有注⾳符号,它就⽆法⽤ASCII码表示。于是,⼀些欧洲国家就决定,利⽤字节中闲置的最⾼位编⼊新的符 号。如,法语中的é的编码为130(⼆进制10000010)。这样⼀来,这些欧洲国家使⽤的编码体 系,可以表⽰最多256个符号。但是,这⾥⼜出现了新的问题。不同的国家有不同的字⺟,因此,哪 怕它们都使⽤256个符号的编码⽅式,代表的字⺟却不⼀样,如130在法语编码中代表了é,在希伯来语编码中却代表了字⺟Gimel(后来为了使C语⾔适应国际化,C语⾔的标准中不断加⼊了国际化的⽀持。比如:加⼊和宽字符的类型 wchar_t 和宽字符的输入和输出函数,加头文件,其中提供了允许程序员针对特定 地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数

2.<local.h>本地化(头文件):

提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。 在标准可以中,依赖地区的部分有以下⼏项:

• 数字量的格式

• 货币量的格式

• 字符集

• 日期和时间的表示形式

3.类项:

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

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

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

• LC_MONETARY:影响货币格式

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

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

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

4.setlocale函数:

(1)函数原型:
char* setlocale (int category, const char* locale);
(2)使用:

setlocal就是用来修改当地区域的类项,仅仅修改一个时就使用上述对应的参数就行,但也可以一次性修改全部,也就是对应上述类项分类的最后一个LC_ALL   

同时,这个函数的第二个取值有两种情况“C““ ”

而当你如果想用setlocal来查询当前地区的默认设置,将第二个参数设置为NULL就可以了:

#include<stdio.h>
#include<locale.h>
int main()
{
	char* local;
	local = setlocale(LC_ALL, NULL);
	printf("默认的本地设置是:%s\n", local);

	local = setlocale(LC_ALL, "");
	printf("设置后的本地设置是:%s\n", local);
	return 0;

}

5.宽字符的打印:

(1)宽字符是什么:

‌窄字符(char)‌:通常使用8位(1字节)来表示一个字符,主要用于表示拉丁语系的字符,如英文、西班牙语、法语等,窄字符可以表示ASCII码表中的256个字符
宽字符(wchar_t)‌:通常使用16位或32位来表示一个字符,主要用于表示非拉丁语系的字符,如中文、日文、韩文等,宽字符可以表示更多的字符,支持国际化

(2)打印:

如果要打印宽字符的话,那宽字符的字面量必须加上前缀L,否则C语言会把字面量当作窄字符类项处理。前缀L在单引号前面,表示宽字符,宽字符的打印需用到wprintf,对应wprintf()的占位符为%lc;在双引号前面,表示宽字符串,对应wprintf()的占位符为%ls

#include <stdio.h>
#include<locale.h>
int main() 
{
	setlocale(LC_ALL, "");
	wchar_t ch1 = L'●';
	wchar_t ch2 = L'梅';
	wchar_t ch3 = L'茜';
	wchar_t ch4 = L'★';
	printf("%c%c\n", 'a', 'b');

	wprintf(L"%c\n", ch1);
	wprintf(L"%c\n", ch2);
	wprintf(L"%c\n", ch3);
	wprintf(L"%c\n", ch4);
	return 0;
}

从输出的结果来看,我们发现⼀个普通字符占⼀个字符的位置,但是打印⼀个汉字字符,占⽤2个字符的位置,那么我们如果 要在贪吃蛇中使⽤宽字符,就得处理好地图上坐标:

6.地图的实现和打印:

(1)具体代码与预期实现效果:
//打印地图
void CreatMap()
{
	//上
	for (int i = 0; i < 57; i += 2)
	{
		wprintf(L"%lc", L'□');
	}
	//下
	SetPos(0, 26);
	for (int i = 0; i < 57; i += 2)
	{
		wprintf(L"%lc", L'□');
	}
	//左
	for (int i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", L'□');
	}
	//右
	for (int i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", L'□');
	}
}

打印效果:

(2)解释:

这里需要注意的有三点:

一是我这里对使用宽字符的使用的解释:主要还是因为宽字符可以更好的支持多种语言的字符显示,特别是对于那些需要多个字节来表示的字符(如汉字,英文等),宽字符可以更好的保障他们的显示而且宽字符虽然会占用更大的内存,但他同时可以减少内存碎片,提高内存的使用效率

二则是对这里打印边框的方式的解释:即通过设置光标的出现位置(这一点非常重要,设置光标的函数我也附在下面,上面有些解释可以照着看看)然后加以循环实现(即SetPos函数)

//定位光标位置
void SetPos(short x, short y)
{
	COORD pos = { x, y };
    //COORD表示一种结构体类型,专门用来表示光标的存储位置,属于Win32 API的内容
	HANDLE houtput = NULL;
	//获取标准输出的句柄(⽤来标识不同设备的数值) 
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(houtput, pos);
}

三则是横着需要设置变量范围达到58而竖着要打印28次,主要还是因为光标的形状问题,我附一下这个图应该会好理解一些:


二.游戏逻辑与大体框架

1.游戏逻辑与大体框架:

2.游戏初步实现(GameStart函数):

(1)欢迎界面与地图的打印:

在上述的介绍中,我们已经初步实现了创建地图的操作,也就是我们的CreateMap,其实关于设置游戏窗口大小设置窗口名字的操作我已经在Win32 API详解里写过了,所以这里就简单的展示一下代码和注释,也就不再过多赘述了,同样打印欢迎界面的操作也跟上述定义地图的操作使出同门,即先设置光标的位置然后打印出自己想要的句子就行:

void GameStart(pSnake ps)
{
	//0.光标隐藏,窗口大小设置
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");
	//光标隐藏
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获得键盘句柄
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息 
	CursorInfo.bVisible = false; //隐藏控制台光标 
	CursorInfo.dwSize = 25;
	SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态 
	
	//1.打印环境界面与功能介绍
	WelcomeToGame();
	//2.绘制地图
	CreatMap();
	//3.创建蛇
	InitSnake(ps);
	//4.创建食物
	CreatFood(ps);
}
//打印欢迎界面
void WelcomeToGame()
{
	//光标定位的问题
	//设置光标的坐标 
	SetPos(40,14);
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	
	SetPos(42, 20);
	system("pause");

	system("cls");//清理屏幕
	SetPos(25, 14);
	wprintf(L"用↑,↓,←,→来控制蛇的移动,按F3加速,F4减速\n");
	SetPos(25, 15);
	wprintf(L"加速可以得到更高的分数\n");//两次设置光标位置打印提示
	system("pause");

	SetPos(42, 20);
	system("cls");
	system("pause");
}

实现效果:

 (2)蛇身与食物的介绍:

效果展示:

初始化状态,假设蛇的长度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处,比如(24,5)处开始出现 蛇(连续5个节点),注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有一半出现在墙体中,另外⼀般在墙外的现象,坐标不好对齐。 关于⻝物,就是在墙体内随机⽣成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的⾝体重合,然 后打印★

效果展示就像这个样子(当然蛇头和食物的坐标都是随机出现的,且不能发生重叠而且必须保证范围,即必须出现在墙体之内),下面咱来仔细说一下代码:)

3.蛇身的初始化:

蛇身的初始化我大体分为两步,一个当然是各种结构体的创建,另外一个就是以链表为基础的对各个结点(即蛇身的链接)

(1)结构体的创建:
//蛇的结点
typedef struct SnakeNode
{
	//坐标
	int x;
	int y;
	//指向下一个结点的指针
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;//对这个结构体指针进行重命名为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;

这里首先有两种结构体:一种是为了定位蛇的位置,另外一个则是包含了所有与蛇有关的相关变量,其中在第二种结构体中也设置有第一种结构体类型的变量,其他的变量的枚举,定义和相关头文件的引用我也附在下面(这些结构体的创建,枚举的创建等都是放在自己另外创建的头文件里,我这里先称呼它为Snake.h):

#pragma once
#define  _CRT_SECURE_NO_WARNINGS
#include<locale.h>
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>//true和false
#include<time.h>
#define POS_X 24
#define POS_Y 5
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'

//蛇的方向
enum DIRECTION
{
	UP,
	DOWN,
	LEFT,
	RIGHT,
};

//蛇的状态
//正常,撞墙,撞到自己,正常退出
enum GAME_STATUS
{
	OK,
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF,//撞自己
	END_NORMAL,//正常退出
};
(2)蛇的初始化(主要是结点间的链接):
//初始化蛇基本信息
void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	for (int i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));

		if (cur == NULL)
		{
			perror("TnitSnake()failed");
			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", L'●');
		cur = cur->next;
	}
	//初始化贪吃蛇的其他属性
	ps->_dir = RIGHT;
	ps->_score = 0;
	ps->_food_weight = 10;
	ps->_sleep_time = 200;//单位是毫秒
	ps->_status = OK;
	
}

4.食物的创建与初始化:

在具体介绍详细代码之前,我感觉有些代码底层的逻辑得先说明一下(也许可以帮助你的理解):首先就是关于我为什么会在蛇的结构体(Snake)里同时将食物的相关变量也放在里面并且将其一并初始化,主要就是在代码的最底层,也就是剥离掉食物和蛇的外形,仅仅将其看作一堆数据时你就会发现我们这里写的无论是食物还是蛇它们其实都是在我们界面上随机生成的两个不同的坐标而已,而我们所谓的蛇吃食物也只不过是代码之间的相互判断,因此,本质上,蛇和食物根本没有区别,只不过是运行的方式不同罢了(这也是我在下面创建食物的代码函数里穿的是pSnake指针的原因)

//创建食物
void CreatFood(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 + 2;
	} 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()failed");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;

	//打印食物
	SetPos(x, y);
	wprintf(L"%lc", FOOD);这里的FOOD我在头文件里定义过,其实打印的是食物对应的特殊的符号,我为了图省事就在头文件里定义了一下

	ps->_pFood = pFood;
}

三.游戏运行逻辑(GameRun函数)的具体实现

1.打印右侧提示信息( PrintHelpInfo函数):

经过初始界面的讲解,这里对提示信息的打印应该也很好理解了,无非就是那两步:设置光标位置和打印需要的文字

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"梅茜Mercy制作");
}

实现效果:

2.游戏状态的实现:

这一步是这整个游戏最重要也是最复杂的一步,它涉及着游戏每一个瞬时状态的数据,并且需要对下一步的反应进行预测和调整,在具体实现的过程中,为了使思路更加清晰,我这里暂且将这个游戏状态的实现分为两步:

一是食物的存在与销毁的分析

二则是对蛇走一步的过程的具体分析

(1)食物的分析:

仔细考虑我们会发现在与食物有关的问题中存在以下几种情况:

食物的创建(CreatFood)在上面我们已经实现过,

蛇下一步吃到食物(EatFood),

蛇下一步没吃到食物(NoFood),

而在以上行为存在的前提还有一个就是对下一步是否是食物的判断(NextIsFood),

这样我们实现与食物有关的函数的思路就清晰多了,下面是具体代码和一些注意点:

//判断下一个坐标是否是食物
int NextIsFood(pSnakeNode pn, pSnake ps)
{
	if (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y)
	return 1;
	else
	return 0;
}


//下一个位置是食物,就吃掉食物
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;
	
		//重新创建食物
	CreatFood(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;
}
(2)蛇每走一步的分析:

这里对蛇的分析为主要分为两点,

一是对蛇下一步走向的控制:

这里就运用到了Win32 API里对按键状态的判断和使用了,具体可以参照Win32 API详解里的内容

#define KEY_PRESS(vk)  ((GetAsyncKeyState(vk)&1)?1:0)//按键状态的宏

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 > 80)
	{
		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;
	}
}

二则是对蛇下一个结点的属性判断:

蛇本质上作为一个动态的链表,每一次的移动都意味着头结点的创建和尾结点的销毁(蛇没吃到食物时的状态),同时伴随着对下一个结点是否是食物自身的判断

//蛇走一步的过程
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 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 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;
	}
}
(3)汇总:

这里其实有一点我没有着重强调就是我过程中对分数的记录:加速加分数,减速减分数,吃到食物加分数,这些依据个人喜好来看,实现方法也很容易,通过->访问对应的结构体成员就行

void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();
	do
	{
		//打印总分数和食物的分值
		SetPos(64, 10);
		printf("总分数:%d\n", ps->_score);
		SetPos(64, 10);
		printf("当前食物的分数:%d\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 > 80)
			{
				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);
	
}

这里还要补充的一点就是对sleep()函数的使用:

#include <windows.h>
#include <stdio.h>

int main() {
    printf("Starting...\n");
    
    // 休眠5000毫秒(5秒)
    Sleep(5000);
    
    printf("Ended after 5 seconds.\n");
    return 0;
}


//在Windows系统上,使用Sleep函数,其接受的参数是毫秒数(1秒=1000毫秒)

四.游戏收尾(GameEnd函数)

这一块的代码相比就要轻松不少,一共也分为两部分:

结束提示和释放空间:

(但这里的结束也要分成几种情况讨论)

//游戏善后的工作
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);
}
	}

这样咱的贪吃蛇游戏也就到此为止,很高兴你可以一直阅读到这里,接下来我把源码放在这儿,要的话直接那就行,希望可以对你有些帮助:

text.c


#define  _CRT_SECURE_NO_WARNINGS
#include"snake.h"

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

	//运行游戏
	//GameRun();
	//结束游戏 - 善后工作
	//GameEend();
	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;
}
Snake.h


#pragma once
#define  _CRT_SECURE_NO_WARNINGS
#include<locale.h>
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>//true和false
#include<time.h>
#define POS_X 24
#define POS_Y 5
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'

//蛇的方向
enum DIRECTION
{
	UP,
	DOWN,
	LEFT,
	RIGHT,
};

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

//蛇的结点
typedef struct SnakeNode
{
	//坐标
	int x;
	int y;
	//指向下一个结点的指针
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;//对这个结构体指针进行重命名为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 GameStart(pSnake ps);
//游戏欢迎界面
void WelcomeToGame();
//初始化蛇
void InitSnake(pSnake ps);
//绘制地图
void CreatMap(ps);
//创建食物
void CreatFood(ps);
//运行游戏
void GameRun(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);
Snake.c


#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 CreatFood(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 + 2;
	} 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()failed");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;

	//打印食物
	SetPos(x, y);
	wprintf(L"%lc", FOOD);

	ps->_pFood = pFood;
}



//打印欢迎界面
void WelcomeToGame()
{
	SetPos(40, 14);
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	SetPos(42, 20);
	system("pause");
	system("cls");
	SetPos(25, 14);
	wprintf(L"用 ↑. ↓ . ← . → 来控制蛇的移动,按F3加速,F4减速\n");
	SetPos(25, 15);
	wprintf(L"加速能够得到更高的分数\n");

	SetPos(42, 20);
	system("pause");
	system("cls");
}

//打印地图
void CreatMap()
{
	//上
	for (int i = 0; i < 57; i += 2)
	{
		wprintf(L"%lc", L'□');
	}
	//下
	SetPos(0, 26);
	for (int i = 0; i < 57; i += 2)
	{
		wprintf(L"%lc", L'□');
	}
	//左
	for (int i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", L'□');
	}
	//右
	for (int i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", L'□');
	}
}

//初始化蛇基本信息
void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	for (int i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));

		if (cur == NULL)
		{
			perror("TnitSnake()failed");
			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", L'●');
		cur = cur->next;
	}
	//初始化贪吃蛇的其他属性
	ps->_dir = RIGHT;
	ps->_score = 0;
	ps->_food_weight = 10;
	ps->_sleep_time = 200;//单位是毫秒
	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);
	}
}



//检测蛇是否撞到自己
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 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 GameStart(pSnake ps)
{
	//0.光标隐藏,窗口大小设置
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");
	//光标隐藏
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获得键盘句柄
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息 
	CursorInfo.bVisible = false; //隐藏控制台光标 
	CursorInfo.dwSize = 25;
	SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态 
	
	//1.打印环境界面与功能介绍
	WelcomeToGame();
	//2.绘制地图
	CreatMap();
	//3.创建蛇
	InitSnake(ps);
	//4.创建食物
	CreatFood(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"梅茜Mercy制作");
}

#define KEY_PRESS(vk)  ((GetAsyncKeyState(vk)&1)?1:0)//按键状态的宏

//下一个位置不是食物
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;
}
//判断下一个坐标是否是食物
int NextIsFood(pSnakeNode pn, pSnake ps)
{
	if (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y)
	return 1;
	else
	return 0;
}
//下一个位置是食物,就吃掉食物
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;
	
		//重新创建食物
	CreatFood(ps);
}

//蛇走一步的过程
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 Pause()
{
	while (1)
	{
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();
	do
	{
		//打印总分数和食物的分值
		SetPos(64, 10);
		printf("总分数:%d\n", ps->_score);
		SetPos(64, 10);
		printf("当前食物的分数:%d\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 > 80)
			{
				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);
	
}

全文终

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

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

相关文章

医学AI前沿进展:图像分割以及细胞分割领域的最新研究|文献速递·24-12-17

小罗碎碎念 今天推文和大家分享医学AI领域中&#xff0c;图像分割以及细胞分割方面的三个工作。 首先看一下图像分割以及细胞分割方面&#xff0c;近五年的一个论文发表情况&#xff0c;我们可以看到&#xff0c;这个领域在前几年的热度基本持平&#xff0c;到了24年迎来了一个…

Endnote | 查看文献所在分组

软件版本&#xff1a;Endnote X8 第一种方式&#xff1a; 在文献上右键——记录摘要&#xff0c;即可在弹出页面上看到自定义和智能组的分组情况。 第二种方式&#xff1a; 在菜单栏点击文献——记录摘要&#xff0c;也可以查看分组情况。 注&#xff1a; 新版本的endnote软件…

ElasticSearch 数据聚合与运算

1、数据聚合 聚合&#xff08;aggregations&#xff09;可以让我们极其方便的实现数据的统计、分析和运算。实现这些统计功能的比数据库的 SQL 要方便的多&#xff0c;而且查询速度非常快&#xff0c;可以实现近实时搜索效果。 注意&#xff1a; 参加聚合的字段必须是 keywor…

34. 在排序数组中查找元素的第一个和最后一个位置 二分法

34. 在排序数组中查找元素的第一个和最后一个位置 class Solution { public:vector<int> searchRange(vector<int>& nums, int target) {vector<int> res(2,-1);res[0]findleft(nums,target);if(res[0] -1) return res;res[1] findright(nums,target);…

回型矩阵:JAVA

解题思路&#xff1a; 通过定义四条边界&#xff1b;top,left,right,bottom,来循环&#xff0c;当top>bottom&&left>right的时候循环终止 循环结束的条件&#xff1a; 链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 来源&#xff1a;牛客网 题目描述…

基于单片机的农田灌溉系统(论文+源码)

1.系统设计 本系统主要实现如下目标&#xff1a; 1&#xff0e;可以实时监测土壤湿度&#xff1b; 2&#xff0e;土壤湿度太低时&#xff0c;进行浇水操作&#xff1b; 3&#xff0e;可以按键设置湿度的触发阈值&#xff1b; 4. 可以实现远程操控 5&#xff0e;可以实现手…

QoS分类和标记

https://zhuanlan.zhihu.com/p/160937314 1111111 分类和标记是识别每个数据包优先级的过程。 这是QoS控制的第一步&#xff0c;应在源主机附近完成。 分组通常通过其分组报头来分类。下图指定的规则仔细检查了数据包头 &#xff1a; 下表列出了分类标准&#xff1a; 普通二…

Python脚本基于Tesseract-OCR实现图文识别

一、了解Tesseract-OCR 开源地址&#xff1a;https://github.com/tesseract-ocr/tesseract Tesseract-OCR 是一个开源的光学字符识别&#xff08;OCR&#xff09;引擎&#xff0c;能够识别图片中的文字并将其转化为可编辑的文本。它最初由惠普公司&#xff08;Hewlett-Packard…

软件集成测试内容和作用简析

在现代软件开发过程中&#xff0c;软件集成测试作为关键的一环&#xff0c;日益受到重视。特别是随着信息技术的快速发展&#xff0c;各类软件系统日益庞大复杂&#xff0c;如何确保系统不同模块的顺畅合作&#xff0c;成为了每个项目成功的重要基础。集成测试是指在软件开发过…

23. 合并 K 个升序链表(java)

题目描述&#xff1a; 给你一个链表数组&#xff0c;每个链表都已经按升序排列。 请你将所有链表合并到一个升序链表中&#xff0c;返回合并后的链表。 示例 1&#xff1a; 输入&#xff1a;lists [[1,4,5],[1,3,4],[2,6]] 输出&#xff1a;[1,1,2,3,4,4,5,6] 解释&#xff…

Vscode搭建C语言多文件开发环境

一、文章内容简介 本文介绍了 “Vscode搭建C语言多文件开发环境”需要用到的软件&#xff0c;以及vscode必备插件&#xff0c;最后多文件编译时tasks.json文件和launch.json文件的配置。即目录顺序。由于内容较多&#xff0c;建议大家在阅读时使用电脑阅读&#xff0c;按照目录…

解决并发情况下调用 Instruct-pix2pix 模型推理错误:index out of bounds 问题

解决并发情况下调用 Instruct-pix2pix 模型推理错误&#xff1a;index out of bounds 问题 背景介绍 在对 golang 开发的 图像生成网站 进行并发测试时&#xff0c;调用基于 Instruct-pix2pix 模型和 FastAPI 的图像生成 API 遇到了以下错误&#xff1a; Model inference er…

ARM Linux 虚拟环境搭建

一、目标 在没有arm硬件的情况下&#xff0c;使用QEMU模拟器&#xff0c;在PC上模拟一块ARM开发板&#xff0c;对ARM Linux进行学习。 二、搭建步骤 首先先有一个Linux 开发环境&#xff0c;我目前使用的是Ubuntu20. 首先安装qemu&#xff0c;qemu的官网&#xff1a;https:…

百度2020校招Web前端工程师笔试卷(第二批)

百度2020校招Web前端工程师笔试卷&#xff08;第二批&#xff09; 2024/12/17 1.FIFO为先进先出的顺序来完成页面的访问&#xff0c;而如果在采用先进先出页面淘汰算法的系统中&#xff0c;一进程在内存占3块&#xff08;开始为空&#xff09;&#xff0c;页面访问序列为1、2、…

java--抽象类(abstract)和接口(interface)

一.抽象类(abstract) 1.概念: 当父类中的一些方法不能确定实现的具体功能时,可以用abstract关键字来修饰该方法,此时,该方法就是抽象方法,该方法不需要实现方法体.可由其子类实现父类的抽象方法, abstruct不能用来修饰属性, 用abstract修饰的类叫做抽象类 // 抽象类&#x…

Web 毕设篇-适合小白、初级入门练手的 Spring Boot Web 毕业设计项目:教室信息管理系统(前后端源码 + 数据库 sql 脚本)

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 1.0 项目介绍 开发工具&#xff1a;IDEA、VScode 服务器&#xff1a;Tomcat&#xff0c; JDK 17 项目构建&#xff1a;maven 数据库&#xff1a;mysql 8.0 系统用户前台和管理…

Qt之修改窗口标题、图标以及自定义标题栏(九)

Qt开发 系列文章 - titles-icons-titlebars&#xff08;九&#xff09; 目录 前言 一、修改标题 二、添加图标 三、更换标题栏 1.效果演示 2.创建标题栏类 3.定义相关函数 4.使用标题栏类 总结 前言 在我们利用Qt设计软件时&#xff0c;经常需要修改窗口标题、更改软…

JumpServer开源堡垒机搭建及使用

目录 一,产品介绍 二,功能介绍 三,系统架构 3.1 应用架构 3.2 组件说明 3.3 逻辑架构 3.3 逻辑架构 四,linux单机部署及方式选择 4.1 操作系统要求(JumpServer-v3系列版本) 4.1.1 数据库 4.1.3创建数据库参考 4.2 在线安装 4.2.1 环境访问 4.3 基于docker容…

Pytorch | 从零构建GoogleNet对CIFAR10进行分类

Pytorch | 从零构建Vgg对CIFAR10进行分类 CIFAR10数据集GoogleNet网络结构特点网络整体架构特征图尺寸变化应用与影响 GoogleNet结构代码详解结构代码代码详解Inception 类初始化方法前向传播 forward GoogleNet 类初始化方法前向传播 forward 训练和测试训练代码train.py测试代…

简单了解一下 Go 语言的构建约束?

​构建约束是一种在 Go 语言中控制源文件编译条件的方法&#xff0c;它可以让您指定某些文件只在特定的操作系统、架构、编译器或 Go 版本下编译&#xff0c;而在其他环境中自动忽略。这样可以方便您针对不同的平台或场景编写不同的代码&#xff0c;实现条件编译的功能。 构建…