C语言贪吃蛇实现

When the night gets dark,remember that the Sun is also a star.

当夜幕降临时,请记住太阳也是一颗星星。

------------《去月球海滩篇》

目录

文章目录

一、《贪吃蛇》游戏介绍

二、WIN32部分接口简单介绍

[2.1 控制台窗口大小设置](#2.1 控制台窗口大小设置)

[2.2 命令行窗口的名称的变更](#2.2 命令行窗口的名称的变更)

[2.3 vs平台下的调试窗口的设置](#2.3 vs平台下的调试窗口的设置)

[2.4 隐藏光标](#2.4 隐藏光标)

[2.5 控制光标的位置](#2.5 控制光标的位置)

[2.6 获取键盘的值的情况](#2.6 获取键盘的值的情况)

[2.7 字符问题](#2.7 字符问题)

三、贪吃蛇的实现

[3.1 游戏开始前的准备 GameStart函数](#3.1 游戏开始前的准备 GameStart函数)

[3.1.1 设置好控制台界面的行和列,隐藏控制台光标](#3.1.1 设置好控制台界面的行和列,隐藏控制台光标)

[3.1.2 打印欢迎界面](#3.1.2 打印欢迎界面)

[3.1.3 绘制地图](#3.1.3 绘制地图)

[3.1.4 初始化蛇身](#3.1.4 初始化蛇身)

[3.1.5 创建食物](#3.1.5 创建食物)

[3.2 游戏运行 GameRun函数](#3.2 游戏运行 GameRun函数)

[3.2.1 打印帮助信息](#3.2.1 打印帮助信息)

[3.2.2 判断玩家按下的按键](#3.2.2 判断玩家按下的按键)

[3.2.3 蛇的移动](#3.2.3 蛇的移动)

[3.2.3.1 NextFood](#3.2.3.1 NextFood)

[3.2.3.2 EatFood](#3.2.3.2 EatFood)

[3.2.3.3 Kill_Wall](#3.2.3.3 Kill_Wall)

[3.2.3.4 Kill_Self](#3.2.3.4 Kill_Self)

[3.2.4 .Sleep函数、循环移动](#3.2.4 .Sleep函数、循环移动)

[3.3 贪吃蛇游戏结束的善后](#3.3 贪吃蛇游戏结束的善后)

[3.4 再来一局,不言弃!!](#3.4 再来一局,不言弃!!)


一、《贪吃蛇》游戏介绍

也许正在看的你玩过一款名为《贪吃蛇》的小游戏,《贪食蛇》中玩家控制一条不断移动的蛇,在屏幕上吃掉出现的食物。每吃掉一个食物,蛇的身体就会变长。游戏的目标是尽可能长时间地生存下去,同时避免蛇头撞到自己的身体或屏幕边缘。玩家需要灵活操作,利用策略在有限的空间内避免碰撞,挑战高分。

通过对经典的《贪吃蛇》的简单介绍,可以知道这篇文章中实现的贪吃蛇选哟实现以下的几点:

  1. 通过按方向键上下左右,来改变蛇的移动方向
  2. 通过按F3键实现蛇的加速行进,按F4键可以降低蛇的移速。
  3. 按空格键可实现暂停,暂停后按任意键继续游戏。
  4. 按Esc键可直接退出游戏。

除此之外,本游戏还拥有计分系统,可保存玩家的历史最高记录。和食物分值显示,蛇的移速的不同可以改变食物的分值。

二、WIN32部分接口简单介绍

在贪吃蛇的实现过程当中,会使用部分WIN32的部分接口,此处简单介绍贪吃蛇的实现过程当中会使用到的API的功能以及简单说明:

2.1 控制台窗口大小设置

复制代码
mode con cols=100 lines=30 #控制窗口,cols为行长度,lines为列行数

按住Win键+R键,打开Windows命令运行框,输入:cmd---------打开Windows控制台命令窗口,输入该指令,就可以调整窗口的大小了,效果如下:

回车运行后,控制台窗口的大小被修改了,通过观察窗口的属性值与我们设定一致

2.2 命令行窗口的名称的变更

命令行窗口的名称的变更,可以如下通过命令的方式来更改:

复制代码
title 贪吃蛇#更改命令行窗口的名称

效果如下:

2.3 vs平台下的调试窗口的设置

在C语言中,需要使用system接口来改变终端 窗口的大小 以及 窗口名称,使用system 接口需要包含 stdlib.h 头文件,例如下面代码:

复制代码
#include<stdio.h>
#include<stdlib.h>//使用system接口的头文件

int main()
{
	system("title 贪吃蛇");//将命令行窗口的名字更改为需要的名字
	system("mode con cols=100 lines=30");//设置命令行窗口的大小
	return 0;
}

2.4 隐藏光标

终端可以看作尾一个坐标系,左上角为坐标原点,向右为x轴,向下位y轴,如下图所示:

在windows窗口上描述一个坐标需要使用一个windows API中定义的一个结构体 COORD,表示一个字符在控制台屏幕缓冲区上的坐标,在C语言中,我们需要包含 windows.h 头文件才能使用,使用实例如下:

复制代码
#include<stdio.h>
#include<windows.h>//调用该api需要的头文件
#include<stdlib.h>

int main()
{
	COORD pos = { 10, 10 };//使用第一个参数为行,第二参数为列
	return 0;
}

为了实现光标隐藏,我们需要先调用 GetStdHandle 函数来获取标准输出句柄,使用这个句柄可以操作设备。

复制代码
HANDLE output = NULL;//HANDLE为结构体指针类型
//获取标准输出句柄来表示不同设备的数值
output = GetStdHandle(STD_OUTPUT_HANDLE);

标准输出通常是指控制台窗口中打印文本的位置。通过使用GetStdHandle函数,可以获取表示标准输出的句柄,以便可以对其进行操作。函数的参数STD_OUTPUT_HANDLE是一个宏,它表示标准输出句柄的常量值。通过将此参数传递给GetStdHandle函数,可以获得标准输出的句柄。获取标准输出句柄后,可以使用其他Windows API函数来更改控制台的输出属性、位置和文本颜色等,以实现更复杂的控制台输出。

要隐藏光标,需要先获得一个光标信息,上面我们已经获取了标准输出相关设备的句柄,接下来我们创建 CONSOLE_CORSOR_INFO 结构体对象(接收 有关主机光标信息的结构体),再调用 GetConsoleCursorInfo 函数来获得光标信息:

复制代码
#include<stdio.h>
#include<windows.h>//调用win32 api所需要的头文件

int main()
{
	HANDLE output = NULL;
	//获取标准输出句柄来表示不同设备的数值
	output = GetStdHandle(STD_OUTPUT_HANDLE);

	CONSOLE_CURSOR_INFO cursor_info;
	GetConsoleCursorInfo(output, &cursor_info);//获取光标的信息
	return 0;
}

CONSOLE_CURSOR_INFO这个结构体包含了控制台光标信息:

复制代码
typedef struct _CONSOLE_CURSOLE_INFO {
	DWORD dwSize;
	BOOL bVisible;
}CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
  • dwSize参数,由光标填充的字符单元格的百分比。值范围为1到100。光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
  • bVisible 参数,设置光标的可见性,默认尾true;如果光标不可见,设置为false。

我们调用结构体的第二个参数设置为false(注意:C语言要包含 stdbool.h 头文件才能使用布尔类型)

GetConsoleCursorInfo 函数介绍

然后再调用SetConsoleCursorInfo 函数来设置更改的光标信息。

复制代码
#include<stdio.h>
#include<stdbool.h>
#include<windows.h>

int main()
{
	HANDLE output = NULL;
	//获取标准输出句柄来表示不同设备的数值
	output = GetStdHandle(STD_OUTPUT_HANDLE);

	CONSOLE_CURSOR_INFO cursor_info;
	GetConsoleCursorInfo(output, &cursor_info);//获取光标的信息
	cursor_info.bVisible = false;

	SetConsoleCursorInfo(output, &cursor_info);//设置更改信息
	int ch = getchar();
	putchar(ch);
	
	return 0;
}

使用getchar putchar来输入输出信息,检测是否隐藏光标成功

下面是光标未被隐藏的结果,输入字符或者数字后,末尾会有一个光标在闪烁:

2.5 控制光标的位置

设置终端光标输出位置,我们首先要获取想要输出位置的坐标,上面我们介绍了COORD结构体,用来设置位置坐标。获取完坐标之后,我们可以调用 SetConsoleCorsorPosition 函数将光标位置设置到获取的坐标位置

复制代码
BOOL SetConsoleCorsorPosition{
	HANDLE output;//句柄
	COORD pos;//位置
};

通过这个接口就可以将光标输出的信息放在想要的位置上了:

复制代码
#include<stdio.h>
#include<stdbool.h>
#include<windows.h>

int main()
{
	HANDLE output = NULL;
	//获取标准输出句柄来表示不同设备的数值
	output = GetStdHandle(STD_OUTPUT_HANDLE);

	COORD pos = { 20, 20 };
	SetConsoleCursorPosition(output, pos);

	int ch = getchar();
	putchar(ch);
	
	return 0;
}

考虑到贪吃蛇中会频繁的改变贯标的位置,不妨把这个接口封装为一个函数,来完成光标定位的功能:

复制代码
//定位光标位置
void SearchLocal(int x,int y) {
	//获取控制台的句柄,控制权限,HANDLE 结构体指针
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD pos = { x,y };
	// 获取设备句柄,传入坐标COORD
	SetConsoleCursorPosition(handle, pos);
}

2.6 获取键盘的值的情况

控制贪吃蛇的移动,加速或者减速,我们都是通过键盘按下对应的按键进行控制,那系统是如何知道我们按下的按键是哪一个呢?

使用 GetAsyncKeyState 函数来获取按键情况,此函数函数原型如下:

复制代码
SHORT GetAsyncKeyState(int vKey);

将键盘上的键值传给函数,通过函数返回值来判断按键的状态。GetAsyncKeyState 返回值是short类型,在上一次调用此函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高位是0,说明按键的状态是抬起;如果最低位被置为1,则说明该按键被按过,否则位0。

如果我们要判断按键是否被按过,只需要判断返回值最低值是否为1即可,我们可以按位与上0x1来获取最低位的值,那么我们就可这样来编写函数:

复制代码
//检测按键是否被按过,间返回的虚拟值按位与上1,相同为1,相异为0,进行检测
//封装为一个宏
#define KEY_PRESS(vk) (GetAsyncKeyState(vk)&0x1?1:0)

前面提出的系统是如何判断我们按下的按键爱是哪一个的疑问,我们可以通过虚拟键码来判断是不同按键的不同状态,这样就可以实现一些按键响应的功能了。 Virtual-Key 代码 (Winuser.h) - Win32 apps | Microsoft Learnhttps://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes

本文贪吃蛇中需要用到的虚拟键值如下:

2.7 字符问题

我们在打印蛇身和墙体的时候,总不能用数字或者字符来表示吧。所以需要用到特殊字符------宽字符。宽字符的长度为2字节,因为不同地区的语言不同,计算机中描述的方式也不太一样,普通的单字节字符并不适合我们的地区,因此C语言加入了宽字符(字符类型:wchar_t 需要包含 locale.h 头文件)允许程序员针对特定地区调整程序行为函数。

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

  • LC_COLLATE:影响字符串比较函数
  • LC_CTYPE:影响字符处理函数行为
  • LC_MONETARY:影响货币格式
  • LC_NUMERIC:影响printf()函数
  • LC_TIME:影响时间格式
  • LC_ALL针对所有类项修改,将以上所有类别设定为给定的语言环境

使用 setlocale 函数修改类项:

复制代码
char* setlocale(int category, const char* locale);

函数的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有类项。C标准给第二个参数定义了2种可能取值:"C"(正常模式)和""(本地模式) 在任意程序执行开始,默认隐式调用:

复制代码
setlocale(LC_ALL, "C");


//我们需要切换到本地环境输出字符,所以:
setlocale(LC_ALL, " ");//这种模式下程序会适应本地环境,切换后程序就支持宽字符的打印了

想要打印宽字符也是与普通打印不同的,宽字符字面量前必须加上L ,否则C语言就会将其当为窄字符,且占位符应当为"%lc ",和"%ls",才可正常打印宽字符。此处打印宽字符用到了wprintf函数,这里简单介绍一下:

wprintf 是C语言中用于宽字符输出的函数,它类似于 printf ,但是用于处理宽字符(wchar_t)和宽字符串(wchar_t*)。这个函数定义在 wchar.h 头文件中,并且可以用来将格式化的数据打印到标准输出。

格式说明符:

wprintf 函数中,格式说明符的使用与 printf 函数相似。例如,%lc 用于宽字符,%ls 用于宽字符串。与 printf 不同的是,wprintf 需要使用 L 前缀来指定宽字符串字面量

复制代码
#include<stdio.h>
#include<locale.h>

int main()
{
	setlocale(LC_ALL, "");
	wchar_t ch[] = L"你好哇";
	wprintf(L"%ls\n", ch);
	return 0;
}

运行结果如下:

三、贪吃蛇的实现

贪吃蛇流程简述:贪吃蛇游戏分为三个大步骤,游戏开始,游戏运行,游戏结束,我们分别用三个函数来完成。

复制代码
//贪吃蛇游戏界面初始化
GameStart(&snake);
//贪吃蛇游戏运行
GameRun(&snake);
//贪吃蛇游戏结束的善后
GameEnd(&snake);

在设计游戏之前,我们需要先写出蛇的相关信息,例如:蛇移动的方向,蛇的状态,食物的分数,当前游戏总分等......我们将这些信息用结构体和枚举列出:

复制代码
//贪吃蛇游戏状态
enum GAMSTATUS {
	OK=1, //正常运行
	EXIT, //按ESC键退出
	KILL_BY_WALL, //撞墙了!
	KILL_BY_SELF  //咬到自己了!
};

//贪吃蛇行走方向
enum SNAKEWAY {
	UP = 1, //前进
	DOWN,   //向下
	LEFT,   //左拐
	RIGHT  //右拐
};

//贪吃蛇,蛇身节点的定义
typedef struct SnakeNode {
	//节点的位置
	int x;
	int y;
	//下一个蛇身节点
	struct SnakeNode* next;
}SnakeNode;


//整局游戏贪吃蛇的维护
typedef struct Snake {
	//贪吃蛇,蛇头指针,方便找到贪吃蛇的位置/维护整条蛇的指针
	SnakeNode* SnakePhead;
	//指向食物的指针
	SnakeNode* FoodNode;
	//玩家当前得分
	int Scour;
	//每个食物的得分
	int FoofScour;
	//蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间月长,蛇的速度越慢
	int SleepTime;
	//贪吃蛇游戏状态
	enum GAMSTATUS Status;
	//贪吃蛇行进的方向
	enum SNAKEWAY way;
}Snake;

3.1 游戏开始前的准备 GameStart函数

下面是GameStart函数:

复制代码
void GameStart(Snake* ps) {
	//设定控制台屏幕大小
	system("mode con cols=100 lines=30");
	//设置控制台标题
	system("title 贪吃蛇");
	//设定光标,进行隐藏
	//获取控制台句柄
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	//创建结构体来接收控制台光标的数据
	//有两个参数:dwSize 光标占空比; bVisible 光标可见度 默认true
	CONSOLE_CURSOR_INFO cursor_info = { 0 };
	GetConsoleCursorInfo(handle, &cursor_info);
	//隐藏光标
	cursor_info.bVisible = false;
	//将修改的光标属性值传给控制台进行控制,参数类型跟GetConsoleCursorInfo一样
	SetConsoleCursorInfo(handle, &cursor_info);
	//游戏开始欢迎界面
	welcome();
	//绘制地图
	GameMap();
	//贪吃蛇初始化
	InitSnake(ps);
	//创建食物
	CreatFood(ps);
}

接下来我们来逐一来了解一下GameStaert的答题流程,以及它实现了哪些功能。

3.1.1 设置好控制台界面的行和列,隐藏控制台光标

在这个大块中,我们首先需要设置好控制台界面的行和列,隐藏控制台光标:

3.1.2 打印欢迎界面

其次打印欢迎界面,用SearchLocal函数来找到合适的位置,使界面变得美观,期间使用了system("cls")来进行清屏操作:

复制代码
//初始化贪吃蛇界面
void welcome() {
	
	//将光标定位到界面的中间位置
	SearchLocal(35, 10);
	printf("欢迎来到贪吃蛇小游戏\n");
	SearchLocal(38, 20);
	system("pause");
	system("cls");

	//功能介绍
	SearchLocal(15, 10);
	printf("用↑ . ↓ . ← . → 来控制蛇的移动,F3为加速按键,F4为减速\n");
	SearchLocal(35, 12);
	printf("加速能够获得更高的分数!!!!");
	SearchLocal(38, 20);
	system("pause");
	system("cls");
}

界面展示如下:

3.1.3 绘制地图

接着,绘制地图,墙壁的范围在控制台窗口的范围如图所示:

如果想在控制台的窗口中指定位置输出信息,我们需要得知该位置的坐标,所以⾸先介绍⼀下控制台窗口的坐标知识。

在控制台窗口中,行和列的大小也是不一样的:

可以看到,在控制台窗口中,行明显要比列更大,那在我们打印墙壁时,就需要用到宽字符:'□',宽字符与普通字符不同,一个宽字符的大小是两个字节,并且一个宽字符在窗口中占一行两列(两个坐标的大小),而一个普通字符的大小是一个字节,一个普通字符在窗口中占一行一列(一个坐标的大小),打印出来会更加美观。

这里我们把墙壁的行列定义为宏,后面可以根据自己的需要修改墙壁的大小,本文用到的是27行,58列的墙壁

复制代码
#define ROW 26  //游戏区行数
#define COL 56  //游戏区列数

这里列的偏移值每次加2,是因为宽字符的x轴的密度与y轴的密度不一致,而设置每次偏移2,这样打印下一个宽字符的时候不会重叠

复制代码
//绘制地图
void GameMap() {
	//上
	SearchLocal(0, 0);
	for (int i = 0; i < COL; i += 2) {
		wprintf(L"%c", WALL);
	}
	//左
	for (int i = 1; i < ROW; i++) {
		SearchLocal(0, i);
		wprintf(L"%lc", WALL);
	}
	//右
	for (int i = 1; i < ROW; i++) {
		SearchLocal(COL-1, i);
		wprintf(L"%lc", WALL);
	}
	//下
	SearchLocal(0, ROW-1);
	for (int i = 0; i < COL; i += 2) {
		wprintf(L"%lc", WALL);
	}
}

3.1.4 初始化蛇身

在这里,得用到数据结构中的链表,每个蛇身视为一个节点(游戏开始时有5个节点),将这5个节点的坐标相邻(利用头插法初始化),依次打印(最后要初始化一下蛇的属性):

初始蛇头的位置:

复制代码
#define Origin_x 24  // 初始蛇头的x坐标
#define Origin_y 5   // 初始蛇头的y坐标

//打印蛇身
void SnakePrint(Snake* ps) {
	SnakeNode* pcur = ps->SnakePhead;
	while (pcur) {
		SearchLocal(pcur->x, pcur->y);
		wprintf(L"%lc", BODY);
		pcur = pcur->next;
	}
}

//初始化蛇
void InitSnake(Snake*ps) {
	//创建5个蛇身节点
	SnakeNode* pcur = NULL;
	for (int i = 0; i < 5; i++) {
		pcur = (SnakeNode*)malloc(sizeof(SnakeNode));
		if (pcur == NULL) {
			perror("malloc");
			return;
			//exit(1)
		}
		//头插
		pcur->x = Origin_x+2*i;
		pcur->y = Origin_y;
		pcur->next = NULL;
		if (ps->SnakePhead == NULL) {    
			ps->SnakePhead = pcur;
		}
		else {
			pcur->next = ps->SnakePhead;
			ps->SnakePhead = pcur;
		}
	}
	
	//打印蛇身
	SnakePrint(ps);

	//初始化
	ps->Scour = 0;
	ps->FoodNode = NULL;
	ps->FoofScour = 10;
	ps->SleepTime = 200;
	ps->Status = OK;
	ps->way = RIGHT;
}

3.1.5 创建食物

将食物也当成一个普通的SnakeNode节点,因为食物也具有和蛇身节点相同的属性,只不过它的next指针永远为空,而它的x和y坐标是随机生成的,所以这里我们需要用到rand函数来生成随机数,不过生成的随机数也需要具备如下特点:

  1. x坐标的值为2~54,y坐标点值为1~25(不能在墙壁上或墙壁外)
  2. 必须为2的倍数(保证蛇可以吃到食物)
  3. 不能和蛇身的坐标重合

确定好以上条件后,我们就可以将食物打印在地图上了。

复制代码
//创建食物
void CreatFood(Snake* ps) {
	//随机生成食物的坐标(x,y)
	//1.不能超过墙,且不能重叠 x:2~54 -- 52+2    y:1~24  -- 23+1
	//2.食物不能与蛇身重叠,遍历蛇身

	//创建食物节点
	SnakeNode* FoodNode = (SnakeNode*)malloc(sizeof(SnakeNode));
	if (FoodNode == NULL) {
		perror("creat FoodNode fail");
		return;
	}

	int x = 0;
	int y = 0;
	again:
	do {
		x = rand() % (COL-4) + 2;
		y = rand() % (ROW-2) + 1;
	} while (x % 2 != 0); //1.不能超过墙,且不能重叠

	//2.食物不能与蛇身重叠,遍历蛇身
	SnakeNode* pcur = ps->SnakePhead;
	while (pcur) {
		if (x == pcur->x && y == pcur->y) {
			goto again;
		}
		pcur = pcur->next;
	}

	//真正创建了属于食物的坐标
	FoodNode->x = x;
	FoodNode->y = y;
	FoodNode->next = NULL;

	ps->FoodNode = FoodNode;

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

由于食物是随机生成的(在蛇可爬行区域内),而食物有几率生成到蛇的身上,所以每生成一个食物节点后,得遍历整条蛇;如果重叠,就重新生成一个食物,直到不重叠为止。此处用到了again:,这里简略的介绍一下:

在 C 语言中, again: 不是一个关键字或预定义标识符,它只是一个标识符的名称,可以用作标签(label)。 在 C 语言中,标签通常用于在嵌套循环或 switch 语句中进行无条件跳转,从而提高代码的可读性和灵活性。 例如,使用 again: 标签可以在循环中使用 goto 语句来实现无限循环。 但是,使用 goto 语句会使代码变得难以理解和维护,因此应该谨慎使用。

3.2 游戏运行 GameRun函数

下面是 GameRun函数:

复制代码
//贪吃蛇游戏运行
void GameRun(Snake* ps) {
	//打印操作介绍
	PrintOption();
	do{
		//打印当前得分情况
		SearchLocal(65, 11);
		printf("当前得分:%d",ps->Scour);
		SearchLocal(78, 11);
		printf("当前食物分值:%02d", ps->FoofScour); //每次移动分值可能变化放到循环中
		//检测按键是否按过
		if (KEY_PRESS(VK_UP)&& ps->way !=DOWN) {
			ps->way = UP;
		}
		else if(KEY_PRESS(VK_DOWN) && ps->way != UP) {
			ps->way = DOWN;
		}
		else if (KEY_PRESS(VK_LEFT) && ps->way != RIGHT) {
			ps->way = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->way != LEFT) {
			ps->way = RIGHT;
		}
		else if (KEY_PRESS(VK_ESCAPE)) {
			ps->way = EXIT;
			break;
		}
		//空格进入暂停,死循环睡眠,在次按下空格进行游戏
		else if (KEY_PRESS(VK_SPACE)) {
			pause();
		}
		//F3加速,睡眠时间减少,食物分值增加,限制条件睡眠时间不为负数
		else if (KEY_PRESS(VK_F3)) {
			//时间不能为负,每次-30,至多减到20;
			if (ps->SleepTime > 20) {
				ps->SleepTime -= 30;
				ps->FoofScour += 2;
			}
		}
		//F4减速,睡眠时间增加,食物分值减少,限制条件食物分值不为0
		else if (KEY_PRESS(VK_F4)) {
			if (ps->FoofScour > 2) {
				ps->SleepTime += 30;
				ps->FoofScour -= 2;
			}
		}
		//贪吃蛇开始移动
		SnakeMove(ps);
		//睡眠一下
		Sleep(ps->SleepTime);

	} while (ps->Status == OK);
}

3.2.1 打印帮助信息

游戏开始后的帮助信息进行打印:

将光标移到指定位置(如果更改墙体的大小,帮助信息的位置光标需要做对应的修改,还有如果超出了窗口大小,可以调整窗口的大小),打印即可:

复制代码
//打印帮助信息
void PrintOption() {
	
	SearchLocal(63, 17);
	printf("用↑ . ↓ . ← . → 来控制蛇的移动");
	SearchLocal(63, 18);
	printf("space(空格)为暂停/回到游戏按键");
	SearchLocal(68, 19);
	printf("F3为加速按键,F4为减速"); 
	SearchLocal(72, 20);
	printf("ESC为退出按键");
	SearchLocal(63, 21);
	printf("注意:不能撞墙,不能咬到自己!!!");
}

3.2.2 判断玩家按下的按键

接下来,玩家会通过按方向键(↑ . ↓ . ← . →)以及F3、F4来控制蛇的移动,我们就需要用到我们上面所定义好的宏,传入各个键的虚拟键值,利用if else语句来判断玩家所按下的键,但是需要注意的是:如果蛇正在向下移动,则蛇的移动方向不能突然变为向上,否则会撞到自己(即向下移动时,按上方向键无效),左右上方向时同理:

复制代码
//休眠函数
void pause() {
	while (1) {
		Sleep(200);
		if (KEY_PRESS(VK_ESCAPE)) {
			break;
		}
	}
}

3.2.3 蛇的移动

可以把下一步要移动到的坐标当做一个SnakeNode节点,malloc出一个新节点(这里命名为pnext),再根据蛇的方向设置这个节点的x,y值,接着,我们需要判断移动到的下一个坐标是不是食物,如果是食物则头插进蛇体中(不要忘记释放掉pnext节点(因为食物节点和pnext的位置是重复的));如果不是食物,也需要将pnext节点头插到蛇体中,要将最后一个节点用" "进行覆盖,不让这个节点出现在屏幕上。然后接着判断这次移动是否撞到了墙或者自己。

复制代码
//贪吃蛇开始移动
void SnakeMove(Snake*ps){
	//创建指针记录蛇移动的下一个位置
	SnakeNode* pnext = (SnakeNode*)malloc(sizeof(SnakeNode));
	if (pnext == NULL) {
		perror("SnakeMove::malloc");
		return;
	}
	pnext->next = NULL;

	//记录蛇移动的下一个位置
	switch (ps->way){
	case UP:
		pnext->x = ps->SnakePhead->x;
		pnext->y = ps->SnakePhead->y - 1;
		break;
	case DOWN:
		pnext->x = ps->SnakePhead->x;
		pnext->y = ps->SnakePhead->y + 1;
		break;
	case LEFT:
		pnext->x = ps->SnakePhead->x-2;
		pnext->y = ps->SnakePhead->y;
		break;
	case RIGHT:
		pnext->x = ps->SnakePhead->x + 2;
		pnext->y = ps->SnakePhead->y;
		break;
	}

	//判断下一个位置是否为食物
	//是食物,吃掉 --- 头插
	if (NextFood(ps,pnext)) {
		EatFood(ps, pnext);
	}
	//不是食物,头插删除尾节点
	else {
		NotEatFood(ps, pnext);
	}
	//打印蛇身
	SnakePrint(ps);

	//检测是否穿墙
	Kill_Wall(ps);
	//检测是否咬到自己
	Kill_Self(ps);
}
3.2.3.1 NextFood

判断下一个节点的坐标是否与食物的节点的坐标一致,是则返回1;反之返回0。

复制代码
//下一个位置是否为食物;
int NextFood(Snake* ps, SnakeNode* pnext) {
	if (pnext->x == ps->FoodNode->x && pnext->y == ps->FoodNode->y) {
		return 1;
	}
	else{
		return 0;
	}
}
3.2.3.2 EatFood
复制代码
//吃食物
void EatFood(Snake* ps, SnakeNode* pnext) {
	pnext->next = ps->SnakePhead;
	ps->SnakePhead = pnext;

	//得分增加
	ps->Scour += ps->FoofScour;

	//释放旧的食物
	free(ps->FoodNode);
	ps->FoodNode = NULL;

	//重新创建食物
	CreatFood(ps);
}
3.2.3.3 Kill_Wall
复制代码
//检测是否穿墙
void Kill_Wall(Snake* ps) {
	if (ps->SnakePhead->x == 0 ||
		ps->SnakePhead->x == (COL-1) ||
		ps->SnakePhead->y == 0 ||
		ps->SnakePhead->y == (ROW-1)) 
	{
		ps->Status = KILL_BY_WALL;
	}
}
3.2.3.4 Kill_Self
复制代码
//检测是否咬到自己
void Kill_Self(Snake* ps) {
	SnakeNode* pcur = ps->SnakePhead->next;
	while (pcur) {
		if (ps->SnakePhead->x == pcur->x && ps->SnakePhead->y == pcur->y) {
			ps->Status = KILL_BY_SELF;
			return;
		}
		pcur = pcur->next;
	}
}

3.2.4 .Sleep函数、循环移动

需要一段时间来间断一下蛇体的移动,并判断蛇的状态进行循环:

此处用到了Sleep函数,依旧简略的介绍一番:

Sleep() 函数是 Windows 平台下的一个 API,定义在**windows.h** 头文件中。其功能是挂起(暂停)当前线程的执行一段时间。输入参数是毫秒数。

3.3 贪吃蛇游戏结束的善后

打印出提示信息告诉玩家游戏失败的原因,最后,因为蛇身的节点是我们动态malloc开辟的,为了避免内存泄露,我们需要一一free掉。

复制代码
//贪吃蛇游戏结束的善后
void GameEnd(Snake* ps) {
	SearchLocal(15, 10);
	//打印结束状态
	switch (ps->Status) {
	case EXIT:
		printf("正常退出,游戏结束了");
		break;
	case KILL_BY_WALL:
		printf("很遗憾,游戏结束,你撞到墙了!!!");
		break;
	case KILL_BY_SELF:
		printf("不小心咬到自己了,游戏结束了!!!");
		break;
	}

	//释放空间
	SnakeNode* pcur = ps->SnakePhead;
	SnakeNode* del = NULL;
	while (pcur) {
		del = pcur->next;
		free(pcur);
		pcur = del;
	}
	free(ps->FoodNode);
	ps = NULL;

}

3.4 再来一局,不言弃!!

游戏失败后,为了使游戏更完整,设置循环提示玩家是否再来一局,这里用do while循环来实现:

复制代码
void snakegame() {
	
	int ch = 0;
	do {
		//创建贪吃蛇
		Snake snake = { 0 };
		//贪吃蛇游戏界面初始化
		GameStart(&snake);
		//贪吃蛇游戏运行
		GameRun(&snake);
		//贪吃蛇游戏结束的善后
		GameEnd(&snake);
		SearchLocal(15, 8);
		printf("要再来一局吗?? Y/N:");
		ch = getchar();
		getchar();// 清理\n
		SearchLocal(0, 27);
	} while (ch == 'Y' || ch == 'y');
}
相关推荐
云半S一11 分钟前
性能测试笔记
经验分享·笔记·压力测试
齐尹秦32 分钟前
HTML5 拖放(Drag and Drop)学习笔记
笔记·学习·html5
sakabu1 小时前
Linux安装MySQL数据库并使用C语言进行数据库开发
linux·c语言·数据库·笔记·mysql·数据库开发
lwewan2 小时前
26考研——图_图的代码实操(6)
数据结构·笔记·考研·算法·深度优先
Peter_Deng.3 小时前
单片机 - 位运算详解(`&`、`|`、`~`、`^`、`>>`、`<<`)
c语言·单片机·嵌入式硬件
程序员Linc4 小时前
《数字图像处理》第三章 3.8 基于模糊技术的图像强度变换与空间滤波学习笔记
笔记·学习·数字图像处理·模糊技术·强度变换·空间滤波
阿巴~阿巴~4 小时前
2024年3月全国计算机等级考试真题(二级C语言)
c语言
vv啊vv5 小时前
使用android studio 开发app笔记
android·笔记·android studio
王RuaRua5 小时前
[数据结构]1.时间复杂度和空间复杂度
c语言·数据结构·算法