贪吃蛇项目实现(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

相关推荐
喵叔哟1 分钟前
重构代码中引入外部方法和引入本地扩展的区别
java·开发语言·重构
尘浮生7 分钟前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
hopetomorrow21 分钟前
学习路之PHP--使用GROUP BY 发生错误 SELECT list is not in GROUP BY clause .......... 解决
开发语言·学习·php
小牛itbull31 分钟前
ReactPress vs VuePress vs WordPress
开发语言·javascript·reactpress
请叫我欧皇i39 分钟前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
闲暇部落42 分钟前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
GIS瞧葩菜1 小时前
局部修改3dtiles子模型的位置。
开发语言·javascript·ecmascript
chnming19871 小时前
STL关联式容器之set
开发语言·c++
带多刺的玫瑰1 小时前
Leecode刷题C语言之统计不是特殊数字的数字数量
java·c语言·算法
熬夜学编程的小王1 小时前
【C++篇】深度解析 C++ List 容器:底层设计与实现揭秘
开发语言·数据结构·c++·stl·list