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:
- To serve as a preview for oncoming
FallingBlocks
pieces. - 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
.
public enum FallingBlocksShapeType
{
Square,
LShape,
LLShape,
ZShape,
ZZShape,
TShape,
IShape,
SlashShape
}
Public Enum FallingBlocksShapeType
Square
LShape
LLShape
ZShape
ZZShape
TShape
IShape
SlashShape
End Enum
A FallingBlocks<code>Shape
object is created based on the FallingBlocksShapeType
parameter passed to the CreateShape()
method:
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;.....
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:
- add a new shape definition to the
FallingBlocksShapeType enum
- add a new
case
block for the new shape in the CreateShape()
method
FallingBlocks Shape
There are two constructors for FallingBlocksShape
:
public FallinBlocksShape()
private FallinBlocksShape(Point[] points,Point pivot,
Color color,int _blocksize,
CellShapeType _blockshape)
Public Sub New()
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:
private FallingBlocksShape Rotate(FallingBlocksShape ts)
{
Point[] points;
Point pivot;
Point location;
if (ts.FallingBlocksPivot.Equals(FallingBlocksShape.NoPivot))
{
return ts;
}
Point[] temppoints=new Point[ts.FallingBlocksPoints.Length];
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];
for(int i=0;i<temppoints.Length;i++)
points[i]=new Point(temppoints[i].Y+ts.FallingBlocksPivot.X ,
-temppoints[i].X+ts.FallingBlocksPivot.Y);
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);
location=new Point(ts.Location.X+minx*_blocksize,
ts.Location.Y +miny*_blocksize);
pivot=new Point(ts.FallingBlocksPivot.X-minx,ts.FallingBlocksPivot.Y -miny);
for(int i=0;i<points.Length;i++)
{
points[i].X=points[i].X-minx;
points[i].Y=points[i].Y-miny;
}
FallingBlocksShape temp=ts.Clone();
temp.FallingBlocksPivot=pivot;
temp.FallingBlocksPoints=points;
temp.Location=location;
temp.Size=size;
if(IsValidPosition(temp))
return temp;
Else
return ts;
}
Private Function Rotate(ts As FallingBlocksShape) As FallingBlocksShape
Dim points As Point()
Dim pivot As Point
Dim location As Point
If ts.FallingBlocksPivot.Equals(FallingBlocksShape.NoPivot) Then
Return ts
End If
Dim temppoints As Point() = New Point(ts.FallingBlocksPoints.Length - 1) {}
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) {}
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
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)
location = New Point(ts.Location.X + minx * _blocksize, _
ts.Location.Y + miny * _blocksize)
pivot = New Point(ts.FallingBlocksPivot.X - minx, ts.FallingBlocksPivot.Y - miny)
For i As Integer = 0 To points.Length - 1
points(i).X = points(i).X - minx
points(i).Y = points(i).Y - miny
Next
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)
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
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 FallingBlocks
Board
. 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()
{
System.Collections.IEnumerator en=Controls.GetEnumerator();
arrShape.Clear();
While (en.MoveNext())
{
if (en.Current.GetType().Equals(typeof(FallingBlocks.FallingBlocksShape)))
{
arrShape.Add (en.Current);
}
}
.....
Protected Overrides Sub OnCreateControl()
Dim en As System.Collections.IEnumerator = Controls.GetEnumerator()
arrShape.Clear()
While en.MoveNext()
If en.Current.[GetType]().Equals(GetType(FallingBlocks.FallingBlocksShape)) Then
arrShape.Add(en.Current)
End If
End While
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
.
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];
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)
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.
private FallingBlocksShape MoveDown(FallingBlocksShape ts)
{
FallingBlocksShape temp=ts.Clone();
temp.Top +=_blocksize;
if(!IsValidPosition(temp))
return ts;
Else
return temp;
}
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;
}
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.
private void DrawBoard()
{
_baseimage=(Image)_cleanbase.Clone();
Graphics g=Graphics.FromImage(_baseimage);
g.SmoothingMode =SmoothingMode.AntiAlias;
g.DrawString("Score:"+_score,new Font("Arial",12,
FontStyle.Bold),
new SolidBrush(_textcolor),
new Point(5,5));
g.Dispose();
DrawPicture();
g=Graphics.FromImage(this.BackgroundImage);
g.SmoothingMode =SmoothingMode.AntiAlias;
foreach(Cell c in _cells)
{
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();
}
}
....
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();
}
Private Sub DrawBoard()
_baseimage = DirectCast(_cleanbase.Clone(), Image)
Dim g As Graphics = Graphics.FromImage(_baseimage)
g.SmoothingMode = SmoothingMode.AntiAlias
g.DrawString("Score:" & _score, New Font("Arial", 12, FontStyle.Bold), _
New SolidBrush(_textcolor), New Point(5, 5))
g.Dispose()
DrawPicture()
g = Graphics.FromImage(Me.BackgroundImage)
g.SmoothingMode = SmoothingMode.AntiAlias
For Each c As Cell In _cells
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
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
{
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
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.
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);
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();
}
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)
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.
private EventHandler onGameOver;
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();
OnGameOver(null);
return;
}
...
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)
End RaiseEvent
End 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
_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:
- Default constructor
- Resize
BackgroundImage
property 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(this.PreviewFallingBlocksShape!=null && !_justloaded)
{
_preview=FallingBlocksShape.CreateShape(_previewcontrol.ShapeType,
_preview.BlockSize);
_preview.FallingBlocksColor=_previewcontrol.FallingBlocksColor;
}
else
{
_preview=GetNextPreviewShape();
if(_previewcontrol!=null){
_previewcontrol.ShapeType=_preview.ShapeType;
_previewcontrol.FallingBlocksColor=_preview.FallingBlocksColor;
}
}
_justloaded=false;
}
If _created Then
If Me.PreviewFallingBlocksShape IsNot Nothing AndAlso Not _justloaded Then
_preview = FallingBlocksShape.CreateShape(_previewcontrol.ShapeType, _preview.BlockSize)
_preview.FallingBlocksColor = _previewcontrol.FallingBlocksColor
Else
_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:
- Using
System.Media.SoundPlayer
- Using the Windows Media Player control
- 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:
- Footprint is small. No additional DLL needed to be distributed
- Support many Multimedia format, including MP3 and wav
- 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.