Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Falling Blocks Board and Shape Control

4.86/5 (52 votes)
2 Jul 2014CPOL12 min read 106.1K   3K  
Implementing the all time favourite game as .NET custom controls, complete with animation and sound for full gaming experience

Image 1

Introduction

Yet another Tetris clone.

Yes, it is one of the best games that offers the programmer with virtually boundless possibilities limited only by the programmer's imagination and ability. In this article, I will be sharing with the reader my version of the game.

Background

The name of the game is Falling Blocks. It comes in the form of two custom controls:

  • FallingBlocks Board
  • FallingBlocks Shape

The FallingBlocks Board control is the main control. It can be dragged and dropped into a form and when the form is run, the game can be activated by clicking on the control. And while the game is active, clicking on the control ends the game.

The FallingBlocks Shape control can be used for generally two purposes:

  1. To serve as a preview for oncoming FallingBlocks pieces.
  2. To restrict the number of shapes in the FallingBlocks Board.

Each FallingBlocks Board is made up of an array of FallingBlocks cells, which collectively hold the status of the board. Each FallingBlocks Shape is assigned a FallingBlocks Shape type, which is an enumeration of the various FallingBlocks shapes.

FallingBlocks Shape type

Currently, the shapes defined are in the FallingBlocksShapeType enum.

C#
//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
}
VB.NET
'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

Image 2

A FallingBlocks<code>Shape object is created based on the FallingBlocksShapeType parameter passed to the CreateShape() method:

C#
//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;.....
VB.NET
'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	

To create a new FallingBlocksShape type, simply:

  1. add a new shape definition to the FallingBlocksShapeType enum
  2. add a new case block for the new shape in the CreateShape() method

FallingBlocks Shape

There are two constructors for FallingBlocksShape:

C#
//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)
VB.NET
'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)

A FallingBlocksShape object is defined by a set of points and an optional privot point. The properties for each point are:

  • color
  • the size of each cell block
  • the cell shape of each cell block

For example, the IShape FallingBlocksShape is defined by the four points (0,0) (1,0) (2,0) (3,0). The coordinates are based on screen coordinates with x-coordinate increasing to the right and y-coordinate increasing downwards. These numbers indicate block units, not pixel units.

The pivot point is used for the purpose of rotation. It is the point that will stay invariant by the rotation operation. For example, the pivot for the IShape object is (1,0). After being rotated by 90 deg anticlockwise, it will remain at (1,0) while the rest of the points change. The diagram and the code below illustrate how the rotation is performed:

Image 3

C#
//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;
}
VB.NET
'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

The color, blocksize and blockshape passed into the constructor are used for the purpose of rendering the object.

The FallingBlocks<code>Shape object by default is not visible. Its main purpose is to serve as a holder of various properties used by the FallingBlocksBoard. The board calls the FallingBlocksShape drawing functions: EraseShape() and DrawShape() to erase and paint the FallingBlocks pieces respectively.

The other use of FallingBlocksShape object is to serve as a preview for the next oncoming FallingBlocks piece. For this purpose, the object must be associated with the PreviewFallingBlocksShape property of the FallingBlocksBoard. When used as such, the FallingBlocksShape will be visible and is self rendering using its overridden OnPaint() method..

The default play time behaviour of the FallingBlocks game is such that any of the defined shapes in the FallingBlocksShapeType enum can be selected. However, this behaviour changes when the FallingBlocksShape objects are dragged into the FallingBlocksBoard during design time. When a FallingBlocksShape is contained in the FallingBlocksBoard and is not associated with the PreviewFallingBlocksShape property, it takes on a new purpose of restricting the type of shapes that could be played on the board. Only these contained shapes will appear during play time.

FallingBlocks Board

It is a grid of FallingBlocks cells. Each FallingBlocks cell has a color and avail property. Initially the cell color is set to Black and avail to true.

The color property indicates the color to be used for rendering the cell. Each time the board is drawn, it repaints all the cells which have avail value set to false.

When a FallingBlocks piece is ready for play, it is placed at the top of the board. Each of the points on the FallingBlocksShape will overlap a cell in the FallingBlocksBoard. For a FallingBlocks piece to be in a valid location, each overlapped cell must have an avail property with a true value. If this condition is not met, then the FallingBlocks piece cannot be played and the game ends.

The board also contains a timer which controls the speed of the pieces downward movement. The speed can be adjusted using the FallingBlocksDelay property. It has a valid range of 100 - 1000 ms. The larger the FallingBlocksDelay value, the slower the speed of play.

The board also contains a textbox whose width is set to 0 to make it virtually invisible. The reason for using the textbox is because the FallingBlocks Board which is derived from the Panel class has no keyboard event handler. The textbox serves the purpose of capturing the keyboard events. The keys used for the play are arrow keys:<(left) for moving the piece left, >(right) for moving right, ^(up) for 90 deg anti-clockwise rotation and V (down) for fast landing.

When a piece is to be moved, the board calls the shape's EraseShape() method and moves the piece to a valid position, then redraws the piece by calling the shape's DrawShape() method.

When a piece has landed (about to hit a cell below with avail value false), the shape's Erase() method is called, and the board calls its PasteShape() method to update the status (color and avail) of cells overlapped by the shape, so that when the board is next redrawn, these cells will be painted with the landed shape's color.

Containment

The FallingBlocksShape's OnCreateControl() method is called when the FallingBlocksBoard control is being created in the form. This happens when the form is loaded and has completed loading the FallingBlocksBoard control. The control's Controls collection is queried for any FallingBlocksShape objects and these objects are added to the arrShape ArrayList.

C#
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);
    }
  }

.....
VB.NET
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

In the NewShape() method which creates a new FallingBlocksShape piece for play, the arrShape list is queried. If the arrShape list contains any FallingBlocksShape object, then the next shape would be taken from this list, else it would be picked from any of the defined shapes in the FallingBlocksShapeType enum.

C#
//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;
}
VB.NET
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

Preview

The FallingBlocks<code>Board's PreviewFallingBlocksShape property is used to indicate to the FallingBlocksBoard about the presence of a FallingBlocksShape serving as a preview for the oncoming pieces. If a preview FallingBlocksShape object is present, its various properties are set to the shape selected and its Visible property is set to true so that the FallingBlocksShape object can render itself.

C#
...
_preview=GetNextPreviewShape();
if(this.PreviewFallingBlocksShape !=null)
{
    this._previewcontrol.Visible =true;
    this._previewcontrol.ShapeType =_preview.ShapeType;
    this._previewcontrol.FallingBlocksColor=this._preview.FallingBlocksColor;
}
...
VB.NET
_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

Movements

In each tick event of the timer, the FallingBlocks piece is moved down by the MoveDown() method.

The player can also use the keyboard arrow keys or call the public FallingBlocksMove..() methods to cause the current piece to move.

Each time the piece is to be moved, a clone is created and moved. The clone's new position is validated by the IsValidPosition() method. If not validated, the original piece is returned, else the clone piece will be returned with its updated position.

C#
//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;
}
VB.NET
 '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

Rendering Background Image and Fallingblocks Board

The BackgroundImage property is one of the control properties, which is quite difficult to manage. This property is persistent. Visual Studio saves the image every time it is updated or when the design time form is closed for whatever purpose. I have noted that Visual Studio closes a design time form every time before it runs it. Thus, persistent BackgroundImage property will always be saved.

The advantage of directly painting on the Background image is that you have direct control as to when you want your drawing to be updated. This allows for some form of buffered update. You paint on the Background image and when all the painting is done, you can call the Refresh() method to show all the updates at once. This technique allows you to create smoother animation.

One problem with the Background image is that it will be tiled if the image does not fit exactly into the control. To solve this problem, the ResizeImage() is called when any image is to be assigned to the BackgroundImage property.

To assist in the updating of the background image, two images: _baseimage and _cleanbase are used. Both are copies of the Background image at design time.

_baseimage is updated with the score during run time. Each time the score changes, _cleanbase is copied to _baseimage (to erase the previous score), and then the _baseimage is updated with the latest score. It is then copied to Background image via the DrawPicture() method.

All the cells in FallingBlocks<code>Board are then painted on the Background image.

C#
//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();
}
VB.NET
'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

Rendering Shape

When a FallingBlocks<code>Shape object is visible, it is painted by its OnPaint() method. This is applicable for the FallingBlocksShape object that is used for preview purposes.

C#
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);
}
VB.NET
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

For those FallingBlocksShape objects that are used otherwise, they are not rendered directly. Rendering is done by the FallingBlocksBoard onto its Background image by calling DrawShape() and EraseShape() methods.

C#
//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();
}
VB.NET
'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

Events

There is only one event that is defined for the FallingBlocksB<code>oard. It is the GameOver event which is fired (as the name suggests) when the game is over.

C#
//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;
}
...
VB.NET
'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

Control Loading Considerations

There is no direct way to tell if your controls are loaded by Visual Studio for design-time purposes or run time. I found out that the order in which the control is loaded at design time or run time is:

  1. Default constructor
  2. Resize
  3. BackgroundImage property
  4. OnControlCreated

From here, we know that by the time OnControlCreated() method is called, the BackgroundImage property has already been set.

This is useful because we want to make sure that we do not make use of a BackgroundImage that is invalid.

For the preview FallingBlocksShape object, we may want to update it with the next selected shape when the FallingBlocksBoard is first loaded as it (the preview FallingBlocksShape object) may not be holding a valid shape. We can make use of a flag _justloaded which is default to true and is only set to false after OnControlCreated() method (_created flag set) is called and after having queried the PreviewFallingBlocksShape property.

C#
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;

}
VB.NET
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

Enhancements

Some of the enhancements made since the first release of this article are:

  • The remapping of the keyboard keys to allow the user to use other keys to control FallingBlocks pieces movements.
  • Pausing the game.
  • Option to add obstacles for each new game.
  • Customizing the shape of cells.
  • Option for smooth animation.
  • Customizing the row removal animation.
  • Option to add in grid lines
  • Option to play sound and background music

Sample Application

The new sample application allows for the new features to be demonstrated. You can key "s' to start the game, and "e" to end it. The space bar will toggle, pausing the game. When the Obstacles checkbox is checked, the new games will have some cells colored white as obstacles. You can also click each of the check boxes/radio buttons to experiment with different options of smooth animation, row removal animation and cell shapes. Sliding the FallingBlocks delay track bar will cause the speed of the game to change immediately.

Sound

There are at least 3 ways to play sound files in Windows Forms Application:

  1. Using System.Media.SoundPlayer
  2. Using the Windows Media Player control
  3. Using winmm.dll calling the mciSendString function

Although the easiest to use, System.Media.SoundPlayer can only play wave files and only one file can be played at any one time. Windows Media Player control is a COM component, not a .NET assembly and there are Inter-operability overheads involved.

Although not a .NET component, winmm.dll comes with Windows OS and there are many advantages to using it:

  1. Footprint is small. No additional DLL needed to be distributed
  2. Support many Multimedia format, including MP3 and wav
  3. Support multiple media files to be played simultaneously

I have created a wrapper class to encapsulate the required functionalities:

The FallingBlocks.MciPlayer class constructor:

C#
MciPlayer(string filename, string alias)

takes in a media (mp3 or wav) full path file name and an assigned alias.

C#
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;
VB.NET
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

In the code above, one player is created for each animation. For the row removal animation "Blink", we look for the Blink.mp3 file in the current directory. If the file is found, we use the filename and the alias "Blink" to create a new MciPlayer object to be used for playing sound for this animation.

The methods of MciPlayer are:

  • LoadMediaFile(string filename, string alias)
  • PlayFromStart()
  • PlayLoop()
  • StopPlaying()
  • CloseMediaFile()

When a new MciPlayer is created, LoadMediaFile is called. If successfully loaded, the rest of the methods can be called.

  • PlayFromStart() plays the media file from the start to the end once.
  • PlayLoop() plays the media file from the start once again when it reaches the end.
  • StopPlaying() stops the file from playing. If we want to play the media again, we can call either of the play methods without reloading.
  • CloseMediaFile() unloads the media file. If we want to play the file again, we have to load it again with the LoadMediaFile function

Conclusion

Game writing is not only rewarding but it also helps the programmer to quickly pick up the intended language for writing the game. I had done this many times when I wanted to pick up Visual Basic, Turbo Pascal and Java. In fact, the original intention for this .NET version of the Tetris game is to pick up C#.

I hope that the reader would not only enjoy playing the game, but also get satisfaction from finding how each feature of the game is implemented.

History

  • 28th May, 2014 : FallingBlocks V1
  • 29th May, 2014: FallingBlocks V1a: Fix minor bugs and shorten the "I" shape to 3 cells for easier play
  • 30th May, 2014: Added 2 more row removal animations: Suck-In and Explode
  • 1st June, 2014: FallingBlocks V2: Added in sound, grid lines and background music
  • 3rd June, 2014: FallingBlocks V2b. Use winmm.dll mciSendString to play music/sound exclusively
  • 2nd July, 2014: Add in VB.NET source codes

References

  • The "Explode" row removal animation makes use of the code (complied as Particle.dll) from the Code Project article, A Basic Particle System.
  • The sound files are from Sound Bible.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)