在学完按键和点阵屏后,其实就可以做这个贪吃蛇了,只要理解了8*8点阵屏,贪吃蛇的显示就不成问题,理解了贪吃蛇的运作机制,做出贪吃蛇就不成问题,在网上也有很多人做出了这个程序,主要思想应该都是差不多的,我自认为我构思的思路还是很清晰的,在构思完后很快就将程序写出来了,这里我将我的思路以及代码分享给大家。
这是我的源码,不过是codeblocks+sdcc的环境编译的,无法用keil直接打开,如果要运行,进行移植一下就可以了,可以用作参考。
链接:https://pan.baidu.com/s/1xw-I-7jr_xupAGmat7nQRw
提取码:p96r
主要实现方法:
一、软件层:
1.地图:用一个8个单元数组来模拟点阵屏上的显示情况,第i个单元代表第i行的显示状况,所以每个元素我们使用一个16进制数来赋值给P0端口,实现点阵屏每一行的显示。
2.蛇:用结构体来表示,结构体变量包含:行走方向,每个身体结点的横坐标x[]和纵坐标y[],蛇的长度,蛇的生命。
3.食物:通过伪随机产生函数rand(),随机产生一个0~64的数,对应8*8数组点阵上的每一点,判断该点是否可以产生食物,然后在该点生成食物。
4.吃食物:如果蛇头坐标与食物坐标重合则食物消失,蛇长度加一。
5.移动:蛇在移动时,每次移动先从蛇尾开始,令它保存上一点的坐标信息,这个过程在循环中完成,然后再根据方向改变蛇头的坐标。
6.死亡:在每次移动后,判断是否撞墙(超出边界,横坐标和纵坐标是否小于0或者大于7),判断是否撞身体(蛇头和身体坐标是否有重合)。如果判断蛇死亡则将蛇的生命值置为0,在程序中判断然后宣布游戏结束。
二、底层:
1.显示:利用8*8点阵屏完成显示,其中应该改变平常的纵向显示(一列一列显示),采用横向显示(一行一行显示),因为这样可以和8*8数组很好地契合。具体实现方法就是用74HC595进行位选,然后将数组地每一行转化为由16进制数表示的模式,一行一行显示。
2.按键:选择4个矩阵按键控制方向(上下左右),然后按下按键后改变蛇的方向信息。还可以拓展一些功能比如暂停(将定时器停止计数),加速减速(改变移动频率)。
3.数码管:显示当前的蛇长
4.计时器:用51单片机的计时器功能计时,每过多久就进行移动一次。
三、软件层主要函数
1.Void INIT_SNAKE();//初始化时钟、蛇、创建食物。
2.Void CreatFood(void);//创建食物
3.Void RUN_SNAKE(void);//蛇移动,以及吃到食物增加身体长度。
4.Void DEAD_SNAKE(void);//判断蛇是否死亡
5.Void KEY_SCAN_SNAKE(void);//通过查询按键获得方向,以及对其他功能键功能实现
6.Void DIGPORT_SHOW(void);//将蛇和食物的数据通过该函数转化成在点阵显示屏上显示的数据并显示。
7.Void GameOver(void);//判断游戏是否结束
然后中途遇到一个大坑,好大好大的坑,虽然很早地完成了半成品,可以移动吃食物长身体,也就是完成了主要功能,但是实际上还有一个很大的缺陷,那就是这只蛇最多只有16节,再往上就会造成数据溢出,导致显示错误。这是为什么呢,因为我在定义时有个地方一直出错,结构体定义蛇结点坐标x和y我都用了一个数组,也就是int x[64],int y[64],但是实际上51单片机(在程序存储空间)并没有这么大的空间,最多只能定义到16,这让我很头疼。也是搜索了各种资料,走了好多弯路,尝试了好多方法,甚至还想自己改一个用汇编语言写的启动文件,最后终于找到了合理的解决方案,并且学习到了一个很重要的知识点。那就是关于51单片机存储器的知识。
首先如果不加定义将默认把变量存储到程序存储空间,这里可用的空间比较少,所以我只能开一个最多16的数组。所以就需要改变存储的区域了,这里在定义结构体变量时需要用到xdata,意思是存储到外部数据存储器(外部RAM),可以存放较多的数据。修改编译通过,把烧写进单片机,贪吃蛇可以运行,但是又出现了一个问题,贪吃蛇的显示经常会出现断片的现象,走着走着身体少了一节,但是实际上这节身体还是存在的。寻找了好久的原因,实际上是由于接口接触不好导致的,将接口部分稳定住就没有这种情况发生了。
http://blog.sina.com.cn/s/blog_94994f7b01010s1h.html
这里是我查阅到的资料,的确是我知识的盲区,在视频课程中也没有学到:
code 指定数据是存储在代码区,数据是在编程的时候跟代码一起写入代码存储器,运行过程中不能改变;
xdata 指定数据是存储在外部数据存储器了;
data 指定数据存储在内部低128字节数据存储器里,如果变量不指定存储位置,默认就是data型,这部分存储器寻址速度最快;
idata 指定数据存储在内部低256字节数据存储器里,但51只有128字节内部RAM,52才有256字节;
pdata 指定数据存储在外部低256字节数据存储器里,这时候寻址用8位寄存器R0和R1,而不用16位的DPTR,寻址速度比xdata快。
部分代码解释
我在主函数里完成整个流程,从初始化到获取键值再到定时移动设计,再到判断死亡判断游戏结束,最后将蛇长显示在数码管上将贪吃蛇显示在点阵屏上
void main(void)
{
int time=0;
INIT_SNAKE();
TR0=1;//开始计时
while(1)
{
KEY_SCAN_SNAKE();//获取键值,改变方向、调速
time=TH0*256+TL0;
if(time >= 10000)//10毫秒记一次
{
t++;
}
if(t>=tlimit)//每隔一段时间移动一次,我在按键中设置了调速的功能,就是改变tlimit的值实现的
{
RUN_SNAKE();//蛇移动一次
TH0=0;//时间置零
TL0=0;
t=0;
}
DEAD_SNAKE();
GameOver();
gDigValue[0]=snake.lenth/10;
gDigValue[1]=snake.lenth%10;
DigDisplay();//将蛇长显示在数码管上
DIGPORT_SHOW();
}
}
初始化过程:
void INIT_SNAKE(void)//初始化时钟、蛇、数码管、创建食物。
{
TMOD = 0x01; // 设T0为方式1,GATE=1;
TH0 = 0;
TL0 = 0;
ET0 = 1; // 允许T0中断
EA = 1; // 开启总中断
int0init();//开启中断
int1init();
for(u8 i=0;i<8;i++)//初始化数码管
gDigValue[i]=17;//17对应的是不亮
snake.life=1;
snake.dir=2;//初始化方向,0为上,1为左,2为右,3为下
snake.x[1]=0;//初始位置设置
snake.y[1]=0;
snake.x[0]=1;
snake.y[0]=0;
snake.lenth=2;
CreatFood();//建立食物
}
蛇的移动就和我思路说的一样,在移动时,每次移动先从蛇尾开始,令它保存上一点的坐标信息,这个过程在循环中完成,然后再根据方向改变蛇头的坐标,然后根据蛇头坐标判断是否吃到食物。
void RUN_SNAKE(void)//蛇移动,以及吃到食物增加身体长度。
{
for(int i=snake.lenth; i>0; i--)//先从蛇尾的身体结点开始,保存上一点的坐标信息
{
snake.y[i]=snake.y[i-1];
snake.x[i]=snake.x[i-1];
}
switch(snake.dir)//根据方向改变蛇头的坐标
{
case 0:snake.y[0]-=1;break;//上
case 1:snake.x[0]-=1;break;//左
case 2:snake.x[0]+=1;break;//右
case 3:snake.y[0]+=1;break;//下
default:break;
}
if(snake.x[0]==food.x && snake.y[0]==food.y)//如果吃到食物就加长然后再次创建食物
{
snake.lenth++;
CreatFood();
}
}
创建食物,就是利用stdlib.h头文件包含的rand()创建一个0~64随机值,然后转换成对应在点阵屏上的坐标,由于不能生成在蛇身上,就需要进行一个判断,如果在蛇的身上就需要重新创建食物。
void CreatFood(void)//创建食物
{
u8 n=0;
n=rand()%64;
food.x=n%8;
food.y=n/8;
for(u8 i=0;i<snake.lenth;i++)//判断食物是否建立在蛇的身体上
{
if(food.x==snake.x[i] && food.y==snake.y[i])//食物与蛇重合
{
CreatFood();//递归调用重新产生一个食物
return;
}
}
}
这个显示函数算是比较核心的了,你创建了蛇,创建了食物,完成了蛇的移动,这些都是存在一个虚拟的8*8数组中的,作为游戏肯定要将它显示在点阵屏上。首先要构造出这个8行的显示状况,然后再按行显示,这里采用的是74HC595位选,一行一行地显示。这个构造地方法就是先将这个地图每一行置为0,将蛇结点对应的行对应的位与进去,最后就完成了蛇和食物在点阵屏显示的情况。
这里需要注意的是,在底层函数的点阵屏显示中用到了~按位取反,因为对于按行显示我们是给P0端口赋值,而P0端口对应阴极,取0才会亮,所以每个位正好需要反一下。
void DIGPORT_SHOW(void)//将蛇和食物的数据通过该函数转化成在点阵显示屏上显示的数据并显示。
{
for(u8 i=0;i<8;i++)//初始化蛇在点阵屏的位置
{
gDig[i]=0x00;
}
for(u8 i=0;i<snake.lenth;i++)//将蛇的每一个结点的位置移入16进制数中
{
gDig[snake.y[i]] |= 1 << snake.x[i];
}
gDig[food.y] |= 1 << food.x;
SHOW_MAP(gDig);
}
/*一下是包含在DotMatrix.c中的代码,
这里拿过来用作说明
*/
void SHOW_MAP(u8 *character)
{
for(u8 i=0;i<8;i++)
{
P0=~character[i];//逐行显示 这里用到了按位取反
HC595(m[i]);//显示第i行,位选
HC595(0x00);//消影
}
}
我觉得比较核心的内容就是有关于蛇的创建、移动、显示了,完成了这些内容就相当于完成了整个功能了,但是其他功能也必不可少,像是判断蛇死亡、判断胜利、获取蛇的方向,这些相对简单,可以在我的源码中看到。
以下是我设置的其中一个小功能吧,我给一个中断按键设置了暂停功能,然后又给另一个中断按键设置了重生功能,将判断死亡函数和这个中断结合参考。
void int1(void) __interrupt (2) __using (0)//重生!!!
{
delay10ms();
if(P3_3 == 0)
{
if(snake.life==0)
{
snake.life=1;
snake.dir=2;
for(u8 i=1;i<snake.lenth;i++)
{
snake.x[i]=0;
snake.y[i]=0;
}
snake.x[0]=1;
snake.y[0]=0;
}
}
}
void GameOver(void)//判断游戏是否结束
{
if(snake.lenth==64)
{
while(1)
DigDisplay();
}
if(snake.life==0)
{
while(snake.life==0)
DigDisplay();
}
}
也许还有很多小功能可以加上,大家可以多去试一试!!