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:
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.
IsFrozen
as Boolean - This property is used to prevent pieces from moving after they have landed and should not be able to move anymore.
Color
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
.
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.
MoveLeft
()
- This method calls GetNewPoints
and replaces the points currently stored in the shape object with the points returned from GetNewPoints
.
MoveRight
()
- This method calls GetNewPoints
and replaces the points currently stored in the shape object with the points returned from GetNewPoints
.
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 ShapeType
s 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
Dim x, y As Integer
x = Points(2).X
y = Points(2).Y
If (Points(0).X = Points(3).X - 2) Then
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
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
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
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
Dim x, y As Integer
x = Points(2).X
y = Points(2).Y
If (Points(0).X = x - 1) Then
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
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
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
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
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
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
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
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
For i As Integer = 0 To 19
counter(i) = 0
Next
For Each shp As Shape In boardPieces
For Each pnt As Point In shp.Points
counter(pnt.Y) = counter(pnt.Y) + 1
Next
Next
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 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
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
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
shp.IsFrozen = True
Next
End If
Next
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