这里只给了关键代码进行分析,并非全部代码。
项目概述和测试见文章
基于STM32F407的俄罗斯方块小游戏的设计_钻仰弥坚的博客-CSDN博客
一、方块编码的方式
首先需要知道俄罗斯方块本质上为4个小方块组成的正方形。总共有7种基本类型。每种方块又可以通过旋转而变化出1到4种形状,一共就有19种。具体关系如图1所示。
图1 方块对应关系图
关于方块的编码形式,我们可以借用Ucos-II的一些思想。我们知道在Ucos-II中的任务就绪表记录了系统中所有处于就绪状态的任务,从代码上来看它就是一个类型为INT8U的一维数组OSRdyTbl[]。那么在俄罗斯方块中,我们也可以定义一个一维数组,设置一种规律,也就是利用枚举法,将19种方块进行编码,这样我们也就可以随机生成方块了。
举个例子,我们假设存在一个4×4的方块,这又16个小方块组成,对16个小方块编一个序号,对于19种方块都可以容纳进这样的4×4的方块之中,这里我们对一个L型的方块进行编码,如图2所示,该L型方块占据了15,14,7,6号方块,那么我们可以用0和1来区分该区域是否有方块,为1时存在方块,为0时为空,那么该图形二进制的形式编码为0100 0100 0110 0000,转换成16进制的是0x4460,那么我们可以把0x4460当成数组中的一个元素。
图2 方块示例
代码如下:
void LCD_DrawBlock(u16 sx,u16 sy,u16 color)
{
LCD_DrawRectangle(sx,sy,sx+BPIXEL-1,sy+BPIXEL-1);
LCD_Fill(sx+2,sy+2,sx+BPIXEL-3,sy+BPIXEL-3,color);
}
类似地,我们可以对19种方块都进行编码,对应关系如图3所示:
图3 方块对应编码图
二、单个方块的创建与消除
首先我们可以设定每个方块占20个像素,对于这20像素的宽度,我们可以细分为一个空心正方形嵌套一个实心正方形,他们具体的长度如图4所示。
图4 小方块长度图
可以把创建一个20像素的正方形和16像素的实心正方形写成一个函数,并且对该位置标记已占据,这样其余模块可以调用这个函数创建各种各样的方块。同理如果我们需要消除方块,也可以直接用白背景直接覆盖住这个方块,并且取消占据标记即可完成。
三、方块的移动
我们阅读STM32的底层代码时,发现STM32的按键实验和红外线模块实验均是使用while函数内加上一个switch函数来实现按键或者遥控器的控制,那么在俄罗斯方块的项目中,我们也可以这样完成方块的操纵,而方块的移动我们可以更加细化一点,就是需要实现原来位置方块处的消失,新位置方块的生成,无论方块是如何移动,这样的基本步骤不变,如图5所示。
图5 方块移动流程图
这里需要提一下旋转函数,这里使用的是枚举法。我们可以根据上面提到的图3的对应关系,通过switch函数来进行转换对应的编号。
代码如下:
u16 Tetris[19]={0x0F00,0x4444,0x0660,0x4460,0x02E0,0x6220,0x0740,0x2260,0x0E20,0x6440,0x0470,0x0C60,0x2640,0x0360,0x4620,0x04E0,0x2620,0x0E40,0x4640};
u8 Judge(u16 sx,u16 sy,u8 n,u8 mode)
{
int cx,cy,temp1=Tetris[n],temp2=Tetris[n];
u8 a,b,i,Flag=0;
switch(mode)
{
case 1: cx=sx-BPIXEL;cy=sy;break;
case 2: cx=sx+BPIXEL;cy=sy;break;
case 3: cx=sx;
cy=sy;
switch(NewShape.CurNum)
{
case 0:temp2=1;break;
case 1:temp2=0;break;
case 2:temp2=2;break;
case 3:temp2=4;break;
case 4:temp2=5;break;
case 5:temp2=6;break;
case 6:temp2=3;break;
case 7:temp2=8;break;
case 8:temp2=9;break;
case 9:temp2=10;break;
case 10:temp2=7;break;
case 11:temp2=12;break;
case 12:temp2=11;break;
case 13:temp2=14;break;
case 14:temp2=13;break;
case 15:temp2=16;break;
case 16:temp2=17;break;
case 17:temp2=18;break;
case 18:temp2=15;break;
}
NewShape.TurnNum=temp2;
temp2=Tetris[temp2];
break;
case 5: cx=sx;cy=sy+BPIXEL;break;
default:cx=sx;cy=sy;break;
}
for(i=0;i<16;i++)
{
a=i/4;
b=i%4;
if(temp1&0x8000)BoxSR[(sy/BPIXEL)+a]&=~(1<<((sx/BPIXEL)+b)); //将变形前填充的模块置0
temp1<<=1;
}
for(i=0;i<16;i++)
{
a=i/4;
b=i%4;
if(temp2&0x8000)
{
if(BoxSR[cy/BPIXEL+a]&(1<<((cx/BPIXEL)+b))) Flag=1; //如果&为真,表示变化后有模块冲突,不能实现变形
}
temp2<<=1;
}
if(Flag==0)return 1; //可以实现变形返回1,不能变形返回0
else return 0;
}
四、方块冲突检测
关于冲突检测也是最重要的一个环节,这样确保方块在下方有方块时不能继续下落,在四周有障碍物的时候不能移动。无论方块是如何移动的,冲突检测方式不会变。首先是假设他可以移动,确定好移动的目标位置,然后对计算出来目标位置进行检测看该处是否有方块占据,如果有方块占据则不能显示,如果没有方块占据则可以显示出来。冲突检测流程图如图6所示。而检测是否有占据的方法便是查看对应的位是置0还是置1,如果置1,说明方块已经被占据,返回无法移动的标识,如果置0,说明可以方块移动,返回可以移动的标识。
图6 冲突检测流程图
代码:
if(Draw_Ready==1) //可以左移
{
LCD_ClearShape(NewShape.x,NewShape.y,NewShape.CurNum,BOXS_COLOR);
NewShape.x-=BPIXEL;
LCD_DrawShape(NewShape.x,NewShape.y,NewShape.CurNum,SHAPE_COLOR);
}
if(Draw_Ready==1) //可以右移
{
LCD_ClearShape(NewShape.x,NewShape.y,NewShape.CurNum,BOXS_COLOR); //清除右移前的模块显示
NewShape.x+=BPIXEL;
LCD_DrawShape(NewShape.x,NewShape.y,NewShape.CurNum,SHAPE_COLOR); //显示右移后的模块
}
五、满行检测
俄罗斯方块的游戏每下落一次方块还需要进行满行的检测,当存在满行的时候该行就会消除,位于其上方的方块全部下移。之后会给玩家以加分。前面我们已经说明了整个游戏界面是一个一维数组,只要我们每次方块停止下落的时候,遍历一次数组,如果存在某个值为0xFFFF,这样这个数组所代表的方块行将会被消除,在该行之上的方块整体下落。检测流程图如图7所示。
图7 满行检测流程图
代码:
for(i=4;i<Y_BOXS+4-1;i++) //扫描除了最顶上5行之外的行
{
if(BoxSR[i]==0xFFFF) //一行填满,进行数据更新和消行操作
{
Game.score=Game.score+40;
Game.level=1+Game.score/100;
if(Game.score>=Game.bestscore)
Game.bestscore=Game.score;
LCD_ShowNum(160,640,Game.score,3,16);
LCD_ShowNum(160,660,Game.level,3,16);
LCD_ShowNum(160,680,Game.bestscore,3,16);
Clear_Flag=1;
for(j=i;j>3;j--)BoxSR[j]=BoxSR[j-1]; //将满行之上有模块的行平移到当前行
}
}
if(Clear_Flag)LCD_Fill(20,20,300,440,BOXS_COLOR);
for(i=3;i<Y_BOXS+4-1;i++)
{
temp=BoxSR[i];
temp>>=1;
for(j=1;j<15;j++) //更新每行显示
{
if(temp&0x0001)LCD_DrawBlock(j*BPIXEL,i*BPIXEL,SHAPE_COLOR);
temp>>=1;
}
}
Create_Shape();