c语言实现贪吃蛇小游戏

一、Win32 API

这是windows系统中自带的函数,这些函数可以帮应⽤程序达到开启 视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application), 所以便称之为:Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows 32位平台的应⽤程序编程接⼝。

我们实现贪吃蛇小游戏就要利用到这里面的几个函数。

二、控制台程序

如上图所示,我们平时运行程序的黑框就是控制台程序。

2.1 cmd命令

在使用该命令时得包含头文件<stdlib.h>

我们可以使用如下代码来控制控制台程序的行数和列数:

system("mode con cols=100 lines=30")

上述代码便是将控制台窗口的行数设置为30行,列数设置为10列。

为了方便观察,将窗口的列数调为50列,调整完后可以明显看出控制台窗口的大小变小了。

2.2 title命令

通过title命令我们能够修改控制台窗口的名称

system("title 贪吃蛇")

通过上述代码,便能将控制台窗口的名字修改为贪吃蛇。

可能很多人运行完程序后会发现,窗口的名字并没有被修改为贪吃蛇,这是为什么呢?

原因在于程序运行结束了,名字不再是程序运行中的贪吃蛇了,在程序结束之前,名字一直都会是贪吃蛇,我们可以通过添加一个getchar()来验证,因为getchar()要输入一个字符,如果没有接收到字符,那么程序就不会结束。

我们添加了getchar()后,在没有输入字符之前(也就是程序还未结束)显示的窗口名字是贪吃蛇。

2.3控制台屏幕上的坐标COORD

COORD 是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕幕缓冲区上的坐标,坐标系 (0,0) 的原点位于缓冲区的顶部左侧单元格。

COORD结构体的声明:

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

通过这个类型,我们可以给坐标赋值

 COORD pos = { 10, 15 };

上述代码便是将x和y的值赋为了10和15。

2.4GetStdHandle 

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

HANDLE GetStdHandle(DWORD nStdHandle);

上面代码是GetStdHandle函数的声明,可以发现该函数的返回类型是HANDLE类型,接收返回值时需注意要定义一个HANDLE类型的变量。

实例:

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

GetStdHandle函数参数:

2.5 GetConsoleCursorInfo

检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息

GetConsoleCursorInfo函数声明:

BOOL WINAPI GetConsoleCursorInfo(
 HANDLE hConsoleOutput,
 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

实例:

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

通过上述代码便能够获取到控制台的光标信息。

2.5.1 CONSOLE_CURSOR_INFO

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

CONSOLE_CURSOR_INFO结构体声明:

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

dwSize:由光标填充的字符单元格的百分⽐。 此值介于1到100之间。 光标外观会变化,范围从完 全填充单元格到单元底部的⽔平线条。

bVisible:游标的可⻅性。 如果光标可⻅,则此成员为 TRUE。 

我们主要是要修改bVisible为false,这样便能隐藏控制台光标,因为我们在进行游戏时如果控制台光标一直在闪烁的话不利于游戏体验。

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

2.6 SetConsoleCursorInfo 

通过SetConsoleCursorInfo 可以设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性。

SetConsoleCursorInfo 函数声明:

BOOL WINAPI SetConsoleCursorInfo(
 HANDLE hConsoleOutput,
 const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);

实例:

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

通过上述代码,我们便能将控制台的光标隐藏。使用前得包含头文件<windows.h>和<stdbool.h>。

结果:

2.7 SetConsoleCursorPosition

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

SetConsoleCursorPosition函数声明:

BOOL WINAPI SetConsoleCursorPosition(
 HANDLE hConsoleOutput,
 COORD pos
);

前面我们所讲到的COORD 可以给坐标赋值,且GetStdHandle可以获得句柄来操作设备,结合这两个我们便可以给光标定位。

实例:

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

结果:

设置光标位置前:

设置光标位置后:

2.7.1 封装⼀个设置光标位置的函数

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

根据上面所讲,我们可以封装出这么一个设置光标位置的函数,因为我们设置了棋盘后还得打印帮助信息,为了使帮助信息不覆盖我们的棋盘,我们必须要控制帮助信息打印的位置,而设置光标位置就能解决这个问题。 

2.8 GetAsyncKeyState

通过GetAsyncKeyState可以获取按键情况 。

GetAsyncKeyState函数声明:

SHORT GetAsyncKeyState(
 int vKey
);

GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果 返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬 起;如果最低位被置为1则说明,该按键被按过,否则为0。

如果最低位为1则说明案件被按过,我们可以通过这个来检测按键是否被按过。

我们可以定义一个宏来判断按键是否被按过:

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

参数VK是按键的虚拟键值,大家可以进入该网址进行查询:虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn 

最低位与1相与,与操作两个1才为真,真时返回1,假时返回0。

实例:

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

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

int main()
{
	while (1)
	{
		if (KEY_PRESS(0x30))
		{
			printf("0\n");
		}
		else if (KEY_PRESS(0x31))
		{
			printf("1\n");
		}
		else if (KEY_PRESS(0x32))
		{
			printf("2\n");
		}
		else if (KEY_PRESS(0x33))
		{
			printf("3\n");
		}
		else if (KEY_PRESS(0x34))
		{
			printf("4\n");
		}
		else if (KEY_PRESS(0x35))
		{
			printf("5\n");
		}
		else if (KEY_PRESS(0x36))
		{
			printf("6\n");
		}
		else if (KEY_PRESS(0x37))
		{
			printf("7\n");
		}
		else if (KEY_PRESS(0x38))
		{
			printf("8\n");
		}
		else if (KEY_PRESS(0x39))
		{
			printf("9\n");
		}
	}
	return 0;
}

结果:

每按下去一个1-9的按键,便会打印出来对应的数字,判断该按键被按过。

2.9 <locale.h>本地化

提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。

因为我们在贪吃蛇游戏的设计中所使用的都是宽字符,而c语言的标准中并没有宽字符,对于中国地区而言才有宽字符。

2.9.1 类项

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

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

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

• LC_MONETARY:影响货币格式。

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

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

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

2.9.2 setlocale函数

setlocale函数声明:

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

setlocale 函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。 setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参 数是LC_ALL,就会影响所有的类项。

C标准给第⼆个参数仅定义了2种可能取值:"C"(正常模式)和" "(本地模式)。 

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

当我们执行了上述代码后,便切换到了本地模式,这个时候便能进行宽字符的打印。

2.9.3 宽字符的打印

想要打印宽字符,就要用到函数wprintf,而且宽字符的字⾯量必须加上前缀“L”,否则 C 语⾔会把字⾯量当作窄字符类型处理。前缀“L”在单引号前⾯,表⽰宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前⾯,表⽰宽字符串,对应 wprintf() 的占位符为 %ls 。

实例:

#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<locale.h>


int main()
{
	setlocale(LC_ALL, "");
	wprintf(L"%lc\n", L'▲');
	wprintf(L"%lc\n", L'□');
	return 0;
}

结果:

需要注意的是,宽字符所占的字节数为2字节,而且想要打印宽字符要先将环境切换为本地模式。 

三、实现贪吃蛇

我们将贪吃蛇的实现分为三个部分:

游戏开始:(GameStart)完成游戏的初始化

游戏运⾏:(GameRun)完成游戏运⾏逻辑的实现

游戏结束:(GameEnd)完成游戏结束的说明,实现资源释放

3.1 游戏开始

该部分完成游戏的初始化任务

3.1.1 控制台窗口大小的设置

只需使用上面讲到的cmd命令便能控制控制台窗口的大小。

 具体如何设置请看前文2.1的cmd命令。

3.1.2 控制台窗口名字的设置

只需使用上面讲到的title命令就可以修改控制台窗口的名字。

 具体如何设置请看前文2.2的title命令。

3.1.3 鼠标光标的隐藏

只需使用上面讲到的GetConsoleCursorInfo函数和SetConsoleCursorInfo函数就可以隐藏鼠标光标。

 具体如何隐藏鼠标光标请看前文2.5的GetConsoleCursorInfo函数和2.6的SetConsoleCursorInfo函数。

3.1.4 打印欢迎界面

我们可以封装一个函数welcome_to_the_game来实现。

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

打印欢迎界面十分容易,只需要设置好光标位置就好了。

 

3.1.5 打印地图

打印地图就是将墙体打印出来,需要注意的便是坐标,我们容易将坐标计算错误。

我们一样封装一个函数create_map来实现地图打印。

为了方便编写代码,我们可以事先将墙体进行定义。

#define WALL L'□'

 

//打印地图
void create_map()
{
	int i = 0;
	//上(0,0)-(56, 0)
	set_pos(0, 0);
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	//下(0,26)-(56, 26)
	set_pos(0, 26);
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	//左
	//x是0,y从1开始增⻓
	for (i = 1; i < 26; i++)
	{
		set_pos(0, i);
		wprintf(L"%c", WALL);
	}
	//x是56,y从1开始增⻓
	for (i = 1; i < 26; i++)
	{
		set_pos(56, i);
		wprintf(L"%c", WALL);
	}
}

看完这个代码可能有的人会有疑惑,为什么打印上下两行的墙体时i+=2呢?这是因为宽字符占两个字节,所以打印完一个墙体后i要加2。

最后打印的地图如下:

3.1.6 初始化蛇

因为蛇的身体是一个一个的连在一起的,那么我们便可以用链表去维护它。

我们首先创建一个蛇身结构体来维护蛇身:

//蛇身的节点类型
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;

蛇身结构体内的成员x和y用来表示该蛇身的坐标,next指针用于指向下一蛇身节点。

但此时我们仅仅只维护了蛇身,还有许多其他的信息像食物,蛇的方向等等信息我们还没有进行维护,这时我们再 创建一个贪吃蛇结构体来维护其它信息。

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



enum game_status
{
	OK=1,
	KILL_BY_WALL,
	KILL_BY_SELF,
	END_NORMAL
};



//维护贪吃蛇
typedef struct Snake
{
	pSnakeNode _psnake;
	pSnakeNode _pfood;
	enum game_status _status;
	enum direction _dir;
	int _food_weight;
	int _score;
	int _sleeptime;
}Snake,*pSnake;

蛇的方向以及状态我们用枚举类型来定义 ,便于我们使用,可以枚举出来蛇的每一种状态和方向。里面还有蛇头节点和食物节点,每个食物的分数,总分数以及蛇的速度。

这里要注意的是,其实食物也是蛇的节点,食物被蛇吃掉后就成为蛇的节点,而蛇的速度我们是用Sleep函数来控制的,睡眠时间越小,蛇的速度就越快。

3.1.7 打印蛇身并初始化蛇的信息

我们初始化蛇的长度为5,也就是创建5个节点,将它们头插到一起,最后打印出来。

与之前一样,为了方便编写程序,我们可以提前定义好蛇身的宽字符以及第一个蛇身节点的坐标。

#define BODY L'●'
#define POS_X 24
#define POS_Y 5

我们一样封装一个函数snake_init来实现这一功能

//蛇的初始化
void snake_init(pSnake ps)
{
	pSnakeNode cur = NULL;
	int i = 0;
	for (i = 0; i < 5; i++)//创建五个节点
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("snake_init malloc");
			return;
		}
		cur->next = NULL;
		cur->x = POS_X + 2 * i;//i*2因为宽字符占两个字节
		cur->y = POS_Y;
        //头插法
		if (ps->_psnake == NULL)
		{
			ps->_psnake = cur;
		}
		else
		{
			cur->next = ps->_psnake;
			ps->_psnake = cur;
		}
	}
	cur = ps->_psnake;
    //打印蛇身
	while (cur)
	{
		set_pos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->_dir = RIGHT;//初始化蛇为向右移动
	ps->_food_weight = 10;//初始食物分数为10分
	ps->_score = 0;
	ps->_sleeptime = 200;//睡眠时间为200ms,来控制蛇的移速
	ps->_status = OK;//初始状态为OK
}

  这便是创建好的蛇 ,初始化蛇的方向为向右,所以最右边的节点为蛇头。

3.1.8 创建食物

因为食物是随机生成的,所以我们要用rand函数来控制食物的坐标,但食物的坐标也有要求,因为食物也是宽字符,所以食物坐标的x必须为2的倍数,而且食物的坐标也不能和蛇身的坐标重复。

为了方便编写代码,我们可以提前定义好食物的宽字符。

#define FOOD L'★'

我们一样封装一个函数create_food来实现这一功能。 

//创建食物
void create_food(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("create_food malloc");
		return;
	}

	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;

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

	ps->_pfood = pFood;
}

 

这便是随机创建的第一个食物。

3.2 游戏运行

 根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。 如果游戏继续,就是检测按键情况,确定蛇下⼀步的⽅向,或者是否加速减速,是否暂停或者退出游戏。

3.2.1 打印帮助信息

我们一样封装一个函数print_help_info来实现这一功能:

//打印帮助信息
void print_help_info()
{
	set_pos(64, 14);
	wprintf(L"%ls", L"不能穿墙,不能撞到自己");
	set_pos(64, 15);
	wprintf(L"%ls", L"用↑.↓.←.→键来控制蛇的移动");
	set_pos(64, 16);
	wprintf(L"%ls", L"按F3加速,F4减速");
	set_pos(64, 17);
	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
}

 

这就是我们所打印的帮助信息,只需要注意打印时光标的坐标就行了。

 3.2.1 检测按键是否被按下来改变移动方向或游戏状态

前面讲到了可以定义一个宏KEY_PRESS来检测按键是否被按下。

do
{
	//打印总分数
	set_pos(64, 10);
	printf("总分数为:%d\n", ps->_score);
	//打印当前食物的分数
	set_pos(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_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 = END_NORMAL;
	}
	else if (KEY_PRESS(VK_F3))//加速
	{
		if (ps->_sleeptime > 80)
		{
			ps->_sleeptime -= 30;
			ps->_food_weight += 2;
		}
	}
	else if (KEY_PRESS(VK_F4))//减速
	{
		if (ps->_food_weight > 2)
		{
			ps->_sleeptime += 30;
			ps->_food_weight -= 2;
		}
	}
	snake_move(ps);
	Sleep(ps->_sleeptime);
} while (ps->_status == OK);

因为当游戏还在进行时,方向和游戏状态几乎是随时都会改变的,所以当游戏状态还为OK时,就一直循环判断按键是否被按下,要注意的是,当你向上移动时,是不能改变方向为向下移动的。

3.2.3 蛇的移动

判断蛇移动的下一个坐标是否是食物:

//下一个坐标是食物
int next_is_food(pSnakeNode pn, pSnake ps)
 {
	return (ps->_pfood->x == pn->x && ps->_pfood->y == pn->y);
 }

如果下一个是食物,就吃掉食物:

//吃掉食物
void eat_food(pSnakeNode pn, pSnake ps)
{
	ps->_pfood->next = ps->_psnake;//头插
	ps->_psnake = ps->_pfood;
	free(pn);//释放下一节点
	pn = NULL;
	pSnakeNode cur = ps->_psnake;
	while (cur)
	{
		set_pos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->_score += ps->_food_weight;
	create_food(ps);//食物被吃掉后再创建一个食物
}

不是食物就要将最后一个节点打印为空格并释放掉最后一个节点:

//下一坐标不是食物
void no_food(pSnakeNode pn, pSnake ps)
{
	pn->next = ps->_psnake;
	ps->_psnake = pn;
	pSnakeNode cur = ps->_psnake;
	while (cur->next->next !=NULL)
	{
		set_pos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	set_pos(cur->next->x, cur->next->y);
	printf("  ");//将最后一个节点打印为空格
	//注意是两个空格,因为宽字符占两个字节
	free(cur->next);//释放最后一个节点
	cur->next = NULL;
}

检测蛇是否撞墙而死:

//撞到墙
void kill_by_wall(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 kill_by_self(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 snake_move(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;//向上移动时y坐标减1
		break;
	case DOWN:
		pNextNode->x = ps->_psnake->x;
		pNextNode->y = ps->_psnake->y + 1;//向下移动时y坐标加1
		break;
	case LEFT:
		pNextNode->x = ps->_psnake->x - 2;//向左移动时x坐标减2
		pNextNode->y = ps->_psnake->y;
		break;
	case RIGHT:
		pNextNode->x = ps->_psnake->x + 2;//向右移动时x坐标加2
		pNextNode->y = ps->_psnake->y;
		break;
	}

	//检测下一个坐标处是否是食物
	if (next_is_food(pNextNode, ps))
	{
		eat_food(pNextNode, ps);
	}
	else
	{
		no_food(pNextNode, ps);
	}

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

蛇每次移动时都要创建一个节点,这也是为什么没有吃到食物时要将蛇身的最后一个节点打印为空格并且释放掉。

3.3 游戏结束

游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇⾝节点。

判断游戏状态,是以什么方式结束的游戏,并打印游戏的成绩,最后释放掉蛇身节点:

//游戏结束
void game_end(pSnake ps)
{
	set_pos(24, 12);
	switch (ps->_status)//判断游戏状态
	{
	case END_NORMAL:
		printf("您主动结束游戏!\n");
		break;
	case KILL_BY_SELF:
		printf("您撞到了自己,游戏结束!\n");
		break;
	case KILL_BY_WALL:
		printf("您撞到了墙,游戏结束!\n");
		break;
	}
	print_score(ps);//打印当次游戏的成绩
	pSnakeNode cur = ps->_psnake;
	while (cur)//释放蛇身节点
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
}

四、所有代码

snake.h文件:

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<time.h>
#include<stdbool.h>

#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define POS_X 24
#define POS_Y 5

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

enum game_status
{
	OK=1,
	KILL_BY_WALL,
	KILL_BY_SELF,
	END_NORMAL
};

//蛇身的节点类型
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;

//维护贪吃蛇
typedef struct Snake
{
	pSnakeNode _psnake;
	pSnakeNode _pfood;
	enum game_status _status;
	enum direction _dir;
	int _food_weight;
	int _score;
	int _sleeptime;
}Snake,*pSnake;

//定位光标位置
void set_pos(int x, int y);

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

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

//打印地图
void create_map();

//蛇的初始化
void snake_init(pSnake ps);

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

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

//暂停游戏
void pause();

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

//游戏运行
void game_run(pSnake ps);

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

//吃掉食物
void eat_food(pSnakeNode pn, pSnake ps);

//下一坐标不是食物
void no_food(pSnakeNode pn, pSnake ps);

//撞到墙
void kill_by_wall(pSnake ps);

//撞到自己
void kill_by_self(pSnake ps);

//游戏结束
void game_end(pSnake ps);

//打印分数信息
void print_score(pSnake ps);

snake.c文件:

#include"snake.h"



//定位光标位置
void set_pos(int x, int y)
{
	COORD pos = { x,y };
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(houtput, pos);
}

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

//打印地图
void create_map()
{
	int i = 0;
	//上(0,0)-(56, 0)
	set_pos(0, 0);
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	//下(0,26)-(56, 26)
	set_pos(0, 26);
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	//左
	//x是0,y从1开始增⻓
	for (i = 1; i < 26; i++)
	{
		set_pos(0, i);
		wprintf(L"%c", WALL);
	}
	//x是56,y从1开始增⻓
	for (i = 1; i < 26; i++)
	{
		set_pos(56, i);
		wprintf(L"%c", WALL);
	}
}

//蛇的初始化
void snake_init(pSnake ps)
{
	pSnakeNode cur = NULL;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("snake_init 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)
	{
		set_pos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->_dir = RIGHT;//初始化蛇为向右移动
	ps->_food_weight = 10;//初始食物分数为10分
	ps->_score = 0;
	ps->_sleeptime = 200;//睡眠时间为200ms,来控制蛇的移速
	ps->_status = OK;//初始状态为OK
}


//创建食物
void create_food(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("create_food malloc");
		return;
	}

	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;

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

	ps->_pfood = pFood;
}

//游戏的初始化
void game_start(pSnake ps)
{
	system("mode con cols=100 lines=32");
	system("title 贪吃蛇");
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(houtput, &CursorInfo);
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(houtput, &CursorInfo);
	welcome_to_the_game();
	create_map();
	snake_init(ps);
	create_food(ps);
}

//打印帮助信息
void print_help_info()
{
	set_pos(64, 14);
	wprintf(L"%ls", L"不能穿墙,不能撞到自己");
	set_pos(64, 15);
	wprintf(L"%ls", L"用↑.↓.←.→键来控制蛇的移动");
	set_pos(64, 16);
	wprintf(L"%ls", L"按F3加速,F4减速");
	set_pos(64, 17);
	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
}

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

//暂停游戏
void pause()
{
	while (1)
	{
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

//下一个坐标是食物
int next_is_food(pSnakeNode pn, pSnake ps)
 {
	return (ps->_pfood->x == pn->x && ps->_pfood->y == pn->y);
 }

//吃掉食物
void eat_food(pSnakeNode pn, pSnake ps)
{
	ps->_pfood->next = ps->_psnake;//头插
	ps->_psnake = ps->_pfood;
	free(pn);//释放下一节点
	pn = NULL;
	pSnakeNode cur = ps->_psnake;
	while (cur)
	{
		set_pos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->_score += ps->_food_weight;
	create_food(ps);//食物被吃掉后再创建一个食物
}

//下一坐标不是食物
void no_food(pSnakeNode pn, pSnake ps)
{
	pn->next = ps->_psnake;
	ps->_psnake = pn;
	pSnakeNode cur = ps->_psnake;
	while (cur->next->next !=NULL)
	{
		set_pos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	set_pos(cur->next->x, cur->next->y);
	printf("  ");//将最后一个节点打印为空格
	//注意是两个空格,因为宽字符占两个字节
	free(cur->next);//释放最后一个节点
	cur->next = NULL;
}

//撞到墙
void kill_by_wall(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 kill_by_self(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 snake_move(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;//向上移动时y坐标减1
		break;
	case DOWN:
		pNextNode->x = ps->_psnake->x;
		pNextNode->y = ps->_psnake->y + 1;//向下移动时y坐标加1
		break;
	case LEFT:
		pNextNode->x = ps->_psnake->x - 2;//向左移动时x坐标减2
		pNextNode->y = ps->_psnake->y;
		break;
	case RIGHT:
		pNextNode->x = ps->_psnake->x + 2;//向右移动时x坐标加2
		pNextNode->y = ps->_psnake->y;
		break;
	}

	//检测下一个坐标处是否是食物
	if (next_is_food(pNextNode, ps))
	{
		eat_food(pNextNode, ps);
	}
	else
	{
		no_food(pNextNode, ps);
	}

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

void game_run(pSnake ps)
{
	print_help_info();
	do
	{
		//打印总分数
		set_pos(64, 10);
		printf("总分数为:%d\n", ps->_score);
		//打印当前食物的分数
		set_pos(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_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 = END_NORMAL;
		}
		else if (KEY_PRESS(VK_F3))//加速
		{
			if (ps->_sleeptime > 80)
			{
				ps->_sleeptime -= 30;
				ps->_food_weight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))//减速
		{
			if (ps->_food_weight > 2)
			{
				ps->_sleeptime += 30;
				ps->_food_weight -= 2;
			}
		}
		snake_move(ps);
		Sleep(ps->_sleeptime);
	} while (ps->_status == OK);
}

//打印分数信息
void print_score(pSnake ps)
{
	set_pos(0, 27);
	if (ps->_score < 300)
	{
		printf("您的分数为:%d,您的分数一般,再接再厉\n", ps->_score);
	}
	else if (ps->_score >= 300 && ps->_score < 600)
	{
		printf("您的分数为:%d,您的分数不错,再接再厉\n", ps->_score);
	}
	else if (ps->_score >= 600 && ps->_score < 900)
	{
		printf("您的分数为:%d,您的实力已经超越大部分人了,再接再厉\n", ps->_score);
	}
	else if (ps->_score >= 900)
	{
		printf("您的分数为:%d,您的实力已经登峰造极了!\n", ps->_score);
	}
}

//游戏结束
void game_end(pSnake ps)
{
	set_pos(24, 12);
	switch (ps->_status)//判断游戏状态
	{
	case END_NORMAL:
		printf("您主动结束游戏!\n");
		break;
	case KILL_BY_SELF:
		printf("您撞到了自己,游戏结束!\n");
		break;
	case KILL_BY_WALL:
		printf("您撞到了墙,游戏结束!\n");
		break;
	}
	print_score(ps);//打印当次游戏的成绩
	pSnakeNode cur = ps->_psnake;
	while (cur)//释放蛇身节点
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
}

test.c文件:

#include"snake.h"
#include<locale.h>

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

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

	} while (ch == 'Y' || ch == 'y');
	set_pos(0, 28);
}

int main()
{
	setlocale(LC_ALL, "");
	srand((unsigned int)time(NULL));
	test();
	return 0;
}

五、总结

讲到这基本上就能够基本上实现出贪吃蛇小游戏了,对于API等Windows自带的函数,我们进行了解会使用就可,希望这些能对你有所帮助,记得三连哦!

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 12
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值