一个星期前,我把单片机带回了寝室,然后。。。就诞生了个小小的贪吃蛇小游戏程序

先提前说好,在这个函数中涉及到几个函数需要替换的清手动替换

OLED_Clear();        //oled清屏函数
OLED_Rect();         //oled画空心矩形函数
OLED_ShowString();   //oled显示字符串函数
OLED_DrawDot();      //oled描点函数
OLED_ShowNum();      //oled显示变量值函数
delay_ms();          //延时函数

那么,让我们先看一下这个.h文件

#ifndef __OLED_SNAKE_H
#define __OLED_SNAKE_H		

#define Key_UP 		P00
#define Key_DOWN	P01
#define Key_Right	P02
#define Key_Left	P03 
#define Key_UP_number 		0
#define Key_DOWN_number 	1
#define Key_Right_number 	2
#define Key_Left_number 	3
//按键宏定义

void OLED_snake();
#endif

这个.h文件是把下面的.c文件里用到的按键和一些特殊定义的值给宏定义一下,然后整个直接在main函数里调用OLED_snake();这个函数就行了。

所以.h就完成了一些片面操作,主要还得看.c里面的算法

#include "oled.h"
#include "oled_snake.h"
#include "stdlib.h"				//用随机数函数=rand();

char snake_x[100];
char snake_y[100];
char snake_food[2];
char fraction = 0;	//分数
char fraction_temporary = 0;	//分数临时值
void Game_Over();
void OLED_snake()
{
	char x = 20,y = 31;	//蛇初始位置
	char i = 0;					//蛇增量
	char Key_number = Key_Right_number;	//默认向右
	char a = 1;
	OLED_Clear();		//清除上一局的残骸
	snake_x[0] = x;
	snake_y[0] = y;
	snake_food[0] = 60;
	snake_food[1] = 31;
	OLED_Rect(0,0,95,63);								//设置贪吃蛇内部范围
	OLED_ShowString(96,0,"GET:",0);  		//显示分数文字
	//这个设置大概是,蛇在(1,1)到(94,62)[不包括线]内有效-----------------------------------------------
	OLED_DrawDot(snake_food[0],snake_food[1],1);	//先默认生成出来一个食物
	while(1)
	{
		OLED_ShowNum(100,1,fraction,3,16);  		//显示分数文字
		//把分数值显示出来
		if(Key_number == Key_UP_number | Key_number == Key_DOWN_number)		//检测当前的蛇是不是向上或者向下
		{
			if(Key_Right == 1)
			{
				Key_number = Key_Right_number;	//向右
			}else if(Key_Left == 1)
			{
				Key_number = Key_Left_number;		//向左
			}
		}
		if(Key_number == Key_Right_number | Key_number == Key_Left_number)	//检测当前的蛇是不是向左或者向右
		{
			if(Key_UP == 1)
			{
				Key_number = Key_UP_number;
			}else if(Key_DOWN == 1)
			{
				Key_number = Key_DOWN_number;
			}
		}
		//这里是通过判断蛇当前走向进行转向的控制,毕竟没有哪个贪吃蛇是可以回头的吧--------------------------
		OLED_DrawDot(snake_x[fraction],snake_y[fraction],0);		//把尾巴消除掉
		if(Key_UP != 1 | Key_DOWN != 1 | Key_Right != 1 | Key_Left != 1)
		{
		switch(Key_number)
		{
			case Key_Right_number:
				x = x + 1;
				y = y;
				break;
			case Key_Left_number:
				x = x - 1;
				y = y;
				break;
			case Key_UP_number:
				x = x;
				y = y + 1;
				break;
			case Key_DOWN_number:
				x = x;
				y = y - 1;
				break;
		}
		}
		fraction_temporary = fraction;
		for(fraction_temporary;fraction_temporary>0;fraction_temporary--)	//通过分数决定循环次数
		{
			snake_x[fraction_temporary] = snake_x[fraction_temporary - 1];
			snake_y[fraction_temporary] = snake_y[fraction_temporary - 1];
		}
		snake_x[0] = x;
		snake_y[0] = y;	//计算好的坐标存入数组头
		
		fraction_temporary = fraction;
		for(fraction_temporary;fraction_temporary>=0;fraction_temporary--)	//
		{
			OLED_DrawDot(snake_x[fraction_temporary],snake_y[fraction_temporary],1);
		}
		//把蛇的身体完整的显示出来-------------------------------------------------------------
		if(snake_x[0] == snake_food[0] & snake_y[0] == snake_food[1])
		{
			fraction++;
			
			while(a)
			{
				snake_food[0] = rand();
				snake_food[1] = rand();
				if(snake_food[0]>=92 | snake_food[1]>=61 | snake_food[1]<=3 | snake_food[0]<=3)
				{
					a++;
				}
				a--;
			}
			a = 1;
		}
		OLED_DrawDot(snake_food[0],snake_food[1],1);
		//判断蛇是不是吃到食物了,吃到了就生成下一个food----------------------------------------
		fraction_temporary = fraction;
		for(fraction_temporary;fraction_temporary>0;fraction_temporary--)
		{
			if(snake_x[0] == snake_x[fraction_temporary] & snake_y[0] == snake_y[fraction_temporary])
			{
				Game_Over();
			}
		}
		//判断蛇头是不是跟蛇身体重叠了----------------------------------------------------------
		i++;
		if(fraction >= 50)
		{
		}else{
			delay_ms(50 - fraction);//算是速度吧,分越高速度越快
		}
		if(x>=94|x<=1|y>=62|y<=1)
		{
			Game_Over();
		}
	}
}

void Game_Over()
{
	OLED_ShowString(12,2,"GAME OVER",0);
	fraction_temporary = fraction;
	while(1)
	{
		//delay_ms(500);
		if(Key_UP == 1 | Key_DOWN == 1 | Key_Right == 1 | Key_Left == 1)
		{
			while(Key_UP|Key_DOWN|Key_Right|Key_Left);
			for(fraction_temporary;fraction_temporary>=0;fraction_temporary--)	//
			{
				OLED_DrawDot(snake_x[fraction_temporary],snake_y[fraction_temporary],0);
			}
			OLED_DrawDot(snake_food[0],snake_food[1],0);
			fraction = 0;
			OLED_Clear();
			OLED_snake();
		}
	}
}

嚯嚯,看起来可真长,实则不然,都是一个一个区域的功能叠加出来的功能罢了;看到昂,这个.c里一共有两个函数名,分别是Game_Over();OLED_Snake();,不用看,前面的gameover是给后面OledSnake服务的,也就是显示"GameOver"的功能,

那,让我们把OLED_Snake();这个大函数拆开来看看吧

一、基础参数定义部分

char snake_x[100];
char snake_y[100];
char snake_food[2];
char fraction = 0;	//分数
char fraction_temporary = 0;	//分数临时值

在整个大函数开始前,我是定义了三个数组和两个cahr变量,三个数组的前两个snake_x[100];snake_y[100];是保存整个蛇的身体的数组,这个数组内部的赋值是根据下面的fraction分数值来决定的,就好比分数是0,那么蛇的身体只有1,那么保存蛇身体位置坐标信息只保存一次即可;在如果我分数是10,那么蛇的身体就是11,那这个数组就会保存11组蛇的身体的坐标。fraction_temporary也是储存了分数值,只是后面经常要对fraction分数值进行操作,所以就衍生出来一个"分身"来代替原来的分数进行操作。

讲解完定义部分之后,我们就进入void OLED_snake()内部看看(其实吧整个函数看一遍之后会发现,我已经大概写好了一部分到才一部分的范围都是从哪儿到哪儿了)

二、对蛇以及食物各方面的初始化

char x = 20,y = 31;	//蛇初始位置
char i = 0;					//蛇增量
char Key_number = Key_Right_number;	//默认向右
char a = 1;
OLED_Clear();		//清除上一局的残骸
snake_x[0] = x;
snake_y[0] = y;
snake_food[0] = 60;
snake_food[1] = 31;
OLED_Rect(0,0,95,63);								//设置贪吃蛇内部范围
OLED_ShowString(96,0,"GET:",0);  		//显示分数文字
//这个设置大概是,蛇在(1,1)到(94,62)[不包括线]内有效-----------------------------------------------
OLED_DrawDot(snake_food[0],snake_food[1],1);	//先默认生成出来一个食物

这一段开头先是定义了蛇头的坐标x,y,并赋值初始值是在(20,31)的位置;

紧接着定义了一个键值Key_number来保存现在蛇应该往哪个方向去,还记得之前.h文件里那一堆#define宏定义吗,在这里就派上用场了,把数字宏定义成文字,是大大增加了程序的可读性;

至于这个a,是后面生成蛇食物的时候用到的一个参数,到那个地方的时候在说;

在之后就把刚才初始化的蛇头位置赋值给了蛇身体数组的第"0"个位置里;

之后就把蛇的食物的初始位置(60,31)也赋值给了食物数组里,因为食物就一个,一个也就一个坐标也就是两个数固定了,所以用1个数组就够了;

OLED_Rect(0,0,95,63);这个函数就是画了个矩形,让玩家能看到边界;

OLED_ShowString(96,0,"GET:",0); 在矩形范围的右边显示分数信息;

OLED_DrawDot(snake_food[0],snake_food[1],1);把刚刚初始的食物显示出来

至此,初始化的内容就完成了,接下来就是进入整个函数的核心while循环里了,整个程序运行也就是在这个while循环里实现的

三、按键检测控制蛇头方向

if(Key_number == Key_UP_number | Key_number == Key_DOWN_number)		//检测当前的蛇是不是向上或者向下
{
	if(Key_Right == 1)
	{
		Key_number = Key_Right_number;	//向右
	}else if(Key_Left == 1)
	{
		Key_number = Key_Left_number;		//向左
	}
}
if(Key_number == Key_Right_number | Key_number == Key_Left_number)	//检测当前的蛇是不是向左或者向右
{
	if(Key_UP == 1)
	{
		Key_number = Key_UP_number;
	}else if(Key_DOWN == 1)
	{
		Key_number = Key_DOWN_number;
	}
}
//这里是通过判断蛇当前走向进行转向的控制,毕竟没有哪个贪吃蛇是可以回头的吧--------------------------

这里通过两个if来控制蛇头变化的方向,我们常见的蛇的转向就是上转右,右转下之类的,不管你们玩没玩过,反正我是没玩过直接掉头的贪吃蛇;这个区域就是通过判断蛇头方向来控制检测的按键:假如蛇头现在方向向上,那么只有向左向右的按键可以使用。

下面就是蛇移动和显示的关键部分之一了!

四、蛇头,蛇身体坐标位置计算和储存

OLED_DrawDot(snake_x[fraction],snake_y[fraction],0);		//把尾巴消除掉
if(Key_UP != 1 | Key_DOWN != 1 | Key_Right != 1 | Key_Left != 1)
{
	switch(Key_number)
	{
		case Key_Right_number:
			x = x + 1;
			y = y;
			break;
		case Key_Left_number:
			x = x - 1;
			y = y;
			break;
		case Key_UP_number:
			x = x;
			y = y + 1;
			break;
		case Key_DOWN_number:
			x = x;
			y = y - 1;
		    break;
		}
	}
fraction_temporary = fraction;
for(fraction_temporary;fraction_temporary>0;fraction_temporary--)	//通过分数决定循环次数
{
	snake_x[fraction_temporary] = snake_x[fraction_temporary - 1];
	snake_y[fraction_temporary] = snake_y[fraction_temporary - 1];
}
snake_x[0] = x;
snake_y[0] = y;	//计算好的坐标存入数组头
		
fraction_temporary = fraction;
for(fraction_temporary;fraction_temporary>=0;fraction_temporary--)	//
{
	OLED_DrawDot(snake_x[fraction_temporary],snake_y[fraction_temporary],1);
}
//把蛇的身体完整的显示出来-------------------------------------------------------------

    从开头的一个擦除就能看出来,整个蛇的身体应该是先擦在写的显示方法

那么开头为什么说是先擦除蛇尾呢?OLED_DrawDot(snake_x[fraction],snake_y[fraction],0);这个函数可以看到,它是读取蛇身体坐标数组里第"fraction"个数来进行擦除行为,这时候进行模拟一下,此时分数是0,那么之前固定写进第0位的数在这里会因为分数"fraction"为0而擦除,当然,如果分数为1,那擦的就是第1个数。

    这个地方解释起来很迷,是因为这个地方和下面的地方连接的太紧密了,容我接着解释

紧跟着尾巴清除的就是一个通过识别Key_number的值得一个switch来控制蛇头坐标的变化的地方,四个方向对应着四种坐标变化方式,在下次有效按键按下之前,坐标都会以固定方法变化。

    Ok到目前为止还能解释的清楚,下面的两个for说实话我认为还挺复杂的,我们在拆开来说

接着,fraction_temporary = fraction;分数的分身继承了分数的值,来进行下面for的操作

for(fraction_temporary;fraction_temporary>0;fraction_temporary--)	//通过分数决定循环次数
{
	snake_x[fraction_temporary] = snake_x[fraction_temporary - 1];
	snake_y[fraction_temporary] = snake_y[fraction_temporary - 1];
}

这个for,把坐标数组里的值整体向后移动了一位,这样就能把之前"蛇头"的坐标转变成"蛇身";加入分数为0,那么这个for就不会运行。如果分数为10,那么这个for就会运行10次,数组里面的数也会向后移动10位,加上蛇头,一共就是11个数。

在这个for下面snake_x[0] = x; snake_y[0] = y;是把刚刚蛇头计算好的坐标重新赋值给数组0位里,成为了新的"蛇头"

    哎嘿,光把这些坐标信息保存起来没用啊,得让它显示出来啊,于是就有了下面一个for

同样的,fraction_temporary = fraction;分数的分身继承了分数的值,来进行下面for的操作

for(fraction_temporary;fraction_temporary>=0;fraction_temporary--)	//
{
	OLED_DrawDot(snake_x[fraction_temporary],snake_y[fraction_temporary],1);
}

这个for和上面的for有一个不同就是,这个for的判断变成了>=,这样就算分数值是0也会运行一次用来显示蛇。

至此,整条蛇的显示是可以根据分数来显示了,单贪吃蛇贪吃蛇,不吃怎么行呢

五、蛇吃食物的判定和新食物的生成

if(snake_x[0] == snake_food[0] & snake_y[0] == snake_food[1])
{
	fraction++;
	
	while(a)
	{
		snake_food[0] = rand();
		snake_food[1] = rand();
		if(snake_food[0]>=92 | snake_food[1]>=61 | snake_food[1]<=3 | snake_food[0]<=3)
		{
			a++;
		}
		a--;
	}
	a = 1;
}
OLED_DrawDot(snake_food[0],snake_food[1],1);
//判断蛇是不是吃到食物了,吃到了就生成下一个food----------------------------------------

哦哦!是a!之前定义的a在这里出现了!

开头一个if先判定蛇头坐标是否和食物坐标重合,如果重合就执行下面的食物随机生成

其实在我看来这段代码很傻,秉承着随机的态度,我使用了stdlib.h里的rand();函数,但通过翻阅相关文档发现,rand();这个函数生成的随机数非常非常大,范围为1-32767,所以在while里我放了个a,a的初始值是1,那么while(a)就会循环,在随机完后就a--变成了0,理论就不会循环了,但我又加了个判断,判断这个食物的坐标在不在判定的矩形范围内,如果不在就a++给它加回去,可以说是很弱智了。。。在while满足循环完后,立马重置a=1作为下一次循环,毕竟食物不止一个嘛。

OLED_DrawDot(snake_food[0],snake_food[1],1);把刚刚随机出来的食物坐标显示出来

光吃食物不行啊,如果能一直吃的话玩得好到最后整个屏幕都是蛇就不行了,贪吃蛇的经典玩法里有个蛇吃到身体就会死的设定,所以这个也不能少啦

六、蛇吃到自己的判定

fraction_temporary = fraction;
for(fraction_temporary;fraction_temporary>0;fraction_temporary--)
{
	if(snake_x[0] == snake_x[fraction_temporary] & snake_y[0] == snake_y[fraction_temporary])
	{
		Game_Over();
	}
}
//判断蛇头是不是跟蛇身体重叠了----------------------------------------------------------

经典的分数的分身继承分数的值,这个for的思路就是对比蛇头坐标和蛇身体坐标是否吻合,吻合就会判定为吃到了身子,游戏就会结束。这里的for用的判断方法是>是为了防止出现蛇头坐标和蛇头坐标对比的情况,那这样游戏还没开始就会结束了。

七、最后的收尾(刷新速度和出界判定)

i++;
if(fraction >= 50)
{
}else{
	delay_ms(50 - fraction);//算是速度吧,分越高速度越快
}
if(x>=94|x<=1|y>=62|y<=1)
{
	Game_Over();
}

i的值是早期调试的时候用的变量,在程序几乎完善的时候基本就没用到,所以我们忽略这个i++(其实是我忘了删了);delay_ms(50 - fraction);这个延时会随着分数的增加越来越短,当分数到0的时候,延迟也变成了0,但是程序运行本身就是有延迟的,所以这个时候蛇移动的会快一些,但还不至于到瞬移的程度,但分数超过50之后延时负值程序会崩溃的,所以加了个判断分数是不是大于50来跳过那个函数防止程序崩溃;

在之后就是判断蛇头是不是跟边框框重合了,重合即为撞墙,就会判断为游戏结束

至此,整个贪吃蛇的核心就已经讲完了,还有一个函数

八、Game_Over函数

void Game_Over()
{
	OLED_ShowString(12,2,"GAME OVER",0);
	fraction_temporary = fraction;
	while(1)
	{
		//delay_ms(500);
		if(Key_UP == 1 | Key_DOWN == 1 | Key_Right == 1 | Key_Left == 1)
		{
			while(Key_UP|Key_DOWN|Key_Right|Key_Left);
			for(fraction_temporary;fraction_temporary>=0;fraction_temporary--)	//
			{
				OLED_DrawDot(snake_x[fraction_temporary],snake_y[fraction_temporary],0);
			}
			OLED_DrawDot(snake_food[0],snake_food[1],0);
			fraction = 0;
			OLED_Clear();
			OLED_snake();
		}
	}
}

只要一调用这个函数,就会在矩形中间显示"GAME OVER"的字样,此时重制所有的值并检测按键,通过调用核心函数OLED_snake();来开始下一局游戏。

我在上周六的时候写完了这个小游戏,从想法诞生到完成实践也就用了将近三个小时的时间,还是挺简单的

最后经典运行一下看看结果,因为没法发视频,就发个截图算了

我定义蛇身体数组的时候只定义100,所以理论蛇的身体最大也就100长度,而char变量最大可以到255,嘛,反正我最大就这样了。

这个算法好处就是,只要对应屏幕有描点,画矩形函数,这套就能移植到任何屏幕上去(没试过android),算法并没有进行优化,有什么不好的地方还请指出

源码公开,仅供学习

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐