项目实践:贪吃蛇

引言

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

我们最终的游戏的这样:

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

一.Win32 API介绍

1.Win32 API

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

2.控制台程序

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

cpp 复制代码
#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类型的结构体声明:

cpp 复制代码
typedef struct _COORD {
 SHORT X;
 SHORT Y;
} COORD, *PCOORD;

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

cpp 复制代码
COORD pos = { 10, 15 };

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

4.GetStdHandle

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

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

cpp 复制代码
HANDLE GetStdHandle(DWORD nStdHandle);

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

比如:

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

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

5.GetConsoleCursorInfo

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

语法:

cpp 复制代码
BOOL WINAPI GetConsoleCursorInfo(
 HANDLE hConsoleOutput,
 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

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

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

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

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

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

5.1.CONSOLE_CURSOR_INFO

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

cpp 复制代码
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;

cpp 复制代码
CursorInfo.bVisible = false; //隐藏控制台光标 

6.SetConsoleCursorInfo

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

语法:

cpp 复制代码
BOOL WINAPI SetConsoleCursorInfo(
 HANDLE hConsoleOutput,
 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

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

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

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

7.SetConsoleCursorposition

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

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

语法:

cpp 复制代码
BOOL WINAPI SetConsoleCursorPosition(
 HANDLE hConsoleOutput,
 COORD pos
);

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

看一个实例:

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

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

8.GetAsyncKeyState

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

语法:

cpp 复制代码
SHORT GetAsyncKeyState(
 int vKey
);

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

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

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

cpp 复制代码
#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中。

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

cpp 复制代码
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函数就可以切换到本地模式,这种模式下程序会适应本地环境。

例如:

cpp 复制代码
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.贪吃蛇的节点结构

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

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

3.2.蛇的方向

cpp 复制代码
enum DIRECTION
{
	UP = 1,//上
	DOWN,//下
	LEFT,//左
	RIGHT//右
};

3.3.游戏状态

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

3.4.维护贪吃蛇

cpp 复制代码
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)

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

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

cpp 复制代码
//游戏开始
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)

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

cpp 复制代码
void SetPos(short x, short y)
{
	COORD pos = { x,y };
	HANDLE houtpot = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(houtpot, pos);
}

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

cpp 复制代码
//打印欢迎界面
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定义一下。

cpp 复制代码
#define WALL L'□'

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

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

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

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

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

cpp 复制代码
//打印地图
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)

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

cpp 复制代码
#define BODY L'●'

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

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

cpp 复制代码
#define POS_X 24
#define POS_Y 5

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

cpp 复制代码
//初始化蛇
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)

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

cpp 复制代码
#define FOOD L'★'

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

cpp 复制代码
//初始化食物的节点
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)

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

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

cpp 复制代码
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)

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

cpp 复制代码
//游戏运行
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)

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

cpp 复制代码
//打印右侧帮助信息
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)

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

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

2.3.蛇身移动(SnakeMove)

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

cpp 复制代码
//蛇的移动
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.

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

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

cpp 复制代码
//蛇的下一个节点有食物
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)

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

cpp 复制代码
//蛇的下一个节点没有食物
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)
cpp 复制代码
//撞到墙
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)
cpp 复制代码
//撞到自己
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的时候,游戏就结束了。

cpp 复制代码
//游戏的善后
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

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
#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;
}

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

相关推荐
梓仁沐白2 小时前
ubuntu+windows双系统切换后蓝牙设备无法连接
windows·ubuntu
Theodore_10223 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
网易独家音乐人Mike Zhou3 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
zy张起灵3 小时前
48v72v-100v转12v 10A大功率转换电源方案CSM3100SK
经验分享·嵌入式硬件·硬件工程
----云烟----5 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024065 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
开心工作室_kaic5 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it5 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
武子康5 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
九鼎科技-Leo6 小时前
什么是 WPF 中的依赖属性?有什么作用?
windows·c#·.net·wpf