系列文章目录
前言
《俄罗斯方块》,一款经典的、怀旧的小游戏,单片机入门必写程序。
有两个版本,一个使用普中A2开发板上的8X8LED点阵屏,一个使用外接的MAX7219驱动的8X8LED点阵屏。
本文代码使用的是普中A2开发板的板载8X8LED点阵屏。
【单片机】STC89C52RC
【频率】12T@11.0592MHz
【外设】8X8点阵屏、矩阵按键
效果查看/操作演示:B站搜索“甘腾胜”或“gantengsheng”查看。
源代码下载:B站对应视频的简介有工程文件下载链接。
一、效果展示
二、原理分析
1、屏幕显示
屏幕通过P0口和74HC595芯片驱动,由定时器扫描显示,定义一个8字节的数组作为屏幕显存数组,想改变屏幕显示内容,直接改变屏幕显存数组DisplayBuffer的数据就行了。屏幕显存数组数据格式如下所示:
/** 屏幕显存数组数据存储格式:
*
* 纵向8点,高位在下,从左到右
* 每一个Bit对应一个LED
*
* B0 B0 B0 B0
* B1 B1 B1 B1
* B2 B2 B2 B2
* B3 B3 -------> B3 B3
* B4 B4 B4 B4
* B5 B5 B5 B5
* B6 B6 B6 B6
* B7 B7 B7 B7
*/
因为要控制不操作时的方块下落和检测能否旋转、能否左右移动、是否要固定等等,如果只有一个显存数组,很明显不方便,所以类似PhotoShop那样,多定义两个显存数组,作为“图层0”和“图层1”,图层0显存数组Layer0存储已经固定了的方块,图层1显存数组Layer1存储正在下落的方块,想要更新屏幕显示时,将图层0和图层1的显存数组的数据按位或之后赋值给屏幕显存数组DisplayBuffer就可以了。
/**
* 函 数:图层0和图层1的数据叠加,更新屏幕显示
* 参 数:无
* 返 回 值:无
* 说 明:由定时器1自动扫描数组DisplayBuffer的数据进行显示
*/
void UpdateDisplay(void)
{
unsigned char i;
for(i=0;i<8;i++)
{
DisplayBuffer[i] = Layer0[i] | Layer1[i];
}
}
2、方块的显示
为了方便操作,定义一个坐标轴,如下所示。
/** 坐标轴定义:
*
* 左上角为原点(0,0)
* 横向向右为X轴,取值范围:0~7
* 纵向向下为Y轴,取值范围:0~7
*
* 0 X轴 7
* .————————————————>
* 0 |
* |
* |
* Y轴 |
* |
* |
* 7 |
* v
*/
还建立另外一个坐标系,以参考点为原点,同样是向右为X轴正方向,向下为Y轴的正方向。
总共有7种方块,每种方块有4个方向(当然有一些是重复的),通过一个三维数组将方块信息存储在Flash中,存储的是方块相对于参考点的坐标,需要哪个方块就将其取出进行显示就行了,同一种方块的不同方向的第一个索引相差7的倍数。
char code AllBlocks[28][4][2]={
// 口口
// 口口
0,0, 1,0, 0,-1, 1,-1, //0
// 口口口口
0,0, 1,0, 2,0, 3,0, //1
// 口
// 口口口
0,0, 1,0, 2,0, 1,-1, //2
// 口
// 口口
// 口
1,0, 0,-1, 1,-1, 0,-2, //3
// 口
// 口口
// 口
0,0, 0,-1, 1,-1, 1,-2, //4
// 口
// 口口口
0,0, 1,0, 2,0, 2,-1, //5
// 口
// 口口口
0,0, 1,0, 2,0, 0,-1, //6
// 口口
// 口口
0,0, 1,0, 0,-1, 1,-1, //7
// 口
// 口
// 口
// 口
0,0, 0,-1, 0,-2, 0,-3, //8
// 口
// 口口
// 口
1,0, 0,-1, 1,-1, 1,-2, //9
// 口口
// 口口
0,0, 1,0, 1,-1, 2,-1, //10
// 口口
// 口口
1,0, 2,0, 0,-1, 1,-1, //11
...
}
方块下落、左右移动、旋转、加速下落、瞬间到达底部等等,需要更新显示正在下落的方块的时候,先清空图层1的显存数组,再根据参考点的坐标和上边数组的相对坐标,就可以计算出一个方块对应的四个LED的实际坐标,再将图层1的显存数组数据的对应位置1,最后图层1和图层0的数据按位或之后赋值给屏幕显存数组即可更新屏幕显示。
/**
* 函 数:将图层1显存数组全部清零
* 参 数:无
* 返 回 值:无
*/
void Layer1_Clear(void)
{
unsigned char i;
for(i=0;i<8;i++)
{
Layer1[i]=0;
}
}
/**
* 函 数:图层1显存数组更新方块的位置信息
* 参 数:无
* 返 回 值:无
* 说 明:一个方块由四个LED组成
*/
void Layer1_UpdateBlock(void)
{
unsigned char i;
Layer1_Clear(); //图层1清空数据
for(i=0;i<4;i++) //更新方块坐标
{
BlockX[i]=ReferencePointX+AllBlocks[BlockSelect][i][0]; //X坐标,根据参考点坐标和相对参考点的坐标来计算
BlockY[i]=ReferencePointY+AllBlocks[BlockSelect][i][1]; //Y坐标,根据参考点坐标和相对参考点的坐标来计算
}
for(i=0;i<4;i++) //根据方块坐标,将方块的位置信息更新到图层1显存数组
{
Layer1_ControlPoint(BlockX[i],BlockY[i],1);
}
}
/**
* 函 数:图层1显存数组指定位置按指定的数值赋值
* 参 数:X 指定点的横坐标,范围:-128~127,屏幕区域:0~7
* 参 数:Y 指定点的纵坐标,范围:-128~127,屏幕区域:0~7
* 参 数:Value 数值,0:指定Bit置0,非0:指定Bit置1
* 返 回 值:无
*/
void Layer1_ControlPoint(char X,char Y,unsigned char Value)
{
if(Value==0)
{
if(X>=0 && X<=7 && Y>=0 && Y<=7){ Layer1[X] &= ~(0x01<<Y); }
}
else
{
if(X>=0 && X<=7 && Y>=0 && Y<=7){ Layer1[X] |= 0x01<<Y; }
}
}
参考点移动,则“带动”方块的4个LED跟着移动。
3、方块的固定
下落前检测一个方块的四个LED的正下方是否是已经固定的方块或者是屏幕底部,如果是,则固定正在下落的方块,如果不是,则方块继续下落。通过图层0的数据来检测。
/**
* 函 数:图层0显存数组获取指定位置点的值
* 参 数:X 指定点的横坐标,范围:-128~127,屏幕区域:0~7
* 参 数:Y 指定点的纵坐标,范围:-128~127,屏幕区域:0~7
* 返 回 值:屏幕区域内返回0或1,超出屏幕区域返回2(特殊:屏幕正上方会返回0)
*/
unsigned char Layer0_GetPoint(char X,char Y)
{
if(X>=0 && X<=7) //如果是第1列到第8列
{
if(Y>=0 && Y<=7) //如果是第1行到第8行
{
if( Layer0[X] & (0x01<<Y) ) {return 1;} //屏幕区域内,指定的位为1,则返回1
else {return 0;} //屏幕区域内,指定的位为0,则返回0
}
else if(Y<0) //如果是屏幕区域上方,则返回0,目的是让方块未完全进入屏幕区域时不会被固定
{
return 0;
}
else //如果是屏幕区域下方,则返回2,用来检测下落方块是否来到底部
{
return 2;
}
}
else{return 2;} //如果不是第1列到第8列
}
/**
* 函 数:检查是否要固定方块
* 参 数:无
* 返 回 值:要固定方块返回1,不固定方块返回0
*/
unsigned char IsFix(void)
{
unsigned char i;
for(i=0;i<4;i++)
{
if( Layer0_GetPoint(BlockX[i],BlockY[i]+1) )
{ //检查方块的下方是否是区域底部或其他已经固定了的方块
return 1;
}
}
return 0;
}
/**
* 函 数:固定方块
* 参 数:无
* 返 回 值:无
* 说 明:方块下落时,如果下方是区域底部或其他已经固定了的方块,则该正在下落的方块会被固定
* 说 明:用图层0显存数组保存固定后的方块的位置
*/
void FixBlock(void)
{
unsigned char i;
for(i=0;i<8;i++)
{
Layer0[i] = Layer0[i] | Layer1[i];
}
}
图层0和图层1的数据按位或,再赋值给图层0,就可以将正在下落的方块固定了。
4、方块的左右移动
跟是否要固定方块的检测类似,检测能否向左/右移动,则检测方块对应的4个LED的左/右边是否是固定了的方块或者是否是屏幕边界,如果是,则不能向左/右移动,如果不是,则可以移动。同样通过图层0的数组数据进行检测。参考点的X坐标减1,就是向左移动一个像素,参考点的X坐标加1,就是向右移动一个像素。
/**
* 函 数:检查方块能否左移
* 参 数:无
* 返 回 值:能左移返回1,不能左移返回0
*/
unsigned char CanMoveLeft(void)
{
unsigned char i;
for(i=0;i<4;i++)
{
if( Layer0_GetPoint(BlockX[i]-1,BlockY[i]) )
{
return 0;
}
}
return 1;
}
/**
* 函 数:检查方块能否右移
* 参 数:无
* 返 回 值:能右移返回1,不能右移返回0
*/
unsigned char CanMoveRight(void)
{
unsigned char i;
for(i=0;i<4;i++)
{
if( Layer0_GetPoint(BlockX[i]+1,BlockY[i]) )
{
return 0;
}
}
return 1;
}
5、方块的旋转
需要先检测能否旋转,如果旋转之后的方块与已经固定了的方块有重叠,则不能旋转,如果没有重叠,则能旋转。
/**
* 函 数:检查能否旋转
* 参 数:X 参考点的X坐标
* 参 数:Y 参考点的Y坐标
* 返 回 值:能旋转返回1,不能旋转返回0
* 说 明:左上角的LED为原点(0,0),向右为X轴正方向,向下为Y轴正方向
*/
unsigned char CanRotate(char X,char Y)
{
unsigned char i;
for(i=0;i<4;i++) //根据提供的参考坐标,获取旋转后的方块的坐标
{
AfterRotateX[i]=X+AllBlocks[(BlockSelect+7)%28][i][0];
AfterRotateY[i]=Y+AllBlocks[(BlockSelect+7)%28][i][1];
}
for(i=0;i<4;i++)
{ //检测旋转后的方块有没有超过屏幕区域,以及有没有跟固定的方块有重叠
if( Layer0_GetPoint(AfterRotateX[i],AfterRotateY[i]) )
{ //如果有,则返回0,即不能旋转
return 0;
}
}
return 1; //如果没有,则返回1,即能旋转
}
/**
* 函 数:旋转方块
* 参 数:无
* 返 回 值:无
*/
void RotateBlock(void)
{
BlockSelect+=7; //逆时针旋转90°
BlockSelect%=28; //防止越界
Layer1_UpdateBlock(); //更新图层1的数据
UpdateDisplay(); //更新屏幕显示
}
正在下落的方块旋转后如果跟已经固定的方块有重叠,则尝试旋转之后再左移一格(左移一个像素)、左移两格、左移三格,如果左移后没有重叠,则进行旋转,如果左移一、二、三格都还是有重叠,则不旋转。
if(Key(S10,DOWN) && PauseFlag==0) //如果按下S10,且不是暂停状态
{
srand(TL0); //每次按键按下都用定时器0的低8位做种子,从而产生真随机数
if( CanRotate(ReferencePointX,ReferencePointY) )
{ //如果旋转后,没有超出屏幕区域,也没有跟已固定的方块重叠
RotateBlock(); //旋转并更新屏幕显示
}
else if( CanRotate(ReferencePointX-1,ReferencePointY) )
{ //如果旋转后左移一格,没有超出屏幕区域,也没有跟已固定的方块重叠
ReferencePointX--; //更新参考点的X坐标
RotateBlock(); //旋转并更新屏幕显示
}
else if( CanRotate(ReferencePointX-2,ReferencePointY) )
{ //如果旋转后左移两格,没有超出屏幕区域,也没有跟已固定的方块重叠
ReferencePointX-=2; //更新参考点的X坐标
RotateBlock(); //旋转并更新屏幕显示
}
else if( CanRotate(ReferencePointX-3,ReferencePointY) )
{ //如果旋转后左移三格,没有超出屏幕区域,也没有跟已固定的方块重叠
ReferencePointX-=3; //更新参考点的X坐标
RotateBlock(); //旋转并更新屏幕显示
}
//如果以上四种情况都不满足,则不旋转方块
}
6、固定后的检查和消行
每次方块固定后,则从下往上检查每一行,如果一行的所有LED都是点亮的,则消除该行,上方的所有行往下移动一行,下移后,需要重新检查该行。一直检查到上边的第一行。
/**
* 函 数:检查和消除
* 参 数:无
* 返 回 值:所消的行数,范围:0~4
* 说 明:方块固定后从下遍历每一行,行满了后则消除该行
*/
unsigned char CheckAndEliminate(void)
{
unsigned char i,j,k;
unsigned char Count,Temp=0;
for(i=0;i<8;i++) //固定方块后,从下往上,遍历每一行
{
Count=0;
for(j=0;j<8;j++) //统计一行亮着的LED的个数
{
if(Layer0_GetPoint(j,7-i))
{
Count++;
}
}
if(Count==8) //如果某一行的LED全亮
{
Temp++;
for(k=i;k<7;k++) //该行以上的所有像素下移一格
{
for(j=0;j<8;j++)
{
Layer0_ControlPoint(j,7-k,Layer0_GetPoint(j,6-k));
}
}
for(j=0;j<8;j++) //如果有消行,则下移后清空第一行
{
Layer0_ControlPoint(j,0,0);
}
i--; //下移后重新检查该行
}
}
return Temp;
}
7、游戏结束的判定
如果下一个方块出不来一点,则游戏结束。
/**
* 函 数:判断游戏是否结束
* 参 数:无
* 返 回 值:游戏结束返回1,游戏未结束返回0
*/
unsigned char IsGameOver(void)
{
unsigned char i;
for(i=0;i<4;i++)
{
BlockX[i]=3+AllBlocks[BlockSelect][i][0];
BlockY[i]=0+AllBlocks[BlockSelect][i][1];
}
for(i=0;i<4;i++)
{
if(BlockY[i]==0)
{
if(Layer0_GetPoint(BlockX[i],0))
{
return 1;
}
}
}
8、得分的计算
为了增加可玩性,一次所消的行数越多,则得分就越可观。得分是所消行数的平方。
if(FallFlag) //如果方块下落的标志为真
{
FallFlag=0; //方块下落的标志置0
if( IsFix() )
{
FixBlock();
LinePlus=CheckAndEliminate(); //方块固定后,从下往上遍历每一行,行满则消除,并返回所消的行数
Line=Line+LinePlus; //更新存储总行数的变量
Score=Score+LinePlus*LinePlus; //更新得分
ReferencePointX=3;
ReferencePointY=-1;
BlockSelect=NextBlock; //固定方块后,更新要下落的方块
NextBlock=rand()%28; //确定下一个方块
GameOverFlag=IsGameOver(); //判断游戏是否结束
}
if(GameOverFlag==0)
{
ReferencePointY++;
Layer1_UpdateBlock(); //更新正在下落的方块的位置
UpdateDisplay(); //更新屏幕显示
}
}
9、按键的检测
采用全功能检测,即矩阵键盘的16个按键,全部都能进行按住、按下、松开、单击、双击、长按、重复的检测,方法来源:江协科技的编程技巧第二期。经实测,很好用,检测很准确、灵敏,只要硬件没问题,检测就没问题。
简单原理分析请看我的上一篇博客:
基于51单片机和LCD1602的矩阵按键的全功能(按住、按下、松开、单击、双击、长按、重复)演示
三、各模块代码
1、8X8点阵屏
h文件
#ifndef __MATRIXLED__
#define __MATRIXLED__
extern unsigned char DisplayBuffer[];
void MatrixLED_Clear(void);
void MatrixLED_Init(void);
void MatrixLED_HS(unsigned char *Array,unsigned int Offset);
void MatrixLED_VS(unsigned char *Array,unsigned int Offset);
void MatrixLED_Tick(void);
void MatrixLED_ControlPoint(unsigned char X,unsigned char Y,unsigned char State);
unsigned char MatrixLED_GetPoint(unsigned char X,unsigned char Y);
#endif
c文件
#include <REGX52.H>
/** 屏幕显存数组数据存储格式:
*
* 纵向8点,高位在下,从左到右
* 每一个Bit对应一个像素点
*
* B0 B0 B0 B0
* B1 B1 B1 B1
* B2 B2 B2 B2
* B3 B3 -------> B3 B3
* B4 B4 B4 B4
* B5 B5 B5 B5
* B6 B6 B6 B6
* B7 B7 B7 B7
*/
/** 屏幕坐标轴定义:
*
* 左上角为原点(0,0)
* 横向向右为X轴,取值范围:0~7
* 纵向向下为Y轴,取值范围:0~7
*
* 0 X轴 7
* .————————————————>
* 0 |
* |
* |
* Y轴 |
* |
* |
* 7 |
* v
*/
//引脚配置
sbit _74HC595_SER=P3^4; //串行数据输入引脚
sbit _74HC595_RCK=P3^5; //锁存时钟,上升沿有效
sbit _74HC595_SCK=P3^6; //移位时钟,上升沿有效
/** 点阵屏显存数组
*
* 想改变显示内容,对此显存数组进行修改即可
* 由定时器自动扫描显示
*/
unsigned char DisplayBuffer[8];
/**
* 函 数:将点阵屏显存数组全部清零
* 参 数:无
* 返 回 值:无
* 说 明:显存数组由定时器自动扫描显示
*/
void MatrixLED_Clear(void)
{
unsigned char i;
for(i=0;i<8;i++)
{
DisplayBuffer[i]=0;
}
}
/**
* 函 数:点阵屏初始化
* 参 数:无
* 返 回 值:无
*/
void MatrixLED_Init(void)
{
_74HC595_SCK=0; //移位时钟信号初始化
_74HC595_RCK=0; //锁存时钟信号初始化
MatrixLED_Clear(); //清屏
}
/**
* 函 数:74HC595写入字节
* 参 数:Byte 要写入的字节
* 返 回 值:无
* 说 明:如果是低位先发,则显存数组数据是高位在下
* 说 明:如果是高位先发,则显存数组数据是高位在上
*/
void _74HC595_WriteByte(unsigned char Byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
_74HC595_SER=Byte&(0x01<<i); //低位先发
_74HC595_SCK=1; //移位时钟上升沿时,串行数据写入移位寄存器
_74HC595_SCK=0;
}
_74HC595_RCK=1; //锁存时钟上升沿时,数据从移位寄存器转存到储存寄存器
_74HC595_RCK=0;
}
/**
* 函 数:点阵屏按指定的偏移量显示指定数组的数据(用来控制屏幕左右滚动显示内容)
* 参 数:Array 数组的地址(指针),数组名就是数组的首地址
* 参 数:Offset 偏移量,向左偏移Offset个像素,范围:0~65535
* 返 回 值:无
* 说 明:Offset增加则屏幕向左滚动显示,Offset减小则屏幕向右滚动显示
* 说 明:要求数组数据逐列式取模,高位在下
* 说 明:HS:Horizontal Scroll,水平滚动
*/
void MatrixLED_HS(unsigned char *Array,unsigned int Offset)
{
unsigned char i;
Array+=Offset;
for(i=0;i<8;i++)
{
DisplayBuffer[i]=*(Array+i);
}
}
/**
* 函 数:点阵屏按指定的偏移量显示指定数组的数据(用来控制屏幕上下滚动显示内容)
* 参 数:Array 数组的地址(指针),数组名就是数组的首地址
* 参 数:Offset 偏移量,向上偏移Offset个像素,范围:0~65535
* 返 回 值:无
* 说 明:Offset增加则屏幕向上滚动显示,Offset减小则屏幕向下滚动显示
* 说 明:要求数组数据逐列式取模,高位在下
* 说 明:VS:Vertical Scroll,竖直滚动
*/
void MatrixLED_VS(unsigned char *Array,unsigned int Offset)
{
unsigned char i,m,n;
m=Offset/8;
n=Offset%8;
Array+=m*8;
for(i=0;i<8;i++)
{
DisplayBuffer[i]=( *(Array+i)>>n) | (*(Array+8+i)<<(8-n) );
}
}
/**
* 函 数:点阵屏驱动函数,在定时器中断中调用
* 参 数:无
* 返 回 值:无
*/
void MatrixLED_Tick(void)
{
static unsigned char i=0; //定义静态变量
P0=0xFF; //消影
_74HC595_WriteByte(DisplayBuffer[i]); //将显存数组数据写入到74HC595中锁存
P0=~(0x80>>i); //位选,低电平选中
i++; //下次进中断后显示下一列
i%=8; //显示完第八列后,又从第一列开始显示
}
/**
* 函 数:点阵屏控制指定位置LED按指定状态显示
* 参 数:X 指定点的横坐标,范围:-128~127,屏幕区域:0~7
* 参 数:Y 指定点的纵坐标,范围:-128~127,屏幕区域:0~7
* 参 数:State 亮灭状态,0:灭,非0:亮
* 返 回 值:无
*/
void MatrixLED_ControlPoint(char X,char Y,unsigned char State)
{
if(State==0)
{
if(X>=0 && X<=7 && Y>=0 && Y<=7) //超出屏幕的内容不显示
{
DisplayBuffer[X] &= ~(0x01<<Y); //将显存数组指定位置的一个Bit数据置0
}
}
else
{
if(X>=0 && X<=7 && Y>=0 && Y<=7) //超出屏幕的内容不显示
{
DisplayBuffer[X] |= 0x01<<Y; //将显存数组指定位置的一个Bit数据置1
}
}
}
/**
* 函 数:点阵屏获取指定位置LED的状态
* 参 数:X 指定点的横坐标,范围:-128~127,屏幕区域:0~7
* 参 数:Y 指定点的纵坐标,范围:-128~127,屏幕区域:0~7
* 返 回 值:LED点亮返回1,LED熄灭返回0,超出屏幕范围返回2
*/
unsigned char MatrixLED_GetPoint(char X,char Y)
{
if(X>=0 && X<=7 && Y>=0 && Y<=7)
{
if( DisplayBuffer[X] & (0x01<<Y) ) {return 1;}
else {return 0;}
}
else {return 2;}
}
2、矩阵按键
h文件
/*方法来源:B站江协科技的编程技巧(第二期)*/
#ifndef __MATRIXKEY_H__
#define __MATRIXKEY_H__
//各按键对应数组的索引
#define S1 0
#define S2 1
#define S3 2
#define S4 3
#define S5 4
#define S6 5
#define S7 6
#define S8 7
#define S9 8
#define S10 9
#define S11 10
#define S12 11
#define S13 12
#define S14 13
#define S15 14
#define S16 15
//标志位掩码
#define HOLD 0x01 //按住
#define DOWN 0x02 //按下
#define UP 0x04 //松开
#define SINGLE 0x08 //单击
#define DOUBLE 0x10 //双击
#define LONG 0x20 //长按
#define REPEAT 0x40 //重复
void Key_Clear(void);
unsigned char Key(unsigned char n,unsigned char Flag);
void Key_Tick(void);
#endif
c文件
/*方法来源:B站江协科技的编程技巧(第二期)*/
#include <REGX52.H>
#include "MatrixKey.h"
#define KEY_PRESSED 1 //按键已按下
#define KEY_UNPRESSED 0 //按键未按下
//数值单位:定时器中断函数中相邻两次调用函数Key_Tick的时间间隔
#define KEY_TIME_DOUBLE 0 //双击判定的等待时长
#define KEY_TIME_LONG 4 //长按判定的等待时长
#define KEY_TIME_REPEAT 10 //长按后,重复标志位再次置1的时长
//本案例中,以上数值的单位是:10ms
//按住按键,从将Down标志位置1开始计时,第一次经过140ms将重复标志位置1,之后每隔100ms将重复标志位置1
//引脚配置
#define Port P1
sbit Row1=P1^7; //行1
sbit Row2=P1^6; //行2
sbit Row3=P1^5; //行3
sbit Row4=P1^4; //行4
sbit Column1=P1^3; //列1
sbit Column2=P1^2; //列2
sbit Column3=P1^1; //列3
sbit Column4=P1^0; //列4
/** 按键标志数组
*
* 一个字节对应8位,分别为:B7 B6 B5 B4 B3 B2 B1 B0
* 【B0】1:一直按住,0:未按下
* 【B1】1:按下瞬间,0:不是按下瞬间
* 【B2】1:松开瞬间,0:不是松开瞬间
* 【B3】1:单击,0:不是单击
* 【B4】1:双击,0:不是双击
* 【B5】1:长按,0:不是长按
* 【B6】1:重复,0:未重复
* 【B7】保留位
* 其中单击、双击、长按互斥(即这三个对应的标志位最多只能有一个是1)
* 除B0外,其他标志位在读取后置0
* xdata:变量保存在片外RAM
*/
unsigned char xdata Key_Flag[16];
/**
* 函 数:获取按键状态(检测按键是否按下)
* 参 数:n 按键索引,范围:0~15
* 返 回 值:按下返回1,未按下返回0
*/
unsigned char Key_GetState(unsigned char n)
{
if(n==S1)
{
Row1=0;
if(Column1==0)
{
return KEY_PRESSED;
}
}
else if(n==S2)
{
Row1=0;
if(Column2==0)
{
return KEY_PRESSED;
}
}
else if(n==S3)
{
Row1=0;
if(Column3==0)
{
return KEY_PRESSED;
}
}
else if(n==S4)
{
Row1=0;
if(Column4==0)
{
return KEY_PRESSED;
}
}
else if(n==S5)
{
Row2=0;
if(Column1==0)
{
return KEY_PRESSED;
}
}
else if(n==S6)
{
Row2=0;
if(Column2==0)
{
return KEY_PRESSED;
}
}
else if(n==S7)
{
Row2=0;
if(Column3==0)
{
return KEY_PRESSED;
}
}
else if(n==S8)
{
Row2=0;
if(Column4==0)
{
return KEY_PRESSED;
}
}
else if(n==S9)
{
Row3=0;
if(Column1==0)
{
return KEY_PRESSED;
}
}
else if(n==S10)
{
Row3=0;
if(Column2==0)
{
return KEY_PRESSED;
}
}
else if(n==S11)
{
Row3=0;
if(Column3==0)
{
return KEY_PRESSED;
}
}
else if(n==S12)
{
Row3=0;
if(Column4==0)
{
return KEY_PRESSED;
}
}
else if(n==S13)
{
Row4=0;
if(Column1==0)
{
return KEY_PRESSED;
}
}
else if(n==S14)
{
Row4=0;
if(Column2==0)
{
return KEY_PRESSED;
}
}
else if(n==S15)
{
Row4=0;
if(Column3==0)
{
return KEY_PRESSED;
}
}
else if(n==S16)
{
Row4=0;
if(Column4==0)
{
return KEY_PRESSED;
}
}
Port=0xFF;
return KEY_UNPRESSED;
}
/**
* 函 数:清空所有按键的所有标志位
* 参 数:无
* 返 回 值:无
* 说 明:防止切换模式的时候受上一模式所按按键的影响
*/
void Key_Clear(void)
{
unsigned char i;
for(i=0;i<16;i++)
{
Key_Flag[i]=0;
}
}
/**
* 函 数:获取按键标志位的值
* 参 数:n 按键索引,范围:0~15
* 参 数:Flag 标志位掩码,用来获取标志的某一位的值为1还是0
* 返 回 值:1或者0
*/
unsigned char Key(unsigned char n, unsigned char Flag)
{
if(Key_Flag[n] & Flag)
{
if(Flag != HOLD)
{
Key_Flag[n] &= ~Flag;
}
return 1;
}
return 0;
}
/**
* 函 数:按键检测驱动函数,在定时器中断函数中使用
* 参 数:无
* 返 回 值:无
* 说 明:最后的else的目的是防止S[i]的初值在0~4之外导致检测不了单击、双击、长按、重复
*/
void Key_Tick(void)
{
static unsigned char xdata Count,i;
static unsigned char xdata NowState[16];
static unsigned char xdata LastState[16];
static unsigned char xdata S[16];
static unsigned char xdata KeyTime[16];
for(i=0;i<16;i++)
{
if(KeyTime[i]>0)
{
KeyTime[i]--;
}
}
Count++;
if(Count>=2) //本案例中每隔20ms检测一次按键
{
Count=0;
for(i=0;i<16;i++)
{
LastState[i]=NowState[i];
NowState[i]=Key_GetState(i);
if(NowState[i] == KEY_PRESSED)
{
Key_Flag[i] |= HOLD;
}
else
{
Key_Flag[i] &= ~HOLD;
}
if(NowState[i] == KEY_PRESSED && LastState[i] == KEY_UNPRESSED)
{
Key_Flag[i] |= DOWN;
}
if(NowState[i] == KEY_UNPRESSED && LastState[i] == KEY_PRESSED)
{
Key_Flag[i] |= UP;
}
if(S[i] == 0)
{
if(NowState[i] == KEY_PRESSED)
{
KeyTime[i]=KEY_TIME_LONG;
S[i]=1;
}
}
else if(S[i] == 1)
{
if(NowState[i] == KEY_UNPRESSED)
{
KeyTime[i]=KEY_TIME_DOUBLE;
S[i]=2;
}
else if(KeyTime[i] == 0)
{
KeyTime[i]=KEY_TIME_REPEAT;
Key_Flag[i] |= LONG;
S[i]=4;
}
}
else if(S[i]==2)
{
if(NowState[i] == KEY_PRESSED)
{
Key_Flag[i] |= DOUBLE;
S[i]=3;
}
else if(KeyTime[i] == 0)
{
Key_Flag[i] |= SINGLE;
S[i]=0;
}
}
else if(S[i] == 3)
{
if(NowState[i] == KEY_UNPRESSED)
{
S[i]=0;
}
}
else if(S[i]==4)
{
if(NowState[i] == KEY_UNPRESSED)
{
S[i]=0;
}
else if(KeyTime[i] == 0)
{
KeyTime[i]=KEY_TIME_REPEAT;
Key_Flag[i] |= REPEAT;
S[i]=4;
}
}
else
{
S[i]=0;
}
}
}
}
3、定时器0
h文件
#ifndef __TIMER0_H__
#define __TIMER0_H__
void Timer0_Init(void);
#endif
c文件
#include <REGX52.H>
/**
* 函 数:定时器0初始化
* 参 数:无
* 返 回 值:无
*/
void Timer0_Init(void)
{
TMOD&=0xF0; //设置定时器模式为16位不自动重装模式
TMOD|=0x01; //设置定时器模式为16位不自动重装模式
TL0=0x66; //设置定时初值,定时1ms,12T@11.0592MHz
TH0=0xFC; //设置定时初值,定时1ms,12T@11.0592MHz
TF0=0; //清除TF0标志
TR0=1; //定时器0开始计时
ET0=1; //打开定时器0中断允许
EA=1; //打开总中断
PT0=0; //设置定时器0的优先级
}
/*定时器中断函数模板
void Timer0_Routine() interrupt 1 //定时器0中断函数
{
static unsigned int T0Count; //定义静态变量
TL0=0x66; //设置定时初值,定时1ms,12T@11.0592MHz
TH0=0xFC; //设置定时初值,定时1ms,12T@11.0592MHz
T0Count++;
if(T0Count>=1000)
{
T0Count=0;
}
}
*/
4、定时器1
h文件
#ifndef __TIMER1_H__
#define __TIMER1_H__
void Timer1_Init(void);
#endif
c文件
#include <REGX52.H>
/**
* 函 数:定时器1初始化
* 参 数:无
* 返 回 值:无
*/
void Timer1_Init(void)
{
TMOD&=0x0F; //设置定时器模式为16位不自动重装模式
TMOD|=0x10; //设置定时器模式为16位不自动重装模式
TL1=0x66; //设置定时初值,定时1ms,12T@11.0592MHz
TH1=0xFC; //设置定时初值,定时1ms,12T@11.0592MHz
TF1=0; //清除TF1标志
TR1=1; //定时器1开始计时
ET1=1; //打开定时器1中断允许
EA=1; //打开总中断
PT1=1; //设置定时器1的优先级
}
/*定时器中断函数模板
void Timer1_Routine() interrupt 3 //定时器1中断函数
{
static unsigned int T1Count; //定义静态变量
TL1=0x66; //设置定时初值,定时1ms,12T@11.0592MHz
TH1=0xFC; //设置定时初值,定时1ms,12T@11.0592MHz
T1Count++;
if(T1Count>=1000)
{
T1Count=0;
}
}
*/
四、主函数
main.c
/*by甘腾胜@20250613
【效果查看/操作演示】B站搜索“甘腾胜”或“gantengsheng”查看
【单片机】STC89C52RC
【频率】12T@11.0592MHz
【外设】8X8LED点阵屏、矩阵按键
【简单的原理分析】https://blog.youkuaiyun.com/gantengsheng/article/details/143581157
【注意】点阵屏旁边的跳线帽要接三个排针的左边两个
【操作说明】
(1)循环滚动显示游戏英文名的界面按任意按键开始游戏
(2)按S10旋转方块(逆时针),按S13、S15控制方块左右移动,按S14方块加速下落,按S16方块瞬间下落到底
(3)游戏结束全屏闪烁界面按S1进入滚动显示得分的英文的界面
(5)滚动显示得分的英文的界面可按S1跳过
(6)循环滚动显示得分界面可按S2返回,重新开始游戏
*/
#include <REGX52.H> //51单片机头文件
#include <STDLIB.H> //随机函数
#include "MatrixLED.h" //8X8点阵屏
#include "MatrixKey.h" //矩阵按键
#include "Timer0.h" //定时器0
#include "Timer1.h" //定时器1
/** 图层0显存数组Layer0数据、图层1显存数组Layer1数据存储格式:
*
* 图层0和图层1显存数组存储的是已固定的和正在下落的方块的位置信息
* 显存数组数据为unsigned char型,共8位
* 纵向8点,高位在下,从左到右
* 每一个Bit对应一个像素点,即对应一个LED
*
* B0 B0 B0 B0
* B1 B1 B1 B1
* B2 B2 B2 B2
* B3 B3 -------> B3 B3
* B4 B4 B4 B4
* B5 B5 B5 B5
* B6 B6 B6 B6
* B7 B7 B7 B7
*/
/** 坐标轴定义:
*
* 左上角为原点(0,0)
* 横向向右为X轴,取值范围:0~7
* 纵向向下为Y轴,取值范围:0~7
*
* 0 X轴 7
* .————————————————>
* 0 |
* |
* |
* Y轴 |
* |
* |
* 7 |
* v
*/
unsigned char Mode; //游戏模式,0:循环滚动显示游戏英文名,1:游戏中,2:游戏结束全屏闪烁,3:滚动显示得分的英文,4:循环滚动显示得分
unsigned char LastMode; //上一次的游戏模式,切换模式时用来清空所有按键的所有标志位
unsigned char Offset; //偏移量,用来控制字母或数字向左滚动显示
bit OnceFlag; //各模式中(切换为其他模式前)只执行一次的标志,类似于主函数主循环前的那部分,用于该模式的初始化,1:执行,0:不执行
bit FlashFlag; //闪烁的标志,1:不显示,0:显示
bit ScrollFlag; //字母或数字滚动显示时,切换显示的标志,1:切换,0:不切换
bit PauseFlag; //游戏时暂停的标志,1:暂停,0:不暂停
unsigned char GameOverFlag; //游戏结束的的标志(函数返回值类型为unsigned char,不能定义为bit),1:游戏结束,0:游戏未结束
unsigned int T0Count_0; //定时器0全局计数变量
unsigned char Score; //游戏得分,范围:0~65535
unsigned char ScoreLength; //游戏得分的位数,范围:1~5
unsigned char xdata ScoreShow[]={ //游戏得分(用于循环滚动显示),取模要求:逐列式取模,高位在下,亮点为1,xdata:变量保存在片外RAM
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // 无显示
0x00,0x00,0x00,0x00,0x00,0x00, // 得分最多五位数,每一个数字对应6个字节
0x00,0x00,0x00,0x00,0x00,0x00, // 得分最多五位数,每一个数字对应6个字节
0x00,0x00,0x00,0x00,0x00,0x00, // 得分最多五位数,每一个数字对应6个字节
0x00,0x00,0x00,0x00,0x00,0x00, // 得分最多五位数,每一个数字对应6个字节
0x00,0x00,0x00,0x00,0x00,0x00, // 得分最多五位数,每一个数字对应6个字节
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // 无显示
};
unsigned char code Table1[]={ //游戏名称“俄罗斯方块”的英文:<<TETRIS>>,宽6高8,取模要求:逐列式取模,高位在下,亮点为1,code:数据存储在Flash中
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // 无显示
0x00,0x08,0x14,0x22,0x49,0x14,0x22,0x41, // << 宽8高8(自定义书名号:两个小于号)
0x00,0x01,0x01,0x7F,0x01,0x01, // T
0x00,0x7F,0x49,0x49,0x49,0x41, // E
0x00,0x01,0x01,0x7F,0x01,0x01, // T
0x00,0x7F,0x09,0x19,0x29,0x46, // R
0x00,0x00,0x41,0x7F,0x41,0x00, // I
0x00,0x46,0x49,0x49,0x49,0x31, // S
0x00,0x41,0x22,0x14,0x49,0x22,0x14,0x08, // >>
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // 无显示
};
unsigned char code Table2[]={ //“得分”的英文:“SCORE”,宽6高8,取模要求:逐列式取模,高位在下,亮点为1
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //无显示
0x00,0x46,0x49,0x49,0x49,0x31, // S
0x00,0x3E,0x41,0x41,0x41,0x22, // C
0x00,0x3E,0x41,0x41,0x41,0x3E, // O
0x00,0x7F,0x09,0x19,0x29,0x46, // R
0x00,0x7F,0x49,0x49,0x49,0x41, // E
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //无显示
};
unsigned char code Table3[]={ //游戏得分的字模数据,宽6高8,取模要求:逐列式取模,高位在下,亮点为1
0x00,0x3E,0x51,0x49,0x45,0x3E, // 0
0x00,0x00,0x42,0x7F,0x40,0x00, // 1
0x00,0x42,0x61,0x51,0x49,0x46, // 2
0x00,0x21,0x41,0x45,0x4B,0x31, // 3
0x00,0x18,0x14,0x12,0x7F,0x10, // 4
0x00,0x27,0x45,0x45,0x45,0x39, // 5
0x00,0x3C,0x4A,0x49,0x49,0x30, // 6
0x00,0x01,0x71,0x09,0x05,0x03, // 7
0x00,0x36,0x49,0x49,0x49,0x36, // 8
0x00,0x06,0x49,0x49,0x29,0x1E, // 9
};
bit FallFlag; //方块下落的标志,1:下落一个像素,0:暂不下落
unsigned char ReferencePointX; //参考点的X坐标(以该参考点为原点,新建一个坐标系)
unsigned char ReferencePointY; //参考点的Y坐标(以该参考点为原点,新建一个坐标系)
unsigned char idata Layer0[8]; //图层0的显示缓存,用来存储已经固定的方块的位置,idata:变量保存在片内间接寻址区
unsigned char idata Layer1[8]; //图层1的显示缓存,用来存储正在下落的方块的位置,idata:变量保存在片内间接寻址区
char idata BlockX[4]; //方块的X坐标,左上角的LED为原点(0,0),向右为X轴正方向,向下为Y轴正方向
char idata BlockY[4]; //方块的Y坐标,左上角的LED为原点(0,0),向右为X轴正方向,向下为Y轴正方向
char idata AfterRotateX[4]; //方块旋转后的X坐标,用来判断方块能否旋转
char idata AfterRotateY[4]; //方块旋转后的Y坐标,用来判断方块能否旋转
unsigned char BlockSelect; //方块选择变量,范围:0~27,根据数组AllBlocks来确定是哪个方块
unsigned char NextBlock; //下一个方块,范围:0~27,根据数组AllBlocks来确定是哪个方块
unsigned int Line; //所消的总行数
unsigned char LinePlus; //一次所消的行数
char code AllBlocks[28][4][2]={
//存储了7种方块的28种方向的具体信息(相对于参考点的坐标),程序中根据此数据计算方块在屏幕的位置
//以参考点为原点,建立一个新的坐标系,同样是向右为X轴正方向,向下为Y轴的正方向
//下面数据的每一行(8个字节)对应一个方块相对于参考点的坐标,分别为方块的四小块的X、Y坐标
//以第一个方块(“田”)的数据(前8个数据)为例
//0,0,对应的是“田”的左下角
//1,0,对应的是“田”的右下角
//0,-1,对应的是“田”的左上角
//1,-1,对应的是“田”的右下角
//相对于参考点的X坐标都是大于等于零,相对于参考点的Y坐标都是小于等于零
// 口口
// 口口
0,0, 1,0, 0,-1, 1,-1, //0
// 口口口口
0,0, 1,0, 2,0, 3,0, //1
// 口
// 口口口
0,0, 1,0, 2,0, 1,-1, //2
// 口
// 口口
// 口
1,0, 0,-1, 1,-1, 0,-2, //3
// 口
// 口口
// 口
0,0, 0,-1, 1,-1, 1,-2, //4
// 口
// 口口口
0,0, 1,0, 2,0, 2,-1, //5
// 口
// 口口口
0,0, 1,0, 2,0, 0,-1, //6
// 口口
// 口口
0,0, 1,0, 0,-1, 1,-1, //7
// 口
// 口
// 口
// 口
0,0, 0,-1, 0,-2, 0,-3, //8
// 口
// 口口
// 口
1,0, 0,-1, 1,-1, 1,-2, //9
// 口口
// 口口
0,0, 1,0, 1,-1, 2,-1, //10
// 口口
// 口口
1,0, 2,0, 0,-1, 1,-1, //11
// 口口
// 口
// 口
1,0, 1,-1, 0,-2, 1,-2, //12
// 口
// 口
// 口口
0,0, 1,0, 1,-1, 1,-2, //13
// 口口
// 口口
0,0, 1,0, 0,-1, 1,-1, //14
// 口口口口
0,0, 1,0, 2,0, 3,0, //15
// 口口口
// 口
1,0, 0,-1, 1,-1, 2,-1, //16
// 口
// 口口
// 口
1,0, 0,-1, 1,-1, 0,-2, //17
// 口
// 口口
// 口
0,0, 0,-1, 1,-1, 1,-2, //18
// 口口口
// 口
0,0, 0,-1, 1,-1, 2,-1, //19
// 口口口
// 口
2,0, 0,-1, 1,-1, 2,-1, //20
// 口口
// 口口
0,0, 1,0, 0,-1, 1,-1, //21
// 口
// 口
// 口
// 口
0,0, 0,-1, 0,-2, 0,-3, //22
// 口
// 口口
// 口
0,0, 0,-1, 1,-1, 0,-2, //23
// 口口
// 口口
0,0, 1,0, 1,-1, 2,-1, //24
// 口口
// 口口
1,0, 2,0, 0,-1, 1,-1, //25
// 口
// 口
// 口口
0,0, 1,0, 0,-1, 0,-2, //26
// 口口
// 口
// 口
0,0, 0,-1, 0,-2, 1,-2, //27
};
/**
* 函 数:图层0和图层1的数据叠加,更新屏幕显示
* 参 数:无
* 返 回 值:无
* 说 明:由定时器1自动扫描数组DisplayBuffer的数据进行显示
*/
void UpdateDisplay(void)
{
unsigned char i;
for(i=0;i<8;i++)
{
DisplayBuffer[i] = Layer0[i] | Layer1[i];
}
}
/**
* 函 数:图层0显存数组指定位置按指定的数值赋值
* 参 数:X 指定点的横坐标,范围:-128~127,屏幕区域:0~7
* 参 数:Y 指定点的纵坐标,范围:-128~127,屏幕区域:0~7
* 参 数:Value 数值,0:指定Bit置0,非0:指定Bit置1
* 返 回 值:无
*/
void Layer0_ControlPoint(char X,char Y,unsigned char Value)
{
if(Value==0)
{
if(X>=0 && X<=7 && Y>=0 && Y<=7){ Layer0[X] &= ~(0x01<<Y); }
}
else
{
if(X>=0 && X<=7 && Y>=0 && Y<=7){ Layer0[X] |= 0x01<<Y; }
}
}
/**
* 函 数:图层1显存数组指定位置按指定的数值赋值
* 参 数:X 指定点的横坐标,范围:-128~127,屏幕区域:0~7
* 参 数:Y 指定点的纵坐标,范围:-128~127,屏幕区域:0~7
* 参 数:Value 数值,0:指定Bit置0,非0:指定Bit置1
* 返 回 值:无
*/
void Layer1_ControlPoint(char X,char Y,unsigned char Value)
{
if(Value==0)
{
if(X>=0 && X<=7 && Y>=0 && Y<=7){ Layer1[X] &= ~(0x01<<Y); }
}
else
{
if(X>=0 && X<=7 && Y>=0 && Y<=7){ Layer1[X] |= 0x01<<Y; }
}
}
/**
* 函 数:图层0显存数组获取指定位置点的值
* 参 数:X 指定点的横坐标,范围:-128~127,屏幕区域:0~7
* 参 数:Y 指定点的纵坐标,范围:-128~127,屏幕区域:0~7
* 返 回 值:屏幕区域内返回0或1,超出屏幕区域返回2(特殊:屏幕正上方会返回0)
*/
unsigned char Layer0_GetPoint(char X,char Y)
{
if(X>=0 && X<=7) //如果是第1列到第8列
{
if(Y>=0 && Y<=7) //如果是第1行到第8行
{
if( Layer0[X] & (0x01<<Y) ) {return 1;} //屏幕区域内,指定的位为1,则返回1
else {return 0;} //屏幕区域内,指定的位为0,则返回0
}
else if(Y<0) //如果是屏幕区域上方,则返回0,目的是让方块未完全进入屏幕区域时不会被固定
{
return 0;
}
else //如果是屏幕区域下方,则返回2,用来检测下落方块是否来到底部
{
return 2;
}
}
else{return 2;} //如果不是第1列到第8列
}
/**
* 函 数:将图层0显存数组全部清零
* 参 数:无
* 返 回 值:无
* 说 明:图层0显存数组存储已经固定的方块的位置
*/
void Layer0_Clear(void)
{
unsigned char i;
for(i=0;i<8;i++)
{
Layer0[i]=0;
}
}
/**
* 函 数:将图层1显存数组全部清零
* 参 数:无
* 返 回 值:无
* 说 明:图层1显存数组存储正在下落的方块的位置
*/
void Layer1_Clear(void)
{
unsigned char i;
for(i=0;i<8;i++)
{
Layer1[i]=0;
}
}
/**
* 函 数:图层1显存数组更新方块的位置信息
* 参 数:无
* 返 回 值:无
* 说 明:一个方块由四个LED组成
*/
void Layer1_UpdateBlock(void)
{
unsigned char i;
Layer1_Clear(); //图层1清空数据
for(i=0;i<4;i++) //更新方块坐标
{
BlockX[i]=ReferencePointX+AllBlocks[BlockSelect][i][0]; //X坐标,根据参考点坐标和相对参考点的坐标来计算
BlockY[i]=ReferencePointY+AllBlocks[BlockSelect][i][1]; //Y坐标,根据参考点坐标和相对参考点的坐标来计算
}
for(i=0;i<4;i++) //根据方块坐标,将方块的位置信息更新到图层1显存数组
{
Layer1_ControlPoint(BlockX[i],BlockY[i],1);
}
}
/**
* 函 数:检查是否要固定方块
* 参 数:无
* 返 回 值:要固定方块返回1,不固定方块返回0
*/
unsigned char IsFix(void)
{
unsigned char i;
for(i=0;i<4;i++)
{
if( Layer0_GetPoint(BlockX[i],BlockY[i]+1) )
{ //检查方块的下方是否是区域底部或其他已经固定了的方块
return 1;
}
}
return 0;
}
/**
* 函 数:固定方块
* 参 数:无
* 返 回 值:无
* 说 明:方块下落时,如果下方是区域底部或其他已经固定了的方块,则该正在下落的方块会被固定
* 说 明:用图层0显存数组保存固定后的方块的位置
*/
void FixBlock(void)
{
unsigned char i;
for(i=0;i<8;i++)
{
Layer0[i] = Layer0[i] | Layer1[i];
}
}
/**
* 函 数:检查方块能否左移
* 参 数:无
* 返 回 值:能左移返回1,不能左移返回0
*/
unsigned char CanMoveLeft(void)
{
unsigned char i;
for(i=0;i<4;i++)
{
if( Layer0_GetPoint(BlockX[i]-1,BlockY[i]) )
{
return 0;
}
}
return 1;
}
/**
* 函 数:检查方块能否右移
* 参 数:无
* 返 回 值:能右移返回1,不能右移返回0
*/
unsigned char CanMoveRight(void)
{
unsigned char i;
for(i=0;i<4;i++)
{
if( Layer0_GetPoint(BlockX[i]+1,BlockY[i]) )
{
return 0;
}
}
return 1;
}
/**
* 函 数:检查能否旋转
* 参 数:X 参考点的X坐标
* 参 数:Y 参考点的Y坐标
* 返 回 值:能旋转返回1,不能旋转返回0
* 说 明:左上角的LED为原点(0,0),向右为X轴正方向,向下为Y轴正方向
*/
unsigned char CanRotate(char X,char Y)
{
unsigned char i;
for(i=0;i<4;i++) //根据提供的参考坐标,获取旋转后的方块的坐标
{
AfterRotateX[i]=X+AllBlocks[(BlockSelect+7)%28][i][0];
AfterRotateY[i]=Y+AllBlocks[(BlockSelect+7)%28][i][1];
}
for(i=0;i<4;i++)
{ //检测旋转后的方块有没有超过屏幕区域,以及有没有跟固定的方块有重叠
if( Layer0_GetPoint(AfterRotateX[i],AfterRotateY[i]) )
{ //如果有,则返回0,即不能旋转
return 0;
}
}
return 1; //如果没有,则返回1,即能旋转
}
/**
* 函 数:旋转方块
* 参 数:无
* 返 回 值:无
*/
void RotateBlock(void)
{
BlockSelect+=7;
BlockSelect%=28;
Layer1_UpdateBlock();
UpdateDisplay();
}
/**
* 函 数:检查和消除
* 参 数:无
* 返 回 值:所消的行数,范围:0~4
* 说 明:方块固定后从下遍历每一行,行满了后则消除该行
*/
unsigned char CheckAndEliminate(void)
{
unsigned char i,j,k;
unsigned char Count,Temp=0;
for(i=0;i<8;i++) //固定方块后,从下往上,遍历每一行
{
Count=0;
for(j=0;j<8;j++) //统计一行亮着的LED的个数
{
if(Layer0_GetPoint(j,7-i))
{
Count++;
}
}
if(Count==8) //如果某一行的LED全亮
{
Temp++;
for(k=i;k<7;k++) //该行以上的所有像素下移一格
{
for(j=0;j<8;j++)
{
Layer0_ControlPoint(j,7-k,Layer0_GetPoint(j,6-k));
}
}
for(j=0;j<8;j++) //如果有消行,则下移后清空第一行
{
Layer0_ControlPoint(j,0,0);
}
i--; //下移后重新检查该行
}
}
return Temp;
}
/**
* 函 数:判断游戏是否结束
* 参 数:无
* 返 回 值:游戏结束返回1,游戏未结束返回0
* 说 明:如果下一个方块无法下落一点,则游戏结束
*/
unsigned char IsGameOver(void)
{
unsigned char i;
for(i=0;i<4;i++)
{
BlockX[i]=3+AllBlocks[BlockSelect][i][0];
BlockY[i]=0+AllBlocks[BlockSelect][i][1];
}
for(i=0;i<4;i++)
{
if(BlockY[i]==0)
{
if(Layer0_GetPoint(BlockX[i],0))
{
return 1;
}
}
}
// for(i=3;i<=6;i++)
// {
// if( Layer0_GetPoint(i,0) )
// {
// return 1;
// }
// }
return 0;
}
/**
* 函 数:幂函数/指数函数
* 参 数:X 底
* 参 数:Y 幂
* 返 回 值:X的Y次方
* 说 明:辅助取出Score中的某一位上的数字
*/
unsigned int Pow(unsigned char X,unsigned char Y)
{
unsigned char i;
unsigned int Result=1;
for(i=0;i<Y;i++)
{
Result*=X;
}
return Result;
}
/**
* 函 数:主函数(有且仅有一个)
* 参 数:无
* 返 回 值:无
* 说 明:主函数是程序执行的起点,负责执行整个程序的主要逻辑
*/
void main()
{
unsigned char i,j; //For循环用到的临时变量
P2_5=0; //防止开发板上的蜂鸣器发出声音
Timer0_Init(); //定时器0初始化
Timer1_Init(); //定时器1初始化
MatrixLED_Init(); //点阵屏初始化
OnceFlag=1;
Key_Clear(); //进主循环前清空所有按键的所有标志位
while(1)
{
/*按键处理*/
if(Mode != LastMode) //如果模式发生改变
{
Key_Clear(); //每次切换模式都要清空所有按键的所有标志位,防止本模式置1的标志位影响下一模式
LastMode=Mode;
}
if(Mode==0) //如果是循环滚动显示游戏英文名的界面
{
for(i=0;i<16;i++)
{
if(Key(i,DOWN)) //如果有任意按键按下
{
srand(TL0); //每次按键按下都用定时器0的低8位做种子,从而产生真随机数
Mode=1; //切换到模式1
OnceFlag=1; //切换模式前只执行一次的标志置1
break; //退出循环
}
}
}
else if(Mode==1) //如果是正在游戏的界面
{
if(Key(S1,DOWN)) //如果按下S1
{
srand(TL0); //每次按键按下都用定时器0的低8位做种子,从而产生真随机数
PauseFlag=!PauseFlag; //置反暂停的标志
if(PauseFlag==0) //从暂停变成继续的时候,显示方块
{
Layer1_UpdateBlock(); //图层1中写入正在下落的方块的位置的信息
UpdateDisplay(); //更新屏幕显示
}
}
if( (Key(S13,DOWN) || Key(S13,REPEAT)) && PauseFlag==0) //如果S13按下或重复,且不是暂停状态
{
srand(TL0); //每次按键按下都用定时器0的低8位做种子,从而产生真随机数
if( CanMoveLeft() ) //如果方块能左移
{
ReferencePointX--; //参考点的X坐标减1
Layer1_UpdateBlock(); //更新正在下落的方块的位置
UpdateDisplay(); //更新屏幕显示
}
}
if( (Key(S15,DOWN) || Key(S15,REPEAT)) && PauseFlag==0) //如果S15按下或重复,且不是暂停状态
{
srand(TL0); //每次按键按下都用定时器0的低8位做种子,从而产生真随机数
if( CanMoveRight() ) //如果方块能右移
{
ReferencePointX++; //参考点的X坐标加1
Layer1_UpdateBlock(); //更新正在下落的方块的位置
UpdateDisplay(); //更新屏幕显示
}
}
if(Key(S10,DOWN) && PauseFlag==0) //如果按下S10,且不是暂停状态
{
srand(TL0); //每次按键按下都用定时器0的低8位做种子,从而产生真随机数
if( CanRotate(ReferencePointX,ReferencePointY) )
{ //如果旋转后,没有超出屏幕区域,也没有跟已固定的方块重叠
RotateBlock(); //旋转并更新屏幕显示
}
else if( CanRotate(ReferencePointX-1,ReferencePointY) )
{ //如果旋转后左移一格,没有超出屏幕区域,也没有跟已固定的方块重叠
ReferencePointX--; //更新参考点的X坐标
RotateBlock(); //旋转并更新屏幕显示
}
else if( CanRotate(ReferencePointX-2,ReferencePointY) )
{ //如果旋转后左移两格,没有超出屏幕区域,也没有跟已固定的方块重叠
ReferencePointX-=2; //更新参考点的X坐标
RotateBlock(); //旋转并更新屏幕显示
}
else if( CanRotate(ReferencePointX-3,ReferencePointY) )
{ //如果旋转后左移三格,没有超出屏幕区域,也没有跟已固定的方块重叠
ReferencePointX-=3; //更新参考点的X坐标
RotateBlock(); //旋转并更新屏幕显示
}
//如果以上四种情况都不满足,则不旋转方块
}
if((Key(S14,DOWN) || Key(S14,REPEAT)) && PauseFlag==0) //如果S14按下或重复,且不是暂停状态,则方块加速下落
{
srand(TL0); //每次按键按下都用定时器0的低8位做种子,从而产生真随机数
if( IsFix() ) //如果是固定方块的情况
{
FixBlock(); //则固定方块
LinePlus=CheckAndEliminate(); //方块固定后,从下往上遍历每一行,行满则消除,并返回所消的行数
Line=Line+LinePlus; //更新存储总行数的变量
Score=Score+LinePlus*LinePlus; //更新得分,一次所消的行数越多,得分越多
ReferencePointX=3; //方块固定后,参考点坐标重置
ReferencePointY=-1; //方块固定后,参考点坐标重置
BlockSelect=NextBlock; //固定方块后,更新要下落的方块
NextBlock=rand()%28; //确定下一个方块
GameOverFlag=IsGameOver(); //判断游戏是否结束
T0Count_0=0; //定时器0全局计数变量清零
FallFlag=0; //方块下落的标志置0
}
if(GameOverFlag==0)
{
ReferencePointY++; //方块下落一格
Layer1_UpdateBlock(); //更新正在下落的方块的位置
UpdateDisplay(); //更新屏幕显示
T0Count_0=0; //定时器0全局计数变量清零
FallFlag=0; //方块下落的标志置0
}
}
if(Key(S16,DOWN) && PauseFlag==0) //如果是按下S16,且不是暂停状态
{
srand(TL0); //每次按键按下都用定时器0的低8位做种子,从而产生真随机数
while( IsFix()==0 ) //瞬间到达底部(不显示下落的过程)
{
ReferencePointY++;
Layer1_UpdateBlock(); //更新正在下落的方块的位置
}
FixBlock();
LinePlus=CheckAndEliminate(); //方块固定后,从下往上遍历每一行,行满则消除,并返回所消的行数
Line=Line+LinePlus; //更新存储总行数的变量
Score=Score+LinePlus*LinePlus; //更新得分,一次所消的行数越多,得分越多
UpdateDisplay(); //更新屏幕显示
GameOverFlag=IsGameOver();
T0Count_0=0; //定时器0全局计数变量清零
FallFlag=0; //方块下落的标志置0
if(GameOverFlag==0)
{
ReferencePointX=3;
ReferencePointY=0;
BlockSelect=NextBlock; //固定方块后,更新要下落的方块
NextBlock=rand()%28; //确定下一个方块
Layer1_UpdateBlock(); //更新正在下落的方块的位置
UpdateDisplay(); //更新屏幕显示
}
}
}
else if(Mode==2) //如果是游戏结束全屏闪烁的界面
{
if(Key(S1,DOWN)) //如果按下S1
{
Mode=3;
OnceFlag=1;
}
}
else if(Mode==3) //如果是滚动显示英文“SCORE”的界面
{
if(Key(S1,DOWN)) //如果按下S1
{
Mode=4;
OnceFlag=1;
}
}
else if(Mode==4) //如果是循环滚动显示得分的界面
{
if(Key(S2,DOWN)) //如果按下S2
{
Mode=1; //重新开始游戏
OnceFlag=1;
}
}
/*游戏处理*/
if(Mode==0) //循环滚动显示游戏英文名
{
if(OnceFlag) //切换到其他模式前,此if中的代码只执行1次
{
OnceFlag=0; //只执行一次的标志置0
Offset=0; //滚动显示的偏移量清零
}
if(ScrollFlag) //如果滚动显示的标志ScrollFlag为真(非零即真)
{
ScrollFlag=0; //滚动显示的标志ScrollFlag置0
MatrixLED_HS(Table1,Offset); //滚动显示,向左
Offset++; //每次向左移动一个像素
Offset%=60; //越界清零,循环滚动显示
}
}
else if(Mode==1) //游戏进行中
{
if(OnceFlag)
{ //游戏初始化
OnceFlag=0;
MatrixLED_Clear(); //清屏
Score=0; //得分清零
for(i=0;i<46;i++){ScoreShow[i]=0;} //清空分数显示的缓存数组的数据
PauseFlag=0; //暂停的标志置0
GameOverFlag=0; //游戏结束的标志置0
Line=0; //所消的总行数清零
LinePlus=0; //上一次所消的行数清零
BlockSelect=rand()%28; //开始游戏时随机确定一个方块
NextBlock=rand()%28; //开始游戏时随机确定下一个方块
ReferencePointX=3; //初始化参考点的X坐标
ReferencePointY=0; //初始化参考点的Y坐标
Layer0_Clear(); //图层0清空数据
Layer1_Clear(); //图层1清空数据
Layer1_UpdateBlock(); //更新正在下落的方块的显示
UpdateDisplay(); //更新屏幕的显示
T0Count_0=0; //定时器0全局计数变量清零
FallFlag=0; //方块下落的标志置0
}
if(GameOverFlag==0) //如果游戏未结束
{
if(PauseFlag==0) //如果不是暂停
{
if(FallFlag) //如果方块下落的标志为真
{
FallFlag=0; //方块下落的标志置0
if( IsFix() )
{
FixBlock();
LinePlus=CheckAndEliminate(); //方块固定后,从下往上遍历每一行,行满则消除,并返回所消的行数
Line=Line+LinePlus; //更新存储总行数的变量
Score=Score+LinePlus*LinePlus; //更新得分,一次所消的行数越多,得分越多
ReferencePointX=3;
ReferencePointY=-1;
BlockSelect=NextBlock; //固定方块后,更新要下落的方块
NextBlock=rand()%28; //确定下一个方块
GameOverFlag=IsGameOver(); //判断游戏是否结束
}
if(GameOverFlag==0)
{
ReferencePointY++;
Layer1_UpdateBlock(); //更新正在下落的方块的位置
UpdateDisplay(); //更新屏幕显示
}
}
}
else //如果是暂停状态
{
if(FlashFlag)
{
Layer1_Clear(); //清除图层1正在下落的方块的位置信息
UpdateDisplay(); //更新屏幕显示
}
else
{
Layer1_UpdateBlock(); //重新向图层1写入正在下落的方块的位置信息
UpdateDisplay(); //更新屏幕显示
}
}
}
else //如果游戏结束
{
Mode=2; //切换到模式2
}
}
else if(Mode==2) //游戏结束全屏闪烁
{
//在定时器1中实现全屏闪烁
}
else if(Mode==3) //滚动显示得分的英文“SCORE”
{
if(OnceFlag)
{
OnceFlag=0;
Offset=0;
}
if(ScrollFlag && Offset<=38) //只滚动显示一次英文
{
ScrollFlag=0;
MatrixLED_HS(Table2,Offset); //滚动显示,向左
Offset++;
}
else if(Offset>38) //滚动结束后,自动切换到循环滚动显示得分的模式
{
Mode=4;
OnceFlag=1;
}
}
else if(Mode==4) //循环滚动显示得分
{
if(OnceFlag)
{
OnceFlag=0;
Offset=0;
if(Score>=10000){ScoreLength=5;}//判断得分是多少位数
else if(Score>=1000){ScoreLength=4;}
else if(Score>=100){ScoreLength=3;}
else if(Score>=10){ScoreLength=2;}
else{ScoreLength=1;}
for(j=0;j<ScoreLength;j++)//将得分的数字的字模写入数组ScoreShow中
{
for(i=0;i<6;i++)
{
ScoreShow[8+6*j+i]=Table3[(Score/Pow(10,ScoreLength-1-j)%10)*6+i];
}
}
}
if(ScrollFlag)
{
ScrollFlag=0;
MatrixLED_HS(ScoreShow,Offset); //滚动显示,向左
Offset++;
Offset%=8+ScoreLength*6; //循环滚动显示
}
}
}
}
/**
* 函 数:定时器0中断函数
* 参 数:无
* 返 回 值:无
*/
void Timer0_Routine() interrupt 1
{
static unsigned char T0Count0,T0Count1; //定义计时器静态计数变量
TL0=0x00; //设置定时初值,定时10ms,12T@11.0592MHz
TH0=0xDC; //设置定时初值,定时10ms,12T@11.0592MHz
Key_Tick(); //每隔10ms调用一次按键驱动函数Key_Tick
T0Count0++;
T0Count1++;
if(PauseFlag==0){T0Count_0++;} //不暂停时才计数
if(T0Count0>=50) //每隔500ms置反FlashFlag
{
T0Count0=0;
FlashFlag=!FlashFlag;
}
if(T0Count1>=10) //每隔100ms滚动显示一次字母或数字
{
T0Count1=0;
ScrollFlag=1;
}
if(T0Count_0>=100) //每隔1s方块下落一个像素
{
T0Count_0=0;
FallFlag=1;
}
}
/**
* 函 数:定时器1中断函数
* 参 数:无
* 返 回 值:无
* 说 明:专门用定时器1来扫描显示LED点阵屏,定时器1的优先级要比定时器0的高,否则显示会有闪烁现象
*/
void Timer1_Routine() interrupt 3
{
TL1=0x66; //设置定时初值,定时1ms,12T@11.0592MHz
TH1=0xFC; //设置定时初值,定时1ms,12T@11.0592MHz
if(Mode==2 && FlashFlag){P0=0xFF;} //控制游戏结束后的全屏闪烁
else{MatrixLED_Tick();}
}
总结
之前做贪吃蛇的时候就想做俄罗斯方块了,然后觉得比贪吃蛇复杂很多,因为要控制四个LED的移动、旋转、碰撞检测、是否要固定的检测、是否要消行的检测等等,就一直构思怎么按自己想法来实现。
其实思路很早就已经有的了,只是在做其他很多的小游戏,拖到了现在。
做的过程会发现有很多细节要处理,就一点一点修改,直到最近才做出来。如果还没开始,还在思考一些细节,不去实践,是永远做不出来的。
要在做的过程中调试才会发现一些自己没想到的要处理的细节。
我觉得不要拖拉,要直面困难,遇到困难就一个一个地想办法解决,到你真正去做的时候,会发现,没有你想象中那么难。