C#之四十八 俄罗斯方块设计

1        系统设计要求

1.1   需求分析

本系统为一个用C#实现的为我们所熟悉的简单的俄罗斯方块游戏,该系统的具体功能如下:

1).       能简便的开始游戏,游戏中的方块的功能与日常我们所熟悉的游戏的功能一致,各种块的设置也一致,包括块的旋转,加速下降,平移,满行消去,到顶游戏结束功能;

2).       能够自定义游戏中功能键的具体按键,显示下一方块提示信息,以及游戏数据的统计;

3).       考虑需要解决的问题:怎么样设置图形显示;怎样获取鍵盘输入;怎样控制方块的移动;怎样控制时间间隔(用于游戏中控制形状的下落);游戏中的各种形状及整个游戏空间怎么用数据表示;游戏中怎么判断左右及向下移动的可能性;游戏中怎么判断某一形状旋转的可能性;按向下方向键时加速某一形状下落速度的处理;怎么判断某一形状已经到底;怎么判断某一已经被填满;怎么消去已经被填满的一行怎么消去某一形状落到底后能够消去的所有的行;(如长条最多可以消去四行)怎样判断游戏结束,关于“下一个”形状取法的问题。

2        设计思路

2.1   用面向对象的方法分析系统

从游戏的基本玩法出发,主要就是俄罗斯方块的形状和旋转,在设计中在一个图片框中构造了一个20*20(像素)的小块,由这些小块组合成新的形状,每四个小块连接在一起就可以构造出一种造型,总共设计了7中造型,每种造型又可以通过旋转而变化出2到4种形状,在游戏窗体中用户就可以使用键盘的方向键来控制方块的运动,然后对每一行进行判断,如果有某行的方块是满的,则消除这行的方块,并且使上面的方块自由下落,其中,方块向下的速度是有时钟控件控制的。俄罗斯方块游戏设计主要包括以下10个方面:

1).         游戏界面的设计。

2).         俄罗斯方块的实现。

3).         键盘输入信息的获取。

4).         俄罗斯方块的移动(向左,向右和向下)。

5).         俄罗斯方块的变换。

6).         方块自动下落与速度的选择。

7).         慢行的判断与消行。

8).         游戏结束判断。

9).         用户配置保存。

 

在主窗口中,通过调用俄罗斯方块类来实现程序的表示层,在该窗口中通过两个Panel控件来实现方块叠放窗口和下一方块信息窗口;调用设置窗口,保存设计窗口类传回的信息,并设置到游戏中去,保存在配置文件中;

在设置窗口中,以良好的界面提供用户自定义快捷键的接口,保存相应设置参数,以提供给调用窗口。

2.2运用的控件和主要对象

在设计过程中主要用到的控件有:PictureBox控件,MenuStrip控件,Button控件,Label控件,Timer控件,winmm组件,DirectSound等等。

3        系统功能实现

3.1   屏幕信息初始化

用来显示状态信息的框

privateSystem.Windows.Forms.GroupBox statusBox;

开始按钮

privateSystem.Windows.Forms.Button btnStart;

显示“下一块”的标签

privateSystem.Windows.Forms.Label label3;

显示“分数”的标签

privateSystem.Windows.Forms.Label label2;

显示“等级”的标签

privateSystem.Windows.Forms.Label label1;

用来画下一块方块的区域

privateSystem.Windows.Forms.PictureBox panel1;

游戏区域

privateSystem.Windows.Forms.PictureBox gameArea;

实现如下主界面效果图(图3-1):




1.1   方块的实现

在程序中每一个方块都是一个Block类的实例。Block包括的参数有方块的宽度,高度,最左端横坐标,最上端纵坐标,方块的数组表示。其中一共有7中形状的方块,以数组表示为:

     11     11    1       11     010    10     01

01     10    1       11     111     11    11

01     10    1                      01     10

                   1

方块的7种形状分别以数字0-6来代表,在构造函数中,随机生成0-6中数字,以此来随机生成方块的形状。用来在界面上显示方块的贴图也以0-6的数字来代表,同样以随机数的形式来随机的现实方块的颜色。

1.2   键盘输入事件处理

因为在界面上有一个按钮,并且只有一个按钮,所以该按钮在通常情况下都是默认为焦点。在这种情况下按下某些键,比如空格,就会产生出发按钮事件的情况。因此必须重载整个WinForm的ProcessCmdKey来避免这样的问题。

当按向左,向右及旋转按钮时,只要相应的处理方块的位置或者形状即可,但是当按向下或者立即下落时,需要不同的处理。向下移动时,如果移动到最底部但还未固定,则需要重新设置计时器间隔时间,从而使自动下落时,底部未固定的方块到固定的时间相同。如果方块在最底部而未固定的时候,向下移动,则立即固定。这两种情况,当方块固定后,都需要判断是否消行,立即下落时,需要判断是否消行。

1.3   方块的移动

游戏中方块的移动分为向左移动,向右移动,向下移动和立即落下。

        向左移动:

        public void MoveLeft() {

            int xPos =runBlock.XPos-1;

            intyPos = runBlock.YPos;

            for(int i = 0; i < runBlock.Length; i++)

            {

                if(xPos + runBlock[i].X <0)//如果超出左边界则失败

                {

                    return;

                }

                if(!coorArr[xPos + runBlock[i].X, yPos - runBlock[i].Y].IsEmpty)//如果左边有东西挡则失败

                {

                    return;

                }

            }

            runBlock.erase(gpPaltte);//擦除原来位置的转块

            runBlock.XPos--;

            runBlock.Paint(gpPaltte);//在新位置上画转块

}

向右移动:

public void MoveRight()

        {

            intxPos = runBlock.XPos + 1;

            intyPos = runBlock.YPos;

            for(int i = 0; i < runBlock.Length; i++)

            {

                if(xPos + runBlock[i].X >_width-1)//如果超出右边界则失败

                {

                    return;

                }

                if(!coorArr[xPos + runBlock[i].X, yPos - runBlock[i].Y].IsEmpty)//如果右边有东西挡则失败

                {

                    return;

                }

            }

            runBlock.erase(gpPaltte);//擦除原来位置的转块

            runBlock.XPos++;

            runBlock.Paint(gpPaltte);//在新位置上画转块

        }

向下移动:

public void Drop() {

            timerBlock.Stop();

            while(Down()) ;

            timerBlock.Start();

        }

1.4   方块的变换

        public voidDeasilRotate() //顺时针旋转

        {

            for (int i = 0; i< runBlock.Length;i++ )

            {

                int x = runBlock.XPos + runBlock[i].Y;

                int y = runBlock.YPos + runBlock[i].X;

                if (x < 0 || x > _width - 1)//如果超出左右边界,则失败

                   return;

                if (y < 0 || y > _height - 1)//如果超出上下边界,则失败

                   return;

                if (!coorArr[x, y].IsEmpty)//如果旋转后的位置上有转块,则失败

                    return;

            }

           runBlock.erase(gpPaltte);//擦除原来位置的转块

           runBlock.DeasilRotate();

           runBlock.Paint(gpPaltte);//在新位置上画转块

        }

        public voidContraRotate() //逆时针旋转

        {

            for (int i = 0; i< runBlock.Length; i++)

            {

                int x = runBlock.XPos - runBlock[i].Y;

                int y = runBlock.YPos - runBlock[i].X;

               if (x < 0 || x > _width - 1)//如果超出左右边界,则失败

                   return;

                if (y < 0|| y > _height - 1)//如果超出上下边界,则失败

                   return;

                if (!coorArr[x, y].IsEmpty)//如果旋转后的位置上有转块,则失败

                   return;

            }

           runBlock.erase(gpPaltte);//擦除原来位置的转块

           runBlock.ContraRotate();

           runBlock.Paint(gpPaltte);//在新位置上画转块

        }

 

1.5   判断方块是否到底

public voidCheckAndOverBlock()//检查转块是否到底,如果到底则把当前转块归入coorArr,并产生新的转块

        {

            boolover = false;//设置一个当前运行转块是否到底的标志

            for(int i = 0; i < runBlock.Length;i++ )//遍历当前运行转块的所有小方块

            {

                intx = runBlock.XPos + runBlock[i].X;

                inty = runBlock.YPos - runBlock[i].Y;

                if(y == _height - 1)//如果到达下边界,则结束当前转块

                {

                   over = true;

                    break;

                }

                if(!coorArr[x, y+1].IsEmpty) //如果下面有转块,则当前转块结束

                {

                   over = true;

                   break;

                }

            }

            if(over)

            {

                for(int i = 0; i < runBlock.Length; i++)//把当前转块归入coordinateArr

               {

                    coorArr[runBlock.XPos +runBlock[i].X, runBlock.YPos - runBlock[i].Y] = runBlock.BlockColor;

               }

               //检查是否有满行情况,如果有则删除

               CheckAndDelFullRow();

               //产生新转块

               runBlock = readyBlock;//新的转块为准备好的转块

               runBlock.XPos = _width / 2;//确定当前运行转块的出生位置

               int y = 0;//确定转块Ypos,确定刚出生的转块顶上没空行

               for (int i = 0; i < runBlock.Length; i++)

               {

                    if (runBlock[i].Y > y)

                        y = runBlock[i].Y;

               }

               runBlock.YPos = y;

               //检查新生成的转块所占用的地方是否已经有转块存在,如果有,游戏结束

               for (int i = 0; i <runBlock.Length; i++)

               {

                    if (!coorArr[runBlock.XPos+ runBlock[i].X, runBlock.YPos - runBlock[i].Y].IsEmpty)

                    {

                        StringFormat drawFormat= new StringFormat();

                        drawFormat.Alignment =StringAlignment.Center;

                       gpPaltte.DrawString("GAME OVER",

                                            newFont("Arial Black", 25f),

                                            newSolidBrush(Color.White),

                                            newRectangleF(0, _height * rectPix / 2 - 100, _width * rectPix, 100),

                                           drawFormat);

                        timerBlock.Stop();//关闭定时器

                       return;

                    }

               }

               runBlock.Paint(gpPaltte);

               //获取新的准备转块

               readyBlock = bGroup.GetABlock();

               readyBlock.XPos = 2;

               readyBlock.YPos = 2;

                gpReady.Clear(Color.Black);

               readyBlock.Paint(gpReady);

           }

        }

1.6   满行判断并消行

        privatevoid CheckAndDelFullRow() //检查并删除满行

        {

           //找出当前转块所在行的范围

           int lowRow = runBlock.YPos - runBlock[0].Y;//lowRow代表当前转块的y轴的最小值

           int highRow = lowRow;//highRow代表当前转块y轴的最大值

           for (int i = 0; i < runBlock.Length; i++)//找出当前转块所占行的范围,放入low和high变量内

           {

               int y = runBlock.YPos - runBlock[i].Y;

               if (y < lowRow)

                    lowRow = y;

               if (y > highRow)

                    highRow = y;

           }

           bool repaint = false;//判断是否重画标志

           for (int i = lowRow; i <= highRow; i++) //检查是否满行,如果有,则删除

           {

                bool rowFull = true;

               for (int j = 0; j < _width;j++ )

               {

                    if (coorArr[j,i].IsEmpty)//如果有一行为空,则说明这行不满

                    {

                        rowFull = false;

                        break;

                    }

               }

               if (rowFull) //如果是满行,则删除这一行

               {

                    repaint = true;//如果有要删除的行,则需要重画

                    for (int k = i; k > 0;k--) {//把第n行的值用n-1行的值来代替

                        for (int j = 0; j <_width; j++) {

                            coorArr[j, k] =coorArr[j, k - 1];

                        }

                    }

                    for (int j = 0; j <_width;j++ )//清空第0行

                    {

                        coorArr[j, 0] = Color.Empty;

                    }

               }

           }

           if(repaint)//重画

           {

               PaintBackground(gpPaltte);

           }

        }

3.8  产生下一方块

public bool GeneBlock(int shapeNO, Point firstPos, Colorcolor)//产生下一方块

        {

            this.SetLastPos();

            this.EraseLast();

            this.SetPos(shapeNO,firstPos);         

            if(!this.CanRotate(this.pos))

            {

                this.pos=null;

                returnfalse;

            }

            else

            {

                this.color=color;

                returntrue;

            }

        }

 

3.9        游戏设置

程序中游戏设置的保存方式为配置文件,配置文件中保存着游戏的按键设置,在打开程序时,会载入配置文件中的配置。用户可以在游戏中随时改变配置,改变后的配置将保存到配置文件中并且立即有效。游戏配置界面如下(图3-2):




保存配置

private void SaveSetting()

        {

            try

            {

                XmlDocumentdoc = new XmlDocument();

                XmlDeclarationxmlDec=doc.CreateXmlDeclaration ("1.0","gb2312",null);

 

                XmlElementsetting=doc.CreateElement("SETTING");

                doc.AppendChild(setting);

 

                XmlElementlevel=doc.CreateElement("LEVEL");

                level.InnerText=this.startLevel.ToString();

                setting.AppendChild(level);

 

                XmlElementtrans=doc.CreateElement("TRANSPARENT");

                trans.InnerText=this.trans.ToString();

                setting.AppendChild(trans);

   

                XmlElementkeys=doc.CreateElement("KEYS");   

                setting.AppendChild(keys);

                foreach(Keysk in this.keys)

                {

                    KeysConverterkc=new KeysConverter();  

                    XmlElementx=doc.CreateElement("SUBKEYS");

                    x.InnerText=kc.ConvertToString(k);

                    keys.AppendChild(x);

                }

 

                XmlElementroot=doc.DocumentElement;

                doc.InsertBefore(xmlDec,root);

                doc.Save("c:\\setting.cob");

            }

            catch(Exceptionxe)

            {

                MessageBox.Show(xe.Message);

            }

        }

4  总结

本设计通过Vusial Studio 方便的Windows表单设计界面,增加了相应的按钮单击响应函数,通过与用户的交互,反馈回用户所需要的信息。

设计出的程序符合设计需求,既有基本的游戏逻辑功能,又能保存用户的设置,取得较好的实验结果。

这个学期“C#程序设计”课程让我接触了面向对象的程序设计,Visual stdio的可视化编程环境让我们可以制作界面友好的Windows环境,利用IDE可以快捷地开发出所要的可视化的环境。C#是是一种完全面向对象的语言,使用对象的思想来编程,既可以对相应的数据进行保护,也可以相应的与其他的类共享,有利于程序的结构化,方面程序的编写。Viusal Studio下我们可以快速的进行开发。但是,也要看到其对WindowsApi函数的封装也导致了我们在学习的时候对Windows程序的运行机制不了解,导致了学习时候的迷惑。本学期配套的书籍<< C#实用教程>>虽然简单明了,但是对于机制原理的解释和说明过少,因此,学习的时候应该不只满足于这本书中的内容,应该多找一些书籍进行知识的扩展了加深。

开发一个工程时,应该先制定好程序的框架,规划好相应的功能模块,使程序模块化,易于日后的扩展和完善。其次是程序的数据结构,良好的数据结构能使程序高效化,功能强大。本次实现中最重要的是方块类的编写,其定义的好坏和封装性的良好是整个程序运行的基础,属于程序的业务逻辑功能块,主框架中通过调用该类,实现程序的表示层。再之,优秀的算法能提高程序的效率。



转载于:https://www.cnblogs.com/mjsn/p/6150897.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值