首页 > 编程语言 >贪吃蛇项目实现(C语言)——附源码

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

时间:2024-09-09 21:21:35浏览次数:3  
标签:结点 cur psnake next 源码 贪吃蛇 C语言 snakenode 光标

前言

贪吃蛇是一款十分经典的游戏,其通过控制贪吃蛇的上下左右移动来吃食物,延长自己的身体,也会因为撞到墙体和自身而死亡。下面我们通过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行

mode con cols=100 lines=30;

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

title 贪吃蛇;

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

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

2.3 控制坐标COORD

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

  

声明类型:

typedef struct _COORD{
   short x;
   short y;
  
}COORD,*PCOORD;

赋值:

COORD pos = {10,15};

2.4 隐藏光标

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

2.4.1 GetStdHandle

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

函数的参数:

HANDLE  GetStdHandle(DWORD nStdHandle);

实例:

HANDLE houtput = NULL;
houtput =  GetStdHandle(STD_OUTPUT_HANDLE);

2.4.2 GetConsoleCursorInfo

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

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

实例:

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

2.4.3 CONSOLE_CURSOR_INFO

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

typedef sturct _CONSOLE_CURSOR_INFO
{
DWORD dwSize;
BOOL bVisible;
}_CONSOLE_CURSOR_INFO,*P_CONSOLE_CURSOR_INFO;

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

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

CursorInfo.bVisible = false; //光标隐藏

2.4.4 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);//设置控制台光标状态

2.5 封装设置光标位置函数

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

BOOL WINAPI SetConsoleCursorPosition(
 HANDLE hConsoleOutput,
 COORD pos
);

实例:

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

封装成函数:

//设置光标的坐标
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

标签:结点,cur,psnake,next,源码,贪吃蛇,C语言,snakenode,光标
From: https://blog.csdn.net/Ajiang2824735304/article/details/141786520

相关文章

  • C语言的历史
    C语言的历史目录引言C语言的起源2.1计算机编程语言的早期历史2.2BCPL和B语言C语言的诞生3.1丹尼斯·里奇与贝尔实验室3.2Unix系统的发展C语言的标准化进程4.1ANSIC4.2ISOC标准C语言的应用5.1操作系统开发5.2嵌入式系统5.3编译......
  • 鹏哥C语言14---数组
    //------------------------------------------------------------------9.数组//--------------------------------------------------------9.1数组的定义// arr[]={,,,,,};//数组里边可以存放一组相同类型的元素#include<stdio.h>intmain(){   //---------......
  • 音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现
    =================================================================音视频入门基础:WAV专题系列文章:音视频入门基础:WAV专题(1)——使用FFmpeg命令生成WAV音频文件音视频入门基础:WAV专题(2)——WAV格式简介音视频入门基础:WAV专题(3)——FFmpeg源码中,判断某文件是否为WAV音频文件......
  • C语言学习 --- 修饰变量的关键字
    修饰变量的关键字        修饰变量的关键字+数据类型+变量名 auto:自动默认不写register:        register是作为寄存器的变量,就是在CPU里面存储的地方作用:        将频繁进行IO操作的变量声明成寄存器变量,节省数据读取操作的时间,提高执行效率......
  • java毕业设计-基于springboot+vue的高校运动会管理系统设计和实现,基于springboot+vue
    博主介绍:✌️码农一枚,专注于大学生项目实战开发、讲解和毕业......
  • java毕业设计-基于springboot+vue的篮球吧一体化服务平台设计和实现,-基于springboot的
    博主介绍:✌️码农一枚,专注于大学生项目实战开发、讲解和毕业......
  • C语言指针篇
    一.指针的本质        指针的本质就是有类型的地址。地址指的是地址总线上的地址码表,而类型决定了我们操作这个指针时,要从这个地址码上读写的长度。即指针的类型决定读写一个地址块的数据长度(1字节、2字节、4字节还是结构体定义的长度)。        比如(uint8_t......
  • Springboot计算机毕业设计小区物业管理平台的设计与实现d7rk3(程序+源码+数据库+调试部
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表住户信息,楼房信息,物业设备,车位信息,车位购买,车位租赁,收费信息,报修信息,投诉信息,物业管理开题报告内容一、选题背景与意义随着我国城市化进程的加速,住宅......
  • 短视频seo矩阵源码---MVC框架开发技术分享
    一.短视频矩阵源码数据库建立1.用户表(user):-用户ID(user_id)-用户名(username)-密码(password)-手机号(phone)-邮箱(email)2.账号表(account):二. MVC框架开发技术分享MVC(模型-视图-控制器)是一种常见的软件架构模式,用于将应用程序的不同部分分离开来,以实现更好的可维护性和......
  • Springboot计算机毕业设计小区物业管理平台(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表业主,物业管理员,投诉,报修,通知,费用缴纳,车位信息,业主信息开题报告内容一、选题背景及意义随着电子计算机和通信技术的飞速发展,人类社会已经逐渐进入信息化......