1.1 需求分析
本系统为一个用C#实现的为我们所熟悉的简单的俄罗斯方块游戏,该系统的具体功能如下:
1). 能简便的开始游戏,游戏中的方块的功能与日常我们所熟悉的游戏的功能一致,各种块的设置也一致,包括块的旋转,加速下降,平移,满行消去,到顶游戏结束功能;
2). 能够自定义游戏中功能键的具体按键,显示下一方块提示信息,以及游戏数据的统计;
3). 考虑需要解决的问题:怎么样设置图形显示;怎样获取鍵盘输入;怎样控制方块的移动;怎样控制时间间隔(用于游戏中控制形状的下落);游戏中的各种形状及整个游戏空间怎么用数据表示;游戏中怎么判断左右及向下移动的可能性;游戏中怎么判断某一形状旋转的可能性;按向下方向键时加速某一形状下落速度的处理;怎么判断某一形状已经到底;怎么判断某一已经被填满;怎么消去已经被填满的一行;怎么消去某一形状落到底后能够消去的所有的行;(如长条最多可以消去四行)怎样判断游戏结束,关于“下一个”形状取法的问题。
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等等。
用来显示状态信息的框
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#实用教程>>虽然简单明了,但是对于机制原理的解释和说明过少,因此,学习的时候应该不只满足于这本书中的内容,应该多找一些书籍进行知识的扩展了加深。
开发一个工程时,应该先制定好程序的框架,规划好相应的功能模块,使程序模块化,易于日后的扩展和完善。其次是程序的数据结构,良好的数据结构能使程序高效化,功能强大。本次实现中最重要的是方块类的编写,其定义的好坏和封装性的良好是整个程序运行的基础,属于程序的业务逻辑功能块,主框架中通过调用该类,实现程序的表示层。再之,优秀的算法能提高程序的效率。