Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Basics of a Falling Blocks Game in VB.NET 2005

0.00/5 (No votes)
12 Mar 2008 1  
A remake of the classic falling blocks game in a modern language
fallingblocks_small.GIF

Introduction

The beloved game of falling blocks was once a very popular game, but now it has been overshadowed by a never-ending line of games that just keep coming. I first started this program last month when my Advanced Visual Basic programming course decided to re-create this classic. After spending quite a few class hours looking for code samples and brainstorming aspects of the program, the task was deemed too difficult for the class as a whole. There were many examples of the game, but none were open source or explained how the program could be built. However, I could not just let this great program go without a fight. The result is what you see before you: a basic two-player falling blocks game that encompasses all the basic functionality of the classic. This tutorial will guide you through the creation of this program from start to finish, explaining every detail possible.

The Concept

You may believe that the concept to falling blocks is very simple. However, there are many aspects that need to be covered. Some of the concepts that need to be thought of are:

  • What dimensions must the falling blocks board be?
  • Do you inter-mingle the logical and graphical aspects of the program?
  • How do you toggle the pieces?
  • How do you store the pieces on the board?
  • How do you draw the different pieces on to the board?
  • How do you animate the pieces?
  • How do you manage completed lines?

This is just a sampling of all the questions that must be asked before you can start the falling blocks making process. A few of the questions can be answered with simple Google searches. For example, the official dimension of a falling blocks board is 10x20. For storing pieces, I can only think of one way to store everything... arrays... or for a more maneuverable approach, a collection. For this program, I use the Collection(Of type) class in the System.Collections.ObjectModel namespace.

The last question that can really be answered right now is the inter-mingling of the logical and graphical aspects of the program. When my programming class initially chose falling blocks, one of the goals was to move the program to XNA so the game could be transferred to the XBOX 360. Of course, this will not happen as the class has moved to a different program, but to help facilitate this I wrote this program with the logical and graphical aspects separate. This way, only the graphical layer had to be changed when the program was transferred to XNA.

Logical Layer

To begin the logical layer, we must now answer the question, "How do you store pieces on the board?" As a class, we started by adding one point at a time to the board and then manipulating the four points for the movement of the pieces. This almost immediately led to cumbersome code that would inevitably lead to long and confusing code statements. To correct this issue, we decided to create a class called Shape. This class is basically the heart of the logical layer. Here is the basic outline for the shape class:

prop Points as Collection(Of Point) - This property stores a logical grid of all the points for the current shape. Every shape has a total of four points and each point is stored in this property. Since this is in the logical portion of the program, these points do not need to abide by the pixel coordinates throughout the board. The logical grid is a 10x20 grid where the top of the board is at coordinate 20 and the far right of the board is at coordinate 10.

prop IsFrozen as Boolean - This property is used to prevent pieces from moving after they have landed and should not be able to move anymore.

propColor as Color - Every piece has a different color and this is where the color is stored. Even though one color is assigned to this property, the graphical layer can manipulate the usage of of this color. This application uses this color and color variations through the ControlPaint.Light(c as Color) as Color method to create a LinearGradientBrush.

private method GetNewPoints(direction as NewDirection) as Collection(Of Point) - This function will return a new set of points of where the shape will be if a shape is to move in any direction in the NewDirection enumeration. The NewDirection enumeration contains the following directions: left, right, down, current, toggleOrientation. Movement in this method will be discussed later in the article.

method MoveLeft() - This method calls GetNewPoints and replaces the points currently stored in the shape object with the points returned from GetNewPoints.

method MoveRight() - This method calls GetNewPoints and replaces the points currently stored in the shape object with the points returned from GetNewPoints.

method ToggleOrientation() - This method calls GetNewPoints and replaces the points currently stored in the shape object with the points returned from GetNewPoints.

WillCollide(shp as Shape, direction as NewDirection) as Boolean - I got the inspiration of this function from my AP programming class' Marine Biology case study. This method will check the logical grid of two shapes and check for overlaps. If an overlap in points occurs, then the function will return false. Otherwise, it will return true.

WillCollide(direction as NewDirection) as Boolean - This function is an overload of the first WillCollide function. This overload only deals with the placement of the piece on the logical grid, which is 10x20. If the new direction results in the piece going off the board either horizontally or vertically, this function will return false. Otherwise, it will return true.

Movement of the Shape Object

As explained in the outline above, the GetNewPoints function is one of the main parts of the shape class. The GetNewPoints function basically helps answer the question, "How do you toggle the pieces?" When the shape class is instantiated, a ShapeType is passed through the constructor. The ShapeType enumeration contains all of the shapes that are in falling blocks. The shapes included are: square, pyramid, staircaseLeft, staircaseRight, lshapeLeft, lshapeRight, line and null. All the ShapeTypes are pretty much self-explanatory, but null is the exception and it will be explained later in this article. GetNewPoints is basically a huge Select Case statement that determines how a piece should be moved. The basic part of GetNewPoints is the moving left, right and down.

Dim rval As New Collection(Of Point)
Select Case direction
       Case NewDirection.current
            Return Me.Points
       Case NewDirection.down
            For Each pnt As Point In Me.Points
                rval.Add(New Point(pnt.X, pnt.Y - 1))
            Next
       Case NewDirection.left
            For Each pnt As Point In Me.Points
                rval.Add(New Point(pnt.X - 1, pnt.Y))
            Next
       Case NewDirection.right
            For Each pnt As Point In Me.Points
                rval.Add(New Point(pnt.X + 1, pnt.Y))
            Next
...

As you can tell, the code is fairly straightforward. If the shape moves left, you subtract 1 from the x-value of every point. If the shape moves right, you add 1 to the x-value of every point. Lastly, if the shape moves down, you subtract 1 from the y-value of every point. Keep in mind that this code is still in the logical layer where x is any value between 0 and 10 and the y value is any value between 0 and 20. The code gets a bit more difficult with the ToggleOrientation direction.

Select Case direction
...
        Case NewDirection.toggleOrientation
            Select Case shapeType
...
                    Case falling blocks.ShapeType.lshapeLeft
                        'toggle point is 3rd cube
                        Dim x, y As Integer
                        x = Points(2).X
                        y = Points(2).Y
                        'find the farrest point from center
                        If (Points(0).X = Points(3).X - 2) Then 'pointing left 
                            'switch to up
                            rval.Add(New Point(x, y + 2))
                            rval.Add(New Point(x, y + 1))
                            rval.Add(New Point(x, y))
                            rval.Add(New Point(x - 1, y))
                        ElseIf (Points(0).Y = Points(3).Y + 2) Then 'pointing up 
                                 'switch to right
                            rval.Add(New Point(x + 2, y))
                            rval.Add(New Point(x + 1, y))
                            rval.Add(New Point(x, y))
                            rval.Add(New Point(x, y + 1))
                        ElseIf (Points(0).X = Points(3).X + 2) Then 'pointing 
                                 'right switch to down
                            rval.Add(New Point(x, y - 2))
                            rval.Add(New Point(x, y - 1))
                            rval.Add(New Point(x, y))
                            rval.Add(New Point(x + 1, y))
                        ElseIf (Points(0).Y = Points(3).Y - 2) Then 'pointing down 
                                 'switch to left
                            rval.Add(New Point(x - 2, y))
                            rval.Add(New Point(x - 1, y))
                            rval.Add(New Point(x, y))
                            rval.Add(New Point(x, y - 1))
                        End If
...
                    Case falling blocks.ShapeType.pyramid
                        'toggle point is 3rd cube
                        Dim x, y As Integer
                        x = Points(2).X
                        y = Points(2).Y
                        If (Points(0).X = x - 1) Then 'pointing left switch to up
                            rval.Add(New Point(x, y + 1))
                            rval.Add(New Point(x - 1, y))
                            rval.Add(New Point(x, y))
                            rval.Add(New Point(x + 1, y))
                        ElseIf (Points(0).Y = y + 1) Then 'pointing up switch to right
                            rval.Add(New Point(x + 1, y))
                            rval.Add(New Point(x, y + 1))
                            rval.Add(New Point(x, y))
                            rval.Add(New Point(x, y - 1))
                        ElseIf (Points(0).X = x + 1) Then 'pointing right switch to down
                            rval.Add(New Point(x, y - 1))
                            rval.Add(New Point(x - 1, y))
                            rval.Add(New Point(x, y))
                            rval.Add(New Point(x + 1, y))
                        ElseIf (Points(0).Y = y - 1) Then 'pointing down switch to left
                            rval.Add(New Point(x - 1, y))
                            rval.Add(New Point(x, y - 1))
                            rval.Add(New Point(x, y))
                            rval.Add(New Point(x, y + 1))
                        End If
...

As you can tell by this code statement, it does get a bit more complicated. I chose two code excerpts for toggling the pyramid and lshapeleft. For both of these, each point is manipulated based on a focal point that (at least with how the code is written) is always the third point in the Points collection. I compiled this code through trial and error, so there may be more efficient ways to accomplish this same code. To sum up this code though, you find the focal point, the direction that the shape is currently pointing, and then you manipulate the other points to change the direction that the shape points in.

Collision Detection

The collision system for the shape class only handles collisions between two shape controls at one time. This is accomplished through the overloaded WillCollide(...) functions. Quite simply, the two functions are just nested for loops that check for similar points amongst two controls or points that go outside of the logical grid. The two functions make use of the GetNewPoints(...) method to check the Shape control movement before it actually moves. These collision detection methods must be called before the piece is actually moved to make sure the move is legal.

Public Function WillCollide(ByVal shape As Shape, ByVal direction As NewDirection)_ 
    As Boolean

    Dim pntsToCheck As Collection(Of Point) = GetNewPoints(direction)
    For Each pnt As Point In shape.Points
        For Each pnts As Point In pntsToCheck
            If (pnt.Equals(pnts)) Then
                Return True
            End If
        Next
    Next
End Function
Public Function WillCollide(ByVal direction As NewDirection) As Boolean
    Dim pntsToCheck As Collection(Of Point) = GetNewPoints(direction)
    For Each pnt As Point In pntsToCheck
        If (pnt.X < 0 Or pnt.X > 9 Or pnt.Y > 19 Or pnt.Y < 0) Then
            Return True
        End If
    Next
End Function

The Game Board

As of right now, we can answer 3 of the 7 questions stated at the beginning of the article. Through the Board user control, we can now answer the last four: "How do you store pieces on the board?", "How do you draw the different pieces on to the board?", "How do you animate the pieces?" and "How do you manage completed lines?"

First off, "How do you store pieces on the board?" This is a very similar question to how to store the logical points of the shape class. The main thinking should be arrays, but the Collection(Of type) adds better functionality with regards to adding/removing/manipulating of the objects stored in it. For the Board control, I have added a private variable aptly named boardPieces, which is of type Collection(Of Shape).

The second question ,"How do you draw the different pieces on the board?" is basically the entire graphical layer. For this Board object, the paint event has been handled with the following code:

Private Sub Board_Paint(ByVal sender As Object,_ 
            ByVal e As System.Windows.Forms.PaintEventArgs)_ 
            Handles Me.Paint

    For x As Integer = 0 To 9
        For y As Integer = 0 To 19
            e.Graphics.DrawRectangle(Pens.Black, x * 20, y * 20, 20, 20)
        Next
    Next
    For Each shp As Shape In boardPieces
        For Each pnt As Point In shp.Points
            e.Graphics.FillRectangle(New LinearGradientBrush(_
         New Rectangle(0, 0, 19, 19),_
         shp.Color,_ControlPaint.Light(shp.Color), _
         LinearGradientMode.BackwardDiagonal), _
                   (9 - (9 - pnt.X)) * 20 + 1, (19 - pnt.Y) * 20 + 1, 19, 19)
        Next
    Next
End Sub

This code very simply loops through each point of each shape and draws a rectangle based on the logical grid location of the shapes' points onto the Board control. This is the only segment of code that deals with the pixel coordinates in this falling blocks program. This one segment of code is all that would have to be replaced if it were transferred to another graphical framework.

Animation of the Game Board Pieces

The second to the last question is a very simple question, "How do you animate the pieces?" At the moment, this program does not use the Windows Presentation Foundation of the .NET 3.0 framework, so one of the few ways to accomplish such animation is through the use of timers. For this game board, two timers have been added: tmrMove and tmrIncreaseTmr. tmrMove, as you may have guessed, is the timer that drops all the pieces down from the top of the falling blocks board. tmrIncreaseTmr is just used with each level to increase the speed of the tmrMove timer. tmrMove_Tick is the only method in the Board control that creates new shapes and manages collision detection for the down movement.

Private Sub tmrMove_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs)_
     Handles tmrMove.Tick

    If (paused) Then
        Return
    End If
    If (currentPieceIndex = -1) Then
        'initially create a piece
        Dim randVal As Integer = randPieceGen.Next(0, 7)
        Dim shp As Shape
        Select Case randVal
            Case 0
                shp = New Shape(ShapeType.line)
            Case ...
        End Select
        'set the nextPiece property
        randVal = randPieceGen.Next(0, 7)
        Select Case randVal
            Case 0
                _nextPiece = ShapeType.line
            Case ...
        End Select

        boardPieces.Add(shp)
        currentPieceIndex = boardPieces.IndexOf(shp)
    ElseIf (boardPieces(currentPieceIndex).IsFrozen) Then
        'if frozen then create a new piece
        Dim randVal As Integer = randPieceGen.Next(0, 7)
        Dim shp As Shape = New Shape(_nextPiece)

        randVal = randPieceGen.Next(0, 7)
        Select Case randVal
            Case 0
                _nextPiece = ShapeType.line
            Case ...
        End Select

        For Each shap As Shape In boardPieces
            If (shp.WillCollide(shap, NewDirection.down)) Then
                Me.tmrMove.Enabled = False
                RaiseEvent GameOver(Me, New EventArgs())
                Return
            End If
        Next
        boardPieces.Add(shp)
        currentPieceIndex = boardPieces.IndexOf(shp)
    Else
        'this is the overall movement and scoring along with the arrow key methods
        Dim boolCollision As Boolean = False
        boolCollision = boardPieces(currentPieceIndex).WillCollide(NewDirection.down)
        For Each shp As Shape In boardPieces
            If (Not shp Is boardPieces(currentPieceIndex)) Then
                If (Not boolCollision) Then
                    boolCollision = boardPieces(currentPieceIndex).WillCollide(shp,_
             NewDirection.down)
                End If
            End If
        Next
        If (Not boolCollision) Then
            boardPieces(currentPieceIndex).MoveDown()
            score = score + 5 * level
            RaiseEvent ScoreChange(Me, New EventArgs())
        Else
            boardPieces(currentPieceIndex).IsFrozen = True
            ManageCompleteLines()
        End If
    End If
    Me.Invalidate()
End Sub

As you should be able to see, the tmrMove_Tick event is divided into three segments. The first segment is if the game has just started: the method just adds a new shape control to the boardPieces collection. The second segment handles all other shape creations beyond the initial board setup. It is very similar to the first segment, but it also handles the GameOver event for if a shape is created that hits another piece upon creation.

The third segment handles a whole bunch more. Firstly, the segment checks to see if the current piece is capable of moving down or whether a collision will occur. If a collision occurs, then a method called ManageCompleteLines() is called and the piece gets frozen. If a collision does not occur, then the piece just moves down one. The very last line of this method is what results in a complete redraw of the game board. The redraw will reflect any changes just made to the boardPieces of the Board control. Methods were also created with a similar structure to segment three of this code for user input: MoveLeft, MoveRight, MoveDown, and ToggleOrientation.

Managing Complete Lines

As of right now, we have gone over all the code to create a functioning falling blocks game, but what about our last question? The very last question asked during the conceptual phase of the development of falling blocks was, "How do you manage completed lines?" This method is one of the most important parts of the program. Without this method, there would be no scoring or goal to the program.

Private Sub ManageCompleteLines()
    Dim counter(20) As Integer
    'instantiate everything
    For i As Integer = 0 To 19
        counter(i) = 0
    Next
    'get lines completed
    For Each shp As Shape In boardPieces
        For Each pnt As Point In shp.Points
            counter(pnt.Y) = counter(pnt.Y) + 1
        Next
    Next
    'score for every completed line
    Dim lineCount As Integer = 0
    For Each i As Integer In counter
        If (counter(i) = 10) Then
            lineCount += 1
        End If
    Next
    score = score + (20 * level * lineCount)
    RaiseEvent ScoreChange(Me, New EventArgs())

    'for all completed lines remove points
    For i As Integer = 0 To 19
        If (counter(i) = 10) Then
            Dim shapes As New Collection(Of Shape)
            For Each shp As Shape In boardPieces
                If (shp.IsFrozen) Then
                    'collect all points then remove them
                    Dim pntsToRemove As New Collection(Of Point)
                    For Ipnt As Integer = 0 To shp.Points.Count - 1
                        If (shp.Points(Ipnt).Y = i) Then
                            pntsToRemove.Add(shp.Points(Ipnt))
                        End If
                    Next
                    For Each pnt As Point In pntsToRemove
                        shp.RemovePoint(pnt)
                    Next
                End If
            Next
        End If
    Next
    'move lines down by moving only the points of the objects
    For i As Integer = 19 To 0 Step -1
        If (counter(i) = 10) Then
            For Each shp As Shape In boardPieces
                Dim moveDown As Boolean = False
                Dim pointsToMove As New Collection(Of Point)
                For Each pnt As Point In shp.Points
                    If (pnt.Y > i) Then
                        pointsToMove.Add(pnt)
                    End If
                Next
                For Each pnt As Point In pointsToMove
                    shp.MovePointDown(pnt)
                Next
                'If (moveDown And shp.WillCollide(NewDirection.down) = False) Then
                '    shp.MoveDown()
                'End If
                shp.IsFrozen = True
            Next
        End If
    Next
    'if the board is completely cleared add bonus points
    If (boardPieces.Count = 0) Then
        score = score + 1000 * level
    End If
End Sub

This code basically searches every point contained in every shape of boardPieces and accumulates the amount of points in each row. If a row contains 10 points, then the row is complete and the row should be removed. One of the great things about having shapes freeze when they can no longer move is that you can then manipulate the points in the shape without having to worry about more movement or toggling.

The last for...next method takes full advantage of this functionality by removing every point in a line by looping through the points of each shape. If the point is on the specified completed line, then the point is removed and the shape is moved down. Scoring for completed lines is also calculated in this method. Scoring (at least for this falling blocks game) is multiplied by the number of lines completed at one time and the level at which the lines were completed.

Using this Control

The Board control is a standard user control that can be dragged and dropped on any form that you so choose. The control exposes three events -- ScoreChange, LevelChange and GameOver -- and exposes methods to allow specialized input methods by the control that parents the board control. In this project, I have two boards on mainWindow. To manage the user input for each control, I created a single mainForm_KeyUp method that handles all input received through the keyboard. The input is divided to each control based on the key schemes W, A, S, D and the Up/Down/Left/Right arrow keys, as wells as a pause "P" button that is mapped to both players. The keys are then mapped to the appropriate methods ToggleOrientation, MoveDown, MoveLeft, and MoveRight.

Points of Interest

There are quite a few odds and ends of this program that I have not covered, but this should be enough explanation for others to begin their own falling blocks variations. Some aspects that have not been included, but would be really good features include high scores and loading/saving of games.

History

  • March 12th 2008 - Download and image updated
  • March 10th 2008 - Article edited and moved to The Code Project main article base
  • February 8th 2008 - Corrected issue with pressing W, A, S, D while in single player, fixed issue with the buttons on the main form toggling focus when playing the game with the arrow keys
  • February 7th 2008 - Initial public release
  • January 12th 2008 - Start of program

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here