用200行C语言代码实现贪吃蛇——2.0(EasyX基本版)

1.前言

这个版本使用了EasyX图形库,使动画效果更加丰富,更加接近真实的小游戏。当然这个“小游戏”并不是真正意义上的小游戏,只是个人对C语言的一些理解与应用。本人水平不高,通过博客来分享自己的学习成果,也算是一种复习。

如果大家不知道什么是EasyX图形库,或者说不知道怎么安装、怎么使用,大家可以去网络上搜索一下。安装过程非常简单,使用起来仅仅针对于贪吃蛇这个项目而言,只需要引头文件,和使用头文件里面的库函数。与#include <stdio.h>然后使用printf函数没什么区别。

需要注意的是,EasyX图形库之适用于C++,但我们可以用C语言的语法。意思就是说,我们需要创建的文件必须是cpp的格式,但我们可以用C的语法。

2.游戏效果

在1.0版本中,我们是在控制台窗口下运行的。并且运用到数组。通过数组的状态来描绘空地、食物、蛇的。并且黑框框影响美观、并不好看。并且因为用到了system("cls")清屏,所以屏幕会一卡一卡抽搐,非常影响视觉。

但是在2.0版本中,我们运用了EasyX图形库,这就使得我们不通过控制台那个黑框框来输出图像,而是用EasyX图形库里面的库函数。上下两个GIF动图都是60FPS,但是不难发现2.0版本无论是动画、还是灵敏度,都要优于1.0。

 3.设计思路

3.1界面设置

我们先将需要的头文件写在head.h头文件底下。

#define _CRT_SECURE_NO_WARNINGS 1
#include <easyx.h>//EasyX图形库头文件
#include <stdio.h>

随后我们在主函数main.cpp底下操作。

#include "head.h"
int main()
{
	initgraph(640, 480);//初始化窗口大小,现在为640*480分辨率
	setbkcolor(RGB(255, 148, 209));//设置背景色
	cleardevice();//使用背景色清屏

	//防闪退
	getchar();
	closegraph();
	return 0;
}

 那么我们就得到:

 3.2绘制一条蛇出来

在绘制蛇之前先讲一个知识点。

 这个窗口是有 x、y轴的,并且值增加的方向跟箭头同向。意思就是说,越往右边,x越大;越往下面,y越大。

由此分析,蛇不能够像1.0版本那样用数组来描绘状态了,需要用坐标来描绘蛇。

我们在头文件head.h下定义蛇的结构体,因为需要使用坐标,也同时定义一个坐标结构体:

#define _CRT_SECURE_NO_WARNINGS 1
#include <easyx.h>//EasyX图形库头文件
#include <stdio.h>

#define NUM 200//默认蛇有200个坐标

//坐标的结构体
struct Coor
{
	int x;
	int y;
};

//蛇的结构体
struct Snake
{
	//蛇结构包括
	int len;//长度
	int direc;//方向
	Coor cr[NUM];//坐标
	//一条蛇有N节,每节都有自己的坐标,所以定义一个坐标数组
};

现在蛇的结构已经弄好,接下来就要初始化,也就是把蛇放在哪个位置。

我们在源文件mian.cpp中进入初始化:

#include "head.h"
int main()
{
	initgraph(640, 480);//初始化窗口大小,现在为640*480分辨率
	setbkcolor(RGB(255, 148, 209));//设置背景色
	cleardevice();//使用背景色清屏

	Init_Game();//初始化

	//防闪退
	getchar();
	closegraph();
	return 0;
}

在头文件head.h中的部分函数声明与宏定义:

#define _CRT_SECURE_NO_WARNINGS 1
#include <easyx.h>//EasyX图形库头文件
#include <stdio.h>

#define NUM 200//默认蛇有200个坐标

//宏定义各个方向。与1.0不同的是,本版本使用方向键控制的
#define UP 72
#define DOWN 80
#define LEFT 75
#define RIGHT 77

//坐标的结构体
struct Coor
{
	int x;
	int y;
};

//蛇的结构体
struct Snake
{
	//蛇结构包括
	int len;//长度
	int direc;//方向
	Coor cr[NUM];//坐标
	//一条蛇有N节,每节都有自己的坐标,所以定义一个坐标数组
};

void Init_Game();//初始化函数声明

 现在需要在源文件function.cpp中配置初始化函数:

#include "head.h"

Snake snake;//创建蛇的结构体变量
void Init_Game()
{
	snake.len = 3;//初始化长度

	snake.direc = RIGHT;//初始化方向

	//初始化蛇头坐标
	snake.cr[0].x = 100;
	snake.cr[0].y = 100;

	//蛇身坐标
	snake.cr[1].x = 90;
	snake.cr[1].y = 100;
	snake.cr[2].x = 80;
	snake.cr[2].y = 100;
}

现在有了蛇的详细信息,就要开始着手在窗口把蛇绘制出来了,我们从主函数进入绘制函数:

#include "head.h"

int main()
{
	initgraph(640, 480);//初始化窗口大小,现在为640*480分辨率
	setbkcolor(RGB(255, 148, 209));//设置背景色
	cleardevice();//使用背景色清屏

	Init_Game();//初始化
	Draw_Game();//绘制

	//防闪退
	getchar();
	closegraph();
	return 0;
}

要在头文件中进行函数声明,但是这里省略。

现在需要在function.cpp中配置绘制函数:

void Draw_Game()
{
	for (int i = 0; i < snake.len; i++)
	{
		if (i == 0)//如果是蛇头
		{
			setfillcolor(RED);//填充颜色为红色
			//绘制实心矩形,填充颜色为红色
			fillrectangle(snake.cr[i].x, snake.cr[i].y, snake.cr[i].x + SIZE, snake.cr[i].y + SIZE);//这里的SIZE我在头文件head.h中宏定义为10了
			//四个参数为:    x坐标           y坐标           宽                       高
		}
		else
			rectangle(snake.cr[i].x, snake.cr[i].y, snake.cr[i].x + SIZE, snake.cr[i].y + SIZE);//绘制非实心矩形

	}
}

那么绘制出来的效果就是这样:

 3.3蛇移动的实现

我们在初始化中,让蛇的移动方向为右。

现在我们需要分析蛇是如何移动的。

 

我们现在需要通过源文件main.cpp进入移动函数。注意:移动与绘制,是一个循环过程。

#include "head.h"

int main()
{
	initgraph(640, 480);//初始化窗口大小,现在为640*480分辨率
	setbkcolor(RGB(255, 148, 209));//设置背景色
	cleardevice();//使用背景色清屏

	Init_Game();//初始化

	while (1)
	{
		Move_Snake();//移动蛇
		Draw_Game();//绘制
		Sleep(100);
	}

	//防闪退
	getchar();
	closegraph();
	return 0;
}

在头文件head.h中要引用Sleep函数的头文件,以及移动蛇的函数声明。这里省略。

最后在源文件function.cpp中配置移动蛇函数:


void Move_Snake()
{
	cleardevice();//以当前背景色清屏
	for (int i = snake.len-1; i > 0; i--)
	{
		snake.cr[i].x = snake.cr[i - 1].x;
		snake.cr[i].y = snake.cr[i - 1].y;
	}//数组下标是从 0~n-1 的,所以从i=snake.len-1开始。为什么i要>0而不是>=0
	//是因为在循环里面的语句中,有i-1的操作。最后一次是把snake.cr[0]的坐标给snake.cr[1],如果写>=0,就会产生把 snake.cr[-1]的坐标给snake.cr[0]

	//蛇头的坐标没有了,现在要产生新的蛇头坐标
	switch (snake.direc)
	{
	case UP:
		snake.cr[0].y -= SIZE;
		break;
	case DOWN:
		snake.cr[0].y += SIZE;
		break;
	case LEFT:
		snake.cr[0].x -= SIZE;
		break;
	case RIGHT:
		snake.cr[0].x += SIZE;
		break;
	}
}

现在实现了蛇向右移动的功能,那么效果是这样的:

 

现在我们需要控制蛇的方向。我们从源文件main.cpp进入控制蛇的函数:

#include "head.h"

int main()
{
	initgraph(640, 480);//初始化窗口大小,现在为640*480分辨率
	setbkcolor(RGB(255, 148, 209));//设置背景色
	cleardevice();//使用背景色清屏

	Init_Game();//初始化

	while (1)
	{
		if (_kbhit())//如果键盘被敲击就进入控制方向函数
			Change_Move();//控制蛇
		else
		{
			Move_Snake();//移动蛇
			Draw_Game();//绘制
			Sleep(100);
		}
		
	}

	//防闪退
	getchar();
	closegraph();
	return 0;
}

 我们需要在头文件head.h中定义_kbhit函数的头文件以及控制蛇的函数声明。这里省略。

现在要在源文件function.cpp中配置控制蛇函数:

void Change_Move()
{
	int key = 0;
	key = _getch();//接收键盘值
	switch (key)
	{
	case UP:
		if (snake.direc != DOWN)//如果蛇正在往下,就不能改变方向。
			snake.direc = UP;
		break;
	case DOWN:
		if (snake.direc != UP)//如果蛇正在往上,就不能改变方向。
			snake.direc = DOWN;
		break;
	case LEFT:
		if (snake.direc != RIGHT)//如果蛇正在往右,就不能改变方向
			snake.direc = LEFT;
		break;
	case RIGHT:
		if (snake.direc != LEFT)//如果蛇正在往左,就不能改变防线
			snake.direc = RIGHT;
		break;
	}
}

 那么实现的效果是这样的:

3.4食物的生成

食物的生成很简单:第一次初始化生成,我们可以在初始化函数中完成。后续就是蛇吃掉食物后再随机生成。

需要注意的是:我们的蛇是以10为单位移动的,并且每一节的大小都是10*10。我们的食物也要匹配蛇的大小。 

判断食物被是否被吃掉非常简单,只要判断蛇头与食物的坐标是否重合就行。

但是我们难就难在定义食物。

在1.0版本中,我们用数组描绘食物的状态。

但是在本版本中,需要使用结构体,因为食物也包含坐标。

所以这里在头文件head.h中定义食物的结构体(包含上面省略的头文件和函数声明):

#define _CRT_SECURE_NO_WARNINGS 1
#include <easyx.h>//EasyX图形库头文件
#include <stdio.h>
#include <windows.h>//Sleep头文件
#include <conio.h>//kbhit、getch头文件
#define NUM 200//默认蛇有200个坐标

//宏定义各个方向。与1.0不同的是,本版本使用方向键控制的
#define UP 72
#define DOWN 80
#define LEFT 75
#define RIGHT 77

#define SIZE 10
//坐标的结构体
struct Coor
{
	int x;
	int y;
};

//蛇的结构体
struct Snake
{
	//蛇结构包括
	int len;//长度
	int direc;//方向
	Coor cr[NUM];//坐标
	//一条蛇有N节,每节都有自己的坐标,所以定义一个坐标数组
};

//食物的结构体
struct Food
{
	//因为食物它永远只有一个,所以不需要数组
	int x;
	int y;
	int eat;//确定是否被吃
};
void Init_Game();//初始化函数声明
void Draw_Game();//绘制蛇函数声明
void Move_Snake();//蛇移动函数声明
void Change_Move();//控制蛇函数声明

 我们要在源文件function.cpp中对食物初始化:

#include "head.h"

Snake snake;//创建蛇的结构体变量
Food food;//创建食物的结构体变量
void Init_Game()
{
	snake.len = 3;//初始化长度

	snake.direc = RIGHT;//初始化方向

	//初始化蛇头坐标
	snake.cr[0].x = 100;
	snake.cr[0].y = 100;

	//蛇身坐标
	snake.cr[1].x = 90;
	snake.cr[1].y = 100;
	snake.cr[2].x = 80;
	snake.cr[2].y = 100;

	food.eat = 1;//初始化食物是被吃的

}

在绘制之前我们需要定义一下食物的坐标在哪,也就是需要在源文件main.cpp中进入创建食物函数:

定义好了食物的坐标,现在又要回到绘制函数把食物绘制出来:

void Draw_Game()
{
	for (int i = 0; i < snake.len; i++)
	{
		if (i == 0)//如果是蛇头
		{
			setfillcolor(RED);//填充颜色为红色
			//绘制实心矩形,填充颜色为红色
			fillrectangle(snake.cr[i].x, snake.cr[i].y, snake.cr[i].x + SIZE, snake.cr[i].y + SIZE);//这里的SIZE我在头文件head.h中宏定义为10了
			//四个参数为:    x坐标           y坐标           宽                       高
		}
		else
			rectangle(snake.cr[i].x, snake.cr[i].y, snake.cr[i].x + SIZE, snake.cr[i].y + SIZE);//绘制非实心矩形
	}
	//绘制食物
	setfillcolor(GREEN);//填充颜色为绿色
	fillroundrect(food.x, food.y, food.x + SIZE, food.y + SIZE, 5, 5);//圆角矩形
	//6个参数为:  x坐标   y坐标      宽              高       这两个是圆角的程度
}

此时的运行效果就是这样的:

 3.5吃食物以及蛇身加长

这个部分非常简单。

我们先从源文件main.cpp中进入判断是否吃到食物函数:

#include "head.h"

int main()
{
	initgraph(640, 480);//初始化窗口大小,现在为640*480分辨率
	setbkcolor(RGB(255, 148, 209));//设置背景色
	cleardevice();//使用背景色清屏

	Init_Game();//初始化
	srand((unsigned int)time(NULL));
	while (1)
	{
		if (_kbhit())//如果键盘被敲击就进入控制方向函数
			Change_Move();//控制蛇
		else
		{
			Produce_Food();//创建食物
			Eat_Food();//判断食物是否被吃
			Move_Snake();//移动蛇
			Draw_Game();//绘制
			Sleep(100);
		}
		
	}

	//防闪退
	getchar();
	closegraph();
	return 0;
}

我们需要在头文件head.h下进行函数声明。这里省略。

随后在源文件function.cpp中配置是否被吃函数:

void Eat_Food()
{
	if (snake.cr[0].x == food.x && snake.cr[0].y == food.y)//如果蛇头的位置是食物
	{
		food.eat = 1;//食物的状态更新为被吃
		snake.len += 1;//蛇身长度+1
	}
}

3.6判断蛇头是否撞墙或自己

这个模块与1.0版本一摸一样,判断函数都需要一个变量来接收返回值。

我们在源文件main.cpp中观察逻辑:

#include "head.h"

int main()
{
	initgraph(640, 480);//初始化窗口大小,现在为640*480分辨率
	setbkcolor(RGB(255, 148, 209));//设置背景色
	cleardevice();//使用背景色清屏

	Init_Game();//初始化
	srand((unsigned int)time(NULL));
	int n = 0;
	while (1)
	{
		if (_kbhit())//如果键盘被敲击就进入控制方向函数
			Change_Move();//控制蛇
		else
		{
			Produce_Food();//创建食物
			Eat_Food();//判断食物是否被吃
			n = GameOver();//判断蛇头是否撞墙或自己
			if (n == 1)//返回1则撞墙或自己
			{
				closegraph();//关闭窗口
				printf("游戏结束\n");//控制台打印
				break;//结束循环
			}
			else
			{
				Move_Snake();//移动蛇
				Draw_Game();//绘制
				Sleep(100);
			}
			
		}
		
	}

	//防闪退
	getchar();
	closegraph();
	return 0;
}

我们需要在头文件head.h下进行函数声明。这里省略。

然后就是在源文件funtion.cpp中配置函数:

int GameOver()
{
	if (snake.cr[0].x < 0 || snake.cr[0].y < 0 || snake.cr[0].x>640 || snake.cr[0].y>480)//如果撞墙
	{
		return 1;//返回1
	}
	for (int i = 1; i < snake.len; i++) 
	{
		if (snake.cr[0].x == snake.cr[i].x && snake.cr[0].y == snake.cr[i].y)//如果撞自己
			return 1;//返回1
	}
	return 0;//否则返回0
}

4.完整代码

4.1头文件head.h

#define _CRT_SECURE_NO_WARNINGS 1
#include <easyx.h>//EasyX图形库头文件
#include <stdio.h>
#include <windows.h>//Sleep头文件
#include <conio.h>//kbhit、getch头文件
//srand函数需要的头文件
#include <stdlib.h>
#include <time.h>

#define NUM 200//默认蛇有200个坐标

//宏定义各个方向。与1.0不同的是,本版本使用方向键控制的
#define UP 72
#define DOWN 80
#define LEFT 75
#define RIGHT 77

#define SIZE 10
//坐标的结构体
struct Coor
{
	int x;
	int y;
};

//蛇的结构体
struct Snake
{
	//蛇结构包括
	int len;//长度
	int direc;//方向
	Coor cr[NUM];//坐标
	//一条蛇有N节,每节都有自己的坐标,所以定义一个坐标数组
};

//食物的结构体
struct Food
{
	//因为食物它永远只有一个,所以不需要数组
	int x;
	int y;
	int eat;//确定是否被吃
};
void Init_Game();//初始化函数声明
void Draw_Game();//绘制蛇函数声明
void Move_Snake();//蛇移动函数声明
void Change_Move();//控制蛇函数声明
void Produce_Food();//创建食物的函数声明
void Eat_Food();//食物是否被吃的函数声明
int GameOver();//判断是否撞墙或自己的函

4.2源文件main.cpp

#include "head.h"

int main()
{
	initgraph(640, 480);//初始化窗口大小,现在为640*480分辨率
	setbkcolor(RGB(255, 148, 209));//设置背景色
	cleardevice();//使用背景色清屏

	Init_Game();//初始化
	srand((unsigned int)time(NULL));
	int n = 0;
	while (1)
	{
		if (_kbhit())//如果键盘被敲击就进入控制方向函数
			Change_Move();//控制蛇
		else
		{
			Produce_Food();//创建食物
			Eat_Food();//判断食物是否被吃
			n = GameOver();//判断蛇头是否撞墙或自己
			if (n == 1)//返回1则撞墙或自己
			{
				closegraph();//关闭窗口
				printf("游戏结束\n");//控制台打印
				break;//结束循环
			}
			else
			{
				Move_Snake();//移动蛇
				Draw_Game();//绘制
				Sleep(100);
			}
			
		}
		
	}

	//防闪退
	getchar();
	closegraph();
	return 0;
}

4.3源文件function.cpp

#include "head.h"

Snake snake;//创建蛇的结构体变量
Food food;//创建食物的结构体变量
void Init_Game()
{
	snake.len = 3;//初始化长度

	snake.direc = RIGHT;//初始化方向

	//初始化蛇头坐标
	snake.cr[0].x = 100;
	snake.cr[0].y = 100;

	//蛇身坐标
	snake.cr[1].x = 90;
	snake.cr[1].y = 100;
	snake.cr[2].x = 80;
	snake.cr[2].y = 100;

	food.eat = 1;//初始化食物是被吃的

}

void Draw_Game()
{
	for (int i = 0; i < snake.len; i++)
	{
		if (i == 0)//如果是蛇头
		{
			setfillcolor(RED);//填充颜色为红色
			//绘制实心矩形,填充颜色为红色
			fillrectangle(snake.cr[i].x, snake.cr[i].y, snake.cr[i].x + SIZE, snake.cr[i].y + SIZE);//这里的SIZE我在头文件head.h中宏定义为10了
			//四个参数为:    x坐标           y坐标           宽                       高
		}
		else
			rectangle(snake.cr[i].x, snake.cr[i].y, snake.cr[i].x + SIZE, snake.cr[i].y + SIZE);//绘制非实心矩形
	}
	//绘制食物
	setfillcolor(GREEN);//填充颜色为绿色
	fillroundrect(food.x, food.y, food.x + SIZE, food.y + SIZE, 5, 5);//圆角矩形
	//6个参数为:  x坐标   y坐标      宽              高       这两个是圆角的程度
}

void Move_Snake()
{
	cleardevice();//以当前背景色清屏
	for (int i = snake.len-1; i > 0; i--)
	{
		snake.cr[i].x = snake.cr[i - 1].x;
		snake.cr[i].y = snake.cr[i - 1].y;
	}//数组下标是从 0~n-1 的,所以从i=snake.len-1开始。为什么i要>0而不是>=0
	//是因为在循环里面的语句中,有i-1的操作。最后一次是把snake.cr[0]的坐标给snake.cr[1],如果写>=0,就会产生把 snake.cr[-1]的坐标给snake.cr[0]

	//蛇头的坐标没有了,现在要产生新的蛇头坐标
	switch (snake.direc)
	{
	case UP:
		snake.cr[0].y -= SIZE;
		break;
	case DOWN:
		snake.cr[0].y += SIZE;
		break;
	case LEFT:
		snake.cr[0].x -= SIZE;
		break;
	case RIGHT:
		snake.cr[0].x += SIZE;
		break;
	}
}

void Change_Move()
{
	int key = 0;
	key = _getch();//接收键盘值
	switch (key)
	{
	case UP:
		if (snake.direc != DOWN)//如果蛇正在往下,就不能改变方向。
			snake.direc = UP;
		break;
	case DOWN:
		if (snake.direc != UP)//如果蛇正在往上,就不能改变方向。
			snake.direc = DOWN;
		break;
	case LEFT:
		if (snake.direc != RIGHT)//如果蛇正在往右,就不能改变方向
			snake.direc = LEFT;
		break;
	case RIGHT:
		if (snake.direc != LEFT)//如果蛇正在往左,就不能改变防线
			snake.direc = RIGHT;
		break;
	}
}

void Produce_Food()
{
	int func = 0;//用来判断食物的坐标是否与蛇的坐标重合
	if (food.eat == 1)
	{
		while (1)
		{
			food.x = rand() % 64 * 10;//rand()%64的区间在0~63,再*10可实现坐标以10为间隔生成坐标   0  10  20...
			food.y = rand() % 48 * 10;//rand()%48的区间在0~46,再*10可实现坐标以10为间隔生成坐标   0  10  20...
			for (int i = 0; i < snake.len; i++)
			{
				if (food.x == snake.cr[i].x && food.y == snake.cr[i].y)
				{
					func = 1;//如果但凡有坐标重合,func改变为1,表重合
					break;//退出for循环
				}
			}
			if (func == 0)//如果没有重合
			{
				food.eat = 0;//食物就要变为未被吃的状态
				break;
			}
		}
	}
}

void Eat_Food()
{
	if (snake.cr[0].x == food.x && snake.cr[0].y == food.y)//如果蛇头的位置是食物
	{
		food.eat = 1;//食物的状态更新为被吃
		snake.len += 1;//蛇身长度+1
	}
}

int GameOver()
{
	if (snake.cr[0].x < 0 || snake.cr[0].y < 0 || snake.cr[0].x>640 || snake.cr[0].y>480)//如果撞墙
	{
		return 1;//返回1
	}
	for (int i = 1; i < snake.len; i++) 
	{
		if (snake.cr[0].x == snake.cr[i].x && snake.cr[0].y == snake.cr[i].y)//如果撞自己
			return 1;//返回1
	}
	return 0;//否则返回0
}

猜你喜欢

转载自blog.csdn.net/weixin_59913110/article/details/125201471