【C语言】实现贪吃蛇游戏

目录

  • 1. 前言
  • 2. Win32 API 介绍
    • 2.1 Win32 API
    • 2.2 控制台程序
    • 2.3 控制台屏幕上的坐标COORD
    • 2.4 GetStdHandle
    • 2.5 GetConsoleCursorInfo
      • 2.5.1 CONSOLE_CURSOR_INFO
    • 2.6 SetConsoleCursorInfo
    • 2.7 SetConsoleCursorPosition
    • 2.8 GetAsyncKeyState
  • 3. 贪吃蛇游戏设计与分析
    • 3.1 地图
      • 3.1.1 ``本地化
      • 3.1.2 类项
      • 3.1.3 宽字符的打印
      • 3.1.4 地图坐标
    • 3.2 蛇身和食物
    • 3.3 数据结构设计
      • 3.3.1 蛇的方向
      • 3.3.2 游戏状态
    • 3.4 游戏流程设计
  • 4.核心逻辑实现分析
    • 4.1 游戏主逻辑
    • 4.2 游戏开始(GameStart)
      • 4.2.1 打印欢迎界面
      • 4.2.2 创建地图
      • 4.2.3 初始化身
      • 4.2.4 创建第一个食物
    • 4.3 游戏运行(GameRun)
      • 4.3.1 蛇身移动
        • 4.3.1.1 判断蛇头到达的坐标处是否是食物
        • 4.3.1.2 判断蛇头的坐标是否和墙的坐标冲突
        • 4.3.1.3 判断蛇头的坐标是否和蛇身体的坐标冲突
    • 4.4 游戏结束
  • 5. 附代码
    • 5.1 snake.h
    • 5.2 snake.c
    • 5.3 test.c

1. 前言

【C语言】实现贪吃蛇游戏_第1张图片
我们要用C语言来实现贪吃蛇游戏之前,得了解C语言函数、枚举、结构体、动态内存管理、预处理指令、链表和Win32 API等等的一些相关知识。
关于链表和函数,在之前的文章中都有写过了,友友们可以自行查看。剩下知识会在之后的博客中所提及,请大家多多关注。
这里主要介绍Win32 API,及如何一步一步实现贪吃蛇游戏。

2. Win32 API 介绍

下面介绍的结构体和函数在Win32 API 都是现成的,我们就了解一下如何使用就行。

2.1 Win32 API

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

2.2 控制台程序

不知道大家知不知道cmd的程序。
平常我们运行起来的黑框程序其实就是控制台程序
来看看它是什么样的。
【C语言】实现贪吃蛇游戏_第2张图片
我们可以使用cmd命令来设置控制台窗口的长宽:设置控制台窗口的大小。像30行,100列,这些都是可以的。
也可以调用C语言函数system来执行不过在使用时要包含#include

 system("mode con cols=20 lines=20");

我们也可以通过命令设置控制台窗⼝的名字:

system("title 贪吃蛇");

看看效果
【C语言】实现贪吃蛇游戏_第3张图片

2.3 控制台屏幕上的坐标COORD

COORD是WindowsAPI中定义的一个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0) 的原点位于缓冲区的顶部左侧单元格。
就是所示这样
【C语言】实现贪吃蛇游戏_第4张图片

2.4 GetStdHandle

GetStdHandle是一个WindowsAPI函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
就是GetStdHandle来获得某一种设备的控制权限。

HANDLE GetStdHandle(DWORD nStdHandle);

举个例子:如果我们想获得输出程序的句柄
那我们得调用GetStdHandle这个函数。

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

2.5 GetConsoleCursorInfo

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

BOOL WINAPI GetConsoleCursorInfo(
 HANDLE               hConsoleOutput,
 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
 );
 

PCONSOLE_CURSOR_INFO 是指向CONSOLE_CURSOR_INFO结构的指针,该结构接收有关主机游标(光标)的信息。

2.5.1 CONSOLE_CURSOR_INFO

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

typedef struct _CONSOLE_CURSOR_INFO {
 DWORD dwSize;
 BOOL  bVisible;
 } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
  1. dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
  2. bVisible,游标的可见性。如果光标可见,则此成员为TRUE。

bVisible = false就是为了隐藏控制台光标。

CursorInfo.bVisible = false; 
int main()
{
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO cursor_info;
	GetConsoleCursorInfo(handle, &cursor_info);
	cursor_info.dwSize = 100;
	cursor_info.bVisible = true;
	SetConsoleCursorInfo(handle, &cursor_info);
	return 0;
}

把dwSize试着设置为dwSize = 100,把bVisible设置我bVisible = true。我们来看看效果:
【C语言】实现贪吃蛇游戏_第5张图片

2.6 SetConsoleCursorInfo

设置指定控制台屏幕缓冲区的光标的大小和可见性。
光标
就是在程序运行起来的时候,这个光标是有长度和宽度的。而这些属性是可以设置的,就是放在
CONSOLE_CURSOR_INFO结构体变量中的。

int main()
{
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO cursor_info;
	GetConsoleCursorInfo(handle, &cursor_info);
	return 0;
}

就是把控制台里面的光标信息放在 cursor_info放在里面,所以GetConsoleCursorInfo(handle, &cursor_info),里就传了地址。

2.7 SetConsoleCursorPosition

设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
我们来看一个示例:
假设把光标定位到pos = { 5, 20 },输入1后,打印是在下一行打印。

int main()
{
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD pos = { 5, 20 };
	SetConsoleCursorPosition(handle, pos);
    int ch = getchar();
	putchar(ch);
	return 0;
}

【C语言】实现贪吃蛇游戏_第6张图片

用上面这种方式设置一个坐标比较麻烦,那我们直接封装一个函数来确定一个坐标。
代码来实现一下

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

那以后需要设置坐标直接调用就行。

我们来试一下在(10,10)处打印一个hi

#include 
#include 
#include 
#include 
void SetPos(int x, int y)
{
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD pos = { x, y };
	SetConsoleCursorPosition(handle, pos);
}
int main()
{
	SetPos(10, 10);
	printf("hi\n");
	return 0;
}

显然是可以的。
【C语言】实现贪吃蛇游戏_第7张图片

2.8 GetAsyncKeyState

因为我们需要知道玩家在键盘上按键,那我们怎么知道呢?

就是用GetAsyncKeyState获取按键情况,GetAsyncKeyState的函数原型如下:

SHORT GetAsyncKeyState(
 int vKey
 )

这个函数就一个参数,这个参数指的是键盘上每一个键的虚拟值。
在Win32 API中给键盘上的每一个键都编了号。
我们来看看一部分:
【C语言】实现贪吃蛇游戏_第8张图片

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

GetAsyncKeyState的返回值是short类型,在上一次调用GetAsyncKeyState函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
【C语言】实现贪吃蛇游戏_第9张图片
如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1。
那我们如何检测呢?
【C语言】实现贪吃蛇游戏_第10张图片
不管这个数字是几,只要按位与1,结果得到1的话,说明被按过,返回的是0,说明没有被按过。

为了方便,我们封装一个PRESS_KET 来检测vk这虚拟键值对应的按键是否被按过
如果按过返回1,未按过返回0

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

3. 贪吃蛇游戏设计与分析

3.1 地图

我们最终的贪吃蛇大概要是这个样子,那我们的地图如何布置呢?
【C语言】实现贪吃蛇游戏_第11张图片
【C语言】实现贪吃蛇游戏_第12张图片
【C语言】实现贪吃蛇游戏_第13张图片
这里不得不讲一下控制台窗口的一些知识,如果想在控制台的窗口中指定位置输出信息,我们得知道该位置的坐标,所以首先介绍⼀下控制台窗口的坐标知识。
控制台窗口的坐标如图所示,横向的是X轴,从左向右依次增长,纵向是Y轴,从上到下依次增长。
【C语言】实现贪吃蛇游戏_第14张图片
在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★
普通的字符是占一个字节的,这类宽字符是占用2个字节。
这里再简单的讲一下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。
C语言最初假定字符都是单字节的。但是这些假定并不是在世界的任何地方都适用。

后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型wchar_t 和宽字符的输入和输出函数,加入了头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。

3.1.1 本地化

提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。

就像货币的格式:
【C语言】实现贪吃蛇游戏_第15张图片

在标准中,依赖地区的部分有以下几项:

  1. 数字量的格式
  2. 货币量的格式
  3. 字符集
  4. 日期和时间的表示形式

3.1.2 类项

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

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

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

  3. LC_MONETARY:影响货币格式。

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

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

  6. LC_ALL-针对所有类项修改,将以上所有类别设置为给定的语⾔环境。

3.1.3 宽字符的打印

那如果想在屏幕上打印宽字符,怎么打印呢?
宽字符的字面量必须加上前缀“L”,否则C语言会把字面量当作窄字符类型处理。前缀“L”在单引号前面,表示宽字符,对应wprintf() 的占位符为 %lc;在双引号前面,表示宽字符串,wprintf() 的占位符为%ls
我们用代码来看看:

#include 
#include 
#include 
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"%lc\n", ch1);
    wprintf(L"%lc\n", ch2);
    wprintf(L"%lc\n", ch3);
    wprintf(L"%lc\n", ch4);

	return 0;
}

【C语言】实现贪吃蛇游戏_第16张图片
【C语言】实现贪吃蛇游戏_第17张图片

3.1.4 地图坐标

而我们想得到这样的:
我们假设实现⼀个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,
如下:
【C语言】实现贪吃蛇游戏_第18张图片

3.2 蛇身和食物

初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24,5)处开始出现蛇,连续5个节点。

注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半出现在墙体中,另外⼀半在墙外的现象,坐标不好对齐。
关于食物,就是在墙体内随机生成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。
【C语言】实现贪吃蛇游戏_第19张图片

3.3 数据结构设计

在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行,所以蛇节点结构如下:

typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

而我们要管理整条贪吃蛇,我们再封装一个Snake的结构来维护整条贪吃蛇:

typedef struct Snake
{
	pSnakeNode _pSnake;//指向贪吃蛇头结点的指针
	pSnakeNode _pFood;//指向食物结点的指针
	int _Score;//贪吃蛇累计的总分
	int _FoodWeight;//一个食物的分数
	int _SleepTime;//每走一步休息的时间,时间越短,速度越快,时间越长,速度越慢
	enum DIRECTION _Dir;//描述蛇的方向
	enum GAME_STATUS _Status;//游戏的状态:正常、退出、撞墙、吃到自己
}Snake, * pSnake;

3.3.1 蛇的方向

蛇就只有四个方向,向上,向下,向左和向右,,可以一一列举出来,所以使用枚举来描述蛇前进方向。
代码实现:

enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

3.3.2 游戏状态

就像我们玩的一些游戏一样,要能够知道游戏运行的状态,像正常运行、撞到墙、蛇撞到自己和正常游戏结束,我们同样可以一一例举出来,也使用枚举来描述。
来看看代码实现:

enum GAME_STATUS
{
	OK,//正常运行
	END_NORMAL,//按ESC退出
	KILL_BY_WALL,//撞到自己
	KILL_BY_SELF//正常结束
};

3.4 游戏流程设计

同样设置三个文件,一个test.c用来测试代码,一个snake.h用来放函数声明,最后一个snake.c用来放相关函数的实现。
【C语言】实现贪吃蛇游戏_第20张图片

4.核心逻辑实现分析

4.1 游戏主逻辑

主逻辑分为3个过程:

  1. 游戏开始(GameStart)完成游戏的初始化
  2. 游戏运行(GameRun)完成游戏运行逻辑的实现
  3. 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放

4.2 游戏开始(GameStart)

在snake.c中来开始对游戏初始化:

  1. 控制台窗口大小的设置
  2. 控制台窗口名字的设置
  3. 鼠标光标的隐藏
  4. 打印欢迎界⾯
  5. 创建地图
  6. 初始化第蛇
  7. 创建第⼀个食物
void GameStart(pSnake ps)
{
	//控制台窗口的设置
	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; //隐藏控制台光标
	SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

	//打印欢迎界面
	WelComeToGame();
	//创建地图
	CreateMap();
	//初始化贪食蛇
	InitSnake(ps);
	//创建食物
	CreateFood(ps);
}

4.2.1 打印欢迎界面

在游戏开始之前,我们需要对玩家有一些提示:
就是像这样的
【C语言】实现贪吃蛇游戏_第21张图片
【C语言】实现贪吃蛇游戏_第22张图片

void WelComeToGame()
{
	SetPos(40, 14);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(40, 25);
	system("pause");//pause是暂停
	system("cls");
	SetPos(20, 14);
	printf("使用 ↑ . ↓ . ← . → . 分别控制蛇的移动, F3是加速,F4是减速");
	SetPos(40, 25);
	system("pause");
	system("cls");
}

当做好这些后,就要创建地图了。

4.2.2 创建地图

创建地图就是将墙打印出来,因为是宽字符打印,所有使用wprintf函数,打印格式串前使用L"%c"打印地图的关键是要算好坐标,才能在想要的位置打印墙体。
先在屏幕上打印上和下,它们相差的是两个字符。
而左和右的墙,在循环时,就只相差1就行。

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

最终实现的时候是这样的。
【C语言】实现贪吃蛇游戏_第23张图片

4.2.3 初始化身

蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。
创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身体后,将蛇的每一节打印在屏幕上。

在创建蛇身时,我们使用头插,将蛇的身体节点一个一个插入,但最终还要返回蛇头节点,方便之后进行操作。

void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;
		cur->next = NULL;

		//头插法
		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->_Status = OK;
	ps->_Score = 0;
	ps->_pFood = NULL;
	ps->_SleepTime = 200;
	ps->_FoodWeight = 10;
	ps->_Dir = RIGHT;
}

4.2.4 创建第一个食物

要生成食物,先随机生成食物的坐标,而x坐标必须是2的倍数,而且食物物的坐标不能和蛇身体每个节点的坐标重复,也不能和墙的坐标重复。

void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;
again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);//x坐标必须是2的倍数

	//坐标不能和蛇的身体冲突
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		//比较坐标
		if (cur->x == x && cur->y == 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;
	ps->_pFood = pFood;

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

4.3 游戏运行(GameRun)

游戏运行期间,右侧打印帮助信息,提示玩家:
根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。
【C语言】实现贪吃蛇游戏_第24张图片

void GameRun(pSnake ps)
{
	PrintHelpInfo();
	do
	{
		SetPos(64, 10);
		printf("得分:%05d", 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_LEFT) && ps->_Dir != RIGHT)
		{
			ps->_Dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT)
		{
			ps->_Dir = RIGHT;
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = END_NORMAL;
			break;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			Pause();
		}
		else if (KEY_PRESS(VK_F3))//加速
		{
			if (ps->_SleepTime >= 80)
			{
				ps->_SleepTime -= 30;
				ps->_FoodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))//减速
		{
			if (ps->_SleepTime < 320)
			{
				ps->_SleepTime += 30;
				ps->_FoodWeight -= 2;
			}
		}

		Sleep(ps->_SleepTime);
		SnakeMove(ps);
	} while (ps->_Status == OK);
}

4.3.1 蛇身移动

先创建下一个节点,根据移动方向和蛇头的坐标,蛇移动到下一个位置的坐标。
确定了下一个位置后,需要看下一个位置是否是食物(NextIsFood),是食物就做吃食物物处理(EatFood),如果不是食物则做前进一步的处理(NoFood)。
蛇身移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上自己蛇身(KillBySelf),从而影响游戏的状态。
我们来看看代码实现:

void SnakeMove(pSnake ps)
{
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNext == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	pNext->next = NULL;

	switch (ps->_Dir)
	{
	case UP:
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y + 1;
		break;
	case LEFT:
		pNext->x = ps->_pSnake->x - 2;
		pNext->y = ps->_pSnake->y;
		break;
	case RIGHT:
		pNext->x = ps->_pSnake->x + 2;
		pNext->y = ps->_pSnake->y;
		break;
	}

	//判断蛇头到达的坐标处是否是食物
	if (NextIsFood(ps, pNext))
	{
		//吃掉食物
		EatFood(ps, pNext);
	}
	else
	{
		//不吃食物
		NoFood(ps, pNext);
	}

	//蛇是否撞墙
	KillByWall(ps);

	//蛇是否自杀
	KillBySelf(ps);
}
4.3.1.1 判断蛇头到达的坐标处是否是食物

不管是不是食物,节点都直接插入,而后再做下一步的处理。

判断蛇头到达的坐标处是否是食物,如果是那就吃掉,并创建新的食物。
代码实现

void EatFood(pSnake ps, pSnakeNode pnext)
{
	//头插
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;

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

	free(ps->_pFood);
	ps->_Score += ps->_FoodWeight;

	CreateFood(ps);//新创建食物
}

如果不是那就前进,同样是把下一个节点插入,在判断不是食物之后,遍历蛇身,释放最后一个节点。
代码实现:

void NoFood(pSnake ps, pSnakeNode pnext)
{
	//头插
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;

	//打印蛇身
	pSnakeNode cur = ps->_pSnake;
	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;
}
4.3.1.2 判断蛇头的坐标是否和墙的坐标冲突

需要判断蛇头的坐标是否和墙的坐标冲突,如果冲突,那游戏就结束,不冲突,就继续前进。
就只需要判断蛇头节点的x是不是0或者56,或者是蛇头节点的y是不是0或26,就行。
相关代码实现:

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;
}
4.3.1.3 判断蛇头的坐标是否和蛇身体的坐标冲突

需要判断蛇头节点的坐标是不是与身体的坐标重合,重合则返回的状态为KILL_BY_SEL。

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;
		}
		cur = cur->next;
	}
}

4.4 游戏结束

游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,是主动退出,还是撞到墙了,还是撞到蛇自己了,最后要释放蛇身节点。
释放节点时,采用头删的方式,不要忘记把头节点置为NULL。

void GameEnd(pSnake ps)
{
	SetPos(20, 12);
	switch (ps->_Status)
	{
	case END_NORMAL:
		printf("您主动退出游戏\n");
		break;
	case KILL_BY_SELF:
		printf("自杀了,游戏结束\n");
		break;
	case KILL_BY_WALL:
		printf("撞墙了,游戏结束\n");
		break;
	}
	//释放蛇身的结点
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	ps->_pSnake = NULL;

}

5. 附代码

5.1 snake.h

#include 
#include 
#include 
#include 
#include 
#include 


#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 )

enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

enum GAME_STATUS
{
	OK,//正常运行
	END_NORMAL,//按ESC退出
	KILL_BY_WALL,
	KILL_BY_SELF
};


//贪吃蛇结点的描述
typedef struct SnakeNode
{
	//坐标
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;


//
//贪吃蛇的结构
//
typedef struct Snake
{
	pSnakeNode _pSnake;//指向贪吃蛇头结点的指针
	pSnakeNode _pFood;//指向食物结点的指针
	int _Score;//贪吃蛇累计的总分
	int _FoodWeight;//一个食物的分数
	int _SleepTime;//每走一步休息的时间,时间越短,速度越快,时间越长,速度越慢
	enum DIRECTION _Dir;//描述蛇的方向
	enum GAME_STATUS _Status;//游戏的状态:正常、退出、撞墙、吃到自己
}Snake, * pSnake;


//游戏开始 - 完成游戏的初始化动作
void GameStart(pSnake ps);

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

//游戏开始的欢迎界面
void WelComeToGame();

//打印地图
void CreateMap();

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

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

//游戏的正常运行
void GameRun(pSnake ps);

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


//游戏暂定和恢复
void Pause();

//蛇的移动
void SnakeMove(pSnake ps);

//判断蛇头到达的坐标处是否是食物
int NextIsFood(pSnake ps, pSnakeNode pnext);


//吃掉食物
void EatFood(pSnake ps, pSnakeNode pnext);

//不吃食物
void NoFood(pSnake ps, pSnakeNode pnext);

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

//蛇是否自杀
void KillBySelf(pSnake ps);

//游戏结束后的善后处理
void GameEnd(pSnake ps);

5.2 snake.c

#include "snake.h"

//设置光标的坐标
void SetPos(short x, short y)
{
	COORD pos = { x, y };
	HANDLE hOutput = NULL;
	//获取标准输出的句柄(用来标识不同设备的数值)
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置标准输出上光标的位置为pos
	SetConsoleCursorPosition(hOutput, pos);
}

void WelComeToGame()
{
	//定位光标
	SetPos(40, 14);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(40, 25);
	system("pause");//pause是暂停
	system("cls");
	SetPos(20, 14);
	printf("使用 ↑ . ↓ . ← . → . 分别控制蛇的移动, F3是加速,F4是减速");
	SetPos(40, 25);
	system("pause");
	system("cls");
}

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

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

void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;
		cur->next = NULL;

		//头插法
		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->_Status = OK;
	ps->_Score = 0;
	ps->_pFood = NULL;
	ps->_SleepTime = 200;
	ps->_FoodWeight = 10;
	ps->_Dir = RIGHT;
}

void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;
again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);//x坐标必须是2的倍数

	//坐标不能和蛇的身体冲突
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		//比较坐标
		if (cur->x == x && cur->y == 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;
	ps->_pFood = pFood;

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

void GameStart(pSnake ps)
{
	//控制台窗口的设置
	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; //隐藏控制台光标
	SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

	//打印欢迎界面
	WelComeToGame();
	//创建地图
	CreateMap();
	//初始化贪食蛇
	InitSnake(ps);
	//创建食物
	CreateFood(ps);
}

void PrintHelpInfo()
{
	SetPos(64, 15);
	printf("1.不能撞墙,不能咬到自己");
	SetPos(64, 16);
	printf("2.使用 ↑.↓.←.→ 分别控制蛇的移动");
	SetPos(64, 17);
	printf("3.F3加速,F4减速");
	SetPos(64, 18);
	printf("4.ESC-退出, 空格-暂停游戏");

	SetPos(64, 20);
	printf("zxctsclrjjjcph@版权");
}

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

int NextIsFood(pSnake ps, pSnakeNode pnext)
{
	if (ps->_pFood->x == pnext->x && ps->_pFood->y == pnext->y)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

//吃掉食物
void EatFood(pSnake ps, pSnakeNode pnext)
{
	//头插
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;

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

	free(ps->_pFood);
	ps->_Score += ps->_FoodWeight;

	CreateFood(ps);//新创建食物
}

//不吃食物
void NoFood(pSnake ps, pSnakeNode pnext)
{
	//头插
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;

	//打印蛇身
	pSnakeNode cur = ps->_pSnake;
	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(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 (ps->_pSnake->x == cur->x && ps->_pSnake->y == cur->y)
		{
			ps->_Status = KILL_BY_SELF;
		}
		cur = cur->next;
	}
}


void SnakeMove(pSnake ps)
{
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNext == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	pNext->next = NULL;

	switch (ps->_Dir)
	{
	case UP:
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y + 1;
		break;
	case LEFT:
		pNext->x = ps->_pSnake->x - 2;
		pNext->y = ps->_pSnake->y;
		break;
	case RIGHT:
		pNext->x = ps->_pSnake->x + 2;
		pNext->y = ps->_pSnake->y;
		break;
	}

	//判断蛇头到达的坐标处是否是食物
	if (NextIsFood(ps, pNext))
	{
		//吃掉食物
		EatFood(ps, pNext);
	}
	else
	{
		//不吃食物
		NoFood(ps, pNext);
	}

	//蛇是否撞墙
	KillByWall(ps);

	//蛇是否自杀
	KillBySelf(ps);
}


void GameRun(pSnake ps)
{
	PrintHelpInfo();
	do
	{
		SetPos(64, 10);
		printf("得分:%05d", 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_LEFT) && ps->_Dir != RIGHT)
		{
			ps->_Dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT)
		{
			ps->_Dir = RIGHT;
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = END_NORMAL;
			break;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			Pause();
		}
		else if (KEY_PRESS(VK_F3))//加速
		{
			if (ps->_SleepTime >= 80)
			{
				ps->_SleepTime -= 30;
				ps->_FoodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))//减速
		{
			if (ps->_SleepTime < 320)
			{
				ps->_SleepTime += 30;
				ps->_FoodWeight -= 2;
			}
		}

		Sleep(ps->_SleepTime);
		SnakeMove(ps);
	} while (ps->_Status == OK);
}

void GameEnd(pSnake ps)
{
	SetPos(20, 12);
	switch (ps->_Status)
	{
	case END_NORMAL:
		printf("您主动退出游戏\n");
		break;
	case KILL_BY_SELF:
		printf("自杀了,游戏结束\n");
		break;
	case KILL_BY_WALL:
		printf("撞墙了,游戏结束\n");
		break;
	}
	//释放蛇身的结点
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	ps->_pSnake = NULL;

}

5.3 test.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "snake.h"


void test()
{
	Snake snake = { 0 };//创建了贪吃蛇
	//1. 游戏开始 - 初始化游戏
	GameStart(&snake);
	//2. 游戏运行 - 游戏的正常运行过程
	//GameRun(&snake);
	//3. 游戏结束 - 游戏善后(释放资源)
	//GameEnd(&snake);
	int ch = 0;
	do
	{
		Snake snake = { 0 };//创建了贪吃蛇
		//1. 游戏开始 - 初始化游戏
		GameStart(&snake);
		//2. 游戏运行 - 游戏的正常运行过程
		GameRun(&snake);
		//3. 游戏结束 - 游戏善后(释放资源)
		GameEnd(&snake);
		SetPos(20, 18);
		printf("再来一局吗?(Y/N):");
		ch = getchar();
		getchar();// 清理掉\n
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 27);
}


 int main()
{
	//设置程序适应本地环境
	setlocale(LC_ALL, "");
	srand((unsigned int)time(NULL));

	test();
	return 0;
}

有错误欢迎指出,大家一起进步。
如有转载请标注。

你可能感兴趣的:(C语言,数据结构,c语言,游戏,开发语言,数据结构,链表)