基于51单片机和8X8点阵屏、矩阵按键的小游戏《俄罗斯方块》

系列文章目录


前言

《俄罗斯方块》,一款经典的、怀旧的小游戏,单片机入门必写程序。

有两个版本,一个使用普中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的移动、旋转、碰撞检测、是否要固定的检测、是否要消行的检测等等,就一直构思怎么按自己想法来实现。

其实思路很早就已经有的了,只是在做其他很多的小游戏,拖到了现在。

做的过程会发现有很多细节要处理,就一点一点修改,直到最近才做出来。如果还没开始,还在思考一些细节,不去实践,是永远做不出来的。

要在做的过程中调试才会发现一些自己没想到的要处理的细节。

我觉得不要拖拉,要直面困难,遇到困难就一个一个地想办法解决,到你真正去做的时候,会发现,没有你想象中那么难。

内容概要:本文档详细介绍了基于MATLAB实现的无人机三维路径规划项目,核心算法采用蒙特卡罗树搜索(MCTS)。项目旨在解决无人机在复杂三维环境中自主路径规划的问题,通过MCTS的随机模拟与渐进式搜索机制,实现高效、智能化的路径规划。项目不仅考虑静态环境建模,还集成了障碍物检测与避障机制,确保无人机飞行的安全性效率。文档涵盖了从环境准备、数据处理、算法设计与实现、模型训练与预测、性能评估到GUI界面设计的完整流程,并提供了详细的代码示例。此外,项目采用模块化设计,支持多无人机协同路径规划、动态环境实时路径重规划等未来改进方向。 适合人群:具备一定编程基础,特别是熟悉MATLAB无人机技术的研发人员;从事无人机路径规划、智能导航系统开发的工程师;对MCTS算法感兴趣的算法研究人员。 使用场景及目标:①理解MCTS算法在三维路径规划中的应用;②掌握基于MATLAB的无人机路径规划项目开发全流程;③学习如何通过MCTS算法优化无人机在复杂环境中的飞行路径,提高飞行安全性效率;④为后续多无人机协同规划、动态环境实时调整等高级应用打下基础。 其他说明:项目不仅提供了详细的理论解释技术实现,还特别关注了实际应用中的挑战解决方案。例如,通过多阶段优化与迭代增强机制提升路径质量,结合环境建模与障碍物感知保障路径安全,利用GPU加速推理提升计算效率等。此外,项目还强调了代码模块化与调试便利性,便于后续功能扩展性能优化。项目未来改进方向包括引入深度强化学习辅助路径规划、扩展至多无人机协同路径规划、增强动态环境实时路径重规划能力等,展示了广阔的应用前景发展潜力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值