小白入门级贪吃蛇教学

给刚学C语言的计算机小白(部分大一生)

 小雨时宁的游戏小屋     2019-08-13   10828 words    & views

前言

  本教程供予Nova独游社大一新生入社教学用,故使用C语言实现了贪吃蛇的基本功能,可由玩家自己添加更多细节(得分,欢迎界面,颜色布局等)

完整代码


#include<stdio.h>  

#include<Windows.h>
 
 void InitializeGame();  
 void PrintGame(int map[20][20]);  
 void MoveToPos(int x, int y);  
 void RunTheGame();  
 void CreateFood(int map[20][20]);  
 int ChangeSnake();  
 int SnakeMove(int map[20][20]);  
 
 int map[20][20] = {0};
 int snakeNextMove = 1;
 int snakeLength = 4;
 int foodIsAte = 1;

 int main(void)
 {
 	InitializeGame();
 	RunTheGame();
 	return 0;
 }
 
 void RunTheGame()
 {
 	while(1)
 	{
 		int endOrContinue;
 		snakeNextMove = ChangeSnake();
 		system("cls");
 		endOrContinue = SnakeMove(map);
 		if(foodIsAte == 1)
 		{
 			CreateFood(map);
		}
		PrintGame(map);
		if(endOrContinue == -1)
		{
			break;
		}
		Sleep(500);	
	}
 }
 
 void CreateFood(int map[20][20])
 {
 	int food_x;
 	int food_y;
 	srand((int)time(NULL));
 	while(1)
 	{
 		food_x = rand()%20;
 		food_y = rand()%20;
 		if(map[food_x][food_y] == 0)
 		{
 			break;
		}
	}
	map[food_x][food_y] = 1000;
	foodIsAte = 0;
 }
 
 
 int ChangeSnake()
 {
 	char click;
 	if (_kbhit())
	{
		click = _getch();
	}
	if(click == 'w')
	{
		return 1;
	}
	else if(click == 's')
	{
		return 2;
	}
	else if(click == 'a')
	{
		return 3;
	}
	else if(click == 'd')
	{
		return 4;
	}
	else 
	{
		return snakeNextMove;
	}
 } 
 
 int SnakeMove(int map[20][20])
 {
 	int i, j;
 	int nowSnakeHead_x, nowSnakeHead_y, nowSnakeTail_x, nowSnakeTail_y;
	for(i = 0;i < 20;i++)
	{
		for(j = 0;j < 20;j++)
		{
			if(map[i][j] == 2)
			{
				nowSnakeHead_x = i;
				nowSnakeHead_y = j;	
			}
			if(map[i][j] == snakeLength + 1)
			{
				nowSnakeTail_x = i;
				nowSnakeTail_y = j;
			}	
		} 
	}
	
	if(snakeNextMove == 1)
	{
		nowSnakeHead_y -= 1;
	}
	else if(snakeNextMove == 2)
	{
		nowSnakeHead_y += 1;
	}
	else if(snakeNextMove == 3)
	{
		nowSnakeHead_x -= 1;
	}
	else
	{
		nowSnakeHead_x += 1;
	}
	
	if(map[nowSnakeHead_x][nowSnakeHead_y] > 0&&map[nowSnakeHead_x][nowSnakeHead_y] <= 361)
	{
		return -1;	
	}
	else
	{
		if(map[nowSnakeHead_x][nowSnakeHead_y] == 0)
		{
			map[nowSnakeHead_x][nowSnakeHead_y] = 1;
			map[nowSnakeTail_x][nowSnakeTail_y] = 0;
			for(i = 0;i < 20;i++)
			{
				for(j = 0;j < 20;j++)
				{
					if(map[i][j] != 0&&map[i][j] > 1&&map[i][j] != 1000)
					{
						map[i][j]++;
					}
				}
			}
			map[nowSnakeHead_x][nowSnakeHead_y] += 1;
		} 
		else
		{
			map[nowSnakeHead_x][nowSnakeHead_y] = 1;
			snakeLength++; 
			foodIsAte = 1;
			for(i = 0;i < 20;i++)
			{
				for(j = 0;j < 20;j++)
				{
					if(map[i][j] != 0&&map[i][j] > 1&&map[i][j] != 1000)
					{
						map[i][j]++;
					}
				}
			}
			map[nowSnakeHead_x][nowSnakeHead_y] += 1;
		} 
		return 1;
	}		
 } 
 
 void PrintGame(int map[20][20])
 {
 	int i, j;
 	for(i = 0;i < 20;i++)
 	{
 		for(j = 0;j < 20;j++)
		{
			if(map[i][j] >= 1&&map[i][j] <= 361)
			{
				MoveToPos(i * 2, j);
				printf("■"); 
			}
			else if(map[i][j] == 1000)
			{
				MoveToPos(i * 2, j);
				printf("□");	
			} 
		}
	}
 }
 
 void InitializeGame()
 {
 	int i, j;
 	for(i = 0;i < 20;i++)
 	{
 		if(i == 0||i == 19)
 		{
 			for(j = 0;j < 20;j++)
			{
				map[i][j] = 1;	 	
			}	
		}
		else
		{
			map[i][0] = 1;
			map[i][19] = 1;
		} 		
	}
	
	map[8][7] = 2; 
	map[8][8] = 3;
	map[8][9] = 4;
	map[8][10] = 5;
 }
 
 void MoveToPos(int x, int y)
 {
	COORD pos;
	HANDLE hOutput;
	pos.X = x;
	pos.Y = y;
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(hOutput, pos);
 }

  下面,让我们逐步来分析我们的代码

一、贪吃蛇的移动方法

我们先了解一下贪吃蛇的两种移动方式

1、全身移动法

每个蛇的身体都是一个节点,蛇头往前移动,第二级身体移动到蛇头上一秒的位置,第三级身体移动到第二级身体上一秒的位置,以此类推,蛇就整体往前移动了一格

2、移尾移动法

我们把蛇尾的块移动到蛇头下一秒将移动的位置,然后这个块就变成蛇头,我们只移动一个块就实现了蛇的移动,如果当蛇的长度到非常大(比如说我蛇长已经20+或者多少个节点的时候),如果用第一种方法,每次移动都需要移动20多次,而如果用第二种,每次移动依然还是只移动一次,就实现了蛇的移动

当然,这个时候抛一些问题让大家思考一下:

(1)如果我将蛇身涂成一个红一个白一个红一个白……这种,采用哪一种移动方式更合适?为什么?
(2)如果某种方式不适合,有没有什么简单的改动就能使这种移动方式也能适合间隔色涂装的蛇移动

我们这个入门级教程里,用二维数组来表示地图,蛇,食物这些,并通过最简单的输出语句来输出,通过屏幕清除的函数来每次刷新界面(会有一点问题,但是为了要简单。。而且适应大家的水平。。所以先用这个把)

使用的新东西(不需详细知道):移动光标函数,清屏函数

下面开始正式教学

二、打印地图边框

1、光标跳转

我们需要使用到一个光标跳转的函数

 void MoveToPos(int x, int y)
 {
	COORD pos;
	HANDLE hOutput;
	pos.X = x;
	pos.Y = y;
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(hOutput, pos);
 }

隶属于头文件

  #include<window.h>

函数可以实现光标跳转到指定位置的功能

### 2、建一个二维数组,并使用0初始化

   int map[20][20] = {0};

C语言会将map[0][0]这个元素替换为0,然后因为之后的元素程序都没给出,所以系统会默认用0来补全这个数组

3、 1表示地图的边界,我们通过一个函数来把数组边缘一圈赋值为1

   void InitializeGame()
 {
 	int i, j;
 	for(i = 0;i < 20;i++)
 	{
 		if(i == 0||i == 19)
 		{
 			for(j = 0;j < 20;j++)
			{
				map[i][j] = 1;	 	
			}	
		}
		else
		{
			map[i][0] = 1;
			map[i][19] = 1;
		} 		
	}
 }

4、建立一个打印的函数,根据数组的元素来把界面打印出来

 void PrintGame(int map[20][20])
 {
 	int i, j;
 	for(i = 0;i < 20;i++)
 	{
 		for(j = 0;j < 20;j++)
		{
			if(map[i][j] == 1)
			{
				MoveToPos(i * 2, j);
				printf("■"); 
			}
		}
	}
 }

这个函数在遇到1的时候会打印一个黑色方块

5、一阶段收束,我们看一下现在的运行结果

地图成功打印

我们来进入第二阶段,在这个阶段,我们尝试打印出蛇,并且让蛇“动起来”

二、打印蛇并让它移动

1、清屏和睡眠

第二阶段我们需要用到一个叫做清屏的函数(也许不用?我会在后面的部份提出这个,可以继续往后看)

system("cls");

隶属于头文件

include<window.h>

还有一个是睡眠函数,作用是使程序在这个地方“睡眠”一段时间,单位是毫秒

Sleep(500)

隶属于头文件

include<window.h>

意思是睡眠500毫秒(0.5s)

让我们开始操作

2、打印蛇身

 void InitializeGame()
 {
 	int i, j;
 	for(i = 0;i < 20;i++)
 	{
 		if(i == 0||i == 19)
 		{
 			for(j = 0;j < 20;j++)
			{
				map[i][j] = 1;	 	
			}	
		}
		else
		{
			map[i][0] = 1;
			map[i][19] = 1;
		} 		
	}
	
	map[8][7] = 2;   //1是尾部 
	map[8][8] = 3;
	map[8][9] = 4;
	map[8][10] = 5;  //4是头部 
 }

我们使用了数字来“制作蛇身”(前面的程序是我们已经写好了的,新增的语句只有最下面四句),这样的话,我们还需要在打印的语句中略作修改

 void PrintGame(int map[20][20])
 {
 	int i, j;
 	for(i = 0;i < 20;i++)
 	{
 		for(j = 0;j < 20;j++)
		{
			if(map[i][j] >= 1&&map[i][j] <= 361)
			{
				MoveToPos(i * 2, j);
				printf("■"); 
			}
		}
	}
 }

让我们运行一下看看

蛇被打印了出来

好的,那我们开始下一步操作,让蛇移动起来

3、蛇的移动

 int snakeNextMove = 1; // 1表示(0,-1),2表示(0,1),3表示(-1,0),4表示(1,0) 
 
 int snakeLength = 4;   // 表示蛇的最长长度,也用来定位

我们先定义两个变量,一个用来判断蛇头下一次的移动,一个用来判断蛇长度(也方便后面的加分)

我将详细讲解本部份中的重要函数SnakeMove()

 int i, j;
 int nowSnakeHead_x, nowSnakeHead_y, nowSnakeTail_x, nowSnakeTail_y;
 for(i = 0;i < 20;i++)
 {
	for(j = 0;j < 20;j++)
	{
		if(map[i][j] == 2)
		{
			nowSnakeHead_x = i;
			nowSnakeHead_y = j;	
		}
		if(map[i][j] == snakeLength + 1)
		{
			nowSnakeTail_x = i;
			nowSnakeTail_y = j;
		}	
	} 
 }

函数的第一部分,我们定义了nowSnakeHead来记录蛇头的位置,nowSnakeTail来记录蛇尾的位置(因为我们蛇是按照2,3,4,5这样下去的,所以蛇尾的记录数字等于蛇身长度+1)

 if(snakeNextMove == 1)
 {
	nowSnakeHead_y -= 1;
 }
 else if(snakeNextMove == 2)
 {
	nowSnakeHead_y += 1;
 }
 else if(snakeNextMove == 3)
 {
	nowSnakeHead_x -= 1;
 }
 else
 {
	nowSnakeHead_x += 1;
 }

函数的第二部分,用一系列的if else分支来判断蛇头下一步将会走到的位置(如果有能力,最好使用switch case的写法,较容易看懂)

此时,我们蛇头已经到了下一个运动到的节点的坐标,我们需要对这个坐标进行判断,如果这个坐标是空地,我们就可以继续行动,如果是墙或者蛇身,那么我们就不能进行运动,游戏结束

 if(map[nowSnakeHead_x][nowSnakeHead_y] > 0&&map[nowSnakeHead_x][nowSnakeHead_y] <= 361)
 {
 	return -1;	
 }
 else
 {
	if(map[nowSnakeHead_x][nowSnakeHead_y] == 0)
	{
		map[nowSnakeHead_x][nowSnakeHead_y] = 1;
		map[nowSnakeTail_x][nowSnakeTail_y] = 0;
		for(i = 0;i < 20;i++)
		{
			for(j = 0;j < 20;j++)
			{
				if(map[i][j] != 0&&map[i][j] > 1&&map[i][j] != 1000)
				{
					map[i][j]++;
				}
			}
		}
		map[nowSnakeHead_x][nowSnakeHead_y] += 1;
	} 
	return 1;
 }	

我们地图空地是0,所以如果蛇头下一次移动的地方不是0的话,就说明已经撞到东西了,我们返回一个-1 如果前面是空地,那么我们继续下面的操作,首先,把蛇头下一次移动要到的这个地方赋值为1,把蛇尾赋值为0(变成空地) for循环让我们的蛇进行了一次递增的操作,如果你用笔走一遍程序,那么大概是这样的

蛇通过这种方式“动起来”

现在我们的蛇可以移动了,我们来理清一下贪吃蛇移动的思路 (1)清除屏幕 (2)“移动”(不可视) (3)打印(可视) (4)睡眠一段时间 一直重复以上操作,就达到了移动的效果,我们使用一个函数来实现以上操作

 void RunTheGame()
 {
 	while(1)
 	{
 		int endOrContinue;
 		
 		system("cls");
 		endOrContinue = SnakeMove(map);
		PrintGame(map);
		if(endOrContinue == -1)
		{
			break;
		}
		Sleep(500);	
	}
 }

While(1)表示这是一个不停止的循环 定义一个endOrContinue来判断游戏是否继续进行 system(“cls”)清除屏幕 打印屏幕 移动蛇(不可视),如果返回值是-1,游戏结束 让我们运行一下现在的程序

现在蛇已经可以自己运动了,并且会在撞上墙后终止程序

我们可以进入第三步了,控制蛇的移动还有生成食物

三、控制蛇,生成食物

1、获取键盘输入

第三阶段我们需要获取键盘输入,这个时候我们需要使用到获取输入的函数,同样的,不需要大家掌握内部,但是需要大家会用

 char click;
 if (_kbhit())
 {
	click = _getch();
 }

_kbhit()监听输入,_getch()获取输入

2、改变方向

那么我们就需要监听按键,并且改动我们之前设置的变量snakeNextMove,如下

 int ChangeSnake()
 {
 	char click;
 	if (_kbhit())
	{
		click = _getch();
	}
	if(click == 'w')
	{
		return 1;
	}
	else if(click == 's')
	{
		return 2;
	}
	else if(click == 'a')
	{
		return 3;
	}
	else if(click == 'd')
	{
		return 4;
	}
	else 
	{
		return snakeNextMove;
	}
 } 

同时,我们需要将这个函数放在每次运动的间隔中,让我们运动的过程中可以改变方向

 void RunTheGame()
 {
 	while(1)
 	{
 		int endOrContinue;
 		snakeNextMove = ChangeSnake();
 		system("cls");
 		endOrContinue = SnakeMove(map);
		PrintGame(map);
		if(endOrContinue == -1)
		{
			break;
		}
		//snakeNextMove = ChangeSnake();
		Sleep(500);	
	}
 }

现在,我们已经可以改变蛇的方向了(注释掉的那句加上去也没事)

3、生成食物

接下来,我们来加入食物 我们需要食物有什么样的特征? (1)它必须可以碰撞(吃) (2)它必须生成点不在蛇or地图边缘上 (3)它必须可以吃一个刷新一个

我们来逐步实现它的功能

首先,生成的食物不能在蛇的身上,所以我们需要检测生成点

 void CreateFood(int map[20][20])
 {
 	int food_x;
 	int food_y;
 	srand((int)time(NULL));
 	while(1)
 	{
 		food_x = rand()%20;
 		food_y = rand()%20;
 		if(map[food_x][food_y] == 0)
 		{
 			break;
		}
	}
	map[food_x][food_y] = 1000;        //1000表示食物 
	foodIsAte = 0;
 }

srand()是置入随机数种子(让随机数更“随机”),我们用每分每秒都在更新的时间作为种子让他生成随机数 检测食物生成点是不是空地,不是的话继续循环 直到检测到是空地时,给这个地块赋值1000,我们就可以在打印函数里面把食物打印出来了

 void PrintGame(int map[20][20])
 {
 	int i, j;
 	for(i = 0;i < 20;i++)
 	{
 		for(j = 0;j < 20;j++)
		{
			if(map[i][j] >= 1&&map[i][j] <= 361)
			{
				MoveToPos(i * 2, j);
				printf("■"); 
			}
			else if(map[i][j] == 1000)
			{
				MoveToPos(i * 2, j);
				printf("□");	
			} 
		}
	}
 }

1000的地方会生成空白的方块

同时,我们需要它能够被蛇碰触到,这个时候我们需要考虑两点

(1)蛇的碰撞 (2)蛇碰撞后的身体增加 于是我们需要修改两个地方的代码

 if(map[nowSnakeHead_x][nowSnakeHead_y] > 0&&map[nowSnakeHead_x][nowSnakeHead_y] <= 361)
 {
	return -1;	
 }

我们把碰撞返回-1(游戏结束)的函数中,限定碰撞到的块如果时在(0,361]范围内的话,就游戏结束,因为食物块是1000,所以避免了碰撞后游戏结束

那么,怎么使蛇能够身体增加呢?我们想到了我们最开始的操作,蛇移动的时候把最后那块身体去掉了,如果我们保留下来的话,就会变成“身体增长”的情况了,我们在移动的块中这样改

 if(map[nowSnakeHead_x][nowSnakeHead_y] == 0) //空地
 {
	map[nowSnakeHead_x][nowSnakeHead_y] = 1;
	map[nowSnakeTail_x][nowSnakeTail_y] = 0;
	for(i = 0;i < 20;i++)
	{
		for(j = 0;j < 20;j++)
		{
			if(map[i][j] != 0&&map[i][j] > 1&&map[i][j] != 1000)
			{
				map[i][j]++;
			}
		}
	}
	map[nowSnakeHead_x][nowSnakeHead_y] += 1;
 } 

移动的下一块是空地的话照常移动,如果下一块是食物的话

 else                  //吃到食物 
 {
	map[nowSnakeHead_x][nowSnakeHead_y] = 1;
	//map[nowSnakeTail_x][nowSnakeTail_y] = 0;
	snakeLength++; 
	foodIsAte = 1;
	for(i = 0;i < 20;i++)
	{
		for(j = 0;j < 20;j++)
		{
			if(map[i][j] != 0&&map[i][j] > 1&&map[i][j] != 1000)
			{
				map[i][j]++;
			}
		}
	}
	map[nowSnakeHead_x][nowSnakeHead_y] += 1;
 } 

思考:

(1)两个代码之间差距大不大? (2)不大的话,试试怎么能把两个代码合成一块,节省代码量

然后我们需要食物能随时更新,于是我们可以来定义一个变量

 int foodIsAte = 1;     // 开局产生食物 

当每次生成完食物,我们把这个值设为0,当每次吃到食物,我们把这个值设为1,在打印的时候,如果这个值是1的话,那么我们就调用生成食物的函数,就可以生成食物了,并且可以通过这个方法保持食物的有序更新

 void RunTheGame()
 {
 	while(1)
 	{
 		int endOrContinue;
 		snakeNextMove = ChangeSnake();
 		system("cls");
 		endOrContinue = SnakeMove(map);
 		if(foodIsAte == 1)
 		{
 			CreateFood(map);
		}
		PrintGame(map);
		if(endOrContinue == -1)
		{
			break;
		}
		//snakeNextMove = ChangeSnake();
		Sleep(500);	
	}
 }

插入了如果是1,就生成食物的语句 大功告成,让我们跑一下看看我们现在的程序吧

运行完好

小白入门级极简易贪吃蛇教学已经完成 后续有对游戏的细节打磨部份,抛几个问题

(1)尝试自己动手写开始,结束界面
(2)尝试将 的500改成变量,并通过计算蛇的长度来改变这个值,来达到吃得越多速度越快的效果
(3)尝试将游戏染上颜色
(4)游戏是否会出现闪动的现象?如何解决这个问题(尝试不使用清屏法,而是覆盖输出)

千里之行,始于足下。