一. 综述
相对于五子棋,俄罗斯方块算法更是满天飞。我的代码还是相对好移植的,看起来也更清晰,方便学弟学妹们做编程小学期时移植。不过要做就要做的有特色:它运行在软硬件由我们独立开发的平台上。以下是截图:
有如下特点:
跑在独立开发的平台上,平台为MSP430F149,内存2KB,频率8MHz 支持体感:可以通过左右倾斜来左右移动方块,还可以上下抖动改变方块形状 代码精简,方便移植 内存占用率极低 支持横屏和竖屏操作 支持等级:用户在达到一定分数后,等级会上升,从而方块下落速度变得更快 二. 系统设计
我们将问题细化为以下几个方面:
1. 方块的形状如何存储
俄罗斯方块总共有19种形状,每种形状都由四个小方块组成。如何高效存储这些方块的形状是个值得思考的问题。
上图介绍了存储方法:
我们用以下数组保存形状:
const unsigned char BoxShape[19][9]= { { 1,0,0,1,1,1,2,1,1 }, { 1,0,1,1,2,1,1,2,2 }, { 0,0,1,0,2,0,1,1,3 }, { 1,0,0,1,1,1,1,2,0 }, { 1,0,2,0,1,1,1,2,5 }, { 0,0,1,0,2,0,2,1,6 }, { 2,0,2,1,2,2,1,2,7 }, { 0,0,0,1,1,1,2,1,4 }, { 1,0,2,0,2,1,2,2,9 }, { 2,0,0,1,1,1,2,1,10 }, { 1,0,1,1,1,2,2,2,11 }, { 0,0,1,0,2,0,0,1,8 }, { 0,0,0,1,1,1,1,2,13 }, { 1,0,2,0,0,1,1,1,12 }, { 2,0,1,1,2,1,1,2,15 }, { 0,0,1,0,1,1,2,1,14 }, { 1,0,1,1,1,2,1,3,17 }, { 0,1,1,1,2,1,3,1,16 }, { 1,0,2,0,1,1,2,1,18 } };
2. 如何解决各个方块的互相转换的顺序:
以上的数组已经解决了这一问题,多维数组的最后一位即该形状发生改变后的下一个形状。
3. 如何存储当前的画布信息
3. 如何解决碰撞问题:
这大概是算法当中最值得考虑的了,我们用以下四个函数解决:
所有的函数形参类似,它们的定义分别是:
XAxi,YAxi:存储当前形状的左上角的点在画布中的实际偏移量。
Data[][22]:存储的画布数据
Type:当前形状的ID(即BoxSharp的数组偏移量)
u8 Bottom_Anti(u8 XAxi ,u8 YAxi,u8 Data[][22],u8 Type) { u8 i; u8 Value=1; for(i=0;i<4;i++) { if(Data[XAxi+BoxShape[Type][2*i]][YAxi+BoxShape[Type][2*i+1]+1]==1) Value=0; } return Value; } u8 Right_Anti(u8 XAxi ,u8 YAxi,u8 Data[][22],u8 Type) { u8 i; u8 Value=1; for(i=0;i<4;i++) { if(Data[XAxi+BoxShape[Type][2*i]+1][YAxi+BoxShape[Type][2*i+1]]==1) Value=0; } return Value; } u8 Lift_Anti(u8 XAxi ,u8 YAxi,u8 Data[][22],u8 Type) { u8 i; u8 Value=1; for(i=0;i<4;i++) { if(Data[XAxi+BoxShape[Type][2*i]-1][YAxi+BoxShape[Type][2*i+1]]==1) Value=0; } return Value; } u8 Change_Anti(u8 XAxi ,u8 YAxi,u8 Data[][22],u8 *Type) { u8 i; u8 Value=1; for(i=0;i<4;i++) { if(Data[XAxi+BoxShape[BoxShape[*Type][8]][2*i]][YAxi+BoxShape[BoxShape[*Type][8]][2*i+1]]==1) Value=0; //??????????? } return Value; }
算法本身很清晰,分别检测四个方块在改变后的实际位置是否已经有墙壁或方块存在。这个属性用临时变量Value存储,默认为1,如发现碰撞,则赋值为0并返回。
4. 如何判断某一行已经被填充满并计分?
下面是代码,很简单,检测该行的10个方格是不是都填满了,若填满,则加分,并重新刷新LCD
void CheckMark(u8 *mark ,u8 Data[][22],u8 dir) { u8 m,t,s; u8 tMark=0; for(m=20;m>2;m--) { tMark=0; for(t=1;t<11;t++) { if(Data[t][m]==1) tMark++; } if(tMark==10) { for(t=m;t>2;t--) { for(s=1;s<11;s++) { Data[s][t]= Data[s][t-1]; } } m++; TotalRefreshLCD(Data,dir); delay_ms(200); (*mark)++; } } }
5. 如何产生方块?并检查方块已经顶到头?
代码如下:(值得讨论的是,在单片机中如何产生随机数?)
void GenerateBox(u8 *XAxi ,u8 *YAxi,u8 *Type,u8 Data[][22],u8 *FailFlag) { u8 temp; *XAxi=5; *YAxi=2; *Type=random(0,18); //产生随机数 for(temp=2;temp<10;temp++) { if(Data[temp][2]==1) //若在画布上的第3行出现方块,则失败 { *FailFlag=2; return; } } }
好了,这些核心问题我们都讨论完了,下面讨论核心流程。
三 . 核心处理流程
若加上全部处理,包括刷新界面,显示欢迎界面,显示失败和分数界面,那么还是很冗长的。这些对于实现核心算法无益,我们也不需要关心。
下面是流程图:
主要操作代码如下,使用了状态机,请注意看OS_func_state==1的流程,这是系统在运行时的主要流程。
主要核心操作流程
u8 TerisBrick() { u8 grade=0,mark=0; u8 GameGUIData[12][22]; u8 KeyTemp; u8 XAxi,YAxi,Type,OriGrade=0; TickControlEN=1; u8* temp[2]; u8 dir=0; temp[0]="竖版游戏模式"; temp[1]="横版游戏模式"; if(AccControlEN==1) { ADXL345Init(0); } while(OS_func_state<10) { switch(OS_func_state) { case 0: GUI_GameOpen("俄罗斯方块",&grade); OriGrade=grade; dir=ListGUI(temp,"选择游戏方向",2); //dir 表征运动方向,0:横版,1竖版 dir--; Clear_Screen(); OS_func_state=1; break; case 1: TerisBrickInit(GameGUIData,12,22); _EINT(); SetPaintMode(1,COLOR_Black); if(dir==0) { Rectangle(30,22,110,190,1); } else Rectangle(22,32,232,132,1); PutString(275,70,"grade"); PutString(275,110,"mark"); Lcd_disp(2,200,"左右方向键移动,下键加速,上改变形状"); while(OS_func_state==1) { back_light=back_light_set; FontSetTotal(COLOR_Black); NumberDis(275,90,grade,3,1); NumberDis(275,130,mark,3,1); GenerateBox(&XAxi,&YAxi,&Type,GameGUIData,&OS_func_state); while(Bottom_Anti(XAxi,YAxi,GameGUIData,Type)) { key_data=KEYNULL; ShowBoxGUI(XAxi,YAxi,Type,1,dir); if(AccControlEN==1) { ADXL345ReadData(); ADXL345ShowData(0); L3G4200DReadData(); L3G4200DShowData(); } if(KeyTemp==KEYDOWN_DOWN) delay_ms(150); else delay_ms(500-70*grade); ShowBoxGUI(XAxi,YAxi,Type,0,dir); YAxi++; if(AccControlEN==1) { if(dir==0) //横版 { if(AccX>3) key_data=KEYLEFT_UP ; else if(AccX<-3) key_data=KEYRIGHT_UP ; if(GyroY>400||GyroY<-400) { key_data=KEYUP_UP ; delay_ms(200); } } else { if(AccY>3) key_data=KEYLEFT_UP ; else if(AccY<-3) key_data=KEYRIGHT_UP ; if(GyroX>400||GyroX<-400) { key_data=KEYUP_UP ; delay_ms(200); } } } switch(key_data) { case KEYLEFT_UP : if(Lift_Anti(XAxi,YAxi,GameGUIData,Type)!=0) XAxi--; break; case KEYRIGHT_UP : if(Right_Anti(XAxi,YAxi,GameGUIData,Type)!=0) XAxi++; break; case KEYUP_UP : if(Change_Anti(XAxi,YAxi,GameGUIData,&Type)!=0) Type=BoxShape[Type][8]; break; case KEYDOWN_UP : if(Bottom_Anti(XAxi,YAxi,GameGUIData,Type)) YAxi++; break; case KEYCANCEL_UP : if(MessageGui("提示信息","是否跳出?",1)==1) { TickControlEN=1; OS_func_state=2; return 1; } else TotalRefreshLCD(GameGUIData,dir); break; } KeyTemp=key_data; key_data=KEYNULL; } for(u8 t=0;t<4;t++) { GameGUIData[XAxi+BoxShape[Type][2*t]][YAxi+BoxShape[Type][2*t+1]]=1; } ShowBoxGUI(XAxi,YAxi,Type,1,dir); CheckMark(&mark,GameGUIData,dir); grade=OriGrade+mark/10; } break; case 2: if(mark==0) MessageGui("超级菜鸟","学习下游戏规则",0); else if(mark>0&&mark<10) MessageGui("初学者","你还要加油哦",0); else if(mark>9&&mark<20) MessageGui("中级水平","哥们你很牛逼",0); else if(mark>19&&mark<30) MessageGui("高级水平","无敌哥!膜拜",0); else MessageGui("超级无敌","不是一般人!!",0); if( MessageGui("提示信息","是否继续",1)==1) OS_func_state=0; else { TickControlEN=1; OSTaskClose(); } break; } } return 1; }
四. 其他模块
其他模块主要包括界面显示,分数显示,方块显示和键盘输入等。考虑到和平台相关,因此不具备移植性,贴在下面仅供参考。
1. 初始化界面绘图画布数组,主要是建立“围栏”
void TerisBrickInit(u8 Data[][22],u8 i,u8 j) { u8 a,b; for(b=0;b<i;b++) { for(a=0;a<j;a++) Data[b][a]=0; } for(b=0;b<j;b++) { Data[0][b]=1; Data[11][b]=1; } for(a=0;a<i;a++) { Data[a][0]=1; Data[a][21]=1; } }
2. 绘制方块和刷新界面
绘制界面和方块
void ControlBox(u8 x,u8 y,u8 mood,u8 dir) { //dir指出方向,0,为横版,1为竖版 if(mood==1) { SetPaintMode(0,COLOR_Black); if(dir==1) Rectangle(22+10*y,22+10*x,32+10*y,32+10*x,1); else Rectangle(22+8*x,22+8*y,30+8*x,30+8*y,1); SetPaintMode(0,COLOR_Red); if(dir==1) Rectangle(23+10*y,23+10*x,31+10*y,31+10*x,1); else Rectangle(23+8*x,23+8*y,29+8*x,29+8*y,1); } else { SetPaintMode(1,COLOR_Black); if(dir==1) Rectangle(22+10*y,22+10*x,32+10*y,32+10*x,1); else Rectangle(22+8*x,22+8*y,30+8*x,30+8*y,1); } } void TotalRefreshLCD(u8 Data[][22],u8 dir) { u8 t,s; SetPaintMode(1,COLOR_Black); if(dir==0) Rectangle(30,22,110,190,1); else Rectangle(22,32,232,132,1); for(t=1;t<11;t++) { for(s=2;s<21;s++) { if(Data[t][s]==1) ControlBox(t,s,1,dir); } } }
五. 总结
本科一年级时候写过一个俄罗斯方块,代码冗长可读性极差,各种判断和奇怪的变量,最后还有一堆BUG。 这套代码算是对当时愚蠢的我的补偿吧,其实写的也不怎么地。比如,画布大小必须是22*12么?为什么不可变?
蛮想把我开发的这套系统拿出来给大家分享的,可惜自制硬件就是这样:给自己带来了快乐,给别人带来了开发和移植的麻烦。
说多了,呵呵