本文章在于利用51单片机和OLED屏幕实现一个简易的贪吃蛇游戏
所用的51单片机为普中51系列,OLED屏幕属中景园电子,具体实物均可以在某宝购买
OLED模块:
关于OLED模块的相关函数及其.h和.c文件均可以使用由中景园电子商家提供的历程中的代码,经验证都是可靠的
在这里简单提一些OLED屏幕的特性:
一、OLED屏幕是由128*64的像素点组成的,长128列,而宽64的像素点又被分为了八个page,在单片机向其传输数据时,是一次性传输八个位的数据,也就是一个page中的一列,所以我干脆将8*8总共64个像素点当作贪吃蛇的一节蛇身
二、相信使用过OLED屏幕的人都会发现另一个一个特性,就是当你点亮某一块区域,如果你不对这一区域进行清屏再显示其他图像时,那麽之前被点亮的部分在没有被新的数据覆盖的情况下将会继续显示,具体现象可以自行验证
三、中景园商家提供的库函数里面,有两个函数,一个是OLED_Set_Pos(x,y),还有一个是OLED—_WR_Byte()函数,这两个函数一个用于定位OLED屏幕中的位置,其中x是列数,而y是page数,在定位之后,再利用第二个函数向定位处传输图像数据,而且OLED屏幕在传输完一列之后会自动向下一列传输数据,非常方便。
代码部分:
大概阐述一下代码思想,我们使用的是8*8的一块区域作为一节蛇身,所以整个屏幕被我们划分成长16*8,我们将在这个区域内显示我们的蛇身,由于显示区域较小,可以考虑无墙,也可以有墙,完全看个人喜好,最主要的是蛇身的移动以及加长。
首先,定义一个可以表示蛇身坐标的结构体,包含横纵坐标即可,然后定义一个结构体数组以及一个变量用于存储所有蛇身以及食物的位置
struct snake //结构体,用于储存蛇身的位置;
{
unsigned char x;
unsigned char y;
} ;
struct snake Pos[20]; //蛇的身体坐标数据;
struct snake food ;//食物的坐标位置;
由于在单片机中,我们能用到的存储空间非常有限,所以定义的最大长度就是20,在接下来的蛇身加长以及移动模块中还将提及有关51内存的问题。在解决了蛇身位置存储问题之后,就要把蛇身打印在屏幕上
蛇身打印函数:
在OLED模块中已经提及了两个比较实用的函数,我们就可以通过这两个函数来实现蛇身打印
const unsigned char code body[8]={0xFF,0xFF,0xC3,0xC3,0xC3,0xC3,0xFF,0xFF};
void OLED_SnakeBody(unsigned char x, unsigned char y)
{
u8 i;
OLED_Set_Pos(x*8,y);
for(i=0;i<8;i++)
{
OLED_WR_Byte(body[i],OLED_DATA);
}
}
其中body[8]是用来打印蛇身时传输的数据,显示的效果如下图所示:
食物生成函数:
对于我个人而言,食物生成函数是贪吃蛇游戏中比较难的一个部分,因为食物的随机生成意味着要产生一个随机数,但是怎么样的数是随机的呢?在借鉴了部分大佬的代码之后,我决定利用定时器来生成两个随机数,作为食物的横纵坐标位置。(在51单片机中,大可以把THX和TLX看作两个常数,只不过这两个常数是在随着时间做自加和清零动作罢了)具体代码如下:
void Food_Init() //食物初始化函数;
{
u8 i;
while(!foodflag)
{
food.x=count%16;
food.y=count%8;
for(i=0;i<head;i++)
{
if((food.x==Pos[i].x&&food.y==Pos[i].y)||food.x>=15||food.y>=7||food.x<=1||food.y<=1)//一旦满足这个条件,这个食物
//数据就应该被舍弃,因为在蛇身或者在地图上;
giveup=1;
}
if(giveup==1)
{
foodflag=0;
giveup=0;
}
else
{
foodflag=1;
OLED_SnakeBody(food.x,food.y);
giveup=0;
break;
}
}
}
其中有一个标志位:foodflag,这个标志位的作用在于标记食物状态,当食物被吃掉,即foodflag为0的时候,就将进入循环一直对count进行运算,直到取出一个合适的食物坐标为止(其中的count被我放在定时器中做自加运算并且按时清零)。但是,这样的食物生成方式会有一个缺陷,就是当你吃掉食物之后,可能会出现短暂的延时之后才会生成食物,所以可以考虑其他方式来生成随机食物,总之,仁者见仁,智者见智。
独立按键改变蛇的移动方向:
贪吃蛇移动过程中,需要不断改变方向,而51单片机有四个独立按键,虽然位置对于游戏来说不太友好,但是可以减小代码复杂度,所以推荐使用独立按键而非矩阵键盘。在此,我引入了一个变量mode来表示蛇的运动方向,1左2右3上4下,,并且通过独立按键来改变mode的值,在改变方向的时候要注意当蛇在向左运动的时候,不能直接向右,也不能再向左,只能上或者下,其他情况亦如是。
void leftkey()
{
if(left==0&&mode!=1&&mode!=2)
delay(25);
if(left==0&&mode!=1&&mode!=2)
mode=1;
while(!left);
}
void rightkey()
{
if(right==0&&mode!=1&&mode!=2)
delay(25);
if(right==0&&mode!=1&&mode!=2)
mode=2;
while(!right);
}
void upkey()
{
if(up==0&&mode!=3&&mode!=4)
delay(25);
if(up==0&&mode!=3&&mode!=4)
mode=3;
while(!up);
}
void downkey()
{
if(down==0&&mode!=3&&mode!=4)
delay(25);
if(down==0&&mode!=3&&mode!=4)
mode=4;
while(!down) ;
}
void keypros()
{
leftkey();
rightkey();
upkey();
downkey();
}
接下来就是整个程序中比较核心的部分,当然,也是个人认为比较简单的部分:
贪吃蛇移动函数:
在上面的部分中我已经提及了一个OLED屏幕的特性,就是未被清除的部分仍将继续显示,也就是说,在每一次蛇移动的时候,我们仅仅需要清除蛇尾,更新蛇头即可,而中间的身体节点可以保持不变。那吃到食物之后该怎么办呢,更好办,连蛇尾都可以不用去掉,只需更新头部即可。但是,在每一次更新位置的时候,要注意,将数组里的每一节蛇身坐标进行改变(要注意数据移动方向),在吃到食物之后,还需要将蛇身加长,即length++。
具体代码示下,仅提供左移状态函数,其他类似:
void modeleft()
{
u8 i,j;
if(mode==1)
{
//首先判断是否撞墙;
if(Pos[head-1].x==0)
page=2;
//再判断是否咬到自己的身体;
if(page==1)
for(i=0;i<head-1;i++) //这里要注意,千万不要把头部坐标也挤上去,省一点时间;
{
if((Pos[head-1].x-1)==Pos[i].x&&Pos[head-1].y==Pos[i].y)
page=2;
}
//如果既没有撞墙也没有咬到自己 ,那就考虑吃到食物和没有吃到的情况;
if(page==1)
{
//吃到食物的情况;
if((Pos[head-1].x-1)==food.x&&Pos[head-1].y==food.y)
{
head++;//长度加一;
foodflag=0;//刷新下一次食物;
Pos[head-1].x=Pos[head-2].x-1; //改变头部之后,把之前头部的位置运算之后进行赋值;
Pos[head-1].y=Pos[head-2].y;
OLED_SnakeBody(Pos[head-1].x,Pos[head-1].y);//点亮新的狗头,就完事了;
}
else //没吃到食物的情况‘
{
OLED_CLR_Body(Pos[tail].x,Pos[tail].y);//砍断旧尾巴;
for(j=0;j<head-1;j++)
{
Pos[j].x=Pos[j+1].x;
Pos[j].y=Pos[j+1].y;
}
Pos[head-1].x--;
OLED_SnakeBody(Pos[head-1].x,Pos[head-1].y);//点亮新狗头;不能先点亮狗头,不然会造成数据丢失;
}
}
}
}
至此,所有重要的函数部分都已经结束。
剩下的就是蛇身初始化函数,游戏结束函数,重新开始函数等等,但要注意的是,如果有重新开始函数,不仅仅要对蛇身重新初始化,还要对蛇身长度等重要变量和标志位进行重置,不然就等着修bug吧。
当然,本程序存在许多较繁琐的步骤和函数,也是纯属由于博主水平有限,如若文章存在错别字啥的……那就骂我好啦(语文功底是在不行)。
为了方便大家参考,就把所有代码全部放出吧:
#include"reg51.h"
#include"oled.h"
sbit left=P3^1;
sbit right=P3^0;
sbit up=P3^2;
sbit down=P3^3;//按键定义;
bit moveflag=0,foodflag=0,giveup=0,start=0,fun=0; //giveup是食物被舍弃标志位,foodflag是食物刷新标志位,moveflag是移动标志位;
u8 yanshi=60,count=0,page=0,mode=0,time=150,point=1;//page用于显示不同的页面,比如游戏结束页面,开始页面等;mode用于识别蛇的运动状态;
//time分两档,一是150,二是90;
struct snake //结构体,用于储存蛇身的位置;
{
unsigned char x;
unsigned char y;
} ;
u8 head=3,tail=0;//蛇身长度;
struct snake Pos[20]; //蛇的身体坐标数据;
struct snake food ;//食物的坐标位置;
void TimerInit()
{
TMOD=0x01;
TH0=0xec;
TL0=0x77; //10毫秒计时; //还是要改定时初值;选用4毫秒;
ET0=1;
EA=1;
TR0=0; //只有在page2的时候才打开;
}
void delay(u8 i)
{
while(i--);
}
void ShowTen()
{
OLED_ShowChar(0,3,'T');
}
void Game_Restart()
{
if(left==0)
delay(25);
if(left==0)
{
page=0;
OLED_Clear();
}
while(!left);
}
void Snake_Over()//蛇死亡函数,在死亡之后显示game over,同时不断检测复原键是否被按下;
{
if(page==2)
{
mode=0;//必须清零,否则就会在下一次游戏开始时造成蛇的乱移动;
TR0=0;//关闭定时器;
OLED_Clear();//先清屏;
while(page==2)
{
OLED_ShowString(0,0,"game over");
OLED_ShowString(30,3,"press left to restart");
Game_Restart();
}
}
}
void Food_Init() //食物初始化函数;
{
u8 i;
while(!foodflag)
{
food.x=count%16;
food.y=count%8;
for(i=0;i<head;i++)
{
if((food.x==Pos[i].x&&food.y==Pos[i].y)||food.x>=15||food.y>=7||food.x<=1||food.y<=1)//一旦满足这个条件,这个食物
//数据就应该被舍弃,因为在蛇身或者在地图上;
giveup=1;
}
if(giveup==1)
{
foodflag=0;
giveup=0;
}
else
{
foodflag=1;
OLED_SnakeBody(food.x,food.y);
giveup=0;
break;
}
}
}
void Snake_Init() //蛇初始化函数;
{
u8 i;
Pos[0].x=6;
Pos[0].y=3;
Pos[1].x=7;
Pos[1].y=3;
Pos[2].x=8;
Pos[2].y=3;
for(i=0;i<head;i++)
{
OLED_SnakeBody(Pos[i].x,Pos[i].y);
}
}
void leftkey()
{
if(left==0&&mode!=1&&mode!=2)
delay(25);
if(left==0&&mode!=1&&mode!=2)
mode=1;
while(!left);
}
void rightkey()
{
if(right==0&&mode!=1&&mode!=2)
delay(25);
if(right==0&&mode!=1&&mode!=2)
mode=2;
while(!right);
}
void upkey()
{
if(up==0&&mode!=3&&mode!=4)
delay(25);
if(up==0&&mode!=3&&mode!=4)
mode=3;
while(!up);
}
void downkey()
{
if(down==0&&mode!=3&&mode!=4)
delay(25);
if(down==0&&mode!=3&&mode!=4)
mode=4;
while(!down) ;
}
void keypros()
{
leftkey();
rightkey();
upkey();
downkey();
}
void modeleft()
{
u8 i,j;
if(mode==1)
{
//首先判断是否撞墙;
if(Pos[head-1].x==0)
page=2;
//再判断是否咬到自己的身体;
if(page==1)
for(i=0;i<head-1;i++) //这里要注意,千万不要把头部坐标也挤上去,省一点时间;
{
if((Pos[head-1].x-1)==Pos[i].x&&Pos[head-1].y==Pos[i].y)
page=2;
}
//如果既没有撞墙也没有咬到自己 ,那就考虑吃到食物和没有吃到的情况;
if(page==1)
{
//吃到食物的情况;
if((Pos[head-1].x-1)==food.x&&Pos[head-1].y==food.y)
{
head++;//长度加一;
foodflag=0;//刷新下一次食物;
Pos[head-1].x=Pos[head-2].x-1; //改变头部之后,把之前头部的位置运算之后进行赋值;
Pos[head-1].y=Pos[head-2].y;
OLED_SnakeBody(Pos[head-1].x,Pos[head-1].y);//点亮新的狗头,就完事了;
}
else //没吃到食物的情况‘
{
OLED_CLR_Body(Pos[tail].x,Pos[tail].y);//砍断旧尾巴;
for(j=0;j<head-1;j++)
{
Pos[j].x=Pos[j+1].x;
Pos[j].y=Pos[j+1].y;
}
Pos[head-1].x--;
OLED_SnakeBody(Pos[head-1].x,Pos[head-1].y);//点亮新狗头;不能先点亮狗头,不然会造成数据丢失;
}
}
}
}
void moderight()
{
u8 i,j;
if(mode==2)
{
//首先判断是否撞墙;
if(Pos[head-1].x==15)
page=2;
//再判断是否咬到自己的身体;
if(page==1)
for(i=0;i<head-1;i++) //这里要注意,千万不要把头部坐标也挤上去,省一点时间;
{
if((Pos[head-1].x+1)==Pos[i].x&&Pos[head-1].y==Pos[i].y)
page=2;
}
//如果既没有撞墙也没有咬到自己 ,那就考虑吃到食物和没有吃到的情况;
if(page==1)
{
//吃到食物的情况;
if((Pos[head-1].x+1)==food.x&&Pos[head-1].y==food.y)
{
head++;//长度加一;
foodflag=0;//刷新下一次食物;
Pos[head-1].x=Pos[head-2].x+1; //改变头部之后,把之前头部的位置运算之后进行赋值;
Pos[head-1].y=Pos[head-2].y;
OLED_SnakeBody(Pos[head-1].x,Pos[head-1].y);//点亮新的狗头,就完事了;
}
else //没吃到食物的情况‘
{
OLED_CLR_Body(Pos[tail].x,Pos[tail].y);//砍断旧尾巴;
for(j=0;j<head-1;j++)
{
Pos[j].x=Pos[j+1].x;
Pos[j].y=Pos[j+1].y;
}
Pos[head-1].x++;
OLED_SnakeBody(Pos[head-1].x,Pos[head-1].y);//点亮新狗头;不能先点亮狗头,不然会造成数据丢失;
}
}
}
}
void modeup()
{
u8 i,j;
if(mode==3)
{
//首先判断是否撞墙;
if(Pos[head-1].y==0)
page=2;
//再判断是否咬到自己的身体;
if(page==1)
for(i=0;i<head-1;i++) //这里要注意,千万不要把头部坐标也挤上去,省一点时间;
{
if((Pos[head-1].x)==Pos[i].x&&Pos[head-1].y-1==Pos[i].y)
page=2;
}
//如果既没有撞墙也没有咬到自己 ,那就考虑吃到食物和没有吃到的情况;
if(page==1)
{
//吃到食物的情况;
if(Pos[head-1].x==food.x&&(Pos[head-1].y-1)==food.y)
{
head++;//长度加一;
foodflag=0;//刷新下一次食物;
Pos[head-1].x=Pos[head-2].x; //改变头部之后,把之前头部的位置运算之后赋值给新头部;
Pos[head-1].y=Pos[head-2].y-1;
OLED_SnakeBody(Pos[head-1].x,Pos[head-1].y);//点亮新的狗头,就完事了;
}
else //没吃到食物的情况‘
{
OLED_CLR_Body(Pos[tail].x,Pos[tail].y);//砍断旧尾巴;
for(j=0;j<head-1;j++)
{
Pos[j].x=Pos[j+1].x;
Pos[j].y=Pos[j+1].y;
}
Pos[head-1].y--;
OLED_SnakeBody(Pos[head-1].x,Pos[head-1].y);//点亮新狗头;不能先点亮狗头,不然会造成数据丢失;
}
}
}
}
void modedown()
{
u8 i,j;
if(mode==4)
{
//首先判断是否撞墙;
if(Pos[head-1].y==7)
page=2;
//再判断是否咬到自己的身体;
if(page==1)
for(i=0;i<head-1;i++) //这里要注意,千万不要把头部坐标也挤上去,省一点时间;
{
if((Pos[head-1].x)==Pos[i].x&&Pos[head-1].y+1==Pos[i].y)
page=2;
}
//如果既没有撞墙也没有咬到自己 ,那就考虑吃到食物和没有吃到的情况;
if(page==1)
{
//吃到食物的情况;
if(Pos[head-1].x==food.x&&(Pos[head-1].y+1)==food.y)
{
head++;//长度加一;
foodflag=0;//刷新下一次食物;
Pos[head-1].x=Pos[head-2].x; //改变头部之后,把之前头部的位置运算之后赋值给新头部;
Pos[head-1].y=Pos[head-2].y+1;
OLED_SnakeBody(Pos[head-1].x,Pos[head-1].y);//点亮新的狗头,就完事了;
}
else //没吃到食物的情况‘
{
OLED_CLR_Body(Pos[tail].x,Pos[tail].y);//砍断旧尾巴;
for(j=0;j<head-1;j++)
{
Pos[j].x=Pos[j+1].x;
Pos[j].y=Pos[j+1].y;
}
Pos[head-1].y++;
OLED_SnakeBody(Pos[head-1].x,Pos[head-1].y);//点亮新狗头;不能先点亮狗头,不然会造成数据丢失;
}
}
}
}
void choice()
{
if(left==0)
{
delay(20);
if(left==0)
{
point++;
if(point>3)
point=1;
}
while(!left);
}
if(right==0)
{
delay(20);
if(right==0)
{
if(point==1)
{
time=90;
page=1;
start=1;
foodflag=0;
}
if(point==2)
{
time=150;
page=1;
//OLED_ShowString(0,0,"150");
start=1;
foodflag=0;
}
if(point==3)
fun=1;
}
while(!right);
}
}
void modepros()
{
modeup();
modeleft();
modedown();
moderight();
}
void page0()
{
OLED_ShowString(10,0,"greedy snake");
OLED_ShowString(20,2,"difficult");
OLED_ShowString(20,4,"normal");
OLED_ShowString(20,6,"easy") ;
if(point==1)
{
OLED_ShowChar(10,2,'>');
OLED_ShowChar(10,4,' ');
OLED_ShowChar(10,6,' ');
}
if(point==2)
{
OLED_ShowChar(10,2,' ');
OLED_ShowChar(10,4,'>');
OLED_ShowChar(10,6,' ');
}
if(point==3)
{
OLED_ShowChar(10,3,' ');
OLED_ShowChar(10,4,' ');
OLED_ShowChar(10,6,'>');
}
if(fun==1)
{
OLED_Clear();
OLED_ShowString(0,4,"you really have face");
delay(100000);
OLED_Clear();
fun=0;
}
choice();
}
void main()
{
TimerInit();
OLED_Init();
OLED_Clear();
ShowTen();
while(yanshi)
{
delay(30000);
yanshi--;
}
OLED_Clear();
while(1)
{
while(page==0)
{
page0();
}
while(page==1)
{
if(start==1)
{
OLED_Clear();
TR0=1;
head=3;
Snake_Init();
start=0;
}
Food_Init();
keypros();
}
while(page==2)
{
TR0=0;
Snake_Over();
}
}
}
void Timer0() interrupt 1
{
TH0=0xec;
TL0=0x77; //4ms
count++;
//OLED_ShowNum(0,0,count,3,16);
if(count==time)
{
count=0;
modepros();
}
}
由于后期添加了其他功能。部分函数可以直接忽略,只参考有价值的代码段即可。
博主水平有限,尚在学习充能中,如存在部分错误,纰漏,表意不全等缺点,烦请原谅和指教。
谢谢大家支持!!!
|