用c语言+单向链表实现一个贪吃蛇

一、效果:



二、实现步骤:(我写代码是就是按着下面的步骤一步步实现的,顺带在纸上画一画思路)


三、功能:
1.按上下左右方向键运动
2.按+或-加速或减速
3.撞墙或咬到蛇身时游戏失败
4.记录吃食物的数量,即得分

四、难点:如何实现蛇身的移动
在while循环里设置个定时器(Sleep函数),这样每隔0.5秒程序执行一次,实现蛇身的移动。

一般思路:蛇身移动会遇到两种情况:
1.蛇头的下一个节点是食物 :吃掉食物,不释放尾节点内存,重新生成食物
2.蛇头的下一个节点不是食物:在蛇头malloc内存生成一个新节点,将蛇尾节点的内存free掉

我的思路:将食物节点在栈分配内存,重新生成食物只是修改食物节点的x/y值,而蛇身节点是动态分配内存的。

那么实现思路是:
每次循环时,把蛇头第一个节点的x/y坐标相应+1,并用头插法在蛇头第一个节点后再分配内存插入一个节点。
将蛇头第一个节点的x/y坐标与食物节点比较
1.如果判断下一个节点是食物,不释放尾节点内存,重新生成食物。
2.如果判断下一个节点不是食物,释放蛇最后一个节点。

插入新节点图解:


五、代码如下(编译环境:vs2017):
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <windows.h>
#include <time.h>
#include <stdlib.h>

//蛇的状态,U:上 ;D:下;L:左 R:右
#define U 1
#define D 2
#define L 3 
#define R 4 

#define EMPTY 0
#define BIT_SELF 1 //咬到蛇身
#define TOUCH_WALL 2 //碰到墙

#define TRUE 1
#define FALSE 0 

typedef struct SNAKE //蛇身的一个节点 
{
	int x;
	int y;
	struct SNAKE *next;
}snake;

//head用于指向蛇的第一个节点,标识整条蛇,但不属于蛇的节点
snake head, *temp_ptr, food_node;

//direction						键入方向
//game_over_reason				记录游戏失败原因
//speed							移动速度
int direction = R, game_over_reason = EMPTY,  speed = 100;

void locateAndPrint(int x, int y);						//在光标位置输出方块
void locateAndClear(int x, int y);						//在光标位置清除方块
void creatMap();										//创建地图
void createFood();										//创建食物
void intSnake(int snake_len, int start_x, int start_y); //初始化蛇身
void endGame();											//结束游戏
void getEnteredDirection();								//获取键入的方向
void snakeMove();										//蛇移动
void startGame();										//游戏循环

/*在光标位置输出方块*/
void locateAndPrint(int x, int y)
{
	//定位
	COORD pos;
	HANDLE hOutput;
	pos.X = x;
	pos.Y = y;
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(hOutput, pos);

	//输出
	printf("■");
}

/*在光标位置清除方块*/
void locateAndClear(int x, int y)
{
	//定位
	COORD pos;
	HANDLE hOutput;
	pos.X = x;
	pos.Y = y;
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(hOutput, pos);

	//输出
	printf(" ");
}

/*创建地图*/
void creatMap()
{
	//设定黑窗的大小
	system("mode con cols=100 lines=30");

	//注意这里x+2 y+1,在黑框中就是这样约定的,没办法
	int i;
	for (i = 0; i < 58; i += 2)//打印上下边框
	{
		locateAndPrint(i, 0);
		locateAndPrint(i, 26);
	}
	for (i = 1; i < 26; i++)//打印左右边框
	{
		locateAndPrint(0, i);
		locateAndPrint(56, i);
	}
}

/*创建食物*/
void createFood()
{
	//创建食物成功,跳出循环
	int create_food_success_flag = TRUE;

	//如果创建不成功,则重新重建
	while (1) {
		int rand_x, rand_y;
		do {
			//x随机数 2-55  y随机数 3-23
			srand((int)time(NULL));
			rand_x = rand() % 53 + 2;
			rand_y = rand() % 21 + 3;
		} while (rand_x % 2 != 0);//x需要是2的偶数,黑框中x和y的方块有局限性

		//判断生成的食物是否跟蛇身重叠
		temp_ptr = head.next;
		while (temp_ptr != NULL) {
			if (temp_ptr->x == rand_x && temp_ptr->y == rand_y) {
				//重叠,需要重新创建
				create_food_success_flag = FALSE;
				break;
			}
			temp_ptr = temp_ptr->next;
		}

		//food的坐标跟蛇身重叠,重新生成food_node
		if (FALSE == create_food_success_flag) {
			continue;
		}

		//创建成功,在黑框中打印
		create_food_success_flag = TRUE;
		food_node.x = rand_x;
		food_node.y = rand_y;
		locateAndPrint(rand_x, rand_y);

		break;
	}
}

/*初始化蛇身*/
void intSnake(int snake_len, int start_x, int start_y) {

	for (int i = 0; i < snake_len; i++) {
		temp_ptr = (snake *)malloc(sizeof(snake));
		temp_ptr->x = start_x;
		temp_ptr->y = start_y;
		start_x += 2;
		//头插法
		temp_ptr->next = head.next;
		head.next = temp_ptr;
		//输出初始蛇身
		locateAndPrint((head.next)->x, (head.next)->y);
	}
}

/*游戏结束*/
void endGame()
{
	//记录得分
	int snake_length = 0;

	//释放蛇身 malloc分配的资源
	snake * temp_ptr2;//这里为了思路清晰,引入第三个变量temp_ptr2,辅助变量temp_ptr2可以用head.next来替代
	temp_ptr = head.next;
	while (temp_ptr != NULL) {
		temp_ptr2 = temp_ptr->next;
		free(temp_ptr);
		temp_ptr = temp_ptr2;
		snake_length++;
	}

	//清屏并输出游戏失败原因
	system("cls");

	if (BIT_SELF == game_over_reason) {

		printf("蛇头与蛇身相碰,失败!\n得分:%d\n", snake_length - 9);
	}
	else if (TOUCH_WALL == game_over_reason) {
		printf("碰到墙壁,失败!\n得分:%d\n", snake_length - 9);

	}

	getchar();
}

/*获取键入的方向*/
void getEnteredDirection()
{

	if (GetAsyncKeyState(VK_OEM_PLUS))
	{
		speed -= 25;
	}
	else if (GetAsyncKeyState(VK_OEM_MINUS))
	{
		speed += 25;
	}
	else if (GetAsyncKeyState(VK_UP) && direction != D)
	{
		direction = U;
	}
	else if (GetAsyncKeyState(VK_DOWN) && direction != U)
	{
		direction = D;
	}
	else if (GetAsyncKeyState(VK_LEFT) && direction != R)
	{
		direction = L;
	}
	else if (GetAsyncKeyState(VK_RIGHT) && direction != L)
	{
		direction = R;
	}
}

/*蛇移动*/
void snakeMove()
{
	//把原始的head.next的坐标值存起来
	int temp_x = head.next->x, temp_y = head.next->y;

	//判断方向,修改第一个节点的值
	if (direction == R)
	{
		head.next->x += 2;
	}
	else if (direction == L) {
		head.next->x -= 2;
	}
	else if (direction == U) {
		head.next->y -= 1;
	}
	else if (direction == D) {
		head.next->y += 1;
	}

	//case1.下一节点为食物: 蛇身第一节点跟食物节点重合,在蛇身第二个节点新建节点
	//case2.下一节点不为食物:在蛇身第二个节点处新建节点
	//故可合并
	snake * temp = (snake *)malloc(sizeof(snake));
	temp->x = temp_x;
	temp->y = temp_y;
	temp->next = head.next->next;
	head.next->next = temp;

	//判断是否撞墙
	if (head.next->x >= 58 || head.next->x <= 0 || head.next->y <= 0 || head.next->y >= 26) {
		game_over_reason = BIT_SELF;
		endGame();
	}

	//判断下一个节点是否食物
	if (
		food_node.x == head.next->x &&
		food_node.y == head.next->y
		) {
		//下一个节点是食物
		//重新新建食物
		createFood();
	}
	else {
		//下一个节点不是食物,则移动蛇身

		//在蛇前进的第一个位置打印方块
		locateAndPrint(head.next->x, head.next->y);
		
		temp_ptr = head.next;
		while (temp_ptr->next->next != NULL) {

			//判断是否咬到自己
			if (
				temp_ptr->next->next != NULL &&
				temp_ptr->next->next->x == (head.next)->x &&
				temp_ptr->next->next->y == (head.next)->y
				) {
				game_over_reason = BIT_SELF;
				endGame();
			}

			temp_ptr = temp_ptr->next;
		}

		//清除蛇尾的节点
		locateAndClear(temp_ptr->next->x, temp_ptr->next->y);
		free(temp_ptr->next);//释放蛇尾节点内存
		temp_ptr->next = NULL;
	}
}

/*游戏循环*/
void startGame()
{
	//初始方向,默认方向
	direction = R;

	while (1) {
		//判断键盘输入的方向键,但注意“移动方向为上时按下键不起作用”,左右方向同理
		getEnteredDirection();

		//显示蛇身
		snakeMove();//蛇身移动

		Sleep(speed);
	}
}
 
int main()
{
	//创建地图
	creatMap();

	//初始化蛇身
	intSnake(8, 4, 3);

	//创建食物
	createFood();

	//开始游戏 循环不停的判断键入的方向
	startGame();

	return 0;
}

六、总结:
写代码不是一步到位的。
我写贪吃蛇经历了几个步骤:
1.第一次是每次蛇身移动就改变所有蛇身节点的坐标,并清屏,重新输出蛇身所有节点。这样的问题就是界面一直闪烁。
2.第二次是每次蛇身移动分下一节点是否食物写两大段代码。针对两种情况。
3.发现第二次的两种情况的代码可以在第一个头结点后插入新节点进行合并,才有现在的版本。
4....可能以后会有其他改进

猜你喜欢

转载自blog.csdn.net/qq_34881718/article/details/78780130