Introduction
Recently, in one of the tech-blogs I usually read, I saw a post about the best-selling video games through history. The first place in the list is for the classic, extremely addictive Falling Blocks game.
Well, a long time ago, in a galaxy far far away, I was a highly-skilled Falling Blocks player. When I was 18 I worked as DJ in a disco-pub at my hometown. There was a Falling Blocks gaming machine, and I spent hours and hours playing on it. Many nights I saw the blocks falling every time I went to bed and closed my eyes.
It rained some since then, but I still believe that Falling Blocks is one of the best videogames ever made.
In 2000, I developed a Tetris ActiveX control for the Planet Source Code site using Visual Basic 6.0. With that control I won some prize in the Planet Source Code monthly contest (some software pack, I don't remember well). You can still see the page (and download the ActiveX control and its source code) here.
Now I think the time has come to write a .NET version, so this article presents to you the TetrisBox class: a VB.NET class extremely useful to create highly customizable Falling Blocks games in .NET Windows Forms environments.
Background
I wanted my TetrisBox class to be a size-customizable Falling Blocks game board, and to implement all the logic and drawing work to allow play Falling Blocks simply by setting a few property values and calling some simple methods. Once the work has been done, I'm proud because I think I've achieved all of my main goals.
The TetrisBox class inherits from the System.Windows.Forms.PictureBox control, as it is basically a drawing surface. All the drawing work is done in the overwritten OnPaint method. Because the PictureBox inheritance, it has a graphical UI and can be placed on any Windows Form or a user control.
Internally, the box represents the board game and is logical-divided into cells. The number of cells is defined by the Rows and Columns properties, and the size of each cell is defined by the CellSize property, which uses pixels. So, for example, a board with 20 rows, 10 columns and a CellSize of 25 will have a width of 241 pixels and a height of 481 pixels (that's because adjacent cells share the borders).
When a game is running, the control first paints its background (it can be a solid color, a gradient or a picture) and then it draws each cell. The state of each cell is saved in memory using different structures. When the game starts, the board is empty (all the cells are empty), but as well as the blocks are falling, the state of the cells may vary. All the painting work is done every time that the board needs to be repainted: when the player moves the falling block or when the falling block drops because the time interval.
Really, the heavy work in this class is to control each falling block, its position inside the game board and to control the movements and rotations of each block. Now I'm going to tell you what I've done about this. I'm sure that can be other approaches to solve this problem, and even I'm sure many of them can be better than mine.
There are 7 different types of block, as seen on this table:
As you can see, the biggest block is the red one (although in TetrisBox class you can customize the color of each block type), which is 4 cells wide (or high). So I decided to represent each block inside a 4x4 matrix of cells, as this:
Each block type has its own number of possible positions inside the 4x4 matrix. For example, type 1 blocks has 2 possible positions, while type 2 blocks has only 1 position and type 3 blocks has until 4 possible positions. In the initialization code for each block I define the possible positions with an array of list of strings. Each character in a string represents a column in the 4x4 matrix; each 4-characters string inside the list represents a row in the 4x4 matrix, and each list of strings inside the array represents one possible position of the block. You can see it better in this picture:
So, the user can make 4 differents movements to a falling block:
- Move Left: moves the block 1 column to the left.
- Move Right: moves the block 1 column to the right.
- Rotate: changes the block position inside the 4x4 block matrix.
- Drop: drops down the block 1 row.
Any time that the player tries to do any of the above movements, the game logic ensures that the filled cells (those with "1" in the 4x4 matrix) of the block keeps inside the board limits, and does not overlap with existing fixed cells. The fixed cells are cells in the board game that are not empty; when a block has fallen down to its ultimate position and therefore it cannot be moved anymore, the filled cells in that falling block are transformed into fixed cells in the game board.
The Timer Selection
When a game starts, a Timer is started to drop down the falling block 1 row at a certain interval. In the .Net Framework there are at least 3 different Timer classes, as you can see on MSDN. I have chosen the System.Timers.Timer class because its metronome accuracy and because it can raise the Elapsed event on the UI thread instead on its own thread, which is useful to change the UI in response to the Elapsed event.
Difficulties
In the gaming machine I mentioned before, there was certain "screens" (or game stages) in which the game added one or more difficulties or handicaps. I have implemented them in the class. The first handicap is to fill an empty cell and transform it into a fixed cell (always above the baseline or above an existing fixed cell). The second handicap is to add a new row at the bottom, moving up 1 row all the fixed cells in the game board. The new row is completely filled except for one empty cell. You can program the interval at which the handicaps appears (for example, you can create a stage in which a new random cell appears every 5 new blocks and a new row appears at the bottom every time the player completes 4 lines).
Using the code
Here's a quick reference of the relevant properties, methods and events:
Properties:
- BackgroundStyle: gets or sets the control's background style (it can be a solid color, a gradient or a picture). If it's a picture, the picture stretches to the control size and must be loaded into the BackgroundImage property. If you want a solid color, you must use the BackColor property.
- Block1Color to Block7Color: gets or sets the color of each block type.
- LeftKey, RightKey, RotateKey and DropKey: gets or sets the key used for each movement, so you can customize the keys used for play. This can be very useful if you make a 2-players game: they can play with the same keyboard, each one with its own play keys.
- RamdomBlockColor: gets or sets the color to use when a random block appears (see Difficulties).
- UncompleteRowColor: gets or sets the color to use for cells when an uncomplete row appears (see Difficulties).
- TimerInterval: gets or sets the Timer interval (game speed). The shorter interval, the higher speed.
- GradientColor1, GradientColor2 and GradientDirection: gets or sets the properties for the gradient background.
- Rows: gets or sets the number of rows in the game board.
- Columns: gets or sets the number of columns in the game board.
- CellSize: gets or sets the cell size in pixels.
Methods:
- StartGame: starts a new game (the Timer is enabled so the blocks start falling).
- StopGame: stops a running game.
- Pause: pauses a running game.
- Resume: resumes a paused game.
- IsRunning: returns True if a game is running (even if it's paused).
- IsPaused: returns True if a running game is paused.
- AddRandomBlock: transforms a free board cell into a fixed cell, always above the baseline or above an existing fixed cell (see Difficulties).
- AddUncompleteLine: adds an uncomplete new line at the bottom of the game board, moving up 1 row all the fixed cells in the board (see Difficulties).
- FreeRowsFromTop: returns the number of completely free (without fixed cells) rows from the top of the game board until the first row with fixed cells. You can use this value to reward the player (the higher number of free rows, the more points he wins).
Events:
- FullRows: fires every time the player drops a block and, doing it, completes from 1 to 4 full rows (he must be rewarded!). In the e argument you can check the NumberOfRows property to see how many lines has been completed at once.
- GameOver: fires when a block has reached the top of the game board.
- Starting: fires when a game is about to start (because a call to the StartGame method).
- NewBlock: fires every time a new block is created and starts to fall. In the e argument you can check the BlockType property (which stores the block type of the block just created) and the NextBlockType property (which stores the block type of the next block that will fall once the just created block has been dropped to its ultimate position; usually it's shown to the player).
The Class
This is the complete code for the TetrisBox class:
Imports System.Drawing
Imports System.Runtime.InteropServices
Imports System.Windows.Forms
Public Class TetrisBox
Inherits System.Windows.Forms.PictureBox
#Region "Public Enumerations"
Public Enum BackgroundStyles
SolidColor
Gradient
Picture
End Enum
#End Region
#Region "Private Classes"
Private Class Block
Private _type As Integer
Public Property Color As Color
Private _rotationsNumber As Integer
Private _rotations As Dictionary(Of Integer, List(Of String)) = New Dictionary(Of Integer, List(Of String))
Private _currentRotation As Integer = 1
Public Property X As Integer = 0
Public Property Y As Integer = 0
Public Sub OffsetRotation()
_currentRotation += 1
If (_currentRotation > _rotationsNumber) Then _currentRotation = 1
End Sub
Public Function FilledCell(ByVal x As Integer, ByVal y As Integer) As Boolean
Return Me.CurrentMatrix()(y).Substring(x, 1).Equals("1")
End Function
Public ReadOnly Property CurrentMatrix As List(Of String)
Get
Return _rotations(_currentRotation)
End Get
End Property
Public ReadOnly Property NextRotationMatrix As List(Of String)
Get
Dim nextRotation As Integer = _currentRotation + 1
If (nextRotation > _rotationsNumber) Then nextRotation = 1
Return _rotations(nextRotation)
End Get
End Property
Public ReadOnly Property Type As Integer
Get
Return _type
End Get
End Property
Public Sub New(ByVal blockType As Integer)
_type = blockType
Select Case _type
Case 1
Call InitializeBlock(New List(Of String) From {"0100", "0100", "0100", "0100"}, New List(Of String) From {"0000", "1111", "0000", "0000"})
Case 2
Call InitializeBlock(New List(Of String) From {"0110", "0110", "0000", "0000"})
Case 3
Call InitializeBlock(New List(Of String) From {"0100", "1110", "0000", "0000"}, New List(Of String) From {"0100", "0110", "0100", "0000"}, New List(Of String) From {"1110", "0100", "0000", "0000"}, New List(Of String) From {"0100", "1100", "0100", "0000"})
Case 4
Call InitializeBlock(New List(Of String) From {"0010", "0110", "0100", "0000"}, New List(Of String) From {"0110", "0011", "0000", "0000"})
Case 5
Call InitializeBlock(New List(Of String) From {"0100", "0110", "0010", "0000"}, New List(Of String) From {"0011", "0110", "0000", "0000"})
Case 6
Call InitializeBlock(New List(Of String) From {"0100", "0100", "0110", "0000"}, New List(Of String) From {"0111", "0100", "0000", "0000"}, New List(Of String) From {"0110", "0010", "0010", "0000"}, New List(Of String) From {"0001", "0111", "0000", "0000"})
Case 7
Call InitializeBlock(New List(Of String) From {"0010", "0010", "0110", "0000"}, New List(Of String) From {"0100", "0111", "0000", "0000"}, New List(Of String) From {"0110", "0100", "0100", "0000"}, New List(Of String) From {"0111", "0001", "0000", "0000"})
End Select
End Sub
Private Sub InitializeBlock(ByVal ParamArray rotations() As List(Of String))
_rotationsNumber = rotations.Length
For k As Integer = 0 To rotations.Length - 1
_rotations.Add(k + 1, rotations(k))
Next
End Sub
End Class
Private Class Cell
Public Property Row As Integer = 0
Public Property Column As Integer = 0
Public Property Fixed As Boolean = False
Public Property Color As Color
Public Sub New(ByVal row As Integer, ByVal column As Integer)
Me.Row = row
Me.Column = column
End Sub
End Class
Private Class CellPoint
Public Property Row As Integer = 0
Public Property Column As Integer = 0
Public Sub New(ByVal row As Integer, ByVal column As Integer)
Me.Row = row
Me.Column = column
End Sub
Public Overrides Function ToString() As String
Return Me.Row.ToString + "," + Me.Column.ToString
End Function
End Class
Private Class KeyboardHook
Implements IDisposable
<DllImport("User32.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.StdCall)> _
Private Overloads Shared Function SetWindowsHookEx(ByVal idHook As Integer, ByVal HookProc As HookProc, ByVal hInstance As IntPtr, ByVal wParam As Integer) As Integer
End Function
<DllImport("User32.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.StdCall)> _
Private Overloads Shared Function CallNextHookEx(ByVal idHook As Integer, ByVal nCode As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Integer
End Function
<DllImport("User32.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.StdCall)> _
Private Overloads Shared Function UnhookWindowsHookEx(ByVal idHook As Integer) As Boolean
End Function
<StructLayout(LayoutKind.Sequential)> _
Private Structure KBDLLHOOKSTRUCT
Public vkCode As UInt32
Public scanCode As UInt32
Public flags As KBDLLHOOKSTRUCTFlags
Public time As UInt32
Public dwExtraInfo As UIntPtr
End Structure
<Flags()> _
Private Enum KBDLLHOOKSTRUCTFlags As UInt32
LLKHF_EXTENDED = &H1
LLKHF_INJECTED = &H10
LLKHF_ALTDOWN = &H20
LLKHF_UP = &H80
End Enum
Public Event KeyDown(ByVal Key As Keys)
Public Event KeyUp(ByVal Key As Keys)
Private Const WH_KEYBOARD_LL As Integer = 13
Private Const HC_ACTION As Integer = 0
Private Const WM_KEYDOWN = &H100
Private Const WM_KEYUP = &H101
Private Const WM_SYSKEYDOWN = &H104
Private Const WM_SYSKEYUP = &H105
Private Delegate Function HookProc(ByVal nCode As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Integer
Private hookDelegate As HookProc = New HookProc(AddressOf KeyboardProc)
Private hookID As IntPtr = IntPtr.Zero
Private Function KeyboardProc(ByVal nCode As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Integer
If (nCode = HC_ACTION) Then
Dim struct As KBDLLHOOKSTRUCT
Select Case wParam
Case WM_KEYDOWN, WM_SYSKEYDOWN
RaiseEvent KeyDown(CType(CType(Marshal.PtrToStructure(lParam, struct.GetType()), KBDLLHOOKSTRUCT).vkCode, Keys))
Case WM_KEYUP, WM_SYSKEYUP
RaiseEvent KeyUp(CType(CType(Marshal.PtrToStructure(lParam, struct.GetType()), KBDLLHOOKSTRUCT).vkCode, Keys))
End Select
End If
Return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam)
End Function
Public Sub New()
hookID = SetWindowsHookEx(WH_KEYBOARD_LL, hookDelegate, IntPtr.Zero, 0)
If hookID = IntPtr.Zero Then
Throw New Exception("Could not set keyboard hook")
End If
End Sub
#Region "IDisposable Support"
Private disposedValue As Boolean
Protected Overridable Sub Dispose(disposing As Boolean)
If Not Me.disposedValue Then
If disposing Then
End If
If Not hookID = IntPtr.Zero Then
UnhookWindowsHookEx(hookID)
End If
End If
Me.disposedValue = True
End Sub
Protected Overrides Sub Finalize()
Dispose(False)
MyBase.Finalize()
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
#End Region
End Class
Private Class Board
Public Property Rows As Integer = 0
Public Property Columns As Integer = 0
Public Property Cells As Dictionary(Of String, Cell)
Public Property FallingBlock As Block = Nothing
Public Property Block1Color As Color = Color.Red
Public Property Block2Color As Color = Color.Blue
Public Property Block3Color As Color = Color.Green
Public Property Block4Color As Color = Color.Aqua
Public Property Block5Color As Color = Color.Brown
Public Property Block6Color As Color = Color.Yellow
Public Property Block7Color As Color = Color.Purple
Private _nextBlock As Integer = 0
Public Event FullRows(sender As Object, e As FullRowsEventArgs)
Public Event GameOver(sender As Object, e As System.EventArgs)
Public Event GotNewBlock(sender As Object, e As NewBlockEventArgs)
Public Sub New(ByVal rows As Integer, ByVal columns As Integer)
Me.Rows = rows
Me.Columns = columns
Me.Cells = New Dictionary(Of String, Cell)
For row As Integer = 0 To Me.Rows - 1
For column As Integer = 0 To Me.Columns - 1
Me.Cells.Add(row.ToString + "," + column.ToString, New Cell(row, column))
Next
Next
End Sub
Private Function GetRandomNumber(ByVal lowerbound As Integer, ByVal upperbound As Integer) As Integer
Return CInt(Math.Floor((upperbound - lowerbound + 1) * Rnd())) + lowerbound
End Function
Public Function Rotate() As Boolean
If CanRotate() Then
Me.FallingBlock.OffsetRotation()
Return True
Else
Return False
End If
End Function
Private Function CanRotate() As Boolean
If Me.FallingBlock IsNot Nothing Then
Dim nextRotation As List(Of String) = Me.FallingBlock.NextRotationMatrix
For row As Integer = 0 To 3
For column As Integer = 0 To 3
If nextRotation(row).Substring(column, 1).Equals("1") Then
Dim pt As CellPoint = BlockToBoard(New CellPoint(row, column))
If (pt.Column < 0) OrElse (pt.Column >= Me.Columns) OrElse (pt.Row < 0) OrElse (pt.Row >= Me.Rows) OrElse Me.Cells(New CellPoint(pt.Row, pt.Column).ToString).Fixed Then
Return False
End If
End If
Next
Next
Return True
Else
Return False
End If
End Function
Public Function MoveLeft() As Boolean
If CanMoveLeft() Then
Me.FallingBlock.X -= 1
Return True
Else
Return False
End If
End Function
Private Function CanMoveLeft() As Boolean
If Me.FallingBlock IsNot Nothing Then
For row As Integer = 0 To 3
For column As Integer = 0 To 3
If Me.FallingBlock.FilledCell(column, row) Then
Dim pt As CellPoint = BlockToBoard(New CellPoint(row, column))
If pt.Column.Equals(0) OrElse Me.Cells(New CellPoint(pt.Row, pt.Column - 1).ToString).Fixed Then
Return False
End If
End If
Next
Next
Return True
Else
Return False
End If
End Function
Private Function CanMoveRight() As Boolean
If Me.FallingBlock IsNot Nothing Then
For row As Integer = 0 To 3
For column As Integer = 3 To 0 Step -1
If Me.FallingBlock.FilledCell(column, row) Then
Dim pt As CellPoint = BlockToBoard(New CellPoint(row, column))
If pt.Column.Equals(Me.Columns - 1) OrElse Me.Cells(New CellPoint(pt.Row, pt.Column + 1).ToString).Fixed Then
Return False
End If
End If
Next
Next
Return True
Else
Return False
End If
End Function
Public Function MoveRight() As Boolean
If CanMoveRight() Then
Me.FallingBlock.X += 1
Return True
Else
Return False
End If
End Function
Public Sub NewBlock()
If _nextBlock.Equals(0) Then
Me.FallingBlock = New Block(GetRandomNumber(1, 7))
Else
Me.FallingBlock = New Block(_nextBlock)
End If
Select Case Me.FallingBlock.Type
Case 1
Me.FallingBlock.Color = Me.Block1Color
Case 2
Me.FallingBlock.Color = Me.Block2Color
Case 3
Me.FallingBlock.Color = Me.Block3Color
Case 4
Me.FallingBlock.Color = Me.Block4Color
Case 5
Me.FallingBlock.Color = Me.Block5Color
Case 6
Me.FallingBlock.Color = Me.Block6Color
Case 7
Me.FallingBlock.Color = Me.Block7Color
End Select
_nextBlock = GetRandomNumber(1, 7)
Me.FallingBlock.X = (Me.Columns - 4) / 2
Me.FallingBlock.Y = 0
RaiseEvent GotNewBlock(Me, New NewBlockEventArgs(Me.FallingBlock.Type, _nextBlock))
End Sub
Public Sub CheckBlock()
If Me.FallingBlock IsNot Nothing Then
Dim overlapBlock As Boolean = False
For row As Integer = 0 To 3
For column As Integer = 0 To 3
If Me.FallingBlock.FilledCell(column, row) Then
Dim pt As CellPoint = BlockToBoard(New CellPoint(row, column))
If Me.Cells(pt.ToString).Fixed Then
overlapBlock = True
Exit For
End If
End If
Next
If overlapBlock Then Exit For
Next
If overlapBlock Then
RaiseEvent GameOver(Me, New System.EventArgs)
Else
Dim fixBlock As Boolean = False
For column As Integer = 0 To 3
For row As Integer = 3 To 0 Step -1
If Me.FallingBlock.FilledCell(column, row) Then
Dim pt As CellPoint = BlockToBoard(New CellPoint(row, column))
If pt.Row.Equals(Me.Rows - 1) OrElse Me.Cells(New CellPoint(pt.Row + 1, pt.Column).ToString).Fixed Then
fixBlock = True
End If
Exit For
End If
Next
If fixBlock Then Exit For
Next
If fixBlock Then
For row As Integer = 0 To 3
For column As Integer = 0 To 3
If Me.FallingBlock.FilledCell(column, row) Then
Dim pt As CellPoint = BlockToBoard(New CellPoint(row, column))
Me.Cells(pt.ToString).Fixed = True
Me.Cells(pt.ToString).Color = Me.FallingBlock.Color
End If
Next
Next
Me.FallingBlock = Nothing
Call CheckFullRows()
End If
End If
End If
End Sub
Private Sub CheckFullRows()
Dim fullRows As List(Of Integer) = New List(Of Integer)
For row As Integer = Me.Rows - 1 To 0 Step -1
Dim fullRow As Boolean = True
For column As Integer = 0 To Me.Columns - 1
If (Not Me.Cells(row.ToString + "," + column.ToString).Fixed) Then
fullRow = False
Exit For
End If
Next
If fullRow Then fullRows.Add(row)
Next
If fullRows.Count > 0 Then
For Each row As Integer In fullRows
Call DeleteRow(row)
Next
RaiseEvent FullRows(Me, New FullRowsEventArgs(fullRows.Count))
End If
End Sub
Private Sub DeleteRow(ByVal row As Integer)
For r As Integer = row To 1 Step -1
For col As Integer = 0 To Me.Columns - 1
Me.Cells(r.ToString + "," + col.ToString).Fixed = Me.Cells((r - 1).ToString + "," + col.ToString).Fixed
Me.Cells(r.ToString + "," + col.ToString).Color = Me.Cells((r - 1).ToString + "," + col.ToString).Color
Next
Next
For col As Integer = 0 To Me.Columns - 1
Me.Cells("0," + col.ToString).Fixed = False
Next
End Sub
Private Function BlockToBoard(ByVal p As CellPoint) As CellPoint
Return New CellPoint(p.Row + Me.FallingBlock.Y, p.Column + Me.FallingBlock.X)
End Function
Private Function BoardToBlock(ByVal p As CellPoint) As CellPoint
Return New CellPoint(p.Row - Me.FallingBlock.Y, p.Column - Me.FallingBlock.X)
End Function
Public Function GetCellColor(ByVal p As CellPoint) As Color
Dim output As Color = Color.Transparent
If Me.Cells(p.Row.ToString + "," + p.Column.ToString).Fixed Then
output = Me.Cells(p.Row.ToString + "," + p.Column.ToString).Color
Else
If (Me.FallingBlock IsNot Nothing) AndAlso CellIsInsideBlock(p.Row, p.Column) Then
Dim pt As CellPoint = BoardToBlock(p)
If Me.FallingBlock.FilledCell(pt.Column, pt.Row) Then
output = Me.FallingBlock.Color
End If
End If
End If
Return output
End Function
Private Function CellIsInsideBlock(ByVal row As Integer, ByVal column As Integer) As Boolean
Return (row >= Me.FallingBlock.Y) AndAlso (row <= (Me.FallingBlock.Y + 3)) AndAlso (column >= Me.FallingBlock.X) AndAlso (column <= (Me.FallingBlock.X + 3))
End Function
End Class
#End Region
#Region "Public Classes"
Public Class FullRowsEventArgs
Inherits System.EventArgs
Public Property NumberOfRows As Integer = 0
Public Sub New(ByVal numberOfRows As Integer)
Me.NumberOfRows = numberOfRows
End Sub
End Class
Public Class NewBlockEventArgs
Inherits System.EventArgs
Public Property BlockType As Integer = 0
Public Property NextBlockType As Integer = 0
Public Sub New(ByVal blockType As Integer, ByVal nextBlockType As Integer)
Me.BlockType = blockType
Me.NextBlockType = nextBlockType
End Sub
End Class
#End Region
#Region "Public Events"
Public Event FullRows(sender As Object, e As FullRowsEventArgs)
Public Event GameOver(sender As Object, e As System.EventArgs)
Public Event Starting(sender As Object, e As System.EventArgs)
Public Event NewBlock(sender As Object, e As NewBlockEventArgs)
#End Region
#Region "Private Variables"
Private _rows As Integer = 20
Private _columns As Integer = 10
Private _cellSize As Integer = 25
Private _backgroundStyle As BackgroundStyles = BackgroundStyles.SolidColor
Private _gradientColor1 As Color = Color.SteelBlue
Private _gradientColor2 As Color = Color.Black
Private _gradientDirection As Drawing2D.LinearGradientMode = Drawing2D.LinearGradientMode.Vertical
Private _timer As System.Timers.Timer = Nothing
Private _board As Board
Private _running As Boolean = False
Private _pause As Boolean = False
Private WithEvents _hook As KeyboardHook = Nothing
#End Region
#Region "Constructor"
Public Sub New()
Me.DoubleBuffered = True
_timer = New Timers.Timer(1000)
_timer.SynchronizingObject = Me
AddHandler _timer.Elapsed, AddressOf TimerElapsed
End Sub
#End Region
#Region "Public Auto-Implemented Properties"
Public Property RandomBlockColor As Color = Color.LightYellow
Public Property UncompleteRowColor As Color = Color.LightYellow
Public Property LeftKey As Keys = Keys.Left
Public Property RightKey As Keys = Keys.Right
Public Property RotateKey As Keys = Keys.Up
Public Property DropKey As Keys = Keys.Down
Public Property Block1Color As Color = Color.Red
Public Property Block2Color As Color = Color.Blue
Public Property Block3Color As Color = Color.Green
Public Property Block4Color As Color = Color.Aqua
Public Property Block5Color As Color = Color.Brown
Public Property Block6Color As Color = Color.Yellow
Public Property Block7Color As Color = Color.Purple
#End Region
#Region "Public Properties"
Public Property TimerInterval As Integer
Get
Return _timer.Interval
End Get
Set(value As Integer)
_timer.Interval = value
End Set
End Property
Public Property GradientColor1 As Color
Get
Return _gradientColor1
End Get
Set(value As Color)
_gradientColor1 = value
If Me.BackgroundStyle = BackgroundStyles.Gradient Then Me.Invalidate()
End Set
End Property
Public Property GradientColor2 As Color
Get
Return _gradientColor2
End Get
Set(value As Color)
_gradientColor2 = value
If Me.BackgroundStyle = BackgroundStyles.Gradient Then Me.Invalidate()
End Set
End Property
Public Property GradientDirection As Drawing2D.LinearGradientMode
Get
Return _gradientDirection
End Get
Set(value As Drawing2D.LinearGradientMode)
_gradientDirection = value
If Me.BackgroundStyle = BackgroundStyles.Gradient Then Me.Invalidate()
End Set
End Property
Public Property BackgroundStyle As BackgroundStyles
Get
Return _backgroundStyle
End Get
Set(value As BackgroundStyles)
_backgroundStyle = value
Me.Invalidate()
End Set
End Property
Public Property Rows As Integer
Get
Return _rows
End Get
Set(value As Integer)
_rows = value
Me.Invalidate()
End Set
End Property
Public Property Columns As Integer
Get
Return _columns
End Get
Set(value As Integer)
_columns = value
Me.Invalidate()
End Set
End Property
Public Property CellSize As Integer
Get
Return _cellSize
End Get
Set(value As Integer)
_cellSize = value
Me.Invalidate()
End Set
End Property
#End Region
#Region "OnPaint"
Protected Overrides Sub OnPaint(e As PaintEventArgs)
Me.Width = (Me.Columns * Me.CellSize) - (Me.Columns - 1)
Me.Height = (Me.Rows * Me.CellSize) - (Me.Rows - 1)
Select Case Me.BackgroundStyle
Case BackgroundStyles.SolidColor
e.Graphics.Clear(Me.BackColor)
Case BackgroundStyles.Gradient
Using b As Drawing2D.LinearGradientBrush = New Drawing2D.LinearGradientBrush(Me.DisplayRectangle, Me.GradientColor1, Me.GradientColor2, Me.GradientDirection)
e.Graphics.FillRectangle(b, Me.DisplayRectangle)
End Using
Case BackgroundStyles.Picture
If Me.BackgroundImage IsNot Nothing Then
e.Graphics.DrawImage(Me.BackgroundImage, 0, 0, Me.Width, Me.Height)
Else
e.Graphics.Clear(Me.BackColor)
End If
End Select
If _board IsNot Nothing Then
For row As Integer = 0 To Me.Rows - 1
For column As Integer = 0 To Me.Columns - 1
Dim c As Color = _board.GetCellColor(New CellPoint(row, column))
If c <> Color.Transparent Then
Using b As SolidBrush = New SolidBrush(c)
e.Graphics.FillRectangle(b, New Rectangle(column * (Me.CellSize - 1), row * (Me.CellSize - 1), Me.CellSize - 1, Me.CellSize - 1))
End Using
e.Graphics.DrawRectangle(Pens.Black, New Rectangle(column * (Me.CellSize - 1), row * (Me.CellSize - 1), Me.CellSize - 1, Me.CellSize - 1))
End If
Next
Next
End If
End Sub
#End Region
#Region "Public Methods And Functions"
Public Sub StartGame()
If (Not Me.IsRunning) AndAlso (Not Me.IsPaused) Then
Randomize()
_board = New Board(Me.Rows, Me.Columns)
With _board
.Block1Color = Me.Block1Color
.Block2Color = Me.Block2Color
.Block3Color = Me.Block3Color
.Block4Color = Me.Block4Color
.Block5Color = Me.Block5Color
.Block6Color = Me.Block6Color
.Block7Color = Me.Block7Color
End With
AddHandler _board.FullRows, AddressOf CatchFullRows
AddHandler _board.GameOver, AddressOf CatchGameOver
AddHandler _board.GotNewBlock, AddressOf CatchNewBlock
_running = True
_hook = New KeyboardHook()
RaiseEvent Starting(Me, New System.EventArgs)
_timer.Start()
End If
End Sub
Public Sub StopGame()
If Me.IsRunning() Then
_timer.Stop()
_running = False
_pause = False
_hook.Dispose()
_hook = Nothing
End If
End Sub
Public Function FreeRowsFromTop() As Integer
Dim freeRows As Integer = 0
If _board IsNot Nothing Then
For row As Integer = 0 To Me.Rows - 1
Dim freeRow As Boolean = True
For column As Integer = 0 To Me.Columns - 1
If _board.Cells(row.ToString + "," + column.ToString).Fixed Then
freeRow = False
Exit For
End If
Next
If freeRow Then
freeRows += 1
Else
Exit For
End If
Next
End If
Return freeRows
End Function
Public Sub AddRandomBlock()
Dim whichColumn As Integer
Do
whichColumn = GetRandomNumber(0, Me.Columns - 1)
Loop Until (Not _board.Cells("0," + whichColumn.ToString).Fixed)
For row As Integer = Me.Rows - 1 To 0 Step -1
If (Not _board.Cells(row.ToString + "," + whichColumn.ToString).Fixed) Then
_board.Cells(row.ToString + "," + whichColumn.ToString).Fixed = True
_board.Cells(row.ToString + "," + whichColumn.ToString).Color = Me.RandomBlockColor
Me.Invalidate()
Exit For
End If
Next
End Sub
Public Sub AddUncompleteRow()
Dim forceGameOver As Boolean = False
If ThereIsSomethingInFirstRow() Then
forceGameOver = True
End If
For row As Integer = 0 To Me.Rows - 2
For column As Integer = 0 To Me.Columns - 1
_board.Cells(row.ToString + "," + column.ToString).Fixed = _board.Cells((row + 1).ToString + "," + column.ToString).Fixed
_board.Cells(row.ToString + "," + column.ToString).Color = _board.Cells((row + 1).ToString + "," + column.ToString).Color
Next
Next
Dim emptyColumn As Integer = GetRandomNumber(0, Me.Columns - 1)
For column As Integer = 0 To Me.Columns - 1
If column.Equals(emptyColumn) Then
_board.Cells((Me.Rows - 1).ToString + "," + column.ToString).Fixed = False
Else
_board.Cells((Me.Rows - 1).ToString + "," + column.ToString).Fixed = True
_board.Cells((Me.Rows - 1).ToString + "," + column.ToString).Color = Me.UncompleteRowColor
End If
Next
Me.Invalidate()
End Sub
Public Function IsRunning() As Boolean
Return _running
End Function
Public Function IsPaused() As Boolean
Return _pause
End Function
Public Sub Pause()
If Me.IsRunning AndAlso (Not Me.IsPaused) Then
_pause = True
End If
End Sub
Public Sub [Resume]()
If Me.IsPaused Then
_pause = False
End If
End Sub
#End Region
#Region "Private Methods And Functions"
Private Sub TimerElapsed(sender As Object, e As System.EventArgs)
If (Not Me.IsPaused) Then
Call RedrawAndCheckBlock()
If _board.FallingBlock Is Nothing Then
Call _board.NewBlock()
Call RedrawAndCheckBlock()
Else
_board.FallingBlock.Y += 1
End If
End If
End Sub
Private Sub RedrawAndCheckBlock()
Me.Invalidate()
Call _board.CheckBlock()
End Sub
Private Sub _hook_KeyDown(Key As Keys) Handles _hook.KeyDown
If (Not Me.IsPaused) Then
If Key = Me.LeftKey Then
If _board.MoveLeft() Then Call RedrawAndCheckBlock()
ElseIf Key = Me.RightKey Then
If _board.MoveRight() Then Call RedrawAndCheckBlock()
ElseIf Key = Me.DropKey Then
Call TimerElapsed(Nothing, Nothing)
ElseIf Key = Me.RotateKey Then
If _board.Rotate() Then Call RedrawAndCheckBlock()
End If
End If
End Sub
Private Sub CatchFullRows(sender As Object, e As FullRowsEventArgs)
RaiseEvent FullRows(Me, e)
Me.Invalidate()
End Sub
Private Sub CatchGameOver(sender As Object, e As System.EventArgs)
Call StopGame()
RaiseEvent GameOver(Me, e)
End Sub
Private Sub CatchNewBlock(sender As Object, e As NewBlockEventArgs)
RaiseEvent NewBlock(sender, e)
End Sub
Private Function ThereIsSomethingInFirstRow() As Boolean
Dim output As Boolean = False
For column = 0 To Me.Columns - 1
If _board.Cells("0," + column.ToString).Fixed Then
output = True
Exit For
End If
Next
Return output
End Function
Private Function GetRandomNumber(ByVal lowerbound As Integer, ByVal upperbound As Integer) As Integer
Return CInt(Math.Floor((upperbound - lowerbound + 1) * Rnd())) + lowerbound
End Function
#End Region
End Class
Final notes
Really, there's no much more to say about the TetrisBox class. I have encapsulated all the code in just one class because it's easier to manage and port (although internally it defines and uses several private classes). Note that this is a 1.0.0.0 version and probably will contain bugs, but the main work is done. If you find some bug, or want any unimplemented feature, write a comment in this article and I will try to fix it.
As you will see if you use the class, the code is well commented. Feel free to download, modify, use and write your own Tetris games. If you improve this class any way, please let me know! I'm always happy to learn. I've tested 2 instances running in parallel (for a 2-player game) and it works well too. I also attach a .zip file containing a Visual Studio 2013 VB.net Winforms project with a simple game example.
Have fun!!
History
- September, 3, 2014 - Initial release.