EasyX学习第二天:贪吃蛇小游戏(C语言版+链表实现)详细内容

之前学习做了打砖块小游戏,感觉还是挺有意思的,正好最近在学数据结构,就想练一下手。

主要参照了两篇文章:

https://blog.csdn.net/qq_40953281/article/details/79315254?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

(还有一篇不小心关了之后就找不到了/(ㄒoㄒ)/~~如果有人看到可以告诉我,我的界面啥的是完全参照那个)

用wasd控制方向,效果图如下:

主要是运用了链表实现蛇的身子,最难的部分是蛇的移动过程。还有一些小Bug存在(比如食物随机生成到了墙里面/(ㄒoㄒ)/~~),有的方法用的可能也不是很好,如果有错误希望指教。还有大部分的代码可能是抄别人的,这里做下注释。

首先调用头文件:

#include <graphics.h>//图形库文件
#include <conio.h>//对键盘进行操作
#include <time.h>//对时间操作,为了随机生成数
#include <stdio.h>

创建一个窗口:

initgraph(640,480);

定义一些常量:

#define MAP_HEIGHT 30  //墙的高度
#define MAP_WIDE 30//墙的宽度
#define SIZE 16//像素点大小
#define SPEED 200 //蛇的移动速度

其中像素点大小是指我们规定一个单位为多大,比如墙的高度是30个单位,像素点大小为16,那么它的实际高度是30*16 = 48,

正好是我们创建的窗口的高度;如果蛇的半径是1个单位,那么实际的半径是16。

定义蛇的结点的数据结构:

typedef struct Snakes
{
	int x;//蛇的结点的位置坐标
	int y;
	struct Snakes *next;//指向下一结点 的指针
}snake;

定义食物的结构:

struct Food
{
	int x;
	int y;
}food; //并创建了一个食物

定义一些变量,后面用到会解释:

int grow = 0; //grow =0 表示没吃到食物,grow = 1表示吃到食物
int score = 0;//得分
char ch = 'a';//初始方向向左

现在可以打印地图啦~  

我们设边框的每块砖为边长为1的正方形,那么可以知道每个点的坐标:

这面这个图是上边框,剩下的下、左、右边框同理,可以自己画一画

首先设置一个背景颜色:

setbkcolor(YELLOW);
cleardevice(); //调用清屏cleardevice用背景色刷新背景

画砖的时候调用这个函数:(参数为长方形左上和右下两个顶点的坐标)

fillrectangle(int left, int top, int right, int bottom);

打印四条边框:(注意要把坐标大小×像素点才是实际的)

for (int i = 0; i < MAP_WIDE; i++)
	{
		setfillcolor(BLUE);//设置填充颜色为蓝色,(边框默认颜色为白色)
		//上边框
		fillrectangle(i*SIZE, 0, (i + 1)*SIZE, SIZE);
		//下边框
		fillrectangle(i*SIZE, (MAP_HEIGHT - 1)*SIZE, (i + 1)*SIZE, MAP_HEIGHT*SIZE);
	}
	for (int i = 0; i < MAP_HEIGHT; i++)
	{
		setfillcolor(BLUE);
		//左边框
		fillrectangle(0, i*SIZE, SIZE, (i + 1)*SIZE);
		//右边框
		fillrectangle((MAP_WIDE - 1)*SIZE, i*SIZE, MAP_WIDE*SIZE, (i + 1)*SIZE);

打印地图函数就完成了~

void DrawMap()//打印地图
{
	setbkcolor(YELLOW);
	cleardevice(); //调用清屏cleardevice用背景色刷新背景
	for (int i = 0; i < MAP_WIDE; i++)
	{
		setfillcolor(BLUE);
		//上边框
		fillrectangle(i*SIZE, 0, (i + 1)*SIZE, SIZE);
		//下边框
		fillrectangle(i*SIZE, (MAP_HEIGHT - 1)*SIZE, (i + 1)*SIZE, MAP_HEIGHT*SIZE);
	}
	for (int i = 0; i < MAP_HEIGHT; i++)
	{
		setfillcolor(BLUE);
		//左边框
		fillrectangle(0, i*SIZE, SIZE, (i + 1)*SIZE);
		//右边框
		fillrectangle((MAP_WIDE - 1)*SIZE, i*SIZE, MAP_WIDE*SIZE, (i + 1)*SIZE);

	}
}

接下来是打印结点和删除结点:

设置蛇的每一节是圆形,食物也是圆形。圆的半径为1/2

//打印结点
void PrintNode(int x, int y)
{
	setfillcolor(RED);
	fillcircle(x*SIZE, y*SIZE, SIZE / 2);
	setcolor(WHITE);边框用白色,为了更好看一点(默认也为白色)
	circle(x*SIZE, y*SIZE, SIZE / 2);
}

删除结点就是用背景色覆盖掉那个点

//删除结点
void DeleteNode(int x,int y)
{
	setfillcolor(YELLOW);
	fillcircle(x*SIZE, y*SIZE, SIZE / 2);
	setcolor(YELLOW);
	circle(x*SIZE, y*SIZE, SIZE / 2);

}

接着来初始化游戏:需要分别初始化蛇和食物

void InitMap()
{
	//初始化蛇,初始时有3个结点
	head = (snake*)malloc(sizeof(snake));
	head->x = (MAP_WIDE) / 2 ; //设置蛇的初始位置在中间
	head->y = (MAP_HEIGHT) / 2 ;
	snake *p = (snake*)malloc(sizeof(snake));
	snake *q = (snake*)malloc(sizeof(snake));
	p->x = head->x+1;
	p->y = head->y;
	q->x = head->x + 2;
	q->y = head->y;

	head->next = p;
	p->next = q;
	tail = q;
	tail->next = NULL;

	snake *temp = head;
	while (temp != NULL)//打印出所有结点
	{
		PrintNode(temp->x, temp->y);
		temp = temp->next;
	}
	//初始化食物
	srand((int)time(NULL));//设置时间为种子
	food.x = rand() % (MAP_WIDE - 2) + 2; //随机生成生成2~MAP_WIDE - 2的随机数
	food.y = rand() % (MAP_HEIGHT - 2) + 2;//随机生成2~MAP_HEIGHT - 2的随机数
	PrintNode(food.x, food.y);
}

测试一下,在主函数中调用这两个函数:

int main()
{
	initgraph(640,480);
	DrawMap();
	InitMap(); 
	_getch();//为了把窗口停下来,否则窗口太快消失我们看不到
}

如果一切正确的话就是这样的:

接下来实现更新食物位置的函数:

什么时候要更新食物的位置呢?当然是蛇“吃”了食物之后。蛇“吃”了食物的意思就是蛇的结点的坐标和食物的坐标相等了,那么此时就要更新食物位置:

void UpdataFood()
{
	//如果蛇的结点与食物的坐标相等了,则需要创造新的食物
	snake *judge = head;
	while(judge->next != NULL)//遍历蛇的所有结点
	{
		if (judge->x == food.x&&judge->y == food.y)
		{
			food.x = rand() % (MAP_WIDE - 2) + 2; //生成2~MAP_WIDE - 2的随机数
			food.y = rand() % (MAP_HEIGHT - 2) + 2; //生成2~MAP_HEIGHT - 2的随机数

			PrintNode(food.x, food.y);//打印新的食物
			score++;
			grow = 1;//表明蛇需有增长
			break;
		}
		judge = judge->next;	
	}
	
}

接下来要实现最难的部分:蛇的移动

蛇的移动效果实际就是在原来位置清空结点,然后在新的位置打印结点。

清空很简单:

//先清空所有结点,再打印,实现动态效果
	snake *p = head;
	while (p != NULL)
	{
		DeleteNode(p->x, p->y);
		p = p->next;
	}

观察一下移动规则:比如蛇向右移动一个单位:

我们可以发现,除了头结点(最右边的结点)外,移动后其余结点的坐标规则为:前一个结点的坐标赋值给后一个结点(如尾结点原来的x坐标是1,移动后x坐标等于它的前一个结点的坐标:2)即 

p->next->x = p->x;
p->next->y = p->y;

需要从头结点开始遍历,令前一个结点的坐标值赋值给后一个,可能会想到直接这样做:

/*
snake *p = head;
while (p->next != NULL)
	{
		p->next->x = p->x;
		p->next->y = p->y;
		p = p->next;
	}
*/

但其实是错误的。因为p->next->x(y)被赋予了新的值,它原来的值没有被存下来,无法给到下一个结点。如第一次循环,第二个结点的值变为5,第二次循环,第三个结点的值等于第二个结点的值5。所以我们需要两个中间变量记录下原来的的值:

void ChangeBody()//除头结点外,蛇结点坐标的改变
{
	snake *p = head;
	int midx, midy, _midx, _midy;
	midx = p->x;
	midy = p->y;
	while (p->next != NULL)
	{
		_midx = p->next->x;
		_midy = p->next->y;
		p->next->x = midx;
		p->next->y = midy;
		midx = _midx;
		midy = _midy;
		p = p->next;
	}
}

身子都变完了,接下来改变头结点。(记住,ChangeBody()这个函数一定要用在改变头结点之前!因为先改变头结点的话,后面的身子坐标就不对了。比如第一次循环,第二个结点的坐标等于原头结点坐标5,如果先操作头结点,第二个结点的坐标就是6了!之前就是这里一直没整明白(╥╯^╰╥))

键盘输入的方向就是用来控制头结点的,我们在上面定义过一个初始方向:

char ch = 'a';//初始方向向左

用键盘输入方向:

if (_kbhit())//如果有键盘输入
	{
		ch = _getch();//新的方向
	}
//如果没有键盘输入,就保持原来的方向

我们还需要对方向就行优化,比如向左动的时候不能向右转,向上的时候不能向下转,输入其他按键方向不变等。

char oldch = ch;//记录原来的方向
	if (_kbhit())
	{
		ch = _getch();//新的方向
	}
	//控制方向:向左的时候不能向右转,向上的时候不能向下,向右不能向左,向下不能向上
	if ((ch == 'd'||ch == 'D')&&(oldch == 'a'||oldch == 'A'))
		//保持原来的方向不变
		ch = oldch;
    if ((oldch == 'w' || oldch == 'W') && (ch == 'S' || ch == 's'))
		ch = oldch;
	if ((oldch == 'd' || oldch == 'D') && (ch == 'A' || ch == 'a'))
		ch = oldch;
	if ((oldch == 'S' || oldch == 's' && ch == 'w' || ch == 'W'))
		ch = oldch;
    //输入其他按键不好使
	if (ch != 's'&& ch != 'S'&&ch != 'W'&&ch != 'w'&&ch != 'a'&&ch != 'A'&&ch != 'd'&&ch != 'D')
		ch = oldch;

头结点的坐标很好控制:

//更新头结点的坐标
	switch (ch)
	{		//向右边转
			case 'd':
			case 'D':
				head->x += 1;
				break;
				//左转
			case 'a':
			case 'A':
				head->x -=1;
				break;
				//向上
			case 'w':
			case 'W':
				head->y--;		
				break;
				//向下
			case 's':
			case 'S':
				head->y++;
				break;
			default:
				break;
	}

上面是蛇的正常移动,如果蛇吃到了食物呢?那就用尾插法在结尾处插入一个结点。如何判断蛇收否吃到食物?在上面定义过一个grow,当吃到食物时grow = 1,吃完要更新回0。最后重新打印所有结点,就实现了蛇的移动的全部控制。

void MoveSnake()
{
	char oldch = ch;//记录原来的方向
	if (_kbhit())
	{
		ch = _getch();//新的方向
	}
	//控制方向:向左的时候不能向右转,向上的时候不能向下,向右不能向左,向下不能向上
	if ((ch == 'd'||ch == 'D')&&(oldch == 'a'||oldch == 'A'))
		//保持原来的方向不变
		ch = oldch;
    if ((oldch == 'w' || oldch == 'W') && (ch == 'S' || ch == 's'))
		ch = oldch;
	if ((oldch == 'd' || oldch == 'D') && (ch == 'A' || ch == 'a'))
		ch = oldch;
	if ((oldch == 'S' || oldch == 's' && ch == 'w' || ch == 'W'))
		ch = oldch;
	if (ch != 's'&& ch != 'S'&&ch != 'W'&&ch != 'w'&&ch != 'a'&&ch != 'A'&&ch != 'd'&&ch != 'D')
		ch = oldch;
	
		
	//先清空所有结点,再打印,实现动态效果
	snake *p = head;
	while (p != NULL)
	{
		DeleteNode(p->x, p->y);
		p = p->next;
	}
	//记录下尾结点的坐标,为吃到食物生成新结点做准备
	int a = tail->x, b = tail->y;
	//除头结点外 ,剩余结点前面结点的坐标赋值给后面的结点
	ChangeBody();
	//更新头结点的坐标
	switch (ch)
	{		//向右边转
			case 'd':
			case 'D':
				head->x += 1;
				break;
				//左转
			case 'a':
			case 'A':
				head->x -=1;
				break;
				//向上
			case 'w':
			case 'W':
				head->y--;		
				break;
				//向下
			case 's':
			case 'S':
				head->y++;
				break;
			default:
				break;
	}
	//如果吃到食物,就用尾插法插入一个结点
	if (grow)
	{
		snake *newnode;
		newnode = (snake*)malloc(sizeof(snake));
		newnode->x = a;
		newnode->y = b;
		tail->next = newnode;
		tail = newnode;//更新尾结点
		tail->next = NULL;
		grow = 0;//更新grow的值
	}
	//重新打印所有结点
	p = head;
	while (p != NULL)
	{
		PrintNode(p->x, p->y);
		p = p->next;
	}
	Sleep(SPEED);//控制速度
}

最后就是判断游戏时候结束:

bool Finish()//判断是否结束
{
	//蛇撞墙,或者蛇头撞到身上,则游戏结束
	if (head->x <= 1 || head->x >= (MAP_WIDE - 1) || head->y <=1 || head->y >= (MAP_HEIGHT-1))
		return 0;
	snake *p = head->next;
	while (p != NULL)
	{
		if (head->x == p->x&&head->y == p->y)
			return 0;
		p = p->next;
	}
	return 1;
}

这样一个简易的贪吃蛇就完成啦~ 最终代码如下:

#include <graphics.h>
#include <conio.h>
#include <time.h>
#include <stdio.h>


#define MAP_HEIGHT 30
#define MAP_WIDE 30
#define SIZE 16
#define SPEED 200

 

//定义蛇的结点
typedef struct Snakes
{
	int x;//蛇的结点的位置坐标
	int y;
	struct Snakes *next;
}snake;
 
snake *head, *tail;

//定义食物的结构
struct Food
{
	int x;
	int y;
}food;

int grow = 0; //grow =0 表示没吃到食物,grow = 1表示吃到食物
int score = 0;
char ch = 'a';//初始方向向左


void DrawMap()//打印地图
{
	setbkcolor(YELLOW);
	cleardevice(); //调用清屏cleardevice用背景色刷新背景
	for (int i = 0; i < MAP_WIDE; i++)
	{
		setfillcolor(BLUE);
		//上边框
		fillrectangle(i*SIZE, 0, (i + 1)*SIZE, SIZE);
		//下边框
		fillrectangle(i*SIZE, (MAP_HEIGHT - 1)*SIZE, (i + 1)*SIZE, MAP_HEIGHT*SIZE);
	}
	for (int i = 0; i < MAP_HEIGHT; i++)
	{
		setfillcolor(BLUE);
		//左边框
		fillrectangle(0, i*SIZE, SIZE, (i + 1)*SIZE);
		//右边框
		fillrectangle((MAP_WIDE - 1)*SIZE, i*SIZE, MAP_WIDE*SIZE, (i + 1)*SIZE);

	}
}
//打印结点
void PrintNode(int x, int y)
{
	setfillcolor(RED);
	fillcircle(x*SIZE, y*SIZE, SIZE / 2);
	setcolor(WHITE);
	circle(x*SIZE, y*SIZE, SIZE / 2);
}
//删除结点
void DeleteNode(int x,int y)
{
	setfillcolor(YELLOW);
	fillcircle(x*SIZE, y*SIZE, SIZE / 2);
	setcolor(YELLOW);
	circle(x*SIZE, y*SIZE, SIZE / 2);

}

void InitMap()
{
	//初始化蛇
	head = (snake*)malloc(sizeof(snake));
	head->x = (MAP_WIDE) / 2 ;
	head->y = (MAP_HEIGHT) / 2 ;
	snake *p = (snake*)malloc(sizeof(snake));
	snake *q = (snake*)malloc(sizeof(snake));
	p->x = head->x+1;
	p->y = head->y;
	q->x = head->x + 2;
	q->y = head->y;

	head->next = p;
	p->next = q;
	tail = q;
	tail->next = NULL;

	snake *temp = head;
	while (temp != NULL)//打印出所有结点
	{
		PrintNode(temp->x, temp->y);
		temp = temp->next;
	}
	//初始化食物
	srand((int)time(NULL));
	food.x = rand() % (MAP_WIDE - 2) + 2;
	food.y = rand() % (MAP_HEIGHT - 2) + 2;
	PrintNode(food.x, food.y);
}
void UpdataFood()
{
	//如果蛇的结点与食物的坐标相等了,则需要创造新的食物
	snake *judge = head;
	while(judge->next != NULL)//遍历所有结点
	{
		if (judge->x == food.x&&judge->y == food.y)
		{
			food.x = rand() % (MAP_WIDE - 2) + 2; //生成2~MAP_WIDE - 2的随机数
			food.y = rand() % (MAP_HEIGHT - 2) + 2; //生成2~MAP_HEIGHT - 2的随机数

			PrintNode(food.x, food.y);
			score++;
			grow = 1;
			break;
		}
		judge = judge->next;	
	}
	
}
void ChangeBody()
{
	snake *p = head;
	int midx, midy, _midx, _midy;
	midx = p->x;
	midy = p->y;
	while (p->next != NULL)
	{
		_midx = p->next->x;
		_midy = p->next->y;
		p->next->x = midx;
		p->next->y = midy;
		midx = _midx;
		midy = _midy;
		p = p->next;
	}
}

void MoveSnake()
{
	char oldch = ch;//记录原来的方向
	if (_kbhit())
	{
		ch = _getch();//新的方向
	}
	//控制方向:向左的时候不能向右转,向上的时候不能向下,向右不能向左,向下不能向上
	if ((ch == 'd'||ch == 'D')&&(oldch == 'a'||oldch == 'A'))
		//保持原来的方向不变
		ch = oldch;
    if ((oldch == 'w' || oldch == 'W') && (ch == 'S' || ch == 's'))
		ch = oldch;
	if ((oldch == 'd' || oldch == 'D') && (ch == 'A' || ch == 'a'))
		ch = oldch;
	if ((oldch == 'S' || oldch == 's' && ch == 'w' || ch == 'W'))
		ch = oldch;
	if (ch != 's'&& ch != 'S'&&ch != 'W'&&ch != 'w'&&ch != 'a'&&ch != 'A'&&ch != 'd'&&ch != 'D')
		ch = oldch;
	
		
	//先清空所有结点,再打印,实现动态效果
	snake *p = head;
	while (p != NULL)
	{
		DeleteNode(p->x, p->y);
		p = p->next;
	}
	//记录下尾结点的坐标,为吃到食物生成新结点做准备
	int a = tail->x, b = tail->y;
	//除头结点外 ,剩余结点前面结点的坐标赋值给后面的结点
	ChangeBody();
	//更新头结点的坐标
	switch (ch)
	{		//向右边转
			case 'd':
			case 'D':
				head->x += 1;
				break;
				//左转
			case 'a':
			case 'A':
				head->x -=1;
				break;
				//向上
			case 'w':
			case 'W':
				head->y--;		
				break;
				//向下
			case 's':
			case 'S':
				head->y++;
				break;
			default:
				break;
	}
	//如果吃到食物,就用尾插法插入一个结点
	if (grow)
	{
		snake *newnode;
		newnode = (snake*)malloc(sizeof(snake));
		newnode->x = a;
		newnode->y = b;
		tail->next = newnode;
		tail = newnode;//更新尾结点
		tail->next = NULL;
		grow = 0;//更新grow的值
	}
	//重新打印所有结点
	p = head;
	while (p != NULL)
	{
		PrintNode(p->x, p->y);
		p = p->next;
	}
	Sleep(SPEED);//控制速度
}
bool Finish()//判断是否结束
{
	//蛇撞墙,或者蛇头撞到身上,则游戏结束
	if (head->x <= 1 || head->x >= (MAP_WIDE - 1) || head->y <=1 || head->y >= (MAP_HEIGHT-1))
		return 0;
	snake *p = head->next;
	while (p != NULL)
	{
		if (head->x == p->x&&head->y == p->y)
			return 0;
		p = p->next;
	}
	return 1;
}



int main()
{
	initgraph(640,480);
	DrawMap();
	InitMap(); 
	while (Finish())
	{
		MoveSnake();
		UpdataFood();
	}
	printf("游戏结束!\n您的得分为:%d", score * 10);
	_getch();
}

 

  • 7
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值