贪吃蛇项目实现(C语言)——附源码

前言

贪吃蛇是一款十分经典的游戏,其通过控制贪吃蛇的上下左右移动来吃食物,延长自己的身体,也会因为撞到墙体和自身而死亡。下面我们通过C语言来实现贪吃蛇。

1.技术要点

C语言枚举,结构体,链表,动态内存管理,预处理指令,函数,Win32 API等。

2. Win 32 API

要使用Win32 API 我们就需要先了解学习一下Win 32 API。

2.1 Win 32 API

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

2.2 控制台程序(Console)

我们平时运行起来的黑框程序就是控制台程序

我们可以使用cmd命令来控制窗口的长和宽:设置长100列,宽30行

cs 复制代码
mode con cols=100 lines=30;

也可以设置控制台窗口的名字:

cs 复制代码
title 贪吃蛇;

这些控制窗口执行的命令可以通过调用system函数来执行。例如:

cs 复制代码
#include<stdio.h>
int main()
{
//设置窗口大小为30行,100列
system("mode con cols=100 lines=30");
//设置窗口名称为贪吃蛇
system("title 贪吃蛇");
return 0;
}

2.3 控制坐标COORD

COORD是一个结构体,表示一个字符在控制台屏幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元

声明类型:

cs 复制代码
typedef struct _COORD{
   short x;
   short y;
  
}COORD,*PCOORD;

赋值:

COORD pos = {10,15};

2.4 隐藏光标

光标的显示会让贪吃蛇游戏的进行不是很友好,所以我们需要将其隐藏。

2.4.1 GetStdHandle

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

函数的参数:

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

实例:

cs 复制代码
HANDLE houtput = NULL;
houtput =  GetStdHandle(STD_OUTPUT_HANDLE);

2.4.2 GetConsoleCursorInfo

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

cs 复制代码
BOOL WINAPI GetConsoleCursorInfo(
 HANDLE hConsoleOutput,
 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
//PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标
(光标)的信息

实例:

cs 复制代码
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息

2.4.3 CONSOLE_CURSOR_INFO

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

cs 复制代码
typedef sturct _CONSOLE_CURSOR_INFO
{
DWORD dwSize;
BOOL bVisible;
}_CONSOLE_CURSOR_INFO,*P_CONSOLE_CURSOR_INFO;

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

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

cs 复制代码
CursorInfo.bVisible = false; //光标隐藏

2.4.4 SetConsoleCursorInfo

设置指定控制台屏幕缓冲区的光标的大小和可见性。

cs 复制代码
BOOL WINAPI SetConsoleCursorInfo(
 HANDLE hConsoleOutput,
 const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);

实例:

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

2.5 封装设置光标位置函数

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

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

实例:

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

封装成函数:

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

3. GetAsyncKeyState

获取按键情况,GetAsyncKeyState的函数原型:

复制代码
short GetAsyncKeyState(
int vKey
);

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

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

我们通过函数与宏定义来判断键是否被按过。判断依据GetAsyncKeyState函数的返回值的最高位。

实现:

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

通过宏定义将函数的结果与1,得到结果,后面再通过多个if表达式实现键值的判断。

4.功能实现

贪吃蛇需要实现一些基本的功能:

  1. 地图绘制

  2. 蛇吃食物的功能(上、下、左、右方向键控制蛇的动作)

  3. 蛇撞墙死亡

  4. 蛇撞自身死亡

  5. 计算得分

  6. 蛇身加速、减速

7.暂停游戏

下面来依次实现这些功能:

4.1 地图绘制

这是贪吃蛇游戏的大纲,我们该如何设置这样的界面呢?

上面讲了,横向为X轴,纵向为y轴,从上到下依次增长。

我们在地图上打印墙体,蛇身,食物的时候需要分别使用使用宽字符□,●,○。

普通字符占一个字节,这类宽字符占两个字节。

4.1.1 <locale.h>本地化

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

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

• 数字量的格式

• 货币量的格式

• 字符集

• 日期和时间的表示形式

4.1.1.1类项

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

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

第一个函数可以是一个类项,也可以是全部类项,第二个参数有两种取值:"C"(正常模式)和""(本地模式)。

任意程序执行开始,都会隐藏式执行调用:

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

当切换到本地模式后,我们才可以输出宽字符。

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

setlocale的返回值是字符串指针,表示已经设置好的格式,如果调用失败,则返回空指针NULL。

将第二个参数设置NULL就可以用来查询当前地区了。

复制代码
#include <locale.h>
int main()
{
 char* loc;
 loc = setlocale(LC_ALL, NULL);
 printf("默认的本地信息:%s\n", loc);
 
 loc = setlocale(LC_ALL, "");
 printf("设置后的本地信息: %s\n", loc);
 return 0;
}

4.1.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"%lc\n", ch1);
 wprintf(L"%lc\n", ch2);
 wprintf(L"%lc\n", ch3);
 wprintf(L"%lc\n", ch4);
 return 0;
}

结果:

从打印结果可以看出,一个宽字符占两个字符的位置。

还需要注意的是,一个字符的长度是宽度的两倍,所以我们在使用宽字符的时候需要处理好地图上坐标的计算。

4.1.3 地图坐标

我们这里实现打印一个27行,58列的棋盘,通过棋盘画出地图。(可通过自己的实际情况修改)

27行(0-26),58列(0-57);其中列必须是2的倍数,因为宽字符的宽度为2,实现地图全是宽字符。

4.1.4 地图绘制

绘制地图可以通过四个for循环实现。

代码实现如下:

这里通过宏定义减少代码量:

复制代码
#define wall L'□'

void create_wall()
{
	//x是横,y是竖
	//上
	for (int i = 0; i <29;i++ )
	{
		wprintf(L"%lc", wall);
	}
	//下
	setpos(0, 26);
	for (int i = 0; i < 29;i++)
	{
		
		wprintf(L"%lc", wall);
	}
	//左
	for (int i = 1; i <=25 ; i ++)
	{
		setpos(0,i);
		wprintf(L"%lc", wall);
	}
	//右
	for (int i = 1; i <= 25; i++)
	{
		setpos(56,i);
		wprintf(L"%lc", wall);
	}
	setpos(0, 27);
}

注意:

其中的上下通过只需要从0到28,因为宽字符占两个字节。打印完上下两部分之后,打印左右两部分的时候,只需要各打印25行,因为打印上下两部分的时候已经打印了两行。每打印完一部分就需要再重新定位光标位置。

4.2 蛇身和食物

蛇身的实现需要运用到链表结果,通过初始化五个宽字符将蛇身链接起来。

需要注意的是:

蛇身的X坐标必须要是2的倍数。不然宽字符打印出来可能在墙体,坐标不好对齐。

食物坐标的生成是随机的,并且坐标不在蛇身和墙体上。

4.2.1 蛇身实现

蛇身的每个结点需要包含:横纵坐标以及一个结点指针指向下一个结点。

复制代码
typedef struct snakenode
{
	int x;
	int y;
	struct snakenode* next;
}snakenode;

为了更好的管理蛇,我们再创建一个结构体:

复制代码
typedef struct snake
{
	snakenode* psnake;//头结点
	snakenode* pfood;//食物结点
	enum direction dir;//蛇的方向
	enum game_states status;
	int food_weight;//食物分数
	int score;///总分数
	int sleep_time;//休息时间。
}snake;

里面包含了所需要的所有东西。

其中蛇的方向以及游戏状态通过枚举一一列举:

蛇的初始化:

复制代码
snakenode* cur = NULL;
int i = 0;
//生成五个结点
for (i = 0; i < 5; i++)
{
	cur = (snakenode*)malloc(sizeof(snakenode));
	if (cur == NULL)
	{
		printf("malloc fail");
		exit(1);
	}
	cur->next = NULL;
	cur->x = snake_x+ 2*i;
	cur->y = snake_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->food_weight = 10;//初始食物十分
ps->score = 0;//总分
ps->sleep_time = 200;//两百毫秒
ps->status = ok;

4.2.2 食物的随机生成

使用真正的随机需要包含头文件<time.h>。

复制代码
	srand((unsigned int)time(NULL));

然后通过随机函数实现食物坐标的随机生成。

思路:

生成一个满足X坐标为2到54,Y坐标1到25,并且X为2的倍数,然后再遍历蛇身,如果与任意一个蛇身结点横纵坐标重合,则再重新进行生成横纵坐标。实现这一过程我们可以使用goto指针。

实现后创建一个食物指针,将横纵坐标都填进去,并将其next指针指向NULL,然后定位到食物结点的坐标,进行打印食物节点,最后将该结点存到snake结构体里面。

代码实现:

复制代码
int x = 0;
int y = 0;
//随机创建
//x在2到54,y在1到25,不和墙重叠do
again:
do
{
	x = rand() % 53 + 2;
	y = rand() % 25 + 1;
} while (x % 2 != 0);
//遍历蛇身判断是否和蛇身重叠
//重叠时使用goto函数进行重置
snakenode* cur = psnake->psnake;
while (cur)
{
	if (cur->x == x && cur->y)
		goto again;
	cur = cur->next;
}
snakenode* pfood = (snakenode*)malloc(sizeof(snakenode));
if (pfood == NULL)
{
	printf("malloc");
	exit(2);
}
pfood->next = NULL;
pfood->x = x;
pfood->y = y;
setpos(x, y);
wprintf(L"%lc", food);
psnake->pfood = pfood;

4.3 蛇移动

蛇的移动有多种情况:首先需要通过按键方向确定下一步的位置,再判断是吃到食物还是撞到墙还是吃到自己还是不是食物。

我们实现移动的方式是,创建一个新结点,将该新节点作为新的头结点,打印新蛇身。

头结点坐标实现:

复制代码
	switch (psnake->dir)
	{
	case left:
		next->x = psnake->psnake->x - 2;
		next->y = psnake->psnake->y ;
		break;
	case right:
		next->x = psnake->psnake->x + 2;
		next->y = psnake->psnake->y;
		break;
	case up:
		next->x = psnake->psnake->x;
		next->y = psnake->psnake->y-1;
		break;
	case down:
		next->x = psnake->psnake->x;
		next->y = psnake->psnake->y + 1;
		break;
	}

分析:向左移动则横坐标-2。向左移动则横坐标+2。向上移动则纵坐标-1。向下移动则纵坐标+1。

4.3.1 获取按键情况

通过宏定义来实现按键的获取:

复制代码
//宏定义按键
#define key_press(vk) ((GetAsyncKeyState(vk)&1)?1:0)

if (key_press(VK_UP) && psnake->dir != down)
{
	psnake->dir = up;
}
else if (key_press(VK_DOWN) && psnake->dir != up)
{
	psnake->dir = down;
}
else if (key_press(VK_LEFT) && psnake->dir != right)
{
	psnake->dir = left;
}
else if (key_press(VK_RIGHT) && psnake->dir != left)
{
	psnake->dir = right;
}
else if (key_press(VK_SPACE))
{
	pause();
}
else if (key_press(VK_ESCAPE))//正常退出
{
	psnake->status = end_normal;
}
else if (key_press(VK_F3))
{
	if (psnake->food_weight <20)
	{
		psnake->sleep_time -= 20;
		psnake->food_weight += 2; 
	}
	
}
else if (key_press(VK_F4))
{
	if (psnake->food_weight > 2)
	{
		psnake->sleep_time += 20;
		psnake->food_weight -= 2;
	}

}

当获取到一个方向时,如果之前的方向与这个方向相反,那么该次按键不实现。当按空格时,实现暂停函数,当按到退出键时,退出函数,当按到F3时,加速,当按到F4时,减速。

4.3.1.1 暂停函数

我们可以通过一个不退出循环的while循环实现永久暂停,直到满足再按一次暂停键。

代码实现:

复制代码
void pause()
{
	while (1)
	{
		
		Sleep(200);
		if (key_press(VK_SPACE))
		{
			break;
		}
	}
}

当按到退出键时,则只需要将游戏状态设置为end_normal即可。

当按到F3(加速)时,我们食物一开始的分数是10,最高分是20,最低分是2。当小于20的时候,我们可以加速,即将休眠时间降低。

当按到F4(减速)时,当大于2的时候,我们就可以减速,即将休眠时间延长。

4.3.2 不是食物

不是食物则将之前蛇身的最后一个结点以" "输出。

复制代码
void notfood(snakenode* pnext, snake* psnake)
{
	pnext->next = psnake->psnake;
	psnake->psnake = pnext;
	snakenode* cur = psnake->psnake;
	while (cur->next->next)
	{
		setpos(cur->x, cur->y);
		wprintf(L"%lc", body);
		cur = cur->next;
	}
	//打印倒数第二个结点的身体
	setpos(cur->x, cur->y);
	wprintf(L"%lc", body);
	//最后一个节点打印空格
	setpos(cur->next->x, cur->next->y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;
}

注意:这里的while循环完之后,cur指针的位置在之前蛇身的倒数第二个结点。然后将最后一个结点free掉并置为零。

4.3.3 是食物

是食物则直接将新结点变为头结点,遍历蛇身,打印蛇身释放掉食物结点。然后再重新生成一个食物结点。

复制代码
void eatfood(snakenode * pnext,snake* psnake)
{
	//头插法,将next结点插入
	pnext->next = psnake->psnake;
	psnake->psnake = pnext;
	snakenode* cur = psnake->psnake;
	while (cur)
	{
		setpos(cur->x, cur->y);
		wprintf(L"%lc", body);
		cur = cur->next;
	}
	psnake->score += psnake->food_weight;
	free(psnake->pfood);
	psnake->pfood = NULL;
	//创建新食物
	foodcreate(psnake);
}

4.3.4 撞墙结束

撞到墙后就将游戏状态设置为撞墙结束。

复制代码
void killbywall(snakenode* next,snake * psnake)
{
	if (next->x == 0 || next->x == 56 || next->y == 0 || next->y == 26)
		psnake->status = kill_by_wall;

}

4.3.5 撞到自己结束

遍历蛇身,如果撞到自己就将游戏状态设置为撞自己结束。

复制代码
void killbyself(snakenode* next, snake* ps)
{
	snakenode* cur = ps->psnake->next;
	while (cur)
	{
		if (cur->x == next->x && cur->y == next->y)
		{
			ps->status = kill_by_self;
			break;
		}
		cur = cur->next;
	}
}

4.4 退出游戏

先通过游戏状态来决定输出怎样的汉字告诉玩家:

复制代码
	switch (snake->status)
	{
	case kill_by_wall:
		printf("您撞到墙啦,游戏结束");
		break;
	case kill_by_self:
		printf("您撞到自己啦,游戏结束");
		break;
	case end_normal:
		printf("您退出游戏,游戏结束");
		break;
	}

再销毁蛇身链表。

复制代码
//链表销毁
snakenode* cur = snake->psnake;
while (cur)
{
	snakenode* del = cur;
	cur = cur->next;
	free(del);
	del = NULL;
	
}

5. 总的包装

为了流畅的实现贪吃蛇游戏,我们需要在test函数中运用循环来实现多次玩游戏。

复制代码
void test()
{
	int ch = 0;
	do
	{
		system("cls");
		snake psnake = { 0 };
		//3.  欢迎界面,开始游戏
		// //打印墙体
		// 初始化蛇体
		//创建食物
		gamestart(&psnake);
		//4. 运行游戏
		gamerun(&psnake);
		//5. 退出游戏
		gameend(&psnake);
		setpos(30, 16);
		system("pause");
		setpos(30, 16);
		printf("还要再玩一次吗?(Y/N):");
		
		ch = getchar();
		while (getchar() != '\n');
	
	} while ((ch == 'y' || ch == 'Y'));
	
	setpos(0, 27);
	
	
}

通过getchar函数得到玩家的答案,判断是否再进行游戏。

需要注意的是,玩完之后要清屏,并且在玩家输入前需要进行pause,避免up和right键调出之前的输入记录。

6. 源码

贪吃蛇实现 · 976d8af · 重邮阿江/c_study_experience - Gitee.com

相关推荐
Scott9999HH4 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社5 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海5 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖6 小时前
JDK 26 新特性详解
java·开发语言
马优晨7 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区8 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大9 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai9 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜9 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa10 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio