下落的方块板和形状控件
将这个最受欢迎的经典游戏作为 .NET 自定义控件实现,配有动画和声音,带来完整的游戏体验。
引言
又一个俄罗斯方块克隆。
是的,这是最棒的游戏之一,它几乎为程序员提供了无限的可能性,唯一的限制就是程序员的想象力和能力。在这篇文章中,我将与读者分享我的游戏版本。
背景
游戏名为 Falling Blocks。它包含两个自定义控件:
FallingBlocks
BoardFallingBlocks
Shape
FallingBlocks
Board 控件是主控件。它可以拖放到窗体上,当窗体运行时,可以通过单击控件来启动游戏。游戏激活期间,单击控件会结束游戏。
FallingBlocks
Shape 控件通常可以用于两个目的:
- 作为即将到来的
FallingBlocks
图块的预览。 - 限制
FallingBlocks
Board 中的图块数量。
每个 FallingBlocks
Board 由一个 FallingBlocks
单元格数组组成,这些单元格共同保存棋盘的状态。每个 FallingBlocks
Shape 都被分配了一个 FallingBlocks Shape
类型,这是一个枚举,代表各种 FallingBlocks
形状。
FallingBlocks Shape 类型
目前,定义的形状在 FallingBlocksShapeType enum
中。
//All the defined shapes
public enum FallingBlocksShapeType
{
Square,
LShape,
LLShape,
ZShape,
ZZShape,
TShape,
IShape,
//a non standard shape to demo that
//we can easily add new shapes
SlashShape
}
'All the defined shapes
Public Enum FallingBlocksShapeType
Square
LShape
LLShape
ZShape
ZZShape
TShape
IShape
'a non standard shape to demo that
'we can easily add new shapes
SlashShape
End Enum
一个 FallingBlocks<code>Shape
对象是根据传递给 CreateShape()
方法的 FallingBlocksShapeType
参数创建的。
//Create a new shape based on the FallinBlocksShapeType param
internal static FallingBlocksShape CreateShape(FallingBlocksShapeType st,
int _blocksize, CellShapeType _blockshape)
{
FallinBlocksShape temp=null;
switch (st)
{
case FallinBlocksShapeType.IShape:
temp= new FallinBlocksShape(new Point[]{new Point(0,0),
new Point(1,0),
new Point(2,0),
new Point(3,0)},
new Point(2,0),
Color.Lime,_blocksize,_blockshape );
break;
case FallinBlocksShapeType.LLShape:
temp=new FallinBlocksShape(new Point[]{new Point(0,0),
new Point(0,1),
new Point(1,1),
new Point(2,1)},
new Point(1,1),
Color.LightBlue,_blocksize,_blockshape );
break;.....
'Create a new shape based on the FallingBlocksShapeType param
Friend Shared Function CreateShape(st As FallingBlocksShapeType, _blocksize As Integer, _
_blockshape As CellShapeType) As FallingBlocksShape
Dim temp As FallingBlocksShape = Nothing
Select Case st
Case FallingBlocksShapeType.IShape
temp = New FallingBlocksShape(New Point() {New Point(0, 0), New Point(1, 0), _
New Point(2, 0)}, New Point(1, 0), Color.Lime, _blocksize, _blockshape)
Exit Select
Case FallingBlocksShapeType.LLShape
temp = New FallingBlocksShape(New Point() {New Point(0, 0), New Point(0, 1), _
New Point(1, 1), New Point(2, 1)}, New Point(1, 1), _
Color.LightBlue, _blocksize, _blockshape)
Exit Select
.....
End Function
要创建一个新的 FallingBlocksShape
类型,只需:
- 向
FallingBlocksShapeType enum
添加一个新的形状定义。 - 在
CreateShape()
方法中为新形状添加一个case
块。
FallingBlocks Shape
FallingBlocksShape
有两个构造函数。
//Default Constructor used by Visual Studio
public FallinBlocksShape()
//The main constructor used privately to
//construct the shape
private FallinBlocksShape(Point[] points,Point pivot,
Color color,int _blocksize,
CellShapeType _blockshape)
'Default Constructor used by Visual Studio
Public Sub New()
'The main constructor used privately to construct the shape
Private Sub New(points As Point(), pivot As Point, _
color As Color, _blocksize As Integer, _blockshape As CellShapeType)
一个 FallingBlocksShape
对象由一组点和一个可选的支点定义。每个点的属性是:
- color
- 每个单元格块的大小。
- 每个单元格块的单元格形状。
例如,IShape
FallingBlocksShape
由四个点 (0,0) (1,0) (2,0) (3,0)
定义。坐标基于屏幕坐标,x 坐标向右增加,y 坐标向下增加。这些数字表示块的单位,而不是像素单位。
支点用于旋转。它是旋转操作中保持不变的点。例如,IShape
对象的支点是 (1,0)
。逆时针旋转 90 度后,它仍将保持在 (1,0)
,而其他点会发生变化。下图和下面的代码说明了旋转是如何执行的。
//rotate the piece anti clockwise
private FallingBlocksShape Rotate(FallingBlocksShape ts)
{
Point[] points;
Point pivot;
Point location;
//do not rotate if there is no pivot defined
if (ts.FallingBlocksPivot.Equals(FallingBlocksShape.NoPivot))
{
return ts;
}
//make a copy of the points
Point[] temppoints=new Point[ts.FallingBlocksPoints.Length];
//perform 90 deg anticlockwise rotation
//1. Refine the points with respect to the pivot by
//subtracting from each point the pivot coordinates
for(int i=0;i<ts.FallingBlocksPoints.Length;i++)
temppoints[i]=new Point(ts.FallingBlocksPoints[i].X-ts.FallingBlocksPivot.X,
ts.FallingBlocksPoints[i].Y-ts.FallingBlocksPivot.Y);
points=new Point[temppoints.Length];
//2. Rotate the refined points and
//add back the pivot coordinates
for(int i=0;i<temppoints.Length;i++)
points[i]=new Point(temppoints[i].Y+ts.FallingBlocksPivot.X ,
-temppoints[i].X+ts.FallingBlocksPivot.Y);
//***********************************
//find out the bounding size of the rotated shape
int minx,maxx,miny,maxy;
minx=points[0].X;
maxx=minx;
miny=points[0].Y;
maxy=miny;
for(int i=1;i<points.Length;i++)
{
if(points[i].X<minx) minx=points[i].X;
if(points[i].X>maxx) maxx=points[i].X;
if(points[i].Y<miny) miny=points[i].Y;
if(points[i].Y>maxy) maxy=points[i].Y;
}
Size size=new Size((maxx-minx+1)*_blocksize,
(maxy-miny+1)*_blocksize);
//***************************************************
//get the new location of the piece after the rotation
//the location is the screen coordinates of the
//top left corner of the piece
location=new Point(ts.Location.X+minx*_blocksize,
ts.Location.Y +miny*_blocksize);
//refined the pivot with reference to the new orientation
pivot=new Point(ts.FallingBlocksPivot.X-minx,ts.FallingBlocksPivot.Y -miny);
//refine each point with reference to the new orientation
for(int i=0;i<points.Length;i++)
{
points[i].X=points[i].X-minx;
points[i].Y=points[i].Y-miny;
}
//make a copy of the object
//change its properties to reflect the
//new orientation
FallingBlocksShape temp=ts.Clone();
temp.FallingBlocksPivot=pivot;
temp.FallingBlocksPoints=points;
temp.Location=location;
temp.Size=size;
//***************************
//return the new object it has
//a valid location
if(IsValidPosition(temp))
return temp;
Else
//otherwise return the original object
return ts;
}
'rotate the piece anti clockwise
Private Function Rotate(ts As FallingBlocksShape) As FallingBlocksShape
Dim points As Point()
Dim pivot As Point
Dim location As Point
'do not rotate if there is no pivot defined
If ts.FallingBlocksPivot.Equals(FallingBlocksShape.NoPivot) Then
Return ts
End If
'make a copy of the points
Dim temppoints As Point() = New Point(ts.FallingBlocksPoints.Length - 1) {}
'perform 90 deg anticlockwise rotation
'1. Refine the points with respect to the pivot by
'subtracting from each point the pivot coordinates
For i As Integer = 0 To ts.FallingBlocksPoints.Length - 1
temppoints(i) = New Point(ts.FallingBlocksPoints(i).X - _
ts.FallingBlocksPivot.X, ts.FallingBlocksPoints(i).Y - _
ts.FallingBlocksPivot.Y)
Next
points = New Point(temppoints.Length - 1) {}
'2. Rotate the refined points and
'add back the pivot coordinates
For i As Integer = 0 To temppoints.Length - 1
points(i) = New Point(temppoints(i).Y + _
ts.FallingBlocksPivot.X, -temppoints(i).X + ts.FallingBlocksPivot.Y)
Next
'***********************************
'find out the bounding size of the rotated shape
Dim minx As Integer, maxx As Integer, miny As Integer, maxy As Integer
minx = points(0).X
maxx = minx
miny = points(0).Y
maxy = miny
For i As Integer = 1 To points.Length - 1
If points(i).X < minx Then
minx = points(i).X
End If
If points(i).X > maxx Then
maxx = points(i).X
End If
If points(i).Y < miny Then
miny = points(i).Y
End If
If points(i).Y > maxy Then
maxy = points(i).Y
End If
Next
Dim size As New Size((maxx - minx + 1) * _blocksize, (maxy - miny + 1) * _blocksize)
'***************************************************
'get the new location of the piece after the rotation
'the location is the screen coordinates of the
'top left corner of the piece
location = New Point(ts.Location.X + minx * _blocksize, _
ts.Location.Y + miny * _blocksize)
'refined the pivot with reference to the new orientation
pivot = New Point(ts.FallingBlocksPivot.X - minx, ts.FallingBlocksPivot.Y - miny)
'refine each point with reference to the new orientation
For i As Integer = 0 To points.Length - 1
points(i).X = points(i).X - minx
points(i).Y = points(i).Y - miny
Next
'make a copy of the object
'change its properties to reflect the
'new orientation
Dim temp As FallingBlocksShape = ts.Clone()
temp.FallingBlocksPivot = pivot
temp.FallingBlocksPoints = points
temp.Location = location
temp.Size = size
temp._shapesize = New Size(size.Width \ _blocksize, size.Height \ _blocksize)
'***************************
'return the new object it has
'a valid location
If IsValidPosition(temp) Then
If SoundOn Then
Dim p As MciPlayer = GetSoundPlayer(AnimationType.Rotate)
If p IsNot Nothing Then
p.PlayFromStart()
End If
End If
Return temp
Else
'if (SoundOn)
'{
' MciPlayer p = GetSoundPlayer(AnimationType.Error);
' if (p != null)
' p.PlayFromStart();
'}
Return ts
End If
End Function
传递到构造函数的 color
、blocksize
和 blockshape
用于渲染对象。
默认情况下,FallingBlocks<code>Shape
对象是不可见的。它的主要目的是作为 FallingBlocks
Board
使用的各种属性的容器。棋盘调用 FallingBlocksShape
的绘图函数:EraseShape()
和 DrawShape()
分别擦除和绘制 FallingBlocks
图块。
FallingBlocksShape
对象的另一个用途是作为下一个即将到来的 FallingBlocks
图块的预览。为此,该对象必须与 FallingBlocksBoard
的 PreviewFallingBlocksShape
属性关联。用作此目的时,FallingBlocksShape
将是可见的,并通过其重写的 OnPaint()
方法进行自渲染。
FallingBlocks
游戏默认的运行时间行为是,可以从 FallingBlocksShapeType enum
中定义的任何形状中选择。然而,当在设计时将 FallingBlocksShape
对象拖放到 FallingBlocksBoard
中时,此行为会发生变化。当一个 FallingBlocksShape
包含在 FallingBlocksBoard
中并且未与 PreviewFallingBlocksShape
属性关联时,它将承担限制棋盘上可玩形状类型的新用途。只有这些包含的形状才会出现在运行时。
FallingBlocks Board
它是一个 FallingBlocks
单元格网格。每个 FallingBlocks
单元格都有一个 color
和 avail
属性。最初,单元格的 color
设置为 Black
,avail
设置为 true
。
color
属性指示用于渲染单元格的颜色。每次绘制棋盘时,它会重绘所有 avail
值设置为 false
的单元格。
当一个 FallingBlocks
图块准备好播放时,它会被放置在棋盘的顶部。FallingBlocksShape
上的每个点都会覆盖 FallingBlocksBoard
中的一个单元格。要使 FallingBlocks
图块处于有效位置,每个被覆盖的单元格必须具有 true
值的 avail
属性。如果此条件不满足,则该 FallingBlocks
图块无法播放,游戏结束。
棋盘还包含一个计时器,用于控制图块向下移动的速度。可以使用 FallingBlocksDelay
属性调整速度。它的有效范围是 100 - 1000 毫秒。FallingBlocksDelay
值越大,游戏速度越慢。
棋盘还包含一个宽度设置为 0
的文本框,使其几乎不可见。使用文本框的原因是,派生自 Panel
类的 FallingBlocks
Board 没有键盘事件处理程序。文本框用于捕获键盘事件。用于游戏的按键是箭头键:<(左) 用于将图块向左移动,>(右) 用于向右移动,^(上) 用于逆时针旋转 90 度,V (下) 用于快速着陆。
当图块需要移动时,棋盘会调用形状的 EraseShape()
方法,将图块移动到有效位置,然后通过调用形状的 DrawShape()
方法重新绘制图块。
当图块已着陆(即将撞到底部 avail
值为 false
的单元格)时,会调用形状的 Erase()
方法,棋盘会调用其 PasteShape()
方法来更新被形状覆盖的单元格的状态(color
和 avail
),以便下次绘制棋盘时,这些单元格将以着陆形状的颜色绘制。
包含
当 FallingBlocksBoard
控件在窗体中创建时,会调用 FallingBlocksShape
的 OnCreateControl()
方法。当窗体加载并完成加载 FallingBlocksBoard
控件时,就会发生这种情况。会查询控件的 Controls
集合以查找任何 FallingBlocksShape
对象,并将这些对象添加到 arrShape
ArrayList
中。
protected override void OnCreateControl()
{
//MessageBox.Show("Create Control " + Controls.Count );
//Put all the included shapes into the array list
System.Collections.IEnumerator en=Controls.GetEnumerator();
arrShape.Clear();
While (en.MoveNext())
{
//MessageBox.Show(en.Current.GetType().ToString());
if (en.Current.GetType().Equals(typeof(FallingBlocks.FallingBlocksShape)))
{
arrShape.Add (en.Current);
}
}
.....
Protected Overrides Sub OnCreateControl()
'MessageBox.Show("Create Control " + Controls.Count );
'Put all the included shapes into the array list
Dim en As System.Collections.IEnumerator = Controls.GetEnumerator()
arrShape.Clear()
While en.MoveNext()
'MessageBox.Show(en.Current.GetType().ToString());
If en.Current.[GetType]().Equals(GetType(FallingBlocks.FallingBlocksShape)) Then
arrShape.Add(en.Current)
End If
End While
'create the background image if not already done so
If _baseimage Is Nothing Then
_baseimage = New Bitmap(_size.Width * _blocksize, _
_size.Height * _blocksize, PixelFormat.Format32bppArgb)
Graphics.FromImage(_baseimage).FillRectangle(New SolidBrush(Me.BackColor), _
0, 0, _size.Width * _blocksize, _size.Height * _blocksize)
MyBase.BackgroundImage = DirectCast(_baseimage.Clone(), Image)
_unscaledbase = DirectCast(_baseimage.Clone(), Image)
_cleanbase = DirectCast(_baseimage.Clone(), Image)
End If
_created = True
MyBase.OnCreateControl()
initBoard()
End Sub
在创建用于播放的新 FallingBlocksShape 图块的 NewShape()
方法中,会查询 arrShape
列表。如果 arrShape
列表包含任何 FallingBlocksShape
对象,则下一个图块将从该列表中获取,否则将从 FallingBlocksShapeType
enum
中定义的任何形状中选取。
//generate a new shape randomly from the
//registered shapes or arrShape
private FallingBlocksShape NewShape()
{
FallingBlocksShape ts;
Random r=new Random();
int n=0;
int i=0;
if(arrShape.Count==0)
{
FallingBlocksShape [] arr=
(FallingBlocksShape [])Enum.GetValues(typeof(FallingBlocksShapeType));
n=arr.GetLength(0);
i=r.Next(1000) % n;
ts=FallingBlocksShape.CreateShape(arr[i],25);
}
else
{
n=arrShape.Count;
i=r.Next(1000)%n;
FallingBlocksShape temp=(FallingBlocksShape )arrShape.ToArray()[i];
//MessageBox.Show(""+temp.ShapeType);
ts=FallingBlocksShape.CreateShape(temp.ShapeType,
temp.BlockSize);
ts.FallingBlocksColor=temp.FallingBlocksColor;
}
int sx=(r.Next(1000) % (_size.Width-4))+1;
ts.Location=new Point(_blocksize*sx,0);
ts.Size=new Size(ts.FallingBlocksShapeSize.Width*_blocksize,
ts.FallingBlocksShapeSize.Height * _blocksize);
return ts;
}
Private Function NewShape() As FallingBlocksShape
Dim ts As FallingBlocksShape
Dim r As New Random()
Dim n As Integer = 0
Dim i As Integer = 0
If arrShape.Count = 0 Then
Dim arr As FallingBlocksShape() = DirectCast([Enum].GetValues_
(GetType(FallingBlocksShapeType)), FallingBlocksShape())
n = arr.GetLength(0)
i = r.[Next](1000) Mod n
ts = FallingBlocksShape.CreateShape(arr(i), 25)
Else
n = arrShape.Count
i = r.[Next](1000) Mod n
Dim temp As FallingBlocksShape = DirectCast(arrShape.ToArray()(i), FallingBlocksShape)
'MessageBox.Show(""+temp.ShapeType);
ts = FallingBlocksShape.CreateShape(temp.ShapeType, temp.BlockSize)
ts.FallingBlocksColor = temp.FallingBlocksColor
End If
Dim sx As Integer = (r.[Next](1000) Mod (_size.Width - 4)) + 1
ts.Location = New Point(_blocksize * sx, 0)
ts.Size = New Size(ts.FallingBlocksShapeSize.Width * _blocksize, _
ts.FallingBlocksShapeSize.Height * _blocksize)
Return ts
End Function
预览
FallingBlocks<code>Board
的 PreviewFallingBlocksShape
属性用于向 FallingBlocksBoard
指示一个 FallingBlocksShape
对象存在,作为即将到来的图块的预览。如果存在预览 FallingBlocksShape
对象,则其各种属性将设置为选定的形状,并且其 Visible
属性设置为 true
,以便 FallingBlocksShape
对象可以自行渲染。
...
_preview=GetNextPreviewShape();
if(this.PreviewFallingBlocksShape !=null)
{
this._previewcontrol.Visible =true;
this._previewcontrol.ShapeType =_preview.ShapeType;
this._previewcontrol.FallingBlocksColor=this._preview.FallingBlocksColor;
}
...
_preview = GetNextPreviewShape()
If Me.PreviewFallingBlocksShape IsNot Nothing Then
Me._previewcontrol.Visible = True
Me._previewcontrol.ShapeType = _preview.ShapeType
Me._previewcontrol.FallingBlocksColor = Me._preview.FallingBlocksColor
End If
移动
在计时器的每个滴答事件中,FallingBlocks
图块通过 MoveDown()
方法向下移动。
玩家还可以使用键盘箭头键或调用 public FallingBlocksMove..()
方法来使当前图块移动。
每次图块需要移动时,都会创建一个克隆并进行移动。克隆的新位置由 IsValidPosition()
方法进行验证。如果不通过验证,则返回原始图块,否则返回具有更新位置的克隆图块。
//move the piece down
private FallingBlocksShape MoveDown(FallingBlocksShape ts)
{
FallingBlocksShape temp=ts.Clone();
temp.Top +=_blocksize;
if(!IsValidPosition(temp))
return ts;
Else
return temp;
}
//check to see if the shape has a valid position on the board
private bool IsValidPosition(FallingBlocksShape ts)
{
int xoff,yoff;
xoff=(ts.Location.X+_blocksize -1)/_blocksize;
yoff=(ts.Location.Y+_blocksize -1)/_blocksize;
foreach(Point p in ts.FallingBlocksPoints)
{
if((p.X+xoff)>=this._size.Width) return false;
if((p.X+xoff)<0) return false;
if((p.Y+yoff)>=this._size.Height) return false;
if((p.Y+yoff)<0) return false;
if (!_cells[p.X+xoff,p.Y+yoff].Avail) return false;
}
return true;
}
'move the piece down
Private Function MoveDown(ts As FallingBlocksShape) As FallingBlocksShape
Dim temp As FallingBlocksShape = ts.Clone()
temp.Top += _blocksize
If Not IsValidPosition(temp) Then
Return ts
Else
Return temp
End If
End Function
Private Function IsValidPosition(ts As FallingBlocksShape) As Boolean
If ts.Location.X < 0 OrElse ts.Location.Y < 0 Then
Return False
End If
Dim xoff As Integer, yoff As Integer
xoff = (ts.Location.X + _blocksize - 1) \ _blocksize
yoff = (ts.Location.Y + _blocksize - 1) \ _blocksize
If xoff < 0 OrElse yoff < 0 Then
Return False
End If
For Each p As Point In ts.FallingBlocksPoints
If (p.X + xoff) >= Me._size.Width Then
Return False
End If
If (p.X + xoff) < 0 Then
Return False
End If
If (p.Y + yoff) >= Me._size.Height Then
Return False
End If
If (p.Y + yoff) < 0 Then
Return False
End If
If Not _cells(p.X + xoff, p.Y + yoff).Avail Then
Return False
End If
Next
Return True
End Function
渲染背景图像和 Fallingblocks Board
BackgroundImage
属性是控件属性之一,管理起来相当困难。此属性是持久的。Visual Studio 会在每次更新图像或设计时窗体因任何原因关闭时保存图像。我注意到,Visual Studio 每次在运行设计时窗体之前都会关闭它。因此,持久的 BackgroundImage
属性将始终被保存。
直接在 Background
图像上绘图的好处在于,您可以直接控制何时更新绘图。这允许某种形式的缓冲更新。您在 Background
图像上绘图,当所有绘图完成后,您可以调用 Refresh()
方法一次性显示所有更新。此技术可以创建更流畅的动画。
Background
图像的一个问题是,如果图像不完全适合控件,它将被平铺。要解决此问题,在为 BackgroundImage
属性分配任何图像时,会调用 ResizeImage()
。
为了辅助更新背景图像,使用了两个图像:_baseimage
和 _cleanbase
。两者都是设计时 Background
图像的副本。
_baseimage
在运行时会用分数进行更新。每次分数更改时,会将 _cleanbase
复制到 _baseimage
(以擦除之前的分数),然后 _baseimage
会用最新分数进行更新。然后通过 DrawPicture()
方法将其复制到 Background
图像。
然后,FallingBlocks<code>Board
中的所有单元格都绘制在 Background
图像上。
//draw the board
private void DrawBoard()
{
//clean up the base image because we need to write
//a new score to it
_baseimage=(Image)_cleanbase.Clone();
Graphics g=Graphics.FromImage(_baseimage);
g.SmoothingMode =SmoothingMode.AntiAlias;
//write the score on the base image
g.DrawString("Score:"+_score,new Font("Arial",12,
FontStyle.Bold),
new SolidBrush(_textcolor),
new Point(5,5));
g.Dispose();
//repaint the background with the _baseimage
DrawPicture();
//paint on the background with all the cells
g=Graphics.FromImage(this.BackgroundImage);
g.SmoothingMode =SmoothingMode.AntiAlias;
foreach(Cell c in _cells)
{
//if(!c.CellColor.Equals(Color.Black))
if(!c.Avail)
{
GraphicsPath p=new GraphicsPath();
p.AddEllipse(c.CellPoint.X*_blocksize,
c.CellPoint.Y*_blocksize,
_blocksize-3,_blocksize-3);
PathGradientBrush br=new PathGradientBrush(p);
br.CenterColor=c.CellColor;
br.SurroundColors=new Color[]{c.CellFillColor};
g.FillPath(br,p);
g.DrawPath(new Pen(c.CellFillColor,1),p);
br.Dispose();
p.Dispose();
}
}
....
//repaint the Background with the _baseimage
private void DrawPicture()
{
Graphics g=Graphics.FromImage(this.BackgroundImage);
g.SmoothingMode=SmoothingMode.AntiAlias;
g.FillRectangle(new SolidBrush(this.BackColor),
0,0,this.Width,this.Height);
if(_baseimage!=null)
{
g.DrawImage(_baseimage,new Rectangle(0,0,
_baseimage.Width,_baseimage.Height ),
new Rectangle(0,0,_baseimage.Width,
_baseimage.Height),
GraphicsUnit.Pixel );
}
if(g!=null) g.Dispose();
}
'draw the board
Private Sub DrawBoard()
'clean up the base image because we need to write
'a new score to it
_baseimage = DirectCast(_cleanbase.Clone(), Image)
Dim g As Graphics = Graphics.FromImage(_baseimage)
g.SmoothingMode = SmoothingMode.AntiAlias
'write the score on the base image
g.DrawString("Score:" & _score, New Font("Arial", 12, FontStyle.Bold), _
New SolidBrush(_textcolor), New Point(5, 5))
g.Dispose()
'repaint the background with the _baseimage
DrawPicture()
'paint on the background with all the cells
g = Graphics.FromImage(Me.BackgroundImage)
g.SmoothingMode = SmoothingMode.AntiAlias
For Each c As Cell In _cells
'if(!c.CellColor.Equals(Color.Black))
If Not c.Avail Then
Dim p As New GraphicsPath()
p.AddEllipse(c.CellPoint.X * _blocksize, c.CellPoint.Y * _blocksize, _
_blocksize - 3, _blocksize - 3)
Dim br As New PathGradientBrush(p)
br.CenterColor = c.CellColor
br.SurroundColors = New Color() {c.CellFillColor}
g.FillPath(br, p)
g.DrawPath(New Pen(c.CellFillColor, 1), p)
br.Dispose()
p.Dispose()
End If
Next
End Sub
''''
'repaint the Background with the _baseimage
Private Sub DrawPicture()
Dim g As Graphics = Graphics.FromImage(Me.BackgroundImage)
g.SmoothingMode = SmoothingMode.AntiAlias
g.FillRectangle(New SolidBrush(Me.BackColor), 0, 0, Me.Width, Me.Height)
If _baseimage IsNot Nothing Then
g.DrawImage(_baseimage, New Rectangle(0, 0, _baseimage.Width, _
_baseimage.Height), New Rectangle(0, 0, _baseimage.Width, _
_baseimage.Height), GraphicsUnit.Pixel)
End If
If g IsNot Nothing Then
g.Dispose()
End If
End Sub
渲染形状
当 FallingBlocks<code>Shape
对象可见时,它由其 OnPaint()
方法绘制。这适用于用作预览的 FallingBlocksShape
对象。
protected override void OnPaint(PaintEventArgs e)
{
try
{
//this.BackColor=Color.Black;
e.Graphics.SmoothingMode=SmoothingMode.AntiAlias;
for(int i=0;i<_points.Length;i++)
{
GraphicsPath p=new GraphicsPath();
p.AddEllipse(_points[i].X*_blocksize,_points[i].Y*_blocksize,
_blocksize-2,_blocksize-2);
PathGradientBrush br=new PathGradientBrush(p);
br.CenterColor =this._color;
br.SurroundColors=new Color[]{this._fillcolor};
e.Graphics.DrawPath(new Pen(_color),p);
e.Graphics.FillPath(br,p);
br.Dispose();
p.Dispose();
}
}
catch(Exception ex){MessageBox.Show(ex.ToString());}
base.OnPaint (e);
}
Protected Overrides Sub OnPaint(e As PaintEventArgs)
Try
'this.BackColor=Color.Black;
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias
For i As Integer = 0 To _points.Length - 1
Dim p As New GraphicsPath()
p.AddEllipse(_points(i).X * _blocksize, _points(i).Y * _blocksize, _
_blocksize - 2, _blocksize - 2)
Dim br As New PathGradientBrush(p)
br.CenterColor = Me._color
br.SurroundColors = New Color() {Me._fillcolor}
e.Graphics.DrawPath(New Pen(_color), p)
e.Graphics.FillPath(br, p)
br.Dispose()
p.Dispose()
Next
Catch ex As Exception
MessageBox.Show(ex.ToString())
End Try
MyBase.OnPaint(e)
End Sub
对于其他用途的 FallingBlocksShape
对象,它们不会被直接渲染。渲染由 FallingBlocksBoard
调用 DrawShape()
和 EraseShape()
方法在其 Background
图像上完成。
//draw the shape onto the parent background image
//with the blocksize passed in
internal void DrawShape(int _blocksize)
{
Image img=((FallingBlocksBoard)Parent).BackgroundImage;
Graphics g=Graphics.FromImage(img);
g.SmoothingMode=SmoothingMode.AntiAlias;
foreach(Point pt in _points)
{
GraphicsPath p=new GraphicsPath();
p.AddEllipse(pt.X*_blocksize+Location.X,
pt.Y*_blocksize+Location.Y,
_blocksize-3,_blocksize-3);
PathGradientBrush br=new PathGradientBrush(p);
br.CenterColor=_color;
br.SurroundColors=new Color[]{_fillcolor};
g.FillPath(br,p);
g.DrawPath(new Pen(_fillcolor,1),p);
br.Dispose();
p.Dispose();
}
g.Dispose();
((FallingBlocksBoard)Parent).Refresh();
}
internal void EraseShape(int _blocksize)
{
Image img=((FallingBlocksBoard)Parent).BackgroundImage;
Image _img=((FallingBlocksBoard)Parent).StoredImage;
Graphics g=Graphics.FromImage(img);
//Graphics g=Graphics.FromHwnd(((FallingBlocksBoard)Parent).Handle);
g.SmoothingMode=SmoothingMode.AntiAlias;
foreach(Point p in _points)
{
g.DrawImage(_img,p.X*_blocksize+Location.X,
p.Y*_blocksize+Location.Y,
new Rectangle(new Point(p.X*_blocksize+Location.X,
p.Y*_blocksize+Location.Y),
new Size(_blocksize,_blocksize)),
GraphicsUnit.Pixel);
}
g.Dispose();
}
'draw the shape onto the parent background image
'with the blocksize passed in
Friend Sub DrawShape(_blocksize As Integer)
Dim img As Image = DirectCast(Parent, FallingBlocksBoard).BackgroundImage
Dim g As Graphics = Graphics.FromImage(img)
g.SmoothingMode = SmoothingMode.AntiAlias
For Each pt As Point In _points
Dim p As New GraphicsPath()
p.AddEllipse(pt.X * _blocksize + Location.X, pt.Y * _
_blocksize + Location.Y, _blocksize - 3, _blocksize - 3)
Dim br As New PathGradientBrush(p)
br.CenterColor = _color
br.SurroundColors = New Color() {_fillcolor}
g.FillPath(br, p)
g.DrawPath(New Pen(_fillcolor, 1), p)
br.Dispose()
p.Dispose()
Next
g.Dispose()
DirectCast(Parent, FallingBlocksBoard).Refresh()
End Sub
Friend Sub EraseShape(_blocksize As Integer)
Dim img As Image = DirectCast(Parent, FallingBlocksBoard).BackgroundImage
Dim _img As Image = DirectCast(Parent, FallingBlocksBoard).StoredImage
Dim g As Graphics = Graphics.FromImage(img)
'Graphics g=Graphics.FromHwnd(((FallingBlocksBoard)Parent).Handle);
g.SmoothingMode = SmoothingMode.AntiAlias
For Each p As Point In _points
g.DrawImage(_img, p.X * _blocksize + Location.X, p.Y * _blocksize + _
Location.Y, New Rectangle(New Point(p.X * _blocksize + Location.X, p.Y * _
_blocksize + Location.Y), New Size(_blocksize, _blocksize)), GraphicsUnit.Pixel)
Next
g.Dispose()
End Sub
事件
FallingBlocksB<code>oard
只定义了一个事件,即 GameOver
事件。该事件在游戏结束时触发(顾名思义)。
//Event Handler for Game Over
private EventHandler onGameOver;
//Method called to fire the onGameOver event
protected virtual void OnGameOver(EventArgs e)
{
if(this.onGameOver!=null)
this.onGameOver(this,e);
}
[Category("FallingBlocksEvent"),Description("Game Over Event Handler")]
public event EventHandler GameOver
{
add{onGameOver += value;}
remove{onGameOver -=value;}
}
...
if(CheckGameOver(_ts))
{
Controls.Remove(_ts);
_gameover=true;
_gameactive=false;
DrawBoard();
//Fire the OnGameOver Event
OnGameOver(null);
return;
}
...
'Event Handler for Game Over
Private onGameOver As EventHandler
<Category("FallingBlocksEvent"), Description("Game Over Event Handler")> _
Public Custom Event GameOver As EventHandler
AddHandler(ByVal value As EventHandler)
onGameOver = DirectCast([Delegate].Combine(onGameOver, value), EventHandler)
End AddHandler
RemoveHandler(ByVal value As EventHandler)
onGameOver = DirectCast([Delegate].Remove(onGameOver, value), EventHandler)
End RemoveHandler
RaiseEvent(ByVal sender As Object, ByVal e As System.EventArgs)
' no need to do anything in here since you will actually '
' not raise this event; it only acts as a "placeholder" for the '
' buttons click event '
End RaiseEvent
End Event
''Method called to fire the onGameOver event
Protected Overridable Sub _OnGameOver(ByVal sender As Object, ByVal e As EventArgs)
If SoundOn Then
Dim p As MciPlayer = GetSoundPlayer(AnimationType.EndGame)
If p IsNot Nothing Then
p.PlayFromStart()
End If
End If
'set to default num obstacles for obstacle play
_obstaclenum = 1
RaiseEvent GameOver(Me, e)
End Sub
控件加载注意事项
没有直接的方法可以知道您的控件是在设计时还是在运行时由 Visual Studio 加载的。我发现控件在设计时或运行时加载的顺序是:
- 默认构造函数
- 调整大小
BackgroundImage
属性OnControlCreated
由此可知,在调用 OnControlCreated()
方法时,BackgroundImage
属性已经被设置。
这一点很有用,因为我们希望确保不使用无效的 BackgroundImage
。
对于预览 FallingBlocksShape
对象,当 FallingBlocksBoard
首次加载时,我们可能希望用下一个选定的形状更新它,因为它(预览 FallingBlocksShape
对象)可能不持有有效形状。我们可以使用一个标志 _justloaded
,它默认为 true
,只有在调用 OnControlCreated()
方法(设置了 _created
标志)之后,并且在查询了 PreviewFallingBlocksShape
属性之后,才会将其设置为 false
。
if(_created)
{
//if the board is just loaded
//we ignore the properties of the _preview control
//as it may hold inappropriate values
if(this.PreviewFallingBlocksShape!=null && !_justloaded)
{
_preview=FallingBlocksShape.CreateShape(_previewcontrol.ShapeType,
_preview.BlockSize);
_preview.FallingBlocksColor=_previewcontrol.FallingBlocksColor;
// MessageBox.Show(""+_justloaded);
}
else
{
//MessageBox.Show(""+_justloaded);
_preview=GetNextPreviewShape();
if(_previewcontrol!=null){
_previewcontrol.ShapeType=_preview.ShapeType;
_previewcontrol.FallingBlocksColor=_preview.FallingBlocksColor;
}
}
_justloaded=false;
}
If _created Then
'if the board is just loaded
'we ignore the properties of the _preview control
'as it may hold inappropriate values
If Me.PreviewFallingBlocksShape IsNot Nothing AndAlso Not _justloaded Then
_preview = FallingBlocksShape.CreateShape(_previewcontrol.ShapeType, _preview.BlockSize)
' MessageBox.Show(""+_justloaded);
_preview.FallingBlocksColor = _previewcontrol.FallingBlocksColor
Else
'MessageBox.Show(""+_justloaded);
_preview = GetNextPreviewShape()
If _previewcontrol IsNot Nothing Then
_previewcontrol.ShapeType = _preview.ShapeType
_previewcontrol.FallingBlocksColor = _preview.FallingBlocksColor
End If
End If
_justloaded = False
End If
增强功能
自本文首次发布以来,已进行了一些增强:
- 重新映射键盘按键,允许用户使用其他按键控制
FallingBlocks
图块的移动。 - 暂停游戏。
- 为每场新游戏添加障碍物的选项。
- 自定义单元格的形状。
- 平滑动画选项。
- 自定义行清除动画。
- 添加网格线的选项。
- 播放声音和背景音乐的选项。
示例应用
新的示例应用程序演示了新功能。您可以按“s
”键开始游戏,按“e
”键结束游戏。空格键将切换暂停游戏。当选中“Obstacles”(障碍物)复选框时,新游戏将有一些单元格显示为白色作为障碍物。您还可以单击每个复选框/单选按钮来尝试不同的平滑动画、行清除动画和单元格形状选项。滑动 FallingBlocks
延迟滑块将立即更改游戏速度。
声音
在 Windows Forms 应用程序中播放声音文件至少有 3 种方法:
- 使用
System.Media.SoundPlayer
。 - 使用 Windows Media Player 控件。
- 使用 winmm.dll 调用
mciSendString
函数。
尽管 System.Media.SoundPlayer
最易于使用,但它只能播放 wave 文件,并且一次只能播放一个文件。Windows Media Player 控件是一个 COM 组件,而不是 .NET 程序集,存在互操作开销。
尽管不是 .NET 组件,但 winmm.dll 随 Windows 操作系统一起提供,并且使用它有很多优点:
- 占用空间小。无需分发额外的 DLL。
- 支持多种多媒体格式,包括 MP3 和 wav。
- 支持同时播放多个媒体文件。
我创建了一个包装器类来封装所需的功能。
FallingBlocks.MciPlayer
类的构造函数
MciPlayer(string filename, string alias)
接收媒体(mp3 或 wav)的完整路径文件名和一个指定的别名。
string appdir = Application.StartupPath;
FallingBlocksSoundPlayer p = new FallingBlocksSoundPlayer();
switch (arr[i])
{
case AnimationType.Blink:
p.Animationtype = AnimationType.Blink;
if (!System.IO.File.Exists(appdir + @"\Blink.mp3")) break;
p.Player = new MciPlayer(appdir + @"\Blink.mp3","Blink");
break;
Dim appdir As String = Application.StartupPath
Dim p As New FallingBlocksSoundPlayer()
Select Case arr(i)
Case AnimationType.Blink
p.Animationtype = AnimationType.Blink
If Not System.IO.File.Exists(appdir & "\Blink.mp3") Then
Exit Select
End If
p.Player = New MciPlayer(appdir & "\Blink.mp3", "Blink")
Exit Select
''''
End Select
在上面的代码中,为每个动画创建一个播放器。对于行清除动画“Blink
”,我们在当前目录中查找 Blink.mp3 文件。如果找到文件,我们使用文件名和别名“Blink
”创建一个新的 MciPlayer
对象,用于为此动画播放声音。
MciPlayer
的方法是:
LoadMediaFile(string filename, string alias)
PlayFromStart()
PlayLoop()
StopPlaying()
CloseMediaFile()
当创建新的 MciPlayer
时,会调用 LoadMediaFile
。如果成功加载,就可以调用其余方法。
PlayFromStart()
从头到尾播放一次媒体文件。PlayLoop()
在媒体文件播放到结尾时再次从头开始播放。StopPlaying()
停止播放文件。如果想再次播放媒体,可以在不重新加载的情况下调用其中一个播放方法。CloseMediaFile()
卸载媒体文件。如果想再次播放文件,必须使用LoadMediaFile
函数重新加载。
结论
编写游戏不仅有益,而且有助于程序员快速掌握编写游戏的语言。我多次这样做,当我想要学习 Visual Basic、Turbo Pascal 和 Java 时。事实上,这个 .NET 版的俄罗斯方块游戏的最初目的是为了学习 C#。
我希望读者不仅能享受游戏,还能从了解游戏的每个功能是如何实现的感到满意。
历史
- 2014年5月28日:
FallingBlocks
V1 - 2014年5月29日:
FallingBlocks
V1a:修复了小错误,并将“I
”形状缩短为 3 个单元格,以便更容易玩。 - 2014年5月30日:增加了 2 种新的行清除动画:吸入式和爆炸式。
- 2014年6月1日:
FallingBlocks
V2:增加了声音、网格线和背景音乐。 - 2014年6月3日:
FallingBlocks
V2b。专门使用 winmm.dllmciSendString
播放音乐/声音。 - 2014年7月2日:添加了 VB.NET 源代码。
参考文献
- “Explode”(爆炸)行清除动画使用了 Code Project 文章 A Basic Particle System 的代码(编译为 Particle.dll)。
- 声音文件来自 Sound Bible。