目录
系列文章目录
前言
《贪吃蛇》,一款经典的、怀旧的小游戏,单片机入门必写程序。
基于51单片机和8X8LED点阵屏(板载74HC595驱动)的矩阵键盘控制的小游戏《贪吃蛇》
基于51单片机和8X8LED点阵屏(MAX7219驱动)的自制独立按键控制的小游戏《贪吃蛇》
基于51单片机和16X16LED点阵屏(MAX7219驱动)的自制独立按键控制的小游戏《贪吃蛇》
基于51单片机和16X16LED点阵屏(MAX7219驱动)的普中开发板矩阵按键控制的小游戏《贪吃蛇》
之前做了8X8LED点阵屏和16X16LED点阵屏的贪吃蛇小游戏,现在做一个LCD1602显示的贪吃蛇小游戏。
用到51单片机最小开发板,用八位自制的独立按键控制,单片机芯片为STC89C52RC,晶振@12.0000MHz。用LCD1602进行显示。
B站对应视频下载链接里有普中开发板矩阵按键版本,有需要的可以前去下载。
效果查看/操作演示:B站搜索“甘腾胜”或“gantengsheng”查看。
源代码下载:B站对应视频的简介有下载链接。
一、效果展示
二、原理分析
整个程序的框架跟点阵屏的差不多,游戏原理也是一样的,不再赘述,不清楚的可以点击前言中8X8LED点阵屏的两个链接查看。
LCD1602贪吃蛇小游戏的难点在于怎么显示,LCD1602只能自定义8个字符,即CGRAM中只能写入64个字节(一个自定义字符的字模需要8个字节,用到每个字节的低5位)。想要显示多个汉字或者蛇身,则需要将整个屏幕分成四个区域,再动态扫描显示。具体一点的原理可以看我写的另一篇文章《基于51单片机和LCD1602的多汉字动态扫描显示》。
该如何显示蛇身呢?如下图所示,LCD1602总共有16X2=32个点阵,每个点阵的像素是5X8=40个,并且相邻点阵之间相隔一个像素,我们可以以4个像素(2X2)作为蛇身的最小单位,这样,“最小单位之间”的间隔是一样的,都是相隔一个像素。
跟多汉字显示不同,汉字的字模保存到ROM中,数据不能改变,显示出来的蛇是需要不停移动的,食物也是真随机生成的,所以动态显示对应的数据是要不断改变的。所以,需要定义一个显示缓存数组,生成新的食物或者蛇移动了,更改缓存数组的数据就行了,剩下的交给定时器和显示函数动态扫描。具体怎么转换数据和动态扫描显示,大家可以自行分析一下代码。
屏幕布局:左半屏是游戏区域,对应96个“最小单位”(每一个点阵可以显示6个“最小单位”),所以需要一个大小为96的一位数组(数据类型为无符号字符型)存储蛇身数据,高四位(范围:0~15)对应1~16列,低四位(范围:0~5)对应1~6行,用一个无符号字符型变量(unsigned char)存储蛇身的行列数据,动态扫描的显示缓存也是需要96个无符号字符型数据。右半屏实时显示得分(即蛇的长度)和已玩时间。
游戏结束全屏闪烁,可以用LCD1602的两个指令实现。屏幕显示的指令:0x0C,屏幕不显示的指令:0x08。
注意:如果用下图所示的小屏,也能玩,不过上下两行的间距较大,不是一个像素的距离,游戏效果差一些。
三、各模块代码
1、定时器0
h文件
#ifndef __TIMER0_H__
#define __TIMER0_H__
void Timer0_Init(void);
#endif
c文件
#include <REGX52.H>
/**
* @brief 定时器0初始化,1毫秒@12.000MHz
* @param 无
* @retval 无
*/
void Timer0_Init(void)
{
TMOD&=0xF0; //设置定时器模式(高四位不变,低四位清零)
TMOD|=0x01; //设置定时器模式(通过低四位设为“定时器0工作方式1”的模式)
TL0=0x18; //设置定时初值,定时1ms
TH0=0xFC; //设置定时初值,定时1ms
TF0=0; //清除TF0标志
TR0=1; //定时器0开始计时
ET0=1; //打开定时器0中断允许
EA=1; //打开总中断
PT0=0; //当PT0=0时,定时器0为低优先级,当PT0=1时,定时器0为高优先级
}
/*定时器中断函数模板
void Timer0_Routine() interrupt 1 //定时器0中断函数
{
static unsigned int T0Count; //定义静态变量
TL0=0x18; //设置定时初值,定时1ms
TH0=0xFC; //设置定时初值,定时1ms
T0Count++;
if(T0Count>=1000)
{
T0Count=0;
}
}
*/
2、八位独立按键
h文件
#ifndef __KEYSCAN_8_H__
#define __KEYSCAN_8_H__
unsigned char Key(void);
void Key_Tick(void);
#endif
c文件
#include <REGX52.H>
sbit Key1=P1^0;
sbit Key2=P1^1;
sbit Key3=P1^2;
sbit Key4=P1^3;
sbit Key5=P1^4;
sbit Key6=P1^5;
sbit Key7=P1^6;
sbit Key8=P1^7;
unsigned char KeyNumber;
/**
* @brief 获取独立按键键码
* @param 无
* @retval 按下按键的键码,范围:0,1~24,0表示无按键按下
*/
unsigned char Key(void)
{
unsigned char KeyTemp=0;
KeyTemp=KeyNumber;
KeyNumber=0; //主程序中获取键码值之后键码值清零,在下一次定时器扫描按键之前再次获取键码值,一定会返回0
return KeyTemp;
}
/**
* @brief 获取当前按下按键的状态,无消抖及松手检测
* @param 无
* @retval 按键值,范围:0~8,无按键按下时返回值为0
*/
unsigned char Key_GetState()
{
unsigned char KeyValue=0;
if(Key1==0){KeyValue=1;}
if(Key2==0){KeyValue=2;}
if(Key3==0){KeyValue=3;}
if(Key4==0){KeyValue=4;}
if(Key5==0){KeyValue=5;}
if(Key6==0){KeyValue=6;}
if(Key7==0){KeyValue=7;}
if(Key8==0){KeyValue=8;}
return KeyValue;
}
/**
* @brief 按键驱动函数,在中断中调用
* @param 无
* @retval 无
*/
void Key_Tick(void)
{
static unsigned char NowState,LastState;
LastState=NowState; //按键状态更新
NowState=Key_GetState(); //获取当前按键状态
//如果上个时间点按键未按下,这个时间点按键按下,则是按下瞬间
if(LastState==0)
{
switch(NowState)
{
case 1:KeyNumber=1;break;
case 2:KeyNumber=2;break;
case 3:KeyNumber=3;break;
case 4:KeyNumber=4;break;
case 5:KeyNumber=5;break;
case 6:KeyNumber=6;break;
case 7:KeyNumber=7;break;
case 8:KeyNumber=8;break;
default:break;
}
}
//如果上个时间点按键按下,这个时间点按键按下,则是一直按住按键
if(LastState && NowState)
{
if(LastState==1 && NowState==1){KeyNumber=9;}
if(LastState==2 && NowState==2){KeyNumber=10;}
if(LastState==3 && NowState==3){KeyNumber=11;}
if(LastState==4 && NowState==4){KeyNumber=12;}
if(LastState==5 && NowState==5){KeyNumber=13;}
if(LastState==6 && NowState==6){KeyNumber=14;}
if(LastState==7 && NowState==7){KeyNumber=15;}
if(LastState==8 && NowState==8){KeyNumber=16;}
}
//如果上个时间点按键按下,这个时间点按键未按下,则是松手瞬间
if(NowState==0)
{
switch(LastState)
{
case 1:KeyNumber=17;break;
case 2:KeyNumber=18;break;
case 3:KeyNumber=19;break;
case 4:KeyNumber=20;break;
case 5:KeyNumber=21;break;
case 6:KeyNumber=22;break;
case 7:KeyNumber=23;break;
case 8:KeyNumber=24;break;
default:break;
}
}
}
3、LCD1602
h文件
#ifndef __LCD1602_H__
#define __LCD1602_H__
void LCD_WriteCommand(unsigned char Command);
void LCD_WriteData(unsigned char Data);
void LCD_SetCursor(unsigned char Line,unsigned char Column);
void LCD_Init();
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char);
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String);
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_MakeChar(void);
void LCD_Clear(void);
void LCD_MoveLeft(void);
void LCD_MoveRight(void);
void LCD_Convert_MoveLeft(unsigned char *Array,Part,Offset);
void LCD_Convert_MoveUp(unsigned char *Array,Part,Offset);
void LCD_ShowChinese(unsigned char Part,PartSum);
#endif
c文件
#include <REGX52.H>
//引脚配置:
sbit LCD_RS=P2^5;
sbit LCD_RW=P2^6;
sbit LCD_EN=P2^7;
#define LCD_DataPort P0
//自定义字符,最多8个(8个字节对应一个字符)
unsigned char idata CGRAMData[64];
//函数定义:
/**
* @brief LCD1602私有延时函数,12MHz调用可延时35us
* @param 无
* @retval 无
*/
void LCD_Delay35us(void)
{
unsigned char i;
i=15;
while(--i);
}
/**
* @brief LCD1602私有延时函数,12MHz调用可延时2ms
* @param 无
* @retval 无
*/
void LCD_Delay2ms(void)
{
unsigned char i, j;
i=4;
j=225;
do
{
while(--j);
}while(--i);
}
/**
* @brief LCD1602写指令
* @param Command 要写入的指令
* @retval 无
*/
void LCD_WriteCommand(unsigned char Command)
{
LCD_RS=0;
LCD_RW=0;
LCD_DataPort=Command;
LCD_EN=1;
LCD_Delay35us();
LCD_EN=0;
LCD_Delay35us();
}
/**
* @brief LCD1602写数据
* @param Data 要写入的数据
* @retval 无
*/
void LCD_WriteData(unsigned char Data)
{
LCD_RS=1;
LCD_RW=0;
LCD_DataPort=Data;
LCD_EN=1;
LCD_Delay35us();
LCD_EN=0;
LCD_Delay35us();
}
/**
* @brief LCD1602设置光标位置
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @retval 无
*/
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{
if(Line==1)
{
LCD_WriteCommand(0x80|(Column-1));
}
else if(Line==2)
{
LCD_WriteCommand(0x80|(Column-1+0x40));
}
}
/**
* @brief LCD1602初始化函数
* @param 无
* @retval 无
*/
void LCD_Init()
{
LCD_WriteCommand(0x38); //八位数据接口,两行显示,5*7点阵
LCD_WriteCommand(0x0C); //显示开,光标关,闪烁关
LCD_WriteCommand(0x06); //数据读写操作后,光标自动加一,画面不动
LCD_WriteCommand(0x01); //光标复位,清屏
LCD_Delay2ms(); //清屏指令执行需要较长时间,需要较长的延时
}
/**
* @brief 在LCD1602指定位置上显示一个字符
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @param Char 要显示的字符
* @retval 无
*/
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
{
LCD_SetCursor(Line,Column);
LCD_WriteData(Char);
}
/**
* @brief 在LCD1602指定位置开始显示所给字符串
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param String 要显示的字符串
* @retval 无
*/
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=0;String[i]!='\0';i++)
{
LCD_WriteData(String[i]);
}
}
/**
* @brief 返回值=X的Y次方
*/
int LCD_Pow(int X,int Y)
{
unsigned char i;
int Result=1;
for(i=0;i<Y;i++)
{
Result*=X;
}
return Result;
}
/**
* @brief 在LCD1602指定位置开始显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~65535
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以有符号十进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-32768~32767
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{
unsigned char i;
unsigned int Number1;
LCD_SetCursor(Line,Column);
if(Number>=0)
{
LCD_WriteData('+');
Number1=Number;
}
else
{
LCD_WriteData('-');
Number1=-Number;
}
for(i=Length;i>0;i--)
{
LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以十六进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~0xFFFF
* @param Length 要显示数字的长度,范围:1~4
* @retval 无
*/
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i,SingleNumber;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
SingleNumber=Number/LCD_Pow(16,i-1)%16;
if(SingleNumber<10)
{
LCD_WriteData(SingleNumber+'0');
}
else
{
LCD_WriteData(SingleNumber-10+'A');
}
}
}
/**
* @brief 在LCD1602指定位置开始以二进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~1111 1111 1111 1111
* @param Length 要显示数字的长度,范围:1~16
* @retval 无
*/
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
}
}
/**
* @brief 在LCD1602的CGRAM中写入8个自定义字符的64个字节,每个5*8点阵横向取模,使用每个字节的低5位
* @param 无
* @retval 无
*/
void LCD_MakeChar(void)
{
unsigned char i;
LCD_WriteCommand(0x40); //CGRAM起始地址为0x40
for(i=0;i<64;i++) //01XX XXXX,最多可以在CGRAM中写入64个数据,一个自定义字符对应8个数据
{
LCD_WriteData(CGRAMData[i]);
}
}
/**
* @brief LCD1602的光标复位,清屏
* @param 无
* @retval 无
*/
void LCD_Clear(void)
{
LCD_WriteCommand(0x01);
LCD_Delay2ms();
}
/**
* @brief LCD1602的屏幕向左移动一个字符位,光标不动
* @param 无
* @retval 无
*/
void LCD_MoveLeft(void)
{
LCD_WriteCommand(0x18);
}
/**
* @brief LCD1602的屏幕向右移动一个字符位,光标不动
* @param 无
* @retval 无
*/
void LCD_MoveRight(void)
{
LCD_WriteCommand(0x1C);
}
/**
* @brief 将汉字取模得到的数据进行处理,处理后保存到CGRAMData缓存数组里,以便写入LCD的CGRAM中
* @param Array 传递过来的地址
* @param Part LCD的区域编号(将整个屏幕分成四部分),范围:1~4
* @param Offset 偏移量,整体向左移动Offset*15个像素(本来应该是16个像素,舍去了汉字的最后一列了),即整体向左移动Offset个汉字
* @retval 无
*/
void LCD_Convert_MoveLeft(unsigned char *Array,Part,Offset) //向左移动
{
unsigned char i;
Array+=Offset*32;
//将一个汉字拆成6个自定义字符
if(Part==1)
{
for(i=0;i<16;i++){CGRAMData[i]=*(Array+i)>>3;}
for(i=0;i<16;i++){CGRAMData[i+16]=(*(Array+i)<<2)|(*(Array+i+16)>>6);}
for(i=0;i<16;i++){CGRAMData[i+32]=*(Array+i+16)>>1;}
for(i=0;i<16;i++){CGRAMData[i+48]=*(Array+i+32)>>3;}
}
if(Part==2)
{
for(i=0;i<16;i++){CGRAMData[i]=(*(Array+i+32)<<2)|(*(Array+i+48)>>6);}
for(i=0;i<16;i++){CGRAMData[i+16]=*(Array+i+48)>>1;}
for(i=0;i<16;i++){CGRAMData[i+32]=*(Array+i+64)>>3;}
for(i=0;i<16;i++){CGRAMData[i+48]=(*(Array+i+64)<<2)|(*(Array+i+80)>>6);}
}
if(Part==3)
{
for(i=0;i<16;i++){CGRAMData[i]=*(Array+i+80)>>1;}
for(i=0;i<16;i++){CGRAMData[i+16]=*(Array+i+96)>>3;}
for(i=0;i<16;i++){CGRAMData[i+32]=(*(Array+i+96)<<2)|(*(Array+i+112)>>6);}
for(i=0;i<16;i++){CGRAMData[i+48]=*(Array+i+112)>>1;}
}
if(Part==4)
{
for(i=0;i<16;i++){CGRAMData[i]=*(Array+i+128)>>3;}
for(i=0;i<16;i++){CGRAMData[i+16]=(*(Array+i+128)<<2)|(*(Array+i+144)>>6);}
for(i=0;i<16;i++){CGRAMData[i+32]=*(Array+i+144)>>1;}
for(i=0;i<16;i++){CGRAMData[i+48]=*(Array+i+160)>>3;}
}
}
/**
* @brief 将汉字取模得到的数据进行处理,处理后保存到CGRAMData缓存数组里,以便写入LCD的CGRAM中
* @param Array 传递过来的地址
* @param Part LCD的区域编号(将整个屏幕分成四部分),范围:1~4
* @param Offset 偏移量,整体向上移动Offset个像素
* @retval 无
*/
void LCD_Convert_MoveUp(unsigned char *Array,Part,Offset)
{
unsigned char i,m,n;
m=Offset/16;
n=Offset%16;
Array+=m*192;
if(Part==1)
{
for(i=0;i<16;i++)
{
if(i<16-n){CGRAMData[i]=*(Array+i+n)>>3;}
else{CGRAMData[i]=*(Array+i+n+176)>>3;}
}
for(i=0;i<16;i++)
{
if(i<16-n){CGRAMData[i+16]=(*(Array+i+n)<<2)|(*(Array+i+16+n)>>6);}
else{CGRAMData[i+16]=(*(Array+i+n+176)<<2)|(*(Array+i+16+n+176)>>6);}
}
for(i=0;i<16;i++)
{
if(i<16-n){CGRAMData[i+32]=*(Array+i+16+n)>>1;}
else{CGRAMData[i+32]=*(Array+i+16+n+176)>>1;}
}
for(i=0;i<16;i++)
{
if(i<16-n){CGRAMData[i+48]=*(Array+i+32+n)>>3;}
else{CGRAMData[i+48]=*(Array+i+32+n+176)>>3;}
}
}
if(Part==2)
{
for(i=0;i<16;i++)
{
if(i<16-n){CGRAMData[i]=(*(Array+i+32+n)<<2)|(*(Array+i+48+n)>>6);}
else{CGRAMData[i]=(*(Array+i+32+n+176)<<2)|(*(Array+i+48+n+176)>>6);}
}
for(i=0;i<16;i++)
{
if(i<16-n){CGRAMData[i+16]=*(Array+i+48+n)>>1;}
else{CGRAMData[i+16]=*(Array+i+48+n+176)>>1;}
}
for(i=0;i<16;i++)
{
if(i<16-n){CGRAMData[i+32]=*(Array+i+64+n)>>3;}
else{CGRAMData[i+32]=*(Array+i+64+n+176)>>3;}
}
for(i=0;i<16;i++)
{
if(i<16-n){CGRAMData[i+48]=(*(Array+i+64+n)<<2)|(*(Array+i+80+n)>>6);}
else{CGRAMData[i+48]=(*(Array+i+64+n+176)<<2)|(*(Array+i+80+n+176)>>6);}
}
}
if(Part==3)
{
for(i=0;i<16;i++)
{
if(i<16-n){CGRAMData[i]=*(Array+i+80+n)>>1;}
else{CGRAMData[i]=*(Array+i+80+n+176)>>1;}
}
for(i=0;i<16;i++)
{
if(i<16-n){CGRAMData[i+16]=*(Array+i+96+n)>>3;}
else{CGRAMData[i+16]=*(Array+i+96+n+176)>>3;}
}
for(i=0;i<16;i++)
{
if(i<16-n){CGRAMData[i+32]=(*(Array+i+96+n)<<2)|(*(Array+i+112+n)>>6);}
else{CGRAMData[i+32]=(*(Array+i+96+n+176)<<2)|(*(Array+i+112+n+176)>>6);}
}
for(i=0;i<16;i++)
{
if(i<16-n){CGRAMData[i+48]=*(Array+i+112+n)>>1;}
else{CGRAMData[i+48]=*(Array+i+112+n+176)>>1;}
}
}
if(Part==4)
{
for(i=0;i<16;i++)
{
if(i<16-n){CGRAMData[i]=*(Array+i+128+n)>>3;}
else{CGRAMData[i]=*(Array+i+128+n+176)>>3;}
}
for(i=0;i<16;i++)
{
if(i<16-n){CGRAMData[i+16]=(*(Array+i+128+n)<<2)|(*(Array+i+144+n)>>6);}
else{CGRAMData[i+16]=(*(Array+i+128+n+176)<<2)|(*(Array+i+144+n+176)>>6);}
}
for(i=0;i<16;i++)
{
if(i<16-n){CGRAMData[i+32]=*(Array+i+144+n)>>1;}
else{CGRAMData[i+32]=*(Array+i+144+n+176)>>1;}
}
for(i=0;i<16;i++)
{
if(i<16-n){CGRAMData[i+48]=*(Array+i+160+n)>>3;}
else{CGRAMData[i+48]=*(Array+i+160+n+176)>>3;}
}
}
}
/**
* @brief 扫描显示函数
* @param Part 要显示的区域的编号,范围:1~4(从左到右)
* @retval 无
*/
void LCD_ShowChinese(unsigned char Part,PartSum)
{
Part--;
//显示一段时间后清空显示,防止多个区域都显示相同的内容
//清空上一个区域的内容
if(Part)
{
LCD_WriteCommand(0x80+(Part-1)*4);
LCD_WriteData(0x20); //清空显示,写“0x20”或直接写“20”都可以
LCD_WriteData(0x20);
LCD_WriteData(0x20);
LCD_WriteData(0x20);
LCD_WriteCommand(0xC0+(Part-1)*4);
LCD_WriteData(0x20);
LCD_WriteData(0x20);
LCD_WriteData(0x20);
LCD_WriteData(0x20);
}
else
{
LCD_WriteCommand(0x80+4*(PartSum-1));
LCD_WriteData(0x20);
LCD_WriteData(0x20);
LCD_WriteData(0x20);
LCD_WriteData(0x20);
LCD_WriteCommand(0xC0+4*(PartSum-1));
LCD_WriteData(0x20);
LCD_WriteData(0x20);
LCD_WriteData(0x20);
LCD_WriteData(0x20);
}
//将CGRAMData数组中的64个数据写入CGRAM进行更新
LCD_MakeChar();
//在对应位置写入8个自定义字符,先写第一行,在写第二行
LCD_WriteCommand(0x80+Part*4);
LCD_WriteData(0);
LCD_WriteData(2);
LCD_WriteData(4);
LCD_WriteData(6);
LCD_WriteCommand(0xC0+Part*4);
LCD_WriteData(1);
LCD_WriteData(3);
LCD_WriteData(5);
LCD_WriteData(7);
}
四、主函数
main.c
/*
by甘腾胜@20241208
效果查看/操作演示:可以在B站搜索“甘腾胜”或“gantengsheng”查看
单片机:STC89C52RC
晶振:12T@12.0000MHz
外设:自制的8位独立按键、LCD1602
注意:因为用了动态扫描的方式显示汉字和蛇,所以会有些模糊,需要将对比度调大一些
*/
#include <REGX52.H> //包含寄存器的定义
#include <STDLIB.H> //包含随机函数的声明
#include "KeyScan_8.h"
#include "LCD1602.h"
#include "Timer0.h"
unsigned char KeyNum; //存储获得的键码值的变量
unsigned char Mode; //游戏模式,0:向上滚动显示游戏名称“《贪吃蛇》”,1:向左滚动显示汉字“难度: 1”,
//2:难度选择界面(数字范围是1~5),3:游戏进行中,4:游戏结束全屏闪烁,
//5:显示作者姓名和编程日期
unsigned char MoveSnakeFlag; //移动蛇身的标志,1:移动,0:不移动
unsigned char Direction=1; //蛇头移动的方向,1:向右,2:向上,3:向左,4:向下,游戏开始时默认向右移动
unsigned char LastDirection=1; //蛇头上一次移动的方向,1:向右,2:向上,3:向左,4:向下,游戏开始时默认向右移动
unsigned char SnakeLength=2; //蛇的长度,初始值为2
unsigned char SnakeHead=1; //保存整条蛇的数据的数组(共96个数据,数据索引为:0~95)中,蛇头对应的数据的索引,蛇的初始长度为2,
//开始时只用了两个数据(数组的第1个数据和第2个数据),蛇头对应的是第2个数据(索引为1),SnakeHead的范围:0~95
unsigned char GameOverFlag; //游戏结束的标志,1:游戏结束,0:游戏未结束
unsigned char FlashFlag; //闪烁的标志,1:不显示,0:显示
unsigned int Food; //保存创造出来的食物的位置,高四位(范围:0~15)对应列(1~16列),低四位(范围:0~5)对应行(1~6行)
//从左往右数,分别是1~16列,从上往下数,分别是1~6行
unsigned int Offset; //偏移量,用来控制汉字滚动显示
unsigned char RollFlag; //滚动的标志,1:滚动,0:不滚动
unsigned char RollDirection; //滚动显示的方向,0:向左滚动,1:向上滚动
unsigned char ExecuteOnceFlag=1; //各模式中只执行一次的标志,1:执行,0:不执行,默认为1
unsigned char SnakeMoveSpeed=100; //蛇移动的速度,值越小,速度越快,上电默认1s移动一次(定时器计时时间为10ms)
unsigned char T0Count0,T0Count1,T0Count2,T0Count3; //定时器计数的变量
unsigned char PauseFlag; //暂停的标志,1:暂停,0:不暂停
unsigned char PartSum=4; //LCD总共要动态扫描显示的区域数,范围:1~4
unsigned char PartSelect; //LCD显示区域的选择,范围:0~PartSum-1(对应区域1~PartSum),中断中使用,用来切换显示的区域
unsigned char *Pointer; //传递给滚动显示函数LCD_Convert_MoveLeft和LCD_Convert_MoveUp的地址(数组首地址)
unsigned char Difficulty=1; //游戏难度,范围:1~5,上电后默认是难度1
unsigned char ChangedFlag; //游戏难度选择界面,游戏难度有变动的标志,1:有变动,0:无变动,用于更新数字的显示
unsigned int GameTime; //游戏时间,玩游戏时用来实时显示已玩时间
unsigned char pdata DisplayBuffer[160]; //显示缓存,用pdata修饰是因为片内RAM不够用,所以保存到片外RAM
unsigned char pdata SnakeBody[96]; //蛇身最长的长度是6X16=96,需要用96个数据记录蛇身的数据
//阴码(亮点为1),行列式取模,高位在左(所有取模都必须按这个格式,否则需要修改函数)
unsigned char code Table1[]={ // “《贪吃蛇》”
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //前面数据为0,是为了要做成向上滚动的效果
0x00,0x00,0x00,0x00,0x01,0x02,0x04,0x09,0x04,0x02,0x01,0x00,0x00,0x00,0x00,0x00,
0x10,0x24,0x48,0x90,0x20,0x40,0x80,0x00,0x80,0x40,0x20,0x90,0x48,0x24,0x10,0x00,/*"《",0*/
0x01,0x02,0x04,0x09,0x30,0xCF,0x00,0x00,0x1F,0x10,0x11,0x11,0x11,0x02,0x0C,0x70,
0x00,0x80,0x40,0x20,0x98,0xE6,0x40,0x80,0xF0,0x10,0x10,0x10,0x10,0x60,0x18,0x04,/*"贪",1*/
0x00,0x00,0x79,0x49,0x4A,0x4C,0x49,0x48,0x48,0x48,0x78,0x49,0x02,0x02,0x01,0x00,
0x80,0x80,0x00,0xFE,0x00,0x00,0xF8,0x08,0x10,0x60,0x80,0x00,0x02,0x02,0xFE,0x00,/*"吃",2*/
0x10,0x10,0x10,0x7D,0x55,0x56,0x54,0x54,0x7C,0x50,0x10,0x14,0x1E,0xE2,0x40,0x00,
0x20,0x10,0x10,0xFE,0x02,0x04,0x80,0x88,0x90,0xA0,0xC0,0x82,0x82,0x82,0x7E,0x00,/*"蛇",3*/
0x08,0x24,0x12,0x09,0x04,0x02,0x01,0x00,0x01,0x02,0x04,0x09,0x12,0x24,0x08,0x00,
0x00,0x00,0x00,0x00,0x80,0x40,0x20,0x90,0x20,0x40,0x80,0x00,0x00,0x00,0x00,0x00,/*"》",4*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*" ",5*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*" ",6*/
};
unsigned char code Table2[]={ // “难度: 1”
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //前面数据为0,是为了要做成向左滚动的效果
0x00,0x00,0xFC,0x05,0x05,0x4B,0x2D,0x29,0x11,0x11,0x29,0x25,0x45,0x81,0x01,0x01,
0xA0,0x90,0x80,0xFE,0x10,0x10,0xFC,0x10,0x10,0xFC,0x10,0x10,0x10,0xFE,0x00,0x00,/*"难",0*/
0x01,0x00,0x3F,0x22,0x22,0x3F,0x22,0x22,0x23,0x20,0x2F,0x24,0x42,0x41,0x86,0x38,
0x00,0x80,0xFE,0x20,0x20,0xFC,0x20,0x20,0xE0,0x00,0xF0,0x10,0x20,0xC0,0x30,0x0E,/*"度",1*/
0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,/*":",2*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*" ",3*/
0x00,0x00,0x00,0x00,0x07,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x0F,0x00,0x00,
0x00,0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xF8,0x00,0x00,/*"1",0*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*" ", */
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*" ", */
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*" ", */
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*" ", */
};
unsigned char code Table3[]={ //难度的数字(16X16)的字模:1~5
0x00,0x00,0x00,0x00,0x07,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x0F,0x00,0x00,
0x00,0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xF8,0x00,0x00,/*"1",1*/
0x00,0x00,0x00,0x0F,0x30,0x38,0x10,0x00,0x00,0x01,0x06,0x08,0x30,0x3F,0x00,0x00,
0x00,0x00,0x00,0xF0,0x18,0x0C,0x18,0x18,0x60,0x80,0x00,0x04,0x0C,0xF8,0x00,0x00,/*"2",2*/
0x00,0x00,0x00,0x0F,0x30,0x38,0x00,0x00,0x01,0x00,0x00,0x38,0x30,0x0F,0x00,0x00,
0x00,0x00,0x00,0xE0,0x18,0x18,0x18,0x60,0xF0,0x18,0x0C,0x0C,0x18,0xE0,0x00,0x00,/*"3",3*/
0x00,0x00,0x00,0x00,0x00,0x01,0x02,0x0C,0x10,0x20,0x7F,0x00,0x00,0x03,0x00,0x00,
0x00,0x00,0x00,0x30,0xF0,0x70,0x70,0x70,0x70,0x70,0xFE,0x70,0x70,0xFE,0x00,0x00,/*"4",4*/
0x00,0x00,0x00,0x1F,0x10,0x10,0x10,0x17,0x18,0x00,0x00,0x38,0x30,0x0F,0x00,0x00,
0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0xF0,0x18,0x0C,0x0C,0x0C,0x18,0xE0,0x00,0x00,/*"5",5*/
};
/**
* @brief 更新显示缓存数组DisplayBuffer的数据
* @param Position 需要更新显示的位置,字节高四位范围:0~15(对应1~16列),字节低四位范围:0~5(对应1~6行)
* @param State 需要更新成的状态,范围:0~1,0:熄灭,1:点亮
* @retval 无
*/
void UpdateDisplay(unsigned char Position,State)
{
char UpdateOffset; //偏移量
if(Position%16/3){UpdateOffset=-1;} //4~6行的偏移量
else{UpdateOffset=1;} //1~3行的偏移量
if(State==1) //如果需要点亮
{
switch(Position/16%6)
{
case 0:
DisplayBuffer[Position/16/3*16+Position%16*3] |= 0xC0; //一个字节对应蛇的三个点(每个点有四个小点)
DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] |= 0xC0; //总共有16行
break;
case 1:
DisplayBuffer[Position/16/3*16+Position%16*3] |= (0xC0>>3);
DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] |= (0xC0>>3);
break;
case 2:
DisplayBuffer[Position/16/3*16+Position%16*3] |= (0xC0>>5);
DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] |= (0xC0>>5);
break;
case 3:
DisplayBuffer[Position/16/3*16+Position%16*3] |= 0xC0;
DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] |= 0xC0;
break;
case 4:
DisplayBuffer[Position/16/3*16+Position%16*3] |= (0xC0>>2);
DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] |= (0xC0>>2);
break;
case 5:
DisplayBuffer[Position/16/3*16+Position%16*3] |= (0xC0>>5);
DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] |= (0xC0>>5);
break;
default:break;
}
}
else //如果需要熄灭
{
switch(Position/16%6)
{
case 0:
DisplayBuffer[Position/16/3*16+Position%16*3] &= ~0xC0;
DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] &= ~0xC0;
break;
case 1:
DisplayBuffer[Position/16/3*16+Position%16*3] &= ~(0xC0>>3);
DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] &= ~(0xC0>>3);
break;
case 2:
DisplayBuffer[Position/16/3*16+Position%16*3] &= ~(0xC0>>5);
DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] &= ~(0xC0>>5);
break;
case 3:
DisplayBuffer[Position/16/3*16+Position%16*3] &= ~0xC0;
DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] &= ~0xC0;
break;
case 4:
DisplayBuffer[Position/16/3*16+Position%16*3] &= ~(0xC0>>2);
DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] &= ~(0xC0>>2);
break;
case 5:
DisplayBuffer[Position/16/3*16+Position%16*3] &= ~(0xC0>>5);
DisplayBuffer[Position/16/3*16+Position%16*3+UpdateOffset] &= ~(0xC0>>5);
break;
default:break;
}
}
}
/**
* @brief 创造出随机位置的食物,数据的高四位(范围:0~15)代表食物所在的列(1~16),数据的低四位(范围:0~5)代表食物所在的行(1~6)
* @brief 从左往右数,分别是1~16列,从上往下数,分别是1~6行
* @param 无
* @retval 创造出的食物位置的数据
*/
unsigned char CreateFood(void)
{
unsigned char FoodTemp;
unsigned char i,j,m,n;
m=rand()%16; //产生一个0~15的随机数
n=rand()%6; //产生一个0~5的随机数
for(j=0;j<6;j++) //产生一个随机位置,判断该位置是否是蛇身,如果不是,就返回该位置所对应的数据
{ //如果该位置不是蛇身的位置,则从该点向周围寻找不是蛇身的空位置
for(i=0;i<16;i++)
{
// if( DisplayBuffer[((m+i)%16)/3*16+((n+j)%6)*3] & (0x80>>(m+i)%16%3*3) == 0 ) //这样写返回的值一定是0x00,暂不知道什么原因
if( !(DisplayBuffer[(m+i)%16/3*16+(n+j)%6*3] & (0x80>>(m+i)%16%3*3)) )
{
FoodTemp=(m+i)%16*16+(n+j)%6;
break; //找到了空位置就退出循环
}
}
}
return FoodTemp;
}
/**
* @brief 控制蛇的移动
* @param 无
* @retval 无
*/
void MoveSnake(void)
{
if(Direction==1) //如果向右移动
{
if((SnakeBody[SnakeHead]/16)==15){GameOverFlag=1;} //移动前判断一下移动后是否撞墙,如果是,则游戏结束,游戏结束的标志置1
//(SnakeHead+1)%96,取余的目的是为了防止越界,SnakeBody数组的索引范围是:0~95
else{SnakeBody[(SnakeHead+1)%96]=SnakeBody[SnakeHead]+16;} //SnakeBody数组中蛇头的下一个数据等于上一个数据加16(即高四位加1),即蛇头移动到了右边这一列
}
if(Direction==2) //如果向上移动
{
if((SnakeBody[SnakeHead]%16)==0){GameOverFlag=1;}
else{SnakeBody[(SnakeHead+1)%96]=SnakeBody[SnakeHead]-1;} //SnakeBody数组中蛇头的下一个数据等于上一个数据减1(即低四位减1),即蛇头移动到了上边这一行
}
if(Direction==3) //如果向左移动
{
if((SnakeBody[SnakeHead]/16)==0){GameOverFlag=1;}
else{SnakeBody[(SnakeHead+1)%96]=SnakeBody[SnakeHead]-16;} //SnakeBody数组中蛇头的下一个数据等于上一个数据减16(即高四位减1),即蛇头移动到了左边这一列
}
if(Direction==4) //如果向下移动
{
if((SnakeBody[SnakeHead]%16)==5){GameOverFlag=1;}
else{SnakeBody[(SnakeHead+1)%96]=SnakeBody[SnakeHead]+1;} //SnakeBody数组中蛇头的下一个数据等于上一个数据加1(即低四位加1),即蛇头移动到了下边这一行
}
if(GameOverFlag==0) //如果没撞墙
{
if(SnakeBody[(SnakeHead+1)%96]==Food) //判断蛇头移动后的位置是否是食物所在的位置
{ //如果是
SnakeLength++; //蛇身长度加1
UpdateDisplay(Food,1); //防止食物闪烁刚好不显示的时候进入此函数,导致蛇身一个点不显示
Food=CreateFood(); //重新创造一个食物
UpdateDisplay(Food,1); //更新显示
FlashFlag=0; //创造出新的食物时,食物暂不闪烁
T0Count2=0; //定时器T0Count2重新计数
}
else if( (DisplayBuffer[SnakeBody[(SnakeHead+1)%96]/16/3*16+(SnakeBody[(SnakeHead+1)%96]%16%6)*3]
& (0x80>>(SnakeBody[(SnakeHead+1)%96]/16%3)*3)) )
{ //如果蛇头移动后的位置撞在蛇身上,则游戏结束
GameOverFlag=1; //游戏结束的标志置1
}
else //如果蛇头移动后的位置不是食物,也不是撞墙,也不是撞到蛇身的话
{
//显示缓存数组DisplayBuffer中蛇头前进后的新位置对应的位写1
UpdateDisplay(SnakeBody[(SnakeHead+1)%96],1);
//显示缓存数组DisplayBuffer中蛇尾的位置清0(蛇身移动,如果没有吃到食物,相当于蛇尾对应的点跑到了
//蛇头的前一个点,变成了蛇头,原来的蛇头变成蛇身)整条蛇中间的数据不用操作
UpdateDisplay(SnakeBody[(SnakeHead+96-SnakeLength+1)%96],0);
//数组SnakeBody中,蛇尾的数据清零,更新数据
//SnakeHead+96:+96是因为SnakeHead为95后再加1,就会变成了0,防止SnakeHead+96-SnakeLength+1为负数
//即我们是循环使用SnakeBody数组中的96个数据,蛇头对应数组SnakeBody的第96个数据(对应索引为95)后,
//再移动一次,蛇头就来到了数组的第1个数据(对应索引为0)
SnakeBody[(SnakeHead+96-SnakeLength+1)%96]=0; //其实可以不清零
}
}
SnakeHead++; //SnakeBody数组中,蛇头对应的数据的序号加1
SnakeHead%=96; //蛇头变量SnakeHeadd的范围是0~95(蛇最长是96个“点”)
}
void main()
{
unsigned char i;
LCD_Init(); //LCD1602初始化
Timer0_Init(); //定时器初始化
Pointer=Table1; //上电后显示游戏名称“《贪吃蛇》”,数组名就是数组的首地址
RollDirection=1; //上电后向上滚动游戏名称“《贪吃蛇》”
while(1)
{
KeyNum=Key(); //获取键码值
if(KeyNum) //如果有按键按下
{
srand(TL0); //以定时器0的低八位数据作为随机数的种子,用来产生真随机的数据
if(Mode==5 && KeyNum==2) //如果是显示作者姓名和编程日期的界面,且按下返回键(K2)
{
Mode=2; //返回难度选择界面
ExecuteOnceFlag=1;
ChangedFlag=1;
}
if(Mode==4) //如果游戏结束全屏闪烁模式
{
if(KeyNum==2) //如果按下返回键(K2)
{
Mode=2; //返回难度选择界面
ExecuteOnceFlag=1;
ChangedFlag=1;
}
if(KeyNum==3) //如果按下K3
{
Mode=5; //切换到显示作者姓名和编程日期的界面
ExecuteOnceFlag=1;
Offset=0;
}
}
if(Mode==3) //如果是游戏进行模式
{
if(KeyNum==1) //按下K1暂停或继续
{
PauseFlag=!PauseFlag;
}
if(PauseFlag==0) //如果不是暂停
{ //长按和短按都进行检测,这样控制方向更有效,防止短按没检测出来导致没能改变方向
if((KeyNum==8 || KeyNum==16) && LastDirection!=1)
{ //如果短按或长按“左”键,且蛇头原来的移动方向不是向右
Direction=3; //则方向蛇头方向改为向左
}
if((KeyNum==7 || KeyNum==15) && LastDirection!=4)
{ //如果短按或长按“上”键,且蛇头原来的移动方向不是向下
Direction=2; //则方向蛇头方向改为向上
}
if((KeyNum==6 || KeyNum==14) && LastDirection!=2)
{ //如果短按或长按“下”键,且蛇头原来的移动方向不是向上
Direction=4; //则方向蛇头方向改为向左
}
if((KeyNum==5 || KeyNum==13) && LastDirection!=3)
{ //如果短按或长按“右”键,且蛇头原来的移动方向不是向左
Direction=1; //则方向蛇头方向改为向左
}
}
}
if(Mode==2) //如果是难度选择界面
{
if(KeyNum==7) //如果按了“上”键
{
Difficulty++;
if(Difficulty>5){Difficulty=1;}
ChangedFlag=1; //更改了难度,更新显示
}
if(KeyNum==6) //如果按了“下”键
{
Difficulty--;
if(Difficulty<1){Difficulty=5;}
ChangedFlag=1; //更改了难度,更新显示
}
if(KeyNum==1) //如果按了开始键(K1),则开始游戏
{
Mode=3; //切换到游戏模式
ExecuteOnceFlag=1;
}
}
if(KeyNum<=8) //如果按下任意按键
{
/*两个if的顺序不能调换,如果调换了,就从模式0直接跳到模式2了*/
if(Mode==1) //跳过汉字“难度: 1”的滚动显示,切换到难度选择界面
{
Mode=2;
ExecuteOnceFlag=1;
ChangedFlag=1;
}
if(Mode==0) //跳过游戏名“《贪吃蛇》”的显示,切换到汉字“难度: 1”的滚动显示界面
{
Mode=1;
ExecuteOnceFlag=1;
}
}
}
if(Mode==0) //如果是显示游戏名称“《贪吃蛇》”的模式
{
if(ExecuteOnceFlag) //进入到该模式后,此if中的内容只执行1次
{
if(RollFlag) //如果滚动的标志RollFlag为1(定时器中每隔0.5s将此标志置1)
{
RollFlag=0; //滚动的标志RollFlag清零
Offset+=2; //每次向上偏移2像素
if(Offset==16){ExecuteOnceFlag=0;} //向上偏移16像素后保持不变
}
}
}
if(Mode==1) //如果是滚动显示汉字“难度: 1”的模式
{
if(ExecuteOnceFlag)
{
ExecuteOnceFlag=0;
Offset=0; //显示的偏移量清零
RollDirection=0; //向左滚动
Pointer=Table2; //显示Table2的内容,即向左滚动显示“难度: 1”
}
if(RollFlag) //如果滚动的标志RollFlag为1(定时器中每隔0.5s将此标志置1)
{
RollFlag=0; //滚动的标志RollFlag清零
Offset+=1; //每次向左偏移一个汉字(15像素)
if(Offset==6) //“难”字滚到最左端就自动切换为难度选择模式
{
Mode=2;
ExecuteOnceFlag=1;
ChangedFlag=1;
}
}
}
if(Mode==2) //如果是难度选择模式
{
if(ExecuteOnceFlag)
{
ExecuteOnceFlag=0;
LCD_WriteCommand(0x0C); //防止模式4(游戏结束全屏闪烁模式)恰好不显示的时候返回模式2,导致屏幕不显示
for(i=0;i<96;i++) //显示LCD的前3个区域(前12列)
{
DisplayBuffer[i]=Table2[i+192];
}
Offset=0;
Pointer=DisplayBuffer;
LCD_Clear(); //清屏
PartSelect=0; //从第一个区域开始扫描,防止出错(防止第四个区域写入CGRAM的字模)
PartSum=3; //难度选择界面动态显示前3个区域
}
if(ChangedFlag)
{
ChangedFlag=0;
for(i=0;i<32;i++){DisplayBuffer[i+96]=Table3[i+32*(Difficulty-1)];}
switch(Difficulty)
{
case 1:SnakeMoveSpeed=100;break; //1s移动一次
case 2:SnakeMoveSpeed=75;break; //0.75s
case 3:SnakeMoveSpeed=50;break; //0.5s
case 4:SnakeMoveSpeed=25;break; //0.25s
case 5:SnakeMoveSpeed=12;break; //0.12s
default:break;
}
}
}
if(Mode==3) //如果是游戏进行模式
{
if(ExecuteOnceFlag) //切换到该模式后,此if中的内容只执行1次
{
ExecuteOnceFlag=0;
for(i=0;i<96;i++) //用到前两个区域,只需用缓存数组的一部分数据
{
DisplayBuffer[i]=0;
}
DisplayBuffer[3]=0x1E; //蛇的初始位置是2列2行(蛇尾)和3列2行(蛇头)
DisplayBuffer[4]=0x1E; //蛇的初始位置是2列2行(蛇尾)和3列2行(蛇头)
LCD_Clear(); //清屏
Pointer=DisplayBuffer; //LCD前两个区域显示缓存数组内容
PartSelect=0;
PartSum=2; //动态扫描显示前两个区域(1~8列)
GameTime=0; //游戏时间清零
GameOverFlag=0; //游戏结束标志清零
PauseFlag=0; //游戏暂停标志清零
Direction=1; //蛇头默认向右移动
LastDirection=1; //上一次蛇头默认向右移动
SnakeLength=2; //蛇的初始长度为2
SnakeHead=1; //蛇头对应数组中的第2个数据(索引为1)
for(i=0;i<96;i++) //蛇身数据全部清零
{
SnakeBody[i]=0; //其实可以不清零
}
SnakeBody[0]=1*16+1; //蛇身数据全部清零后,写入蛇身初始的两个数据
SnakeBody[1]=2*16+1;
Food=CreateFood(); //进入游戏前,先创造出一个食物
UpdateDisplay(Food,1); //更新显示
MoveSnakeFlag=0; //蛇移动的标志清零
T0Count1=0; //定时器计数变量T0Count1清零,重新计数
LCD_ShowString(1,9,"SCORE:");
LCD_ShowString(2,9,"TIME:");
}
if(PauseFlag) //如果暂停了
{
UpdateDisplay(Food,1); //食物不闪烁,一直显示
}
else if(FlashFlag) //如果不暂停,且闪烁标志为1
{
UpdateDisplay(Food,0); //不显示食物
}
else //如果不暂停,且闪烁标志为0
{
UpdateDisplay(Food,1); //显示食物
}
if(MoveSnakeFlag && GameOverFlag==0 && PauseFlag==0)
{ //如果移动的标志为1,且不暂停,且游戏也没结束
LastDirection=Direction; //保存上一次移动的方向,用于按键的判断(蛇不能往后移动)
MoveSnakeFlag=0; //移动标志清零
MoveSnake(); //移动一次
}
if(GameOverFlag==1) //如果游戏结束
{
UpdateDisplay(Food,1); //显示食物
Mode=4; //切换到全屏闪烁模式
ExecuteOnceFlag=1;
}
}
if(Mode==4) //游戏结束全屏闪烁模式
{
//闪烁操作放在定时中断函数中进行,放这里会有显示问题
}
if(Mode==5)
{
if(ExecuteOnceFlag)
{
ExecuteOnceFlag=0;
LCD_Clear();
LCD_WriteCommand(0x0C); //防止模式4(全屏闪烁模式)刚好不显示的时候切换到模式5,导致屏幕不显示
LCD_ShowString(1,1,"by gantengsheng");
LCD_ShowString(2,1,"at 20241207");
}
}
}
}
void Timer0_Routine() interrupt 1 //定时器0中断函数
{
TL0=0xF0; //设置定时初值,定时10ms,晶振@12.00000MHz
TH0=0xD8; //设置定时初值,定时10ms,晶振@12.00000MHz
T0Count0++;
if(PauseFlag==0) //不暂停时,T0Count1和T0Count2才计数
{
T0Count1++;
T0Count2++;
}
T0Count3++;
if(T0Count0>=2) //20ms扫描一次按键
{
T0Count0=0;
Key_Tick();
if(Mode!=5)
{
if(RollDirection){LCD_Convert_MoveUp(Pointer,PartSelect+1,Offset);}
else{LCD_Convert_MoveLeft(Pointer,PartSelect+1,Offset);}
LCD_ShowChinese(PartSelect+1,PartSum);
PartSelect++;
PartSelect%=PartSum;
}
}
if(T0Count1>=SnakeMoveSpeed) //用来控制蛇移动的速度
{
T0Count1=0;
MoveSnakeFlag=1;
}
if(T0Count2>=50) //0.5s取反闪烁标志FlashFlag的值
{
T0Count2=0;
FlashFlag=!FlashFlag;
if(PauseFlag==0 && GameOverFlag==0 && Mode==3){GameTime++;}
}
if(T0Count3>=50) //控制滚动的速度,500ms滚动一次
{
T0Count3=0;
RollFlag=1;
}
if(Mode==3) //游戏时间和分数的更新必须放在中断中执行,否则左半屏动态显示的时候会出错
{
LCD_ShowNum(1,15,SnakeLength,2);
LCD_ShowNum(2,14,GameTime/2,3);
}
if(Mode==4) //闪烁的代码必须放在中断中执行,否则第二行第九列的显示会出问题
{
if(FlashFlag){LCD_WriteCommand(0x08);}
else{LCD_WriteCommand(0x0C);}
}
}
附录A:编程遇到的问题
问题1:由显示4个区域切换为显示前3个区域时(第4个不显示),虽然进行了清屏处理,但是第4个区域还是随机性地有显示。
原因:切换时选择区域显示的变量有四分之一的概率发生越界。
解决:定义一个全局变量PartSelect,切换显示区域时,PartSelect重新赋值,从左边第一个区域开始扫描。
问题2:玩一段时间后,屏幕显示不正常。
原因:保存蛇身数据的数组SnakeBody的下标忘记取余了,导致越界。所以实际上是移动了94次之后就会出现问题。
解决:总共96个数据,下标对96取余就行了。
问题3:实时显示得分和游戏已玩时间时,左半屏第二行显示不正常,会显示得分和时间的数字。
原因:如果在主循环中更新得分和时间时,刚好在设置完LCD的光标后被计时中断打断,返回主循环时,光标被重新设置在左半屏了。
解决:在中断中更新得分和游戏已玩时间。
问题4:游戏结束全屏闪烁时,第二行第九列的英文字母“T”被“点”取代了。
原因:跟问题3的一样。
解决:同样将实现闪烁的代码从主循环中移到定时中断函数中。
总结
相比8X8LED点阵屏和16X16LED点阵屏的贪吃蛇,LCD1602贪吃蛇出现的问题要多一些,还好都把这些问题给解决了。
标签:16,0x00,void,51,unsigned,char,单片机,LCD,小游戏 From: https://blog.csdn.net/gantengsheng/article/details/144324604