65.9K
CodeProject 正在变化。 阅读更多。
Home

下落的方块板和形状控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (51投票s)

2014年5月28日

CPOL

12分钟阅读

viewsIcon

107717

downloadIcon

3102

将这个最受欢迎的经典游戏作为 .NET 自定义控件实现,配有动画和声音,带来完整的游戏体验。

引言

又一个俄罗斯方块克隆。

是的,这是最棒的游戏之一,它几乎为程序员提供了无限的可能性,唯一的限制就是程序员的想象力和能力。在这篇文章中,我将与读者分享我的游戏版本。

背景

游戏名为 Falling Blocks。它包含两个自定义控件:

  • FallingBlocks Board
  • FallingBlocks Shape

FallingBlocks Board 控件是主控件。它可以拖放到窗体上,当窗体运行时,可以通过单击控件来启动游戏。游戏激活期间,单击控件会结束游戏。

FallingBlocks Shape 控件通常可以用于两个目的:

  1. 作为即将到来的 FallingBlocks 图块的预览。
  2. 限制 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 类型,只需:

  1. FallingBlocksShapeType enum 添加一个新的形状定义。
  2. 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

传递到构造函数的 colorblocksizeblockshape 用于渲染对象。

默认情况下,FallingBlocks<code>Shape 对象是不可见的。它的主要目的是作为 FallingBlocksBoard 使用的各种属性的容器。棋盘调用 FallingBlocksShape 的绘图函数:EraseShape()DrawShape() 分别擦除和绘制 FallingBlocks 图块。

FallingBlocksShape 对象的另一个用途是作为下一个即将到来的 FallingBlocks 图块的预览。为此,该对象必须与 FallingBlocksBoardPreviewFallingBlocksShape 属性关联。用作此目的时,FallingBlocksShape 将是可见的,并通过其重写的 OnPaint() 方法进行自渲染。

FallingBlocks 游戏默认的运行时间行为是,可以从 FallingBlocksShapeType enum 中定义的任何形状中选择。然而,当在设计时将 FallingBlocksShape 对象拖放到 FallingBlocksBoard 中时,此行为会发生变化。当一个 FallingBlocksShape 包含在 FallingBlocksBoard 中并且未与 PreviewFallingBlocksShape 属性关联时,它将承担限制棋盘上可玩形状类型的新用途。只有这些包含的形状才会出现在运行时。

FallingBlocks Board

它是一个 FallingBlocks 单元格网格。每个 FallingBlocks 单元格都有一个 coloravail 属性。最初,单元格的 color 设置为 Blackavail 设置为 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() 方法来更新被形状覆盖的单元格的状态(coloravail),以便下次绘制棋盘时,这些单元格将以着陆形状的颜色绘制。

包含

FallingBlocksBoard 控件在窗体中创建时,会调用 FallingBlocksShapeOnCreateControl() 方法。当窗体加载并完成加载 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>BoardPreviewFallingBlocksShape 属性用于向 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 加载的。我发现控件在设计时或运行时加载的顺序是:

  1. 默认构造函数
  2. 调整大小
  3. BackgroundImage 属性
  4. 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 种方法:

  1. 使用 System.Media.SoundPlayer
  2. 使用 Windows Media Player 控件。
  3. 使用 winmm.dll 调用 mciSendString 函数。

尽管 System.Media.SoundPlayer 最易于使用,但它只能播放 wave 文件,并且一次只能播放一个文件。Windows Media Player 控件是一个 COM 组件,而不是 .NET 程序集,存在互操作开销。

尽管不是 .NET 组件,但 winmm.dll 随 Windows 操作系统一起提供,并且使用它有很多优点:

  1. 占用空间小。无需分发额外的 DLL。
  2. 支持多种多媒体格式,包括 MP3 和 wav。
  3. 支持同时播放多个媒体文件。

我创建了一个包装器类来封装所需的功能。

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.dll mciSendString 播放音乐/声音。
  • 2014年7月2日:添加了 VB.NET 源代码。

参考文献

© . All rights reserved.