贪吃蛇的简单实现(c语言)

前言:学完了C语言的基础语法,和一点数据结构的知识,拿贪吃蛇来练练手,并熟悉以前的知识。写完之后,有一种成就感,为以后的学习饱满激情。

注意这里的讲解是由部分到整体的思路。

目录

控制台不能是终端:

mode和title命令

GetStdHandle函数

隐藏光标函数(HideCurso)的实现:

GetConsoleCursorInfo函数

SetConsoleCursorInfo函数

综上,可以利用以上函数分装成一个函数隐藏光标HideCurso:

在窗口内任意位置打印(SetPos)的函数的实习:

SetCursorPostion函数:

SetPos函数的实现:

实现KEY_PRESS来检测按键被按的情况

按键检测函数GetAsynckeyState

KEY_PRESS的实现:

setlocale函数

游戏实现的大体框架:

游戏开始GameStart实现

1.欢迎界面WelcomeGame的实现

2.创建地图CreateMap的实现

3.CreateSnake创建蛇

3.1蛇身体节点的定义

[3.2 蛇的定义](#3.2 蛇的定义)

3.3创建蛇CreateSnake的实现

3.4创建食物并打印,CreateFood函数的实现

3.4将上面的函数分装进函数IniteSnake

4.打印蛇,PrintSnake函数的实现

5.最后一步将上述的函数分装进GameStart函数

游戏运行GameRun函数的实现

[GameRun 中需要实现的逻辑](#GameRun 中需要实现的逻辑)

PrintHelpInfo打印帮助信息函数的实现:

PrintScore的实现

SnakeMove的实现

Pause暂停函数的实现:

SnakeNext蛇走一步函数的实现:

接下来说一下NextIsFood函数的实现:

IsKill函数的实现:

游戏结束GameEnd函数的实现:

整个游戏逻辑的运行逻辑的实现:

整个游戏的源码(有感兴趣的可以自取):


这里先放一张最后的成果图和一段视频来展示效果

贪吃蛇游戏

这里先讲一些可能需要用到的windows的控制台函数和一些系统操作。

控制台不能是终端:

终端的控制台:

修改过程:

mode和title命令

	system("title 贪吃蛇");
	system("mode con cols=100 lines=30");

mode改变控制台窗口的大小,cols表示行,lines表示列。

title就是改变控制台的名称。

效果展示:

GetStdHandle函数

其返回值类型是HANDLE(是一个指针),获得一个句柄。

//获得一个句柄
HANDLE hOutPut = GetStdHandle(STD_OUTPUT_HANDLE);

CONSOLE_CURSOR_INFO CursorInfo;//CONSOLE_CURSOR_INFO是控制台光标标的结构体类型
//CursorInfo是我们创建的变量

CONSOLE_CURSOR_INFO是一个结构体其中有两个成员,dwSize 和 dVisible ,dwSize 表示光标占一个单位光标高度的百分比,比如下图。

dVisiable 表示光标是否可见,将光标的信息为不可见:

CursorInfo.bVisible = false;

隐藏光标函数(HideCurso)的实现:

GetConsoleCursorInfo函数

获取控制台的光标信息:

	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(hOutPut, &CursorInfo);//将光标信息放入CursorInfo这个变量中

SetConsoleCursorInfo函数

设置控制台光标的信息:

将光标的信息设置为不可见。

CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutPut, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(hOutPut, &CursorInfo);//将CursorInfo中的数据设置为控制台的光标的信息。

综上,可以利用以上函数分装成一个函数隐藏光标HideCurso:

void HideCursor()
{
	HANDLE hOutPut = NULL;
	hOutPut = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(hOutPut, &CursorInfo);
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(hOutPut, &CursorInfo);
}

效果:

上图看不见光标。

在窗口内任意位置打印(SetPos)的函数的实习:

SetCursorPostion函数:

他有两个参数一个是句柄Ll类型为HANDLE,另一个类型为COORD这是一个结构体类型。

typedef sruct COORD
{
    short x;
    short y;
}COORD;

定位光标的位置,说到位置这里就不得不聊聊控制台的坐标系的定义

这里注意:一个单位的x不等于一个的单位的y,两个单位的x才等于一个单位的y。

SetPos函数的实现:

void SetPos(short x, short y)
{
    HANDLE hOutPut = NULL;
    hOutPut = GetStdHandle(STD_OUTPUT_HANDLE);
    COORD pos = { x,y };
    SetConsoleCursorPosition(hOutPut, pos);
}

调用函数SetPos可以直接将光标定位到你给的坐标处。

实现KEY_PRESS来检测按键被按的情况

按键检测函数GetAsynckeyState

short GetAsyncKeyState(int vKey);

他的返回值是short,如果按下了一个间他会返回一个二进制形势下最低位为1的数,否则为0。

虚拟键码:这里会用到的比如,

上:VK_UP

下:VK_DOWN

左:VK_LEFT

右:VK_RIGHT

f3(加速):VK_F3

f4(减速):VK_F4

空格暂停:SPACE

esc退出:ESCAE

KEY_PRESS的实现:

#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)

如果vk这个虚拟键位代表的按键被按过,则返回1,否则返回0。

setlocale函数

setlocale(LC_ALL, "");//可以将模式改为当前所在地区的模式,可以打印一些特殊的字符

int main()
{
	char* ret =  setlocale(LC_ALL, "");
	printf("%s\n", ret);
	return 0;
}

宽字符:

一个宽字符是两个字符的大小。

	setlocale(LC_ALL, "");
	printf("ab\n");
	wprintf(L"%lc", L'我');

将C改为当前地区的模式时,可以打印宽字符。

宽字符打印与普通字符打印的区别

|------|------------------------------------------------------|----------------------------------------------|
| | 宽字符 | 普通字符 |
| 使用函数 | wprintf | printf |
| 换位符 | %lc %ls | %c %s |
| 使用 | wprintf(L"%ls",L"helloworld"); wprintf(L"%lc",L'a'); | printf("%s","helloworld"); printf("%c",'a'); |

游戏实现的大体框架:

游戏大揽


游戏开始GameStart实现

1.欢迎界面WelcomeGame的实现

void WelcomeGame()
{
    system("mode con cols=100 lines=30");
    system("title 贪吃蛇");
    HideCursor();
    SetPos(35, 15);
    wprintf(L"%ls", L"欢迎来到贪吃蛇小游戏");
    SetPos(36, 22);
    system("pause");
    system("cls");
    SetPos(30, 12);
    wprintf(L"%ls", L"你可以用↑.↓.←.→来控制蛇的移动");
    SetPos(30, 13);
    wprintf(L"%ls", L"F3加速,F4减速");
    SetPos(36, 22);
    system("pause");
    system("cls");
}

这里实现的结果就是大揽里的前两周照片,SetPos,HideCurso两个函数再前面已经实现完了这里就不讲了。

2.创建地图CreateMap的实现

#define WALL L'□'
void CreateMap()
{
	for (int i = 1; i <= 29; i++)
	{
		wprintf(L"%lc", WALL);
	}
	SetPos(0, 26);
	for (int i = 1; i <= 29; i++)
	{
		wprintf(L"%lc", WALL);
	}

	for (int i = 1; i <= 26; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	for (int i = 1; i <= 26; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
}

注意:system("mode con cols=100 lines=30");这是已开设我们设置的窗口大小,这里我们的地图大小为 (58,27)坐标从零开始并且2x=y并且宽字符的宽度为两个x,所以一行打印29个墙就可以了。

还有一点需要注意,以上代码打印出的墙的x坐标的值都为偶数,所以在后面代码中蛇的节点的坐标和食物的坐标的x值也应为偶数,若为偶数,那么蛇再装到上面墙的是时候总是撞在墙与墙之间的空隙中,影响也不大,但必须保证蛇的x坐标与食物的x坐标都为奇数或都为偶数,否则吃不到食物。

表示墙的这个字符可以再输入法中找到,

3.CreateSnake创建蛇

蛇的身体是用链表的一个一个的节点组成的来的。

3.1蛇身体节点的定义

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

其中的x,y表示这个节点在控制台中的坐标。

这里食物的定义与蛇的节点的定义一样。

3.2 蛇的定义

enum DIRECTION
{
	UP = 1,//上
	DOWN,//下
	RIGHT,//左
	LEFT//右
};
enum STATUS
{
	OK = 1,//状态正常
	KILL_BY_WALL,//撞墙死亡
	KILL_BY_SELF,//自己要到自己死亡
	END_NORMAL//按esc退出
};
typedef struct Snake
{
	pSnakeNode _psnake;//指向蛇的头节点的指针
	int sleep_time;//Sleep(sleep_time)来控制蛇的速度
	enum DIRECTION dir;//蛇的方向
	pSnakeNode _pfood;//一个指向食物的指针
	int food_score;//一个食物的分数
	int score;//总分
	enum STATUS status;//蛇当前的装态
}Snake,* pSnake;

3.3创建蛇CreateSnake的实现

void CreateSnake(pSnake ps)
{
	ps->_psnake = NULL;
	for (int i = 0; i < 5; i++)
	{
		pSnakeNode tmp = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (tmp == NULL)
		{
			perror("CreateSnake():malloc:");
			return;
		}
		tmp->next = NULL;
		if (ps->_psnake == NULL)
		{
			tmp->x = 10;
			tmp->y = 4;
			ps->_psnake = tmp;
		}
		else
		{
			tmp->x = 10 + 2 * i;
			tmp->y = 4;
			tmp->next = ps->_psnake;
			ps->_psnake = tmp;
		}
	}
}

运用for循环创建5个节点作为蛇的身体,注意这五个节点每两个节点的坐标应该相邻,确保蛇的身体是连续的。还有蛇的节点坐标别等于墙的坐标。

注意:这里生成的蛇的节点的坐标都是偶数,那么食物的x坐标必须为偶数,否则蛇的头一半吃到食物另一半吃不到食物,就等于永远吃不到食物,就是一个bug了。

3.4创建食物并打印,CreateFood函数的实现

//注意rand的使用需要有srand((unsigned)time(NULL))这一句,这里没有是因为在源码的main函数中
void RandPos(pSnakeNode* tmp)
{
	int x1 = 0;
	int y1 = 0;
	do
	{
		x1 = rand() % 53 + 2;
		y1 = rand() % 25 + 1;
	} while (x1%2);//保证食物的x坐标为偶数
	tmp->x = x1;
	tmp->y = y1;
}
#define FOOD L'★'
void CreateFood(pSnake ps)
{
	pSnakeNode tmp = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (tmp == NULL)
	{
		perror("CreateFood():malloc:");
		return;
	}
	again:
		RandPos(tmp);
		pSnakeNode cur = ps->_psnake;
        //生成的食物的坐标不能与蛇的节点的坐标一样
		while (cur)
		{
			if (tmp->x == cur->x && tmp->y == cur->y)
				goto again;
			cur = cur->next;
		}
		ps->_pfood = tmp;
		SetPos(tmp->x, tmp->y);
        //打印食物
		wprintf(L"%lc", FOOD);
		return;
}

RandPos函数是随机生成一个坐标,这里需要注意的是生成的坐标的范围,因为我想要的地图的大小为58(x)*27(y)去除墙的所以x的范围为[2,54],y的范围为[1,25],注意这里生成的食物x坐标的奇偶性要与蛇的节点的x保持一致。

注意:生成的食物的坐标不能与蛇的节点的坐标一样。

3.4将上面的函数分装进函数IniteSnake

void IniteSnake(pSnake ps)
{

	CreateSnake(ps);
	CreateFood(ps);
    //以下的为其他初始化蛇的信息
	ps->dir = RIGHT;
	ps->status = OK;
	ps->sleep_time = 200;
	ps->food_score = 10;
	ps->score = 0;
}

4.打印蛇,PrintSnake函数的实现

上面已经初始化完了蛇的节点,那么就可以着手打印了。

void PrintSnake(pSnake ps)
{
	pSnakeNode cur = ps->_psnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
}

初始化完了节点,打印就很简单了,定位光标位置直接按链表的顺序直接打印就完了。

5.最后一步将上述的函数分装进GameStart函数

void GameStart(pSnake ps)
{

	WelcomeGame();
	CreateMap();
	IniteSnake(ps);
	PrintSnake(ps);
    //getchar();可以用getchar函数来观察打印的效果

}

辛苦了这么久看一下打印效果:

游戏运行GameRun函数的实现

这里采用总分的方式来挨个实现:

GameRun 中需要实现的逻辑

void GameRun(pSnake ps)
{
	PrintHelpInfo();//打印游戏提示信息
	do
	{
		PrintScore(ps);//打印游戏分数
		SnakeMove(ps);//蛇的移动
		IsKill(ps);//判断蛇的状态并修改蛇的状态
		Sleep(ps->sleep_time);
	} while (ps->status==OK);//蛇为其他状态时,跳出循环
	return;
}

简单的一句话总结就是,蛇每走一步判断一下状态并更新一下分数。

PrintHelpInfo打印帮助信息函数的实现:

void PrintHelpInfo()
{
	SetPos(60, 20);
	wprintf(L"%ls", L"你可以用↑.↓.←.→来控制蛇的移动");
	SetPos(60, 21);
	wprintf(L"%ls", L"F3加速,得分增加;F4减速,得分减少");
	SetPos(60, 22);
	wprintf(L"%ls", L"空格是暂停");
	SetPos(60, 23);
	wprintf(L"%ls", L"ESC是退出游戏");
}

这里比较简单,需要注意的就是找个合适的位置打印。

PrintScore的实现

void PrintScore(pSnake ps)
{
	SetPos(60, 5);
	printf("食物分数:%2d 总分数:%d", ps->food_score, ps->score);
}

每次打印时都会覆盖上次打印的数据。

SnakeMove的实现

这里比较重要,也比较难一些。

void SnakeMove(pSnake ps)
{
    //以下的判断按键按的情况,并做出相应的反应
    //蛇现在的运动方向为下,那么我们知道,按上时不改变蛇的方向
	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_F3))
	{
        //f3是加速,将系统休眠时间改小就可以了,别忘了每个食物的加分,并限制一下,不能一直加速
		if (ps->sleep_time > 100)
		{
			ps->sleep_time -= 20;
			ps->food_score += 2;
		}
	}
	else if (KEY_PRESS(VK_F4))
	{
        //f4跟f3一样
		if (ps->sleep_time < 300&&ps->food_score>10)
		{
			ps->sleep_time += 20;
			ps->food_score -= 2;
		}
	}
	else if (KEY_PRESS(VK_SPACE))
	{
		Pause();//这个函数实现暂停
	}
	else if (KEY_PRESS(VK_ESCAPE))
	{
        //只需要改变蛇状态就可以了
		ps->status = END_NORMAL;
	}
	SnakeNext(ps);//蛇走下一步
}

注意: 蛇现在的运动方向为下,那么我们根据以前玩贪吃蛇的知识知道,按上时不改变蛇的方向

Pause暂停函数的实现:

void Pause()
{
	while (1)
	{
		if (KEY_PRESS(VK_SPACE))
			break;
		Sleep(200);
	}
}

实现思路:让系统一致Sleep就可以了,当再次检测到你按下空格时,则跳出循环。

SnakeNext蛇走一步函数的实现:

void SnakeNext(pSnake ps)
{
    //创建一个新节点,这个新节点可以理解为新的蛇头
	pSnakeNode movenext = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (movenext == NULL)
	{
		perror("SnakeNext():malloc:");
		return;
	}
    //基于蛇头的坐标和蛇移动的方向,确定新节点的坐标
	if (ps->dir == UP)
	{
		movenext->x = ps->_psnake->x;
		movenext->y = ps->_psnake->y-1;
	}
	else if (ps->dir == DOWN)
	{
		movenext->x = ps->_psnake->x;
		movenext->y = ps->_psnake->y + 1;
	}
	else if (ps->dir == LEFT)
	{
		movenext->x = ps->_psnake->x-2;
		movenext->y = ps->_psnake->y;
	}
	else if (ps->dir == RIGHT)
	{
		movenext->x = ps->_psnake->x+2;
		movenext->y = ps->_psnake->y;
	}
    //判断蛇的下一步是否是食物,如果是食物那么,就将食物的这个节点改为新的蛇头,并释放掉movenext
	if (NextIsFood(ps, movenext))
	{
		SetPos(ps->_pfood->x, ps->_pfood->y);
        //将食物的位置覆盖式打印为蛇的身体,表示被吃掉
		wprintf(L"%lc", BODY);
		ps->_pfood->next = ps->_psnake;
		ps->_psnake = ps->_pfood;
        //总分增加
		ps->score += ps->food_score;
        free(movenext);
        //食物被吃掉,这在重新创建一个新的食物。
		CreateFood(ps);
	}
	else
	{   //下一步没有吃到食物,则将movenext节点头插到蛇的链表中
		movenext->next = ps->_psnake;
		ps->_psnake = movenext;
		pSnakeNode cur = ps->_psnake;
        //循环打印蛇身,注意跳出循环的条件,当cur指向倒数第二个节点时跳出
		while (cur->next->next != NULL)
		{
			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);
		wprintf(L"  ");
        //一定要释放最后一个节点,因为加上了一个节点movenext
		free(cur->next);
        //现在cur指向尾结点,将尾结点的next置为NULL
		cur->next = NULL;
	}
}

思路:根据创建一个新的节点,这个节点是由蛇的方向和头结点的坐标确定的;然后,如果movenext的坐标与食物一样,表示吃到食物,直接将食物的节点头插进蛇的链表;如果movenext与食物的坐标没重合,那么就将movenext头插进链表,并将原链表的尾结点释放。

接下来说一下NextIsFood函数的实现:
int NextIsFood(pSnake ps, pSnakeNode movenext)
{
	if (ps->_pfood->x == movenext->x && ps->_pfood->y == movenext->y)
		return 1;
	else
		return 0;
}

如果与食物坐标重合就返回1,否则返回0.不用怕,这个函数就是很简单。

IsKill函数的实现:

void IsKill(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;
	}
    //判断是否要到自己
	else
	{
        //要从蛇头的下一个节点开始,遍历链表
		pSnakeNode cur = ps->_psnake->next;
		while (cur)
		{
			if (ps->_psnake->x == cur->x && ps->_psnake->y == cur->y)
			{
				ps->status = KILL_BY_SELF;
				return;
			}
			cur = cur->next;
		}
	}
	return;
}

思路:判断是否撞墙,是否要咬自己,如果为真则改变蛇的相应状态,并跳出函数。

这里说一些为什么在判断是否咬到自己这种情况时,不能从蛇头开始遍历,如果是这样,则cur和ps->_psnake都指向蛇头,坐标重合,状态被修改,结果就是一进游戏你就咬到自己,所从蛇头的下一个节点开始遍历。


游戏结束GameEnd函数的实现:

这里实现逻辑比较简单,直接上代码:

void GameEnd(pSnake ps)
{
	SetPos(30, 14);
    //根据蛇的状态打印信息
	if (ps->status== END_NORMAL)
	{
		printf("正常退出\n");
	}
	else if (ps->status == KILL_BY_SELF)
	{
		printf("咬到自己,死亡\n");
	}
	else if (ps->status == KILL_BY_WALL)
	{
		printf("撞到了墙,死亡\n");
	}
    //将开辟的空间释放
	pSnakeNode cur = ps->_psnake;
	pSnakeNode prev = cur;
	while (cur)
	{
		prev = cur;
		cur = cur->next;
		free(prev);
	}
    free(ps->_pfood);
}

注意:释放链表的方式,使用的是前后指针的方法,如果不太懂可以看我双链表的博客。


整个游戏逻辑的运行逻辑的实现:

void test()
{
	char op;
	do
	{
		Snake s;
		GameStart(&s);
		GameRun(&s);
		GameEnd(&s);
		SetPos(30, 15);
		wprintf(L"%ls",L"是否再来一局?(Y/N):");
		scanf(" %c", &op);
	} while (op == 'y' || op == 'Y');
}
int main()
{
	setlocale(LC_ALL, "");
	test();
	return 0;
}

这里就不多说了哈,主要一点就是加入了,游戏结束你是否还要再来一局。

整个游戏的源码(有感兴趣的可以自取):

里面也有单向链表,和双向链表的源码。

这里是我的gitee仓库的链接,项目的名称为Snake追风逐梦又一天/newer_C - 码云 - 开源中国 (gitee.com)https://gitee.com/small-bit-big-dream/newer_-c

到这里就结束了,拜拜!

相关推荐
落落落sss9 分钟前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
简单.is.good27 分钟前
【测试】接口测试与接口自动化
开发语言·python
Yvemil71 小时前
MQ 架构设计原理与消息中间件详解(二)
开发语言·后端·ruby
程序员是干活的1 小时前
私家车开车回家过节会发生什么事情
java·开发语言·软件构建·1024程序员节
我是陈泽1 小时前
一行 Python 代码能实现什么丧心病狂的功能?圣诞树源代码
开发语言·python·程序员·编程·python教程·python学习·python教学
优雅的小武先生1 小时前
QT中的按钮控件和comboBox控件和spinBox控件无法点击的bug
开发语言·qt·bug
虽千万人 吾往矣1 小时前
golang gorm
开发语言·数据库·后端·tcp/ip·golang
创作小达人1 小时前
家政服务|基于springBoot的家政服务平台设计与实现(附项目源码+论文+数据库)
开发语言·python
郭二哈1 小时前
C++——list
开发语言·c++·list
杨荧1 小时前
【JAVA开源】基于Vue和SpringBoot的洗衣店订单管理系统
java·开发语言·vue.js·spring boot·spring cloud·开源