65.9K
CodeProject is changing. Read more.
Home

Falling Blocks Board and Shape Control

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (51 votes)

May 28, 2014

CPOL

12 min read

viewsIcon

106107

downloadIcon

3044

Implementing the all time favourite game as .NET custom controls, complete with animation and sound for full gaming experience

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.

 //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

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

  //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	

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:

    //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)

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:

    //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

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.

    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

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.

    //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

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.

    ...
    _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

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.

    //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

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.

    //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

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.

    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

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.

    //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

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.

    //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

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.

    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

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:

MciPlayer(string filename, string alias)

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

  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

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.