用C语言实现贪吃蛇游戏!!!

news2024/11/24 11:06:12

前言

大家好呀,我是Humble,不知不觉在CSND分享自己学过的C语言知识已经有三个多月了,从开始的C语言常见语法概念说到C语言的数据结构今天用C语言实现贪吃蛇已经有30余篇博客的内容,也希望这些内容可以帮助到各位正在阅读的小伙伴~

本次贪吃蛇项目是Humble继扫雷,通讯录之后分享的第三个项目,大约有五六百行代码的量~

而且不出意外,这应该是HumbleC语言部分的最后一篇博客了,在这之后,Humble未来的规划是分享C++的相关语法内容,C++实现的数据结构与算法,计算机网络,操作系统以及数据库的相关知识,希望未来也请各位多多关照~

好了,因为是一篇具有特殊意义的博客,好像前言说的有些多了(笑),接下来废话不多说,直接进入我们今天的贪吃蛇项目的内容把~

游戏演示

关于贪吃蛇这个游戏,我相信各位并不陌生,小伙伴们也跟我一样在童年的时候都玩过吧~

那么在正式制作之前,先给大家看一下这个即将由我们自己制作的贪吃蛇游戏的演示 ,让各位先对它有个整体的了解之后,之后的制作才会更加容易~

当我们运行起来后,首先看到的是两个初始化界面

然后下面是正式开始游戏的画面

围起来的部分是地图,蛇的初始长度被设置为5个节点,通过按键操作吃掉食物会获得相应得分,同时蛇的节点会增加,其余规则可看界面右方给的文字说明

当我们撞到墙或者自身或者按ESC键自己退出时,会显示游戏结束的界面:

以上就是对这个贪吃蛇小游戏简单的一次总揽,更多细节上的东西我们放在下面再讲

目标

本篇博客的目标:

使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇


贪吃蛇游戏实现基本的功能:

1.初始化以及地图的创建

2.蛇与食物的初始化

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

4.撞墙检测

5.撞自身检测

6.蛇的加速,减速

7.暂停功能

8.计算得分

....



 

技术要点

本次贪吃蛇项目涉及的技术要点主要有以下6点:

C语言函数、枚举、结构体、动态内存管理、链表、Win32 API
 

前面5点都是Humble在之前的博客都有分享过的,各位小伙伴可以访问我的主页去阅读哦

今天在讲贪吃蛇这个项目之前,我先对Win32 API  做个介绍 ,对其进行了解才能更丝滑的写接下来的贪吃蛇项目哦~

Win32 API

一.Win32 API概述

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

WIN32API也就是Microsoft Windows32位平台的应用程序编程接口
 

当然,这里大家不要太在乎这个32位平台,我们就把它统称为WIN32 API

二.控制台程序(Console)

控制台程序其实就是平常我们程序运行起来出现的界面

我们可以设置控制台窗口的长宽:设置控制台窗口的大小,30行,100列

mode con cols=100 lines=30

也可以通过命令设置控制台窗口的名字:

title 贪吃蛇

这些命令,我们可以调用C语言函数system来执行。

代码如下:

#include <windows.h>
int main()
{
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
return 0;
}


 

三.控制台屏幕上的坐标COORD

COORD 是Windows API中定义的一个结构体(具体内容我们可以不必深究,主要是为下面的几个函数服务的,咱们会用就行~),表示一个字符在控制台屏幕上的坐标
 

给坐标赋值:COORD pos = { 10, 15 };

四.GetStdHandle

GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(类似权限),使用这个句柄可以操作设备
 

它的定义如下:

HANDLE GetStdHandle(DWORD nStdHandle);//返回类型是HANDLE

//括号里只能是标准输入、标准输出或标准错误,这些都有对应的单词,比如标准输出写为STD_OUTPUT_HANDLE

我们直接看一下它怎么用吧~举个例子:
 

HANDLE hOutput = NULL;//我们用一个HANDLE的变量接收函数返回值
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//括号里是标准输出

好,掌握了GetStdHandle()之后,接下来就很简单了,我们看一下下面的几个函数把,使用它们可以帮我们实现对光标和虚拟键的操作

五.GetConsoleCursorInfo

GetConsoleCursorInfo,它是用来检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息

实例:

HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出的句柄

CONSOLE_CURSOR_INFO CursorInfo;//以CONSOLE_CURSOR_INFO类型定义一个变量~
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息

这里出现了一个CONSOLE_CURSOR_INFO,是我们前面没出现的,也给大家简单介绍一下~

CONSOLE_CURSOR_INFO,它是结构体,包含有关控制台光标的信息

它的结构如下:

typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO;

dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元底部的水平线条(这个我们今天用不到)
bVisible,游标的可见性。如果光标可见,则此成员为true,我们接下来要实现的贪吃蛇要隐藏光标,所以要将bVisible设置成false,所以要这样写:

CONSOLE_CURSOR_INFO CursorInfo;

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

因为上面的对光标的操作只有使用SetConsoleCursorInfo后才能真正被修改,所以下面来看一下SetConsoleCursorInfo,

六.SetConsoleCursorInfo

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

它有两个参数,这里也不过多赘述,直接看代码:

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

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

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

SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

经过SetConsoleCursorInfo 函数后,上面对光标的隐藏操作也就在控制台上得以实现啦~

七.SetConsoleCursorPosition

SetConsoleCursorPosition:调用这个函数将光标位置设置到指定的位置
 

代码如下:
 

COORD pos = { 10, 5};//我们将想要设置的坐标信息放在COORD类型的pos中

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


SetConsoleCursorPosition(hOutput, pos);//设置标准输出上光标的位置为pos

当然,为了之后写贪吃蛇代码时使用的更加方便,我们可以将这段代码封装成一个函数SetPos

这样我们之后想改变光标到我们想要的位置就会更加得心应手了~

void SetPos(int x, int y)
{

COORD pos = { x, y};

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


SetConsoleCursorPosition(hOutput, pos);


}

最后我们再看一个函数GetAsyncKeyState,它是用来实现游戏的交互,即获取按键情况的~

八.GetAsyncKeyState

当我们电脑上的按键被出发时,键盘上每个键的虚拟键值会传递给函数,函数通过返回值来分辨按键的状态


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

我们可以用#define定义:
 

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

当然,关于VK虚拟键的值,在Win API 的官网都有说明,我们可以去翻阅来获得自己想找的值~

说完了Win API及接下来会用到的这些函数的知识,下面我们进入贪吃蛇游戏数据结构的设计吧~

游戏数据结构设计(链表)

下面我们先对整体的数据结构进行一个设计:


在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信
,那么蛇的每一节其实就是链表的每个节点

每个节点只要记录好蛇身节点在地图上的坐标就行


所以蛇节点结构如下:

typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode, * pSnakeNode;


要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:

typedef struct Snake
{
pSnakeNode _pSnake;//维护整条蛇的指针
pSnakeNode _pFood;//维护食物的指针
enum DIRECTION _Dir;//蛇头的方向
enum GAME_STATUS _Status;//游戏状态
int _Socre;//当前获得分数
int _foodWeight;//默认每食物10分
int _SleepTime;//每⾛一步休眠时间
}Snake, * pSnake;


蛇的方向,可以一一列举,使用枚举:

//方向
enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};


游戏状态,可以一一列举,使用枚举:

//游戏状态
enum GAME_STATUS
{
	OK,//正常运行
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF,//咬到自己
	END_NOMAL//正常结束
};

游戏总流程

对于500到600行这个代码量比较大的项目,我们先对它进行一个规划,这样才能保证我们后面写代码时能够思路清晰

这里Humble做了一张思维导图,大家可以看一下~

游戏开始GameStart

我们先看游戏开始GameStart这个函数,它包含五个函数:

先看第一个:打印欢迎信息WelcomeToGame,因为它与游戏的实现关系不大,做起来也很简单,这里直接附上参考代码~

void WelcomeToGame()
{
	//欢迎信息
	SetPos(35, 10);
	printf("欢迎来到贪吃蛇小游戏\n");

	SetPos(38, 20);
	system("pause");

	system("cls");//清空之前的界面

	//功能介绍信息
	SetPos(15, 10);
	printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");

	SetPos(15, 11);
	printf("加速能得到更高的分数");

	SetPos(38, 20);
	system("pause");

	system("cls");
}


 

接下来第二个是创建地图 CreateMap

关于地图的创建,下面有必要来讲解一下:

前面在演示的时候,我们看过游戏的地图长这个样子~:

无论是打印墙体使用的字符□,还是打印蛇使用的字符●,或者打印食物使用的字符★

它们都是名为宽字符的存在

那么什么是宽字符?它与普通字符有什么区别?

所以我们必须先对宽字符进行了解

从大小上看。普通的字符是占一个字节的,这类宽字符是占用2个字节的
 

这点很重要,因为我们的地图是建立在坐标上的,宽字符的x轴占了2格,而y轴只占1格,我们要考虑好这点,再去设计比如 :地图的大小以及蛇与食物随机生成的坐标的规定等等

知道了这个,我们看一看宽字符是怎么打印的吧~

宽字符的打印

#include <stdio.h
#include<locale.h>

int main() 
{
setlocale(LC_ALL, "");
//这个setlocale函数以及它包含的头文件locale.h是为了将模式切换成本地环境,大家直接用就可以,不用深究~

wchar_t ch1 = L'●';
wchar_t ch2 = L'原';
wchar_t ch3 = L'神';
wchar_t ch4 = L'★';

printf("%c%c\n", 'a', 'b');
wprintf(L"%c\n", ch1);//wprintf是用来打印宽字符的,w即wide,宽的意思
wprintf(L"%c\n", ch2);//宽字符的打印要在%前加上L,别忘了哦~
wprintf(L"%c\n", ch3);
wprintf(L"%c\n", ch4);
return 0;
}

打印结果如下:

我们发现一个普通字符占一个字符的位置

但是打印一个宽字符,占用2个字符的位置

知道了宽字符,接下来我们还要对地图的坐标再进行一个了解,因为所有的东西都是建立在坐标上的,需要我们量化的嘛~

我们假设实现一个棋盘27行,58列的棋盘,再围绕地图画出墙

如下图:

正如上面讲宽字符时提到的:我们的地图是建立在坐标上的,宽字符的x轴占了2格,而y轴只占1格

好,知道了这些,那么这个创建地图的函数 CreateMap()也就不难写了,下面参考代码~

#define WALL L'□' //宽字符的定义前面要加上L
void CreateMap()
{
	int i = 0;


	//上
	SetPos(0, 0);
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}

	//下
	SetPos(0, 25);
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}

	//左
	for (i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}

	//右
	for (i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}

}

再接下来是第三个初始化蛇InitSnake( )

初始化状态,假设蛇的长度是5,蛇身的每个节点是 ●     ,在固定的一个坐标处开始出现
注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半出现在墙体中,另外一般在墙外的现象,坐标不好对齐

如何转化为代码的形式?

蛇最开始为5节,每节对应链表的一个节点,蛇的每一个节点都自己的坐标
创建5个节点,然后将每个节点存放在链表中进行管理创建完蛇身后,将蛇的每一节打印在屏幕上
再设置当前游戏的状态,蛇移动的速度,默认的方向(向右),初始成绩,蛇的状态,每个食物的分数

下面是参考代码:

 #define BODY L'●'  

void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	int i = 0;
	//创建蛇身节点,并初始化坐标
	//头插法
	for (i = 0; i < 5; i++)
	{
		//创建蛇身的节点
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}
		//设置坐标
		cur->next = NULL;
		cur->x = POS_X + 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)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}
	//初始化贪吃蛇数据
	ps->_SleepTime = 200;
	ps->_Socre = 0;
	ps->_Status = OK; ps->_Dir = RIGHT;
	ps->_foodWeight = 10;
}

GameStart包含的第四个子函数是:CreateFood


关于食物,就是在墙体内随机生成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印 ★

随机生成食物的坐标

下面有一些限制:
 x坐标必须是2的倍数
食物的坐标不能和蛇身每个节点的坐标重复

#define FOOD L'★' //食物打印的宽字符
void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;
again:
	//产生的x坐标应该是2的倍数,这样才可能和蛇头坐标对齐
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);
	pSnakeNode cur = ps->_pSnake;//获取指向蛇头的指针
	//食物不能和蛇身冲突
	while (cur)
	{
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}

	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); //创建食物

	if (pFood == NULL)
	{
		perror("CreateFood::malloc()");
		return;
	}

	else
	{
		pFood->x = x;
		pFood->y = y;
		SetPos(pFood->x, pFood->y);
		wprintf(L"%c", FOOD);
		ps->_pFood = pFood;
	}
}


GameStart的最后一个函数是PrintHelpInfo,负责打印文字信息

void PrintHelpInfo()
{
	SetPos(64, 15);
	printf("不能穿墙,不能咬到自己\n");
	SetPos(64, 16);
	printf("用↑.↓.←.→分别控制蛇的移动.");
	SetPos(64, 17);
	printf("F3 为加速,F4 为减速\n");
	SetPos(64, 18);
	printf("ESC :退出游戏. 空格键:暂停游戏.");
	SetPos(64, 20);
}

这样我们的Game Start函数需要的功能也就通过封装成各个子函数实现完辣,我们下面只需对它们进行复用放进GameStart就行~
 

游戏运行GameRun

下面来看一下游戏运行的函数GameRun吧~

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

void pause()//暂停
{
	while (1)
	{
		Sleep(300);
		if (KEY_PRESS(VK_SPACE))//空格键
		     break;
	}
}


void GameRun(pSnake ps)
{
	
	do
	{
		SetPos(64, 10); //每次进来都要对食物分数进行修正
		printf("得分:%d ", ps->_Socre);
		printf("每个食物得分:%d分", ps->_foodWeight);
		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_LEFT) && ps->_Dir != RIGHT)
		{
			ps->_Dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT)
		{
			ps->_Dir = RIGHT;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = END_NOMAL;
			break;
		}
		else if (KEY_PRESS(VK_F3))
		{
			if (ps->_SleepTime >= 50)
			{
				ps->_SleepTime -= 30;
				ps->_foodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			if (ps->_SleepTime < 350)
			{
				ps->_SleepTime += 30;
				ps->_foodWeight -= 2;
				if (ps->_SleepTime == 350)
				{
					ps->_foodWeight = 1;
				}
			}
		}
		//蛇每次一定之间要休眠的时间,时间短,蛇移动速度就快
		Sleep(ps->_SleepTime);
		SnakeMove(ps);
	} while (ps->_Status == OK);

虽然这个是三个函数最复杂的部分,但我们也是一个一个点,按照设计的流程来~

关于虚拟键检测按键状态,我们封装一个宏~

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

下面是蛇身的移动,这个很关键~,这里面有很多功能,我们可以将它们各自封装成函数再放进SnakeMove函数中:

先创建下一个节点,根据移动方向和蛇头的坐标,蛇移动到下⼀个位置的坐标
确定了下⼀个位置后,判断下一个位置是否是食物:NextIsFood

是食物就做:EatFood,如果不是食物:NoFood
蛇身移动后,判断此次移动是否会造成撞墙:KillByWall或者撞上自己:KillBySelf
 

void SnakeMove(pSnake ps)
{

	//创建下一个节点
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));

	//确定下一个节点的坐标,下一个节点的坐标根据,蛇头的坐标和方向确定
	switch (ps->_Dir)
	{
	case UP:
	{
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y = ps->_pSnake->y - 1;
	}
	break;
	case DOWN:
	{
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y = ps->_pSnake->y + 1;
	}
	break;
	case LEFT:
	{
		pNextNode->x = ps->_pSnake->x - 2;
		pNextNode->y = ps->_pSnake->y;
	}
	break;
	case RIGHT:
	{pNextNode->x = ps->_pSnake->x + 2;
	pNextNode->y = ps->_pSnake->y;
	}
	break;
	}

	if (NextIsFood(pNextNode, ps))
	{
		EatFood(pNextNode, ps);
	}

	else//如果没有食物
	{
		NoFood(pNextNode, ps);
	}

	KillByWall(ps);
	KillBySelf(ps);
}

下面是各个子函数的实现~:

int NextIsFood(pSnakeNode psn, pSnake ps)
{
	return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}

void EatFood(pSnakeNode psn, pSnake ps)
{
	//头插法
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;
	pSnakeNode cur = ps->_pSnake;

	//打印蛇
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}
	ps->_Socre += ps->_foodWeight;
	free(ps->_pFood);
	CreateFood(ps);
}

 //将下⼀个节点头插⼊蛇的⾝体,并将之前蛇⾝最后⼀个节点打印为空格,弃掉蛇身的最后⼀个节点
void NoFood(pSnakeNode psn, pSnake ps)
{
	//头插法
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;
	pSnakeNode cur = ps->_pSnake;
	//打印蛇
	while (cur->next->next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}
	//最后一个位置打印空格,然后释放节点
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;
}

int KillByWall(pSnake ps) //判断蛇头的坐标是否和墙的坐标冲突
{
	if ((ps->_pSnake->x == 0)
		|| (ps->_pSnake->x == 56)
		|| (ps->_pSnake->y == 0)
		|| (ps->_pSnake->y == 26))
	{
		ps->_Status = KILL_BY_WALL;
		return 1;
	}
	return 0;
}

int KillBySelf(pSnake ps) //判断蛇头的坐标是否和蛇⾝体的坐标冲突
{
	pSnakeNode cur = ps->_pSnake->next;
	while (cur)
	{
		if ((ps->_pSnake->x == cur->x)
			&& (ps->_pSnake->y == cur->y))
		{
			ps->_Status = KILL_BY_SELF;
			return 1;
		}
		cur = cur->next;
	}
	return 0;
}

呼,终于将最核心的逻辑搞定了~最后来看看游戏结束的函数GameEnd吧~

游戏结束GameEnd

游戏状态不再是OK的时候,要告知游戏结束的原因,并且释放蛇身节点
 

void GameEnd(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	SetPos(24, 12);
	switch (ps->_Status)
	{
	case END_NOMAL:
		printf("您主动退出游戏\n");
		break;
	case KILL_BY_SELF:
		printf("您撞上自己了 ,游戏结束!\n");
		break;
	case KILL_BY_WALL:
		printf("您撞墙了,游戏结束!\n");
		break;
	}
	//释放蛇身的节点
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
}

到这,我们整个贪吃蛇游戏的功能就全部实现了(鼓掌鼓掌)

写完后,大家也可以完整的游玩这个由我们亲手写的贪吃蛇小游戏,怎么样,是不是成就感满满,玩自己亲手做出来的游戏

最后是Humble的参考代码,分3个文件

参考代码

test.c

#define  _CRT_SECURE_NO_WARNINGS
#include "snake.h"


void test()
{
	int ch = 0;
	{
		Snake snake = { 0 };//创建贪吃蛇


		GameStart(&snake);
		GameRun(&snake);
		GameEnd(&snake);

		SetPos(20, 15);//设置光标的位置
		printf("再来一局吗?(Y/N):");

		ch = getchar();
		getchar();//清理\n

	} while (ch == 'Y');
}



int main()
{
	//修改适配本地中文环境
	setlocale(LC_ALL, "");

	test();//贪吃蛇游戏的测试

	return 0;
}

snake.h

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

//宽字符的定义
#define WALL L'□'
#define BODY L'●' 
#define FOOD L'★'

//蛇的初始位置
#define POS_X 24
#define POS_Y 5

//虚拟键
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)

//方向
enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};


//游戏状态
enum GAME_STATUS
{
	OK,//正常运行
	KILL_BY_WALL,//撞墙结束
	KILL_BY_SELF,//咬到自己结束
	END_NOMAL//按键结束
};


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


typedef struct Snake
{
	pSnakeNode  _pSnake;//维护整条蛇的指针
	pSnakeNode  _pFood;//维护食物的指针

	enum DIRECTION  _Dir;//蛇头的方向
	enum GAME_STATUS  _Status;//游戏状态

	int  _Socre;//当前获得分数
	int  _foodWeight;//默认每个食物10分
	int  _SleepTime;//每走一步休眠时间
}Snake, * pSnake;




//设置光标的坐标
void SetPos(int x, int y);

//欢迎界面
void WelcomeToGame();

//创建地图
void CreateMap();

//初始化蛇
void InitSnake(pSnake ps);

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

//打印右侧界面的文字说明
void PrintHelpInfo();

//暂停响应
void pause();

//下一个节点是食物
int NextIsFood(pSnakeNode psn, pSnake ps);

//吃食物
void EatFood(pSnakeNode psn, pSnake ps);

//不吃食物
void NoFood(pSnakeNode psn, pSnake ps);

//撞墙检测
int KillByWall(pSnake ps);

//撞自身检测
int KillBySelf(pSnake ps);

//蛇的移动
void SnakeMove(pSnake ps);

//游戏开始
void GameStart(pSnake ps);

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

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

snake.c

#define  _CRT_SECURE_NO_WARNINGS
#include "snake.h"


//设置光标的位置的函数
void SetPos(int x, int y)
{
	HANDLE hanlde = GetStdHandle(STD_OUTPUT_HANDLE);	//获得设备句柄
	COORD pos = { x, y };	//根据句柄设置光标的位置
	SetConsoleCursorPosition(hanlde, pos);
}



void WelcomeToGame()
{
	//欢迎信息
	SetPos(35, 10);
	printf("欢迎来到贪吃蛇小游戏\n");

	SetPos(38, 20);
	system("pause");

	system("cls");//清空之前的界面

	//功能介绍信息
	SetPos(15, 10);
	printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");

	SetPos(15, 11);
	printf("加速能得到更高的分数");

	SetPos(38, 20);
	system("pause");

	system("cls");
}


void CreateMap()
{
	int i = 0;


	//上
	SetPos(0, 0);
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}

	//下
	SetPos(0, 25);
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}

	//左
	for (i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}

	//右
	for (i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}

}


void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	int i = 0;
	//创建蛇身节点,并初始化坐标
	//头插法
	for (i = 0; i < 5; i++)
	{
		//创建蛇身的节点
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}
		//设置坐标
		cur->next = NULL;
		cur->x = POS_X + 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)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}
	//初始化贪吃蛇数据
	ps->_SleepTime = 200;
	ps->_Socre = 0;
	ps->_Status = OK; ps->_Dir = RIGHT;
	ps->_foodWeight = 10;
}


void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;
again:
	//产生的x坐标应该是2的倍数,这样才可能和蛇头坐标对齐
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);
	pSnakeNode cur = ps->_pSnake;//获取指向蛇头的指针
	//食物不能和蛇身冲突
	while (cur)
	{
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}

	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); //创建食物

	if (pFood == NULL)
	{
		perror("CreateFood::malloc()");
		return;
	}

	else
	{
		pFood->x = x;
		pFood->y = y;
		SetPos(pFood->x, pFood->y);
		wprintf(L"%c", FOOD);
		ps->_pFood = pFood;
	}
}



void PrintHelpInfo()
{
	SetPos(64, 15);
	printf("不能穿墙,不能咬到自己\n");
	SetPos(64, 16);
	printf("用↑.↓.←.→分别控制蛇的移动.");
	SetPos(64, 17);
	printf("F3 为加速,F4 为减速\n");
	SetPos(64, 18);
	printf("ESC :退出游戏. 空格键:暂停游戏.");
	SetPos(64, 20);
}






void pause()//暂停
{
	while (1)
	{
		Sleep(300);
		if (KEY_PRESS(VK_SPACE))//空格键
		     break;
	}
}


int NextIsFood(pSnakeNode psn, pSnake ps)
{
	return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}


void EatFood(pSnakeNode psn, pSnake ps)
{
	//头插法
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;
	pSnakeNode cur = ps->_pSnake;

	//打印蛇
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}
	ps->_Socre += ps->_foodWeight;
	free(ps->_pFood);
	CreateFood(ps);
}


void NoFood(pSnakeNode psn, pSnake ps)
{
	//头插法
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;
	pSnakeNode cur = ps->_pSnake;
	//打印蛇
	while (cur->next->next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}
	//最后一个位置打印空格,然后释放节点
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;
}


int KillByWall(pSnake ps)
{
	if ((ps->_pSnake->x == 0)
		|| (ps->_pSnake->x == 56)
		|| (ps->_pSnake->y == 0)
		|| (ps->_pSnake->y == 26))
	{
		ps->_Status = KILL_BY_WALL;
		return 1;
	}
	return 0;
}


int KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake->next;
	while (cur)
	{
		if ((ps->_pSnake->x == cur->x)
			&& (ps->_pSnake->y == cur->y))
		{
			ps->_Status = KILL_BY_SELF;
			return 1;
		}
		cur = cur->next;
	}
	return 0;
}


void SnakeMove(pSnake ps)
{

	//创建下一个节点
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));

	//确定下一个节点的坐标,下一个节点的坐标根据,蛇头的坐标和方向确定
	switch (ps->_Dir)
	{
	case UP:
	{
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y = ps->_pSnake->y - 1;
	}
	break;
	case DOWN:
	{
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y = ps->_pSnake->y + 1;
	}
	break;
	case LEFT:
	{
		pNextNode->x = ps->_pSnake->x - 2;
		pNextNode->y = ps->_pSnake->y;
	}
	break;
	case RIGHT:
	{pNextNode->x = ps->_pSnake->x + 2;
	pNextNode->y = ps->_pSnake->y;
	}
	break;
	}

	if (NextIsFood(pNextNode, ps))
	{
		EatFood(pNextNode, ps);
	}

	else//如果没有食物
	{
		NoFood(pNextNode, ps);
	}

	KillByWall(ps);
	KillBySelf(ps);
}



void GameStart(pSnake ps)
{


	//设置控制台的信息,窗口大小,窗口名
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//隐藏光标
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(handle, &CursorInfo);
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(handle, &CursorInfo);

	WelcomeToGame();//打印欢迎信息

	CreateMap();	//绘制地图

	InitSnake(ps);	//初始化蛇

	CreateFood(ps);//创建食物
	
	PrintHelpInfo();//打印右侧帮助信息
}





void GameRun(pSnake ps)
{
	
	do
	{
		SetPos(64, 10);
		printf("得分:%d ", ps->_Socre);
		printf("每个食物得分:%d分", ps->_foodWeight);
		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_LEFT) && ps->_Dir != RIGHT)
		{
			ps->_Dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT)
		{
			ps->_Dir = RIGHT;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = END_NOMAL;
			break;
		}
		else if (KEY_PRESS(VK_F3))
		{
			if (ps->_SleepTime >= 50)
			{
				ps->_SleepTime -= 30;
				ps->_foodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			if (ps->_SleepTime < 350)
			{
				ps->_SleepTime += 30;
				ps->_foodWeight -= 2;
				if (ps->_SleepTime == 350)
				{
					ps->_foodWeight = 1;
				}
			}
		}
		//蛇每次一定之间要休眠的时间,时间短,蛇移动速度就快
		Sleep(ps->_SleepTime);
		SnakeMove(ps);
	} while (ps->_Status == OK);//游戏状态是OK则继续游戏


}

void GameEnd(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	SetPos(24, 12);
	switch (ps->_Status)
	{
	case END_NOMAL:
		printf("您主动退出游戏\n");
		break;
	case KILL_BY_SELF:
		printf("您撞上自己了 ,游戏结束!\n");
		break;
	case KILL_BY_WALL:
		printf("您撞墙了,游戏结束!\n");
		break;
	}
	//释放蛇身的节点
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
}

结语

好了,今天的这个贪吃蛇游戏的实现分享就到这里了

作为C语言部分的最后一篇博客(大概),虽然心情复杂,但不知道该说些什么

总之还是那句话:在学习编程的道路上Humble与各位同行,加油吧各位!

希望大家动动小手帮我点个免费的赞或者关注(感谢感谢),也欢迎大家订阅我的专栏呀~

让我们在接下来的时间里一起成长,一起进步吧!

1d8bd2383fe54a7aa576bdd8d41dc462.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1415673.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux cat,tac,more,head,tail命令 查看文本

目录 一. cat 和 tac命令二. head 和 tail 命令三. more命令 一. cat 和 tac命令 cat&#xff1a;用来打开文本文件&#xff0c;从上到下的顺序显示文件内容。tac&#xff1a;用法和cat相同&#xff0c;只不过是从下到上逆序的方式显示文件内容。当文件的内容有很多的时候&…

canvas绘制旋转的大风车

查看专栏目录 canvas实例应用100专栏&#xff0c;提供canvas的基础知识&#xff0c;高级动画&#xff0c;相关应用扩展等信息。canvas作为html的一部分&#xff0c;是图像图标地图可视化的一个重要的基础&#xff0c;学好了canvas&#xff0c;在其他的一些应用上将会起到非常重…

vue中的Mutations

目录 一&#xff1a;介绍 二&#xff1a;例子 一&#xff1a;介绍 Vuex 中的 mutation 非常类似于事件&#xff1a; 每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的函数&#xff0c;并且它会接受 sta…

操作系统-线程的实现方式和多线程模型(用户级线程 内核级线程 多线程模型的情况)和线程的状态,转换,组织,控制

文章目录 线程的实现方式和多线程模型总览线程的实现方式用户级线程内核级线程多线程模型一对一多对一多对多 小结 线程的状态,转换,组织,控制总览 线程的状态与转换线程的组织与控制 线程的实现方式和多线程模型 总览 线程的实现方式 用户级线程 程序自己通过自己设计的线程…

03 Redis之命令(基本命令+Key命令+String型Value命令与应用场景)

Redis 根据命令所操作对象的不同&#xff0c;可以分为三大类&#xff1a;对 Redis 进行基础性操作的命令&#xff0c;对 Key 的操作命令&#xff0c;对 Value 的操作命令。 3.1 Redis 基本命令 一些可选项对大小写敏感, 所以应尽量将redis的所有命令大写输入 首先通过 redis-…

一行命令在 wsl-ubuntu 中使用 Docker 启动 Windows

在 wsl-ubuntu 中使用 Docker 启动 Windows 0. 背景1. 验证我的系统是否支持 KVM&#xff1f;2. 使用 Docker 启动 Windows3. 访问 Docker 启动的 Windows4. Docker Hub 地址5. Github 地址 0. 背景 我们可以在 Windows 系统使用安装 wsl-ubuntu&#xff0c;今天玩玩在 wsl-ub…

数据库查询3

目录 1. 多表查询 1.1.1 介绍 1.1.2 分类 1.2 内连接 1.3 外连接 1.4 子查询 1.4.1 介绍 1.4.2 标量子查询 1.4.3 列子查询 1.4.4 行子查询 1.4.5 表子查询 2. 事务 2.1 操作 2.2 四大特性 数据库总结2 数据库总结1 1. 多表查询 1.1.1 介绍 多表查询&#xff…

RLHF学习

整体流程 三个步骤分解&#xff1a; 预训练一个语言模型 (LM) &#xff1b;聚合问答数据并训练一个奖励模型 (Reward Model&#xff0c;RM) &#xff1b;用强化学习 (RL) 方式微调 LM。 RW RM 的训练是 RLHF 区别于旧范式的开端。这一模型接收一系列文本并返回一个标量奖励&…

1、PDManer 快速入门

文章目录 序言一、快速入门1.1 PDMan 介绍1.2 特点1.3 下载和安装 小结 序言 本人长期以来一直从事于应用软件的研发以及项目实施工作&#xff0c;经常做数据库建模&#xff08;数据表设计&#xff09;。有一款称心如意的数据库建模工具&#xff0c;自然能够事半功倍&#xff0…

【算法路线图】算法小抄题解-一文理解算法体系-费元星

做研发多年&#xff0c;对算法理解一直不够成体系&#xff0c;基本是每次在面试的时候才会去重点看算法&#xff0c;刷一些题&#xff0c;因此在这里&#xff0c;把我多年的总结发出来&#xff0c;希望晚辈站在一个高的位置学习。 最新链接&#xff1a;有道云笔记 -----------…

阿里云部署配置幻兽帕鲁Palworld联机服务器详细教程

阿里云作为国内领先的云计算服务提供商&#xff0c;为企业和个人提供了丰富的云服务。本文将为大家详细介绍如何在阿里云上配置幻兽帕鲁Palworld联机服务器&#xff0c;以便与更多玩家共同体验游戏的乐趣。 第一步&#xff1a;登录服务器创建页 1、进入幻兽帕鲁联机服务快速部…

设计模式⑩ :用类来实现

文章目录 一、前言二、Command 模式1. 介绍2.应用3. 总结 三、Interpreter 模式1. 介绍2. 应用3. 总结 参考文章 一、前言 有时候不想动脑子&#xff0c;就懒得看源码又不像浪费时间所以会看看书&#xff0c;但是又记不住&#xff0c;所以决定开始写"抄书"系列。本系…

GCP :Stackdriver Logging

官方介绍 Logs Explorer 利用 Logs Explorer&#xff0c;您可以通过灵活的查询语句、丰富的直方图视觉呈现、简单的字段浏览器以及保存查询的功能&#xff0c;对日志进行搜索、排序和分析。设置提醒以便在您包含的日志中出现特定消息时通知您&#xff0c;或者使用 Cloud Moni…

GPT-SoVITS 测试

开箱直用版&#xff08;使用 AutoDL&#xff09; step1 打开地址 https://www.codewithgpu.com/i/RVC-Boss/GPT-SoVITS/GPT-SoVITS-Official 选择 AutoDL创建实例&#xff0c;选择 3080ti 机器 step2 创建好实例之后&#xff0c;进入命令行&#xff0c;输入命令 echo {}>…

Kubernetes成本优化

云原生可以帮助团队更精细化利用资源&#xff0c;但如果缺乏工具的帮助&#xff0c;很难采取适当的措施优化资源的使用。本文介绍了若干用于可视化Kubernetes资源使用情况的工具&#xff0c;并且可以自定义策略优化资源使用&#xff0c;实现更好的成本优化。原文: Kubernetes C…

【计算机二级考试C语言】C强制类型转换

C 强制类型转换 强制类型转换是把变量从一种类型转换为另一种数据类型。例如&#xff0c;如果您想存储一个 long 类型的值到一个简单的整型中&#xff0c;您需要把 long 类型强制转换为 int 类型。您可以使用强制类型转换运算符来把值显式地从一种类型转换为另一种类型&#x…

【NodeJS】004- NodeJS的模块化与包管理工具

模块化 1. 介绍 1.1.什么是模块化与模块 ? 将一个复杂的程序文件依据一定规则(规范)拆分成多个文件的过程称之为 模块化 其中拆分出的 每个文件就是一个模块 ,模块的内部数据是私有的,不过模块可以暴露内部数据以便其他模块使用 1.2 什么是模块化项目 ? 编码时是按照模…

openssl3.2 - 测试程序的学习 - test\aesgcmtest.c

文章目录 openssl3.2 - 测试程序的学习 - test\aesgcmtest.c概述笔记能学到的流程性内容END openssl3.2 - 测试程序的学习 - test\aesgcmtest.c 概述 openssl3.2 - 测试程序的学习 aesgcmtest.c 工程搭建时, 发现没有提供 test_get_options(), cleanup_tests(), 需要自己补上…

公考之判断推理(一、图形推理)

一、前言 判断推理这一题型主要具体分为四种题型&#xff1a; 1.图形推理 2.类比推理 3.定义判断 4.逻辑判断每种题型做题方法又不一样。 才本文采用总分的形式结构。 每一小标题的下面紧接着就是总结。二、图形推理常见的命题形式 图形推理常见的命题形式&#xff1a; 1.…

路飞项目--04

分析后端接口 # 用户板块--原型图--分析需要写哪些接口 多方式登录接口 短信登录接口 发送短信接口 短信注册接口 校验手机号是否注册接口 手机号是否存在接口 思路&#xff1a; 1 用了全局异常捕获&#xff0c;直接抛出异常报错 2 路由用了自定义路由&…