不可以转方向的俄罗斯方块经典版

当前位置: &
123456789101112131415161718页
相关标签词“俄罗斯方块”游戏设计电子科技大学软件学院03级02班 周银辉&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& 转载请注明出处
在这之前:在这之前对“俄罗斯方块”游戏的编写尝试过两次,很不幸的是,都不理想了,原因是算法设计的不合理导致制作到中期的时候就导致某些实现显得异常复杂而夭折了,这也从侧面反映了开发方法的不合理,没有将“设计”与“生产”分开。这是第三次尝试,一个比较成功的设计,当然其也是建立在前两次失败的基础之上的。但其精彩的算法与漂亮的程序结构足以让人兴奋了。
平台说明:开发平台&&& ms.net 2003 & msWindowsXPprofessional_sp2开发语言&&& C# 1.0开发日期&&&
特殊名词说明:3.1,象素坐标(Pixel Coordinate):以显示区域所在控件的Client Rectangle的左上角为坐标原点,一个象素点为单位1的坐标
3.2,网格坐标(Gird Coordinate):如果我们将显示区域分成m×n 的网格,那么其中某一个网格所在(列,行)组成的坐标,我们称之为网格坐标,在程序中网格坐标相关项以Gird或g或G开头
3.3,块(Block):一个小格子(比如游戏中的方块图形是由小格子组成的)称为一个块,将由块来组成游戏图形
3.4,形状(Shape):游戏中的由四个小格子组成的图形称为形状
3.5,基础点(Base Point):用于指示目前形状所在位置的一个(列,行)坐标点,以网格坐标为坐标。后面将详细说明其来历。
3.6,动态方块(Dynamic Block):用于构成运动的形状的Block,共4个
设计思想:4.1,屏幕的组成:&&&& 0 ⒈⒉⒊⒋⒌⒍⒎⒏⒐& 0& □□□□□□□□□□& 1& □□□□□□□□□□& 2& □□□□□□□□□□& --------------&&&&&&&&&&&&&&&&&&&&& & 3& □□□□□□□□□□& 4& □□□□□□□□□□& 5& □□□□□□□□□□& 6& □□□□□□□□□□& 7& □□□□□□□□□□& 8& □□□□□□□□□□& 9& □□□□□□□□□□&10& □□□□□□□□□□&11& □□□□□□□□□□&12& □□□□□□□□□□&13& □□□□□□□□□□&14& □□□□□□□□□□&15& □□□□□□□□□□&16& □□□□□□□□□□&17& □□□□□□□□□□&18& □□□□□□□□□□&19& □□□□□□□□□□&20& □□□□□□□□□□&21& □□□□□□□□□□&22& □□□□□□□□□□--------------&23& ■■■■■■■■■■屏幕由23行10列的网格组成;其中0~2行:初始的形状将在这里形成然后下落,这三行用户不可见;3~22行:游戏显示区域;23行,其标记已到屏幕底部。
4.2,形状的组成:& 每一种形状都是由四个方块组成,比如■■■■由四个方块横向排列而成
4.3,形状的统一:& ■■■■等共19种形状(旋转前后的形状归为不同的形状),虽然在玩游戏时我们会去将各种不同的形状命不同的命(比如“条子”,“方块”等),但在设计游戏是它们却是统一的,它们都是“形状”。这一点是游戏成功的基础。&&&& 为了使各种不同的形状达到统一的设计,我设计了如下解决方案:将形状始终放在4×4的格子中,以该4×4格子的第一个格子为“基础点”,只要给出组成形状的四个块相对于该基础点的相对坐标,那么在基础点网格坐标的基础上就能求出各块的网格坐标。★□□□&& ★为基础点,形状各块的相对坐标是相对于这个基础点的□□□□□□□□□□□□那么■■■■在其中就如图:其四个方块相对于基础点的网格坐标就为(0,2)(1,2)(2,2)(3,2)□□□□ □□□□■■■■& □□□□& 假设基础点的网格坐标是(gX, gY),那么此形状的坐标就为(gX+0,gY+2), (gX+1,gY+2), (gX+3,gY+2), (gX+3,gY+2)我们将用一个int[8]记录下这四个相对坐标值(呵呵,用byte[8]就可以了哈)同理:□□□□□□□□■■□□■■□□& 这样,我们只要知道某个形状的相对坐标值数组,就可以轻松地求出它的各方块的排列方式,也就是其形状(样子)
4.4,移动与旋转的统一&& 从上面我们可以看出形状的移动可以这样来实现: 移动基础点的网格坐标,然后组成形状的四个方块按照其与基础点坐标的相对值而改变网格坐标,则表现为移动。&& 旋转与移动的原理一样:设旋转前的形状为A,旋转后的形状为B,组成形状A的四个方块按照B(而不是按照A)的相对于基础点坐标的相对值而改变网格坐标,则表现为旋转。比如,□□□□ □□□□■■■■& □□□□& 移动: 设其基础点网格坐标为(gX,gY),其各方块当前坐标(gX+0,gY+2), (gX+1,gY+2), (gX+3,gY+2), (gX+3,gY+2)。如果其向左移动一格,那么它的基础了坐标gX-=1; gY=gY; 其各方块移动后坐标 (gX+0,gY+2), (gX+1,gY+2), (gX+3,gY+2), (gX+3,gY+2)。旋转:设其基础点网格坐标为(gX,gY),其各方块当前坐标(gX+0,gY+2), (gX+1,gY+2), (gX+3,gY+2), (gX+3,gY+2)。如果其旋转一次,旋转后的形状如图□■□□ □■□□& □■□□□■□□那么其旋转后的各方块坐标 (gX+1,gY+0), (gX+1,gY+1), (gX+1,gY+2), (gX+1,gY+3)
如果我们将各形状编号,比如■■■■编号0,■ 编号1&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ■&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ■&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ■那么0旋转目标为1,1的旋转目标为0所以所有形状便得到了统一,如图:&&&& 形状编号_相对坐标_旋转后的形状编号 □□□□ □□□□■■■■& 0_□□□□& □■□□ □■□□& 1_□■□□□■□□
□□□□■□□□■□□□& 2_■■□□&
□□□□□□□□■■■□& 3_■□□□
□□□□■■□□& □■□□& 4_□■□□
□□□□□□■□■■■□& 5_□□□□
□□□□□□□□■■□□■■□□& 6_
□□□□□■□□■■□□& 7_■□□□&&&
□□□□■■□□□■■□& 8_□□□□
□□□□□■□□□■□□& 9_■■□□&
□□□□■□□□■■■□& 10_□□□□
□□□□□■■□□■□□& 11_□■□□
□□□□□□□□■■■□& 12_□□■□
□□□□■□□□■■□□& 13_□■□□
□□□□□■■□& 14_■■□□□□□□
□□□□□■□□■■■□& 15_□□□□
□□□□□■□□□■■□& 16_□■□□
□□□□□□□□■■■□& 17_□■□□&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
□□□□■□□□■■□□& 18_& □■□□
数据结构5.1,网格坐标:并没有提供这样一个类或结构,但提供了网格坐标与象素坐标的转换公式:&& pCoordinate = gCoordinate*(widthOfGird+distanceOfGird);&& 象素坐标值 = 网格坐标值×(网格边长+网格间距)
5.2,网格点(GirdPoint):一个结构,它表示一个以网格坐标为坐标的点public struct GirdPoint{&&public int X;&&public int Y;&&&&&&& //…各种构造函数}
5.3,块(Block):继承于System.Windows.Forms.Label,用它来生成组成形状的块对象public class Block: System.Windows.Forms.Label{&&private GirdPoint gL// 网格坐标&&//获取或设置 网格坐标值(若设置,那么其象素坐标会自动变化为相应值)&&&&&&&& public GirdPoint GLocation&&&& {&&&get&&&{&&&&return this.gL&&&}&&&set&&&{&&&&this.gLocation =&&&&&&&&&&&&&&&&&& //其中Globals.ToPcoordinate()是一个将网格坐标值转换成象素坐标值的函数&&&&this.Location = new Point( Globals.ToPCoordinate(this.gLocation.X),&&&&&Globals.ToPCoordinate(this.gLocation.Y));&&&}&&}&&&&&&& //……(各种构造函数)}对块位置的操作只需要指定其网格坐标,其象素坐标会自动变化为相应值
5.4,形状(Shape):一个结构,由它保存游戏中的形状的相关信息public struct Shape{&& //获取或设置该形状的索引值&& public int I&& //获取或设置一个数组,其为该形状的相对于基础点的相对网格坐标组&& public int[] DC& // 获取或设置该形状旋转后的新形状的索引&& public int EddiedI&& public Shape(int index, int[] dCoordinates, int eddiedIndex)&& {&&&&&& this.Index =&&&&&& this.DCoordinates = dC&&&&& this.EddiedIndex = eddiedI}
5.5,背景方块数组(BackBlocks):一个Block[,],其维持着23行10列的Block对象,这些对象将作为屏幕的背景,当下落的形状下落到底部时,形状所在位置的背景方块将被点亮,这表现为下落的形状被固定住了
5.6,网格数组(GirdArray):一个int[,],其维持着整个屏幕的背景方块的状态,当该数组中的某个元素值由0变为1时,表示在屏幕中的该位置被某个方块占据,当由1变为0是,表示在屏幕中的该位置被释放而未被占据。被占据表现为屏幕上该位置已经有方块了。
5.7,动态方块数组(DynamicBlocksArray):一个Block[4],其维持着四个Block对象,游戏中的任何运动的形状都是由这四个Block对象变化位置而得到的
5.8,预览网格数组(ViewGirdArray):一个int[,],于GirdArray一样,只不过它维持着屏幕旁边的用于预览下一个即将出项的形状的小屏幕
5.9,预览方块数组(ViewDynamicBlocksArray):一个Block[4],其维持着四个Block对象,游戏中屏幕普遍的小屏幕显示了下一个即将出现的形状,而这个预览形状便是由这四个Block对象变化位置而得到的
5.10,Shapes:一个Shape[19],它维持着游戏中的19个形状&&& public static readonly Shape[] Shapes = new Shape[19]&&&{&&&&new Shape(0,new int[8]{0,2,1,2,2,2,3,2},1),&&&&new Shape(1,new int[8]{1,0,1,1,1,2,1,3},0),&&&&new Shape(2,new int[8]{0,0,0,1,0,2,1,2},3),&&&&new Shape(3,new int[8]{0,2,1,2,2,2,0,3},4),&&&&new Shape(4,new int[8]{0,1,1,1,1,2,1,3},5),&&&&new Shape(5,new int[8]{2,1,0,2,1,2,2,2},2),&&&&new Shape(6,new int[8]{0,2,1,2,0,3,1,3},6),&&&&new Shape(7,new int[8]{1,1,0,2,1,2,0,3},8),&&&&new Shape(8,new int[8]{0,1,1,1,1,2,2,2},7),&&&&new Shape(9,new int[8]{1,1,1,2,0,3,1,3},10),&&&&new Shape(10,new int[8]{0,1,0,2,1,2,2,2},11),&&&&new Shape(11,new int[8]{1,1,2,1,1,2,1,3},12),&&&&new Shape(12,new int[8]{0,2,1,2,2,2,2,3},9),&&&&new Shape(13,new int[8]{0,1,0,2,1,2,1,3},14),&&&&new Shape(14,new int[8]{1,1,2,1,0,2,1,2},13),&&&&new Shape(15,new int[8]{1,1,0,2,1,2,2,2},16),&&&&new Shape(16,new int[8]{1,1,1,2,2,2,1,3},17),&&&&new Shape(17,new int[8]{0,2,1,2,2,2,1,3},18),&&&&&&&&&& new Shape(18,new int[8]{1,1,0,2,1,2,1,3},15),&&&&&&&&&&&&& }
功能的实现:6.1,形状的生成:随机生成形状编号,并根据该编号生成相应的形状&& 设形状编号为indexOfShape,组成形状的四个动态方块已存在DynamicBlockArray中,基础点为BasePoint,所有形状已存在Shapes中void AssembleShape(int indexOfShape){&//重新安排四个动态块的位置以形成新形状&& DynamicBlocksArray[0].GLocation = new GirdPoint(&&&&&&&&&&&&&&&&& Shapes[indexOfShape].DCoordinates[0] + BasePoint.X,&&&&&&&&&&&&&& Shapes[indexOfShape].DCoordinates[1]+ BasePoint.Y);
&& DynamicBlocksArray[1].GLocation = new GirdPoint(&&&&Shapes[indexOfShape].DCoordinates[2] + asePoint.X,&&&&&&&&&&&&&& Shapes[indexOfShape].DCoordinates[3]+ BasePoint.Y);
&& DynamicBlocksArray[2].GLocation = new GirdPoint(&&&&&&&&&&&&& Shapes[indexOfShape].DCoordinates[4] + BasePoint.X,&&&&&&&&&&&&& Shapes[indexOfShape].DCoordinates[5] + BasePoint.Y);
&& DynamicBlocksArray[3].GLocation = new GirdPoint(&&&&&& Shapes[indexOfShape].DCoordinates[6] + BasePoint.X,&&&&&&& Shapes[indexOfShape].DCoordinates[7] + BasePoint.Y);}
6.2,形状的移动,先试探能否向指定方向移动如果能,那么移动,否则不移动。试探,由于左右下三个方向移动的本质是一样的,所以它们可以由统一的函数来实现。移动,则是通过移动基础点位置,然后根据基础点位置重新安排一下四个动态方块的位置来实现,即,移动基础点,然后调用一次上面的AssembleShape函数。&&&& 试探:设当前形状编号为indexOfShape,arg参数指定要试探的方向 ’L’,’R’,’D’ 分别为左,右,下;基础点为BascPoint,所有形状已存再Shapes中,CountOfTier为屏幕网格的列数。取得其试探位置的网格坐标,如果该位置已经超出屏幕或该位置已经被其他方块占据,则试探的位置不可达,如果组成形状的四个方块中有一个方块的试探位置不可达,那么试探函数返回false,说明不可向指定方向移动private static bool canMove(int indexOfShape, char arg){&try&{&&&& GirdPoint tempBaseP
&&&& switch(arg)&&{&&&case 'L':&&&&&& tempBasePoint = new GirdPoint(BasePoint.X-1,BasePoint.Y);&&&&&&&&case 'R':&&&&&& tempBasePoint = new GirdPoint(BasePoint.X+1, BasePoint.Y);&&&&&&&&case 'D':&&&&tempBasePoint = new GirdPoint(BasePoint.X, BasePoint.Y+1);&&&&&&&&&&&&&& case 'E'://不移动,用于判断能否旋转,&&&&&&&&&&&&&&&&&&&&&& //与判断移动不同的是传入的indexOfShape,&&&&&&&&&&&&&&&&&&&&&& //判断旋转用的是旋转后的形状的索引,而移动用的是当前的&&&&&&&&&&&&&&&&&&& tempBasePoint = BaseP&&&&&&&&&&&&&&&&&&&&default:&&&&MessageBox.Show("错误的参数"+arg+"\n应该为'L'或'R'或'D'");&&&&&&}&&&&&&int gX0 = Shapes[indexOfShape].DCoordinates[0]+tempBasePoint.X;&&int gY0 = Shapes[indexOfShape].DCoordinates[1]+tempBasePoint.Y;&&int i =& GirdArray[gY0,gX0];&&&&&&&&&&&& &&int gX1 = Shapes[indexOfShape].DCoordinates[2]+tempBasePoint.X;&&int gY1 = Shapes[indexOfShape].DCoordinates[3]+tempBasePoint.Y;&&int j = GirdArray[gY1,gX1];
&&int gX2 = Shapes[indexOfShape].DCoordinates[4]+tempBasePoint.X;&&int gY2 = Shapes[indexOfShape].DCoordinates[5]+tempBasePoint.Y;&&int m = GirdArray[gY2,gX2];
&&int gX3 = Shapes[indexOfShape].DCoordinates[6]+tempBasePoint.X;&&int gY3 = Shapes[indexOfShape].DCoordinates[7]+tempBasePoint.Y;&&int n = GirdArray[gY3,gX3];
&&//i,j,m,n为其即将到达的新位置的网格值,若为1,说明该网格已被占据&&&&if(gX0&0 || gX0&= CountOfTier ||& i == 1 ||&&&&&& gX1&0 || gX1&= CountOfTier || j == 1 ||&&&gX2&0 || gX2&=& CountOfTier || m == 1 ||&&&gX3&0 || gX3&= CountOfTier || n == 1)&&{&&&&&}&&&&& }&&& catch&&& {&&&&&&& }
&&&}&&&& 移动:移动基础点,并重新组成形状,比如左移,void ToLeft(int indexOfShape){&&& if(canMove(indexOfShape,’L’)&&& {&&BasePoint = new GirdPoint(BasePoint.X-1, BasePoint.Y);&&AssembleShape(indexOfShape);&&& }}
6.3,自动下落,设一个计时器timer,其每隔一定时间调用一次ToDown函数void ToDown(int indexOfShape){&&& if(canMove(indexOfShape,’D’)&&& {&&BasePoint = new GirdPoint(BasePoint.X, BasePoint.Y+1);&&AssembleShape(indexOfShape);&&& }&&& else&&& {&&&&&&& //其他,比如固定下落的形状,判断得分,判断游戏是否已经结束等等&&& }}
6.4,下落形状的固定,当试探到不能下落时,就固定住当前形状。其实组成形状的四个块并没有被固定住,而是当它们当前位置对应的四个背景方块被点亮后,它们离开了当前位置而到屏幕的顶部(0~2行,不可见)组合新形状去了。对应的四个背景方块被点亮表现为形状被固定void FixShape(){&for(int i=0; i&4; i++)&{&&&&&&& //将该位置的背景方块点亮(变为指定的颜色,假设为ColorOfFixedBlock)&&&&&& BackBlocks[DynamicBlocksArray[i].GLocation.Y,& &&&&&&&&&&&&&&&& DynamicBlocksArray[i].GLocation.X].BackColor = ColorOfFixedB &&&&&&& //将对应的网格值设置为1,表示此网格已经被占据&&&&&& GirdArray[DynamicBlocksArray[i].GLocation.Y, DynamicBlocksArray[i].GLocation.X] = 1&&&& }&&& //其他,比如判断是否应该消行加分等&&& //将基础点移动到顶部,并产生新形状等等}
6.5,旋转,与移动的原理一样。先试探能否旋转,如果能的话,取得旋转后的形状的索引,并按照此索引调用函数AssembleShape()。&&& 试探能否旋转:很巧妙地运用了试探能否移动的CanMove函数。与移动不同的是,试探能否旋转传入的是旋转后的形状的索引bool CanEddy(indexOfShape)&&& {&&&&&& int eddiedIndex = AllShapes.Shapes[indexOfShape].EddiedI//旋转后的新形状的索引&&&&&& return canMove(eddiedIndex,’E’);&&& }&& 旋转:很巧妙地运用了移动时的AssembleShape()&& void Eddy(indexOfShape)&& {&&&&&& if(CanEddy(indexOfShape)&&&&&& {&&&&&&&&& int eddiedIndex = AllShapes.Shapes[indexOfShape].EddiedI//旋转后的新形状的索引&&&&&&&&& AssembleShape(eddiedIndex);&&&&&& }&& }这是激动人心的,程序巧妙地将左移、右移、下移、旋转高度地统一了起来,它们的实质都是调用CanMove()函数进行判断,然后再调用AssembelShape()函数移动四个动态方块的位置,而表现出各种效果。多么棒的设计啊(呵呵,自我欣赏了)
6.6,消行,当形状被固定后需要判断是否有满行,如果有,则消去已满的行&&& 扫描是否存在满行:没有必要扫描整个屏幕,而只需要扫描当前被固定的形状的基础点一下的四行(但不能超过第22行,因为第23行始终是满的)。对于某一行而言,如果该行的GirdArray对应值均为1,说明该行已被全部占据(或者说,如果该行的GirdArray对应值至少有一个为1,说明该行没有被全部占据),那么应该消去该行&&& void ScanFullLines(int lineStart,int lineEnd)&&& {&&&&&&&&& int countOfFullLine = 0;//记录此次消除的行数,以便加分
&&&&&& for(int i= lineS i&=lineE i++)&&&&&& {&&&bool isFull =//指示是否已被填满&&&for(int j=0; j&CountOfT j++)//countOfTier:屏幕列数&&&{&&&&if(GirdArray[i,j]==0)&&&&{&&&&&isFull =&&&&}&&&}
&&&if(isFull)&&&{&&&&countOfFullLine++;&&&&DelLine(i);//消去第i行&&&&&&&&& }&&&&&&switch(countOfFullLine)&&&&&&&&&&&&& {&&&&&&&&&&&&&&&& //根据所消行数加分&&&&&&&&&&&&& }&&&&&& 消去指定行:将该行上的背景方块熄灭(与点亮相反,变为另一种颜色,设为ColorOfBackBlock),并将该行以上的所有方块都下移一格。设背景方块数组为BackBlocks,网格数组为GirdArrayvoid DelLine(int indexOfLine){ &//熄灭该行上的背景块,表现为移出了该行上的块&for(int i=0; i&CountOfT i++)&{&&&&&&&& //熄灭对应的背景方块&&BackBlocks[indexOfLine,i].BackColor = ColorOfBackB&&//释放被占据的网格&&GirdArray[indexOfLine,i] = 0;&&&& }&//该行以上的所有行整体下落&for(int i=indexOfLine-1;i&=3; i--)&{&&for(int j=0; j&CountOfT j++)&&{&&&if(GirdArray[i,j] == 1)&&&{&&&&//此块熄灭,其正下方的块点亮,表现为下落了一格&&&&GirdArray[i,j] = 0;&&&&BackBlocks[i,j].BackColor =.ColorOfBackB&&&&GirdArray[i+1,j] = 1;&&&&BackBlocks[i+1,j].BackColor = ColorOfFixedB&&&}&&& }&&& }& }
6.7 游戏存档,当用户欲保存当前游戏以供以后继续时,使用这个功能。需要保存的数据有:目前的速度级别,目前的分数,目前的已屏幕上已被固定的各形状。存档文件仅需要一百多个字节就可以了,是这样做的:第一行写入游戏速度级别,第二行写入游戏分数,从第三行开始写入游戏中网格数组(GirdArray),网格数组中为1的对应的背景方块数组的方块即是被固定的(点亮的)。bool SaveGame(string fileName){&try&{&&&&&&&&& StreamWriter sWriter = &&&&&&&&&&&&&&&&& new StreamWriter(fileName,false,System.Text.Encoding.Default, 100);
&&//写入速度级别&&sWriter.WriteLine(SpeedLevel.ToString());
&&//写入分数&&sWriter.WriteLine(Score.ToString());
&&//写入背景网格状态&&for(int i=0; i&CountOfR i++)&&{&&&for(int j=0; j& CountOfT j++)&&&{&&&&sWriter.Write(GirdArray[i,j].ToString());&&&}&&}
&&sWriter.Close();&}&catch(Exception ex)&{&&MessageBox.Show("未能保存!\n原因是:\n"+ex.Message);&&&}
&&&}比如我的一个游戏存档如下:115001111
6.8 加载游戏存档,将游戏存档读出,并将游戏中的各变量更改为存档中的值就可以了。&&&&&& bool LoadGame(string fileName)&&&&&& {&&&try&&&{&&&&StreamReader sReader =&&&&&new StreamReader(fileName,System.Text.Encoding.Default,false,100);
&&&&&&&&&&&&&&&&& //加载游戏速度&&&&string strSpeedLevel = sReader.ReadLine();&&&&Level = int.Parse(strSpeedLevel));
&&&&//加载游戏分数&&&&string strScore = sReader.ReadLine();&&&&Score = int.Parse(strScore);&&&&&&&&//加载游戏网格状态&&&&char[] buffer = new char[1];&&&&for(int i=0; i&CountOfR i++)&&&&{&&&&&for(int j=0; j&CountOfT j++)&&&&&{&&&&&&sReader.Read(buffer,0,1);&&&&&&GirdArray[i,j] = int.Parse(buffer[0].ToString());&&&&&&&&&&&&&&&&&&&&&&&&&& //如果对应值为1,则点亮对应的背景网格&&&&&&if(GirdArray[i,j]==1)&&&&&&{&&&&&&&BackBlocks[i,j].BackColor = ColorOfFixedB&&&&&&}&&&&&&else&&&&&&{&&&&&&&BackBlocks[i,j].BackColor = ColorOfBackB&&&&&&}&&&&&}&&&&}
&&&&sReader.Close();&&&}&&&catch(Exception ex)&&&{&&&&MessageBox.Show("加载游戏失败\n原因是:\n"+ex.Message);&&&&&&&}
阅读(...) 评论() &12330人阅读
&&&&& 根据上节我们规划好的开发过程,这节我们来实现一个简单的俄罗斯方块程序。当然完成俄罗斯方块的Demo是我们这节课的最终目标。在实现这个目标的同时,我还要跟大家一起去分析俄罗斯方块的需求、算法、具体开发的步骤。并且最终选择C#语言来实现这个demo。下面废话少说,翠花,上内容!
第一步、&& 思考俄罗斯方块的游戏规则:
&&&&& 任何时候我们去开发或者设计一个项目,首先需要思考的就是需求。
&&&&& 俄罗斯方块中,我们要做一个俄罗斯方块的游戏,那么俄罗斯方块游戏的游戏规则就是我们的需求。所以我们必须深入了解俄罗斯方块游戏的游戏规则。俄罗斯方块游戏可能产生下面五种形状的方块。这些方块有我们的方向键的控制可以产生旋转,左右移动和加速前进等,往下掉,只到遇到下面已经有方块挡住停下来。如果一行完全排列满,则会消掉,如果排列到了顶部,则会失败。
第二步、把我们的&需求&从计算机的角度去分析:
&&&& 我们对任何一个项目的最初的需求是从使用者的角度去描述这个项目的,这种描述基本上涵盖了使用者可能用到的功能和要求。我们在做一个项目,特别是为一个非计算机专业做一个项目的时候,客户的描述,或者业务人员承接到项目的时候,他们和客户的沟通,基本上是基于这种初级的需求的,这种初级的需求的有点是:客户和业务人员比较容易理解,沟通起来非常的方便。但是他的缺点是非计算机专业用语,不够规范和严谨。不能拿这个文档和需求来直接要求开发人员。需要一个对市场(客户)比较了解,同时又是资深的程序员把这个需求编程一个专业开发人员相对比较容易理解的需求。甚至直接编程开发的具体思路。然后有开发人员去实现。我们的小游戏实在是太小了,不可能分出来业务人员、项目规划人员、项目架构、程序开发。。。。所以这一切也都有我们开发者一个人独揽了。不过麻雀虽小,五脏俱全哦。我们不是说了嘛,通过学习俄罗斯方块掌握项目开发的经验呢。所以希望各位看客认真的看这些个步骤,如果我哪里说错了,欢迎拍砖。
&&&& a、关于俄罗斯方块游戏中方块的产生。我们假设产生四种方块。实际上俄罗斯方块产生的方块要多余四个。但是咱们这节课是做一个Demo,所以先做四个,具体的四个请见图片1。(因为CSDN不能上传图片,我上传到下载中去了。大家可以下载使用,期待csdn可以正常的上传图片)。
&&& b、俄罗斯方块当需要变化方块的时候,每个方块需要顺时针旋转90&,具体可能产生的形状也见图一。
从数学,或者计算机的角度来看:我们根据图1来看,我们可以把所有的方块看成一个4*4的二维数组。把有&砖&的地方表示为1,没有&砖&的地方表示为0,所以所有的的方块都可以表示为图二的样式。
&&& c、我们也可以把背景看成是14*20的二维数组。
&&&&&& 那么俄罗斯方块的需求,站在程序员的角度上就可以变成:我们随机从方块的4个4*4的矩阵中挑选出来一个,并且随机的挑选一个他的初始化状态(关于状态变化,我们同样可以把他们表示在一个4*4的矩阵中)。然后这个被挑选的矩阵,在一个14*20的矩阵中不断的按照一种速度进行往下运动。我们同时可以使用方向键,对这个举证进行控制,使得它可以左右运动,同时可以循环的变化他的状态。如果这个矩阵在运动的方向上遇到了数值为1的时候则停止运动,在左右运动上表现为不能移动,在往下运动的时候则表现为这个方块运动的结束。把这个矩阵的数值复制到背景矩阵中去。这个时候检查背景矩阵,如果背景矩阵中有一个行全部为1,那么在y轴上比该行小的所有行向下移动一行,用户得分增加100。同理,检查所有的行,并且做同样动作。检查完成后,进入下个方块的随机挑选,下落。当某个方块下落完成的时候。他的y坐标在背景中为0的时候。游戏结束。作为游戏的界面,我们需要在游戏的状态发生改变的时候,把背景矩阵和运动矩阵都绘制出来。数值为0的地方不绘图,数值为1的地方绘制图片。
---------------------------------------------------------------------------
到此为止。我们的游戏已经用人类语言。从计算机的角度来说分析完成了。剩下的就是使用计算机语言表达出来了,不管你是学习的C#,C语言,C++,Delphi,Java,python,Js,VB,&&(汇编我不懂,暂时不包括吧)。只要你能理解了上述的&思路&。那么你可以用任何语言来写出来。我真希望用所有的语言都给大家写写,把IT老祖宗的那句:&编程靠的是思想(想法,思路),语言不重要。"的真理来验证下,可怜CSDN的blog写起来太麻烦了,而且我老婆催着我赶紧回家过年的,所以这次就暂时使用C#来给大家实现下。我们就从一个C#盲来开始看看需要如何学习,如何实现。
&&&&&& 首先、确定我们要用的语言是C#,矩阵的运算需要多是多维数组,数组的运算多需要循环。另外对多种情况的判断需要if,switch等分支判断语句,我们的方块定时运动,需要用到timer控件。所以在开始咱们的项目前,请确保,你已经对C#的语法有了基本的了解,尤其对数组的操作、if语句,for循环语句,switch语句要会用。同时对基本的button、timer控件进行基本的了解。如果你在使用其他语言编程,找你所使用的语言中的类似的东西。其他的东西至少这个项目中不会经常用到。即时用到,查查即可。这个可能就是所谓的&语言没有思想重要吧&。如果你还不懂,去看看&C#入门经典吧&.
&&&&&&& 其次、俄罗斯方块需要绘制图形,这个需要你对C# GDI+有所了解。在其他语言中可能是GDI或者API了,这个你只需要google下即可。如果GDI+编程你还不懂,那么来下载个教程吧。我已经给大家上传到:
&&&&&& 上面的两个条件如果你已经具有了。那么开始我们的C#俄罗斯方块的代码之旅途吧。
&&&&&& 不过在代码开始之前,还是先看看我们的界面吧。请看界面图;
&&&&&& 下面我们来看代码。首先定义我们的砖块和背景吧。
#region 定义砖块int[i,j,y,x] Tricks:i为那块砖,j为状态,y为列,x为行
private int[, , ,] Tricks = {{
{1,0,0,0},
{1,0,0,0},
{1,0,0,0},
{1,1,1,1},
{0,0,0,0},
{0,0,0,0},
{1,0,0,0},
{1,0,0,0},
{1,0,0,0},
{1,1,1,1},
{0,0,0,0},
{0,0,0,0},
{1,1,0,0},
{1,1,0,0},
{0,0,0,0},
{1,1,0,0},
{1,1,0,0},
{0,0,0,0},
{1,1,0,0},
{1,1,0,0},
{0,0,0,0},
{1,1,0,0},
{1,1,0,0},
{0,0,0,0},
{1,0,0,0},
{1,1,0,0},
{0,1,0,0},
{0,1,1,0},
{1,1,0,0},
{0,0,0,0},
{1,0,0,0},
{1,1,0,0},
{0,1,0,0},
{0,1,1,0},
{1,1,0,0},
{0,0,0,0},
{1,1,0,0},
{0,1,0,0},
{0,1,0,0},
{0,0,1,0},
{1,1,1,0},
{0,0,0,0},
{1,0,0,0},
{1,0,0,0},
{1,1,0,0},
{1,1,1,0},
{1,0,0,0},
{0,0,0,0},
#endregion
#region 定义背景
private int[,] bgGraoud ={
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0}
#endregion
砖块定义好了,我们还需要定义几个系统在变化的时候需要的全局变量。我在注视中写清楚了,不絮叨了。
private int[,] CurrentTrick = new int[4, 4]; //当前的砖块
//CurrentTrickNum当前砖块的数目, CurrentStatusNum当前状态, CurrentX当前x, CurrentY当前y, Sorce分数
private int CurrentTrickNum, CurrentStatusNum, CurrentX, CurrentY, S
private int TricksNum = 4;
private int StatusNum = 4;
private Image myI
private Random rand = new Random();
定义好了变量,我们来想想我们的几个函数吧。首先是变化砖块,变化砖块其实就是变化砖块的状态,把砖块数组中的状态位进行循环变化即可:
/// &summary&
/// 变化方块
/// &/summary&
private void ChangeTricks()
if (CurrentStatusNum & 3)
CurrentStatusNum++;
CurrentStatusNum = 0;
for (int y = 0; y & 4; y++)
for (int x = 0; x & 4; x++)
CurrentTrick[y, x] = Tricks[CurrentTrickNum, CurrentStatusNum, y, x];
再接着是检测砖块是否可以向下移动、向左移动、向右移动。思路很简单。看看他的要运动的方向的背景下个位置是不是为1.如果不是。那么就返回true.否则是false:
/// &summary&
/// 检测是否可以向下了
/// &/summary&
/// &returns&&/returns&
private bool CheckIsDown()
for (int y = 0; y & 4; y++)
for (int x = 0; x & 4; x++)
if (CurrentTrick[y, x] == 1)
//超过了背景
if (y + CurrentY + 1 &= 20)
if (x + CurrentX &= 14)
CurrentX = 13 -
if (bgGraoud[y + CurrentY + 1, x + CurrentX] == 1)
/// &summary&
/// 检测是否可以左移
/// &/summary&
/// &returns&&/returns&
private bool CheckIsLeft()
for (int y = 0; y & 4; y++)
for (int x = 0; x & 4; x++)
if (CurrentTrick[y, x] == 1)
if (x + CurrentX - 1 & 0)
if (bgGraoud[y + CurrentY, x + CurrentX - 1] == 1)
/// &summary&
/// 检测是否可以右移
/// &/summary&
/// &returns&&/returns&
private bool CheckIsRight()
for (int y = 0; y & 4; y++)
for (int x = 0; x & 4; x++)
if (CurrentTrick[y, x] == 1)
if (x + CurrentX + 1 &= 14)
if (bgGraoud[y + CurrentY, x + CurrentX+1] == 1)
下面是绘制函数:没啥思路,循环画出即可
&private void Draw()
Graphics g = Graphics.FromImage(myImage);
g.Clear(this.BackColor);
for (int bgy = 0; bgy & 20; bgy++)
for (int bgx = 0; bgx & 14; bgx++)
if (bgGraoud[bgy, bgx] == 1)
g.FillRectangle(new SolidBrush(Color.Blue), bgx * 20, bgy * 20, 20, 20);
//绘制当前的图片
for (int y = 0; y & 4; y++)
for (int x = 0; x & 4; x++)
if (CurrentTrick[y, x] == 1)
g.FillRectangle(new SolidBrush(Color.Blue), (x + CurrentX) * 20, (y + CurrentY) * 20, 20, 20);
Graphics gg = panel1.CreateGraphics();
gg.DrawImage(myImage, 0, 0);
下面的一个函数是比较重要的。向下运动函数。因为基本上向下运动的函数决定了下落的位置:基本思路其实很简单的。先检测是否可以向下下落,如果可以下落就下落了,如果不可以下落,那说明到底了。首先检测当前的坐标是不是为0,如果是,那么说明游戏结束了。否则。检测下。是否有满行的。如果有的,行平移然后并且可以增加积分,这一切完成以后,然后开始下个方块。
/// &summary&
/// 下落方块
/// &/summary&
private void DownTricks()
if (CheckIsDown())
CurrentY++;
if (CurrentY == 0)
timer1.Stop();
MessageBox.Show("哈哈,你玩玩了");
//下落完成,修改背景
for (int y = 0; y & 4; y++)
for (int x = 0; x & 4; x++)
if (CurrentTrick[y, x] == 1)
bgGraoud[CurrentY + y, CurrentX + x] = CurrentTrick[y, x];
CheckSore();
BeginTricks();
好像忘记了如何开始向下落的哦。看代码:太容易了,不解释了。
private void BeginTricks()
//随机生成砖码和状态码
int i = rand.Next(0, TricksNum);
int j = rand.Next(0, StatusNum);
CurrentTrickNum =
CurrentStatusNum =
//分配数组
for (int y = 0; y & 4; y++)
for (int x = 0; x & 4; x++)
CurrentTrick[y,x] = Tricks[i,j,y,x];
CurrentX = 0;
CurrentY = 0;
timer1.Start();
下面我们看看是如何检测方块的背景中是否有满行的,如果满行了把他上面的平移下去。这个是很多初学者感觉到难以解决的地方,其实就是循环。这个循环还有得优化,但是我想想还是把他放在下一个篇的优化篇上讲解吧。看代码,我们说思路。其实思路很简单的,循环看看是否有满行的,如果存在一个为0,说明没满行,继续下个循环,如果没有0,则说明满行了,增加分数,然后比这个y小的矩阵向下平移,当前的坐标=(x,y-1)即可。然后继续循环本行(因为本很是上一行又下来的)。
&private void CheckSore()
for (int y = 19; y & -1; y--)
bool isFull =
for (int x = 13; x & -1; x--)
if (bgGraoud[y, x] == 0)
if (isFull)
//增加积分
Sorce = Sorce + 100;
for (int yy = yy & 0; yy--)
for (int xx = 0; xx & 14; xx++)
int temp = bgGraoud[yy - 1, xx];
bgGraoud[yy, xx] =
label1.Text = Sorce.ToString(); ;
还有几个函数大家都可以看懂的,我就不讲解了。我把所有的代码都贴一遍。另外把所有的图片和源文件打包后上传到csdn,请大家下载。
代码下载地址:
using System.Collections.G
using System.D
using System.D
using System.L
using System.T
using System.Windows.F
namespace Tetris
public partial class Form1 : Form
public Form1()
InitializeComponent();
#region 定义砖块int[i,j,y,x] Tricks:i为那块砖,j为状态,y为列,x为行
private int[, , ,] Tricks = {{
{1,0,0,0},
{1,0,0,0},
{1,0,0,0},
{1,1,1,1},
{0,0,0,0},
{0,0,0,0},
{1,0,0,0},
{1,0,0,0},
{1,0,0,0},
{1,1,1,1},
{0,0,0,0},
{0,0,0,0},
{1,1,0,0},
{1,1,0,0},
{0,0,0,0},
{1,1,0,0},
{1,1,0,0},
{0,0,0,0},
{1,1,0,0},
{1,1,0,0},
{0,0,0,0},
{1,1,0,0},
{1,1,0,0},
{0,0,0,0},
{1,0,0,0},
{1,1,0,0},
{0,1,0,0},
{0,1,1,0},
{1,1,0,0},
{0,0,0,0},
{1,0,0,0},
{1,1,0,0},
{0,1,0,0},
{0,1,1,0},
{1,1,0,0},
{0,0,0,0},
{1,1,0,0},
{0,1,0,0},
{0,1,0,0},
{0,0,1,0},
{1,1,1,0},
{0,0,0,0},
{1,0,0,0},
{1,0,0,0},
{1,1,0,0},
{1,1,1,0},
{1,0,0,0},
{0,0,0,0},
#endregion
#region 定义背景
private int[,] bgGraoud ={
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0}
#endregion
private int[,] CurrentTrick = new int[4, 4]; //当前的砖块
//CurrentTrickNum当前砖块的数目, CurrentStatusNum当前状态, CurrentX当前x, CurrentY当前y, Sorce分数
private int CurrentTrickNum, CurrentStatusNum, CurrentX, CurrentY, S
private int TricksNum = 4;
private int StatusNum = 4;
private Image myI
private Random rand = new Random();
private void Form1_Load(object sender, EventArgs e)
myImage = new Bitmap(panel1.Width, panel1.Height);
Sorce = 0;
protected override void OnPaint(PaintEventArgs e)
base.OnPaint(e);
private void button1_Click(object sender, EventArgs e)
for (int y = 0; y & 20; y++)
for (int x = 0; x & 14; x++)
bgGraoud[y, x] = 0;
timer1.Interval = 1000;
BeginTricks();
this.Focus();
/// &summary&
/// 随机生成方块和状态
/// &/summary&
private void BeginTricks()
//随机生成砖码和状态码
int i = rand.Next(0, TricksNum);
int j = rand.Next(0, StatusNum);
CurrentTrickNum =
CurrentStatusNum =
//分配数组
for (int y = 0; y & 4; y++)
for (int x = 0; x & 4; x++)
CurrentTrick[y,x] = Tricks[i,j,y,x];
CurrentX = 0;
CurrentY = 0;
timer1.Start();
/// &summary&
/// 变化方块
/// &/summary&
private void ChangeTricks()
if (CurrentStatusNum & 3)
CurrentStatusNum++;
CurrentStatusNum = 0;
for (int y = 0; y & 4; y++)
for (int x = 0; x & 4; x++)
CurrentTrick[y, x] = Tricks[CurrentTrickNum, CurrentStatusNum, y, x];
/// &summary&
/// 下落方块
/// &/summary&
private void DownTricks()
if (CheckIsDown())
CurrentY++;
if (CurrentY == 0)
timer1.Stop();
MessageBox.Show("哈哈,你玩玩了");
//下落完成,修改背景
for (int y = 0; y & 4; y++)
for (int x = 0; x & 4; x++)
if (CurrentTrick[y, x] == 1)
bgGraoud[CurrentY + y, CurrentX + x] = CurrentTrick[y, x];
CheckSore();
BeginTricks();
/// &summary&
/// 检测是否可以向下了
/// &/summary&
/// &returns&&/returns&
private bool CheckIsDown()
for (int y = 0; y & 4; y++)
for (int x = 0; x & 4; x++)
if (CurrentTrick[y, x] == 1)
//超过了背景
if (y + CurrentY + 1 &= 20)
if (x + CurrentX &= 14)
CurrentX = 13 -
if (bgGraoud[y + CurrentY + 1, x + CurrentX] == 1)
/// &summary&
/// 检测是否可以左移
/// &/summary&
/// &returns&&/returns&
private bool CheckIsLeft()
for (int y = 0; y & 4; y++)
for (int x = 0; x & 4; x++)
if (CurrentTrick[y, x] == 1)
if (x + CurrentX - 1 & 0)
if (bgGraoud[y + CurrentY, x + CurrentX - 1] == 1)
/// &summary&
/// 检测是否可以右移
/// &/summary&
/// &returns&&/returns&
private bool CheckIsRight()
for (int y = 0; y & 4; y++)
for (int x = 0; x & 4; x++)
if (CurrentTrick[y, x] == 1)
if (x + CurrentX + 1 &= 14)
if (bgGraoud[y + CurrentY, x + CurrentX+1] == 1)
private void Draw()
Graphics g = Graphics.FromImage(myImage);
g.Clear(this.BackColor);
for (int bgy = 0; bgy & 20; bgy++)
for (int bgx = 0; bgx & 14; bgx++)
if (bgGraoud[bgy, bgx] == 1)
g.FillRectangle(new SolidBrush(Color.Blue), bgx * 20, bgy * 20, 20, 20);
//绘制当前的图片
for (int y = 0; y & 4; y++)
for (int x = 0; x & 4; x++)
if (CurrentTrick[y, x] == 1)
g.FillRectangle(new SolidBrush(Color.Blue), (x + CurrentX) * 20, (y + CurrentY) * 20, 20, 20);
Graphics gg = panel1.CreateGraphics();
gg.DrawImage(myImage, 0, 0);
private void timer1_Tick(object sender, EventArgs e)
DownTricks();
private void Form1_KeyDown(object sender, KeyEventArgs e)
if (e.KeyCode == Keys.W)
ChangeTricks();
else if (e.KeyCode == Keys.A)
if (CheckIsLeft())
CurrentX--;
else if (e.KeyCode == Keys.D)
if (CheckIsRight())
CurrentX++;
else if (e.KeyCode == Keys.S)
timer1.Stop();
timer1.Interval = 10;
timer1.Start();
private void Form1_KeyUp(object sender, KeyEventArgs e)
if (e.KeyCode == Keys.S)
timer1.Stop();
timer1.Interval = 1000;
timer1.Start();
private void CheckSore()
for (int y = 19; y & -1; y--)
bool isFull =
for (int x = 13; x & -1; x--)
if (bgGraoud[y, x] == 0)
if (isFull)
//增加积分
Sorce = Sorce + 100;
for (int yy = yy & 0; yy--)
for (int xx = 0; xx & 14; xx++)
int temp = bgGraoud[yy - 1, xx];
bgGraoud[yy, xx] =
label1.Text = Sorce.ToString(); ;
private void button2_Click(object sender, EventArgs e)
if (button2.Text == "暂停游戏")
button2.Text = "开始游戏";
timer1.Stop();
button2.Text = "暂停游戏";
timer1.Start();
在接下来的内容中,我们把这个DEMO做成一个三维的俄罗斯方块。
详情请打开:
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:254101次
积分:4500
积分:4500
排名:第2414名
原创:96篇
转载:10篇
评论:1328条
我最近在做有需要做的朋友欢迎联系我。CSDN博客如果在乱删我的博客,我就不再更新了,建议大家去博客园看看我的博客 /henrui/
(3)(2)(8)(5)(6)(2)(1)(4)(1)(6)(2)(3)(4)(5)(7)(6)(1)(4)(6)(2)(3)(5)(3)(2)(3)(6)(4)(2)(1)

我要回帖

更多关于 双人俄罗斯方块 的文章

 

随机推荐