
These are the standard seven figures which I conditionally named:
This is yet another implementation of the famous Tetris game, written in C#.
Creating the object model took me more time than implementing it in a code. I
have provided a lot of settings, so that the control is as much customizable as
possible. As this is a component you may add it to your toolbox and drop it on a
form. I haven't implemented rich design-time behaviour because as I said I
provided a lot of settings which are serialized to a file and to keep
design-time and run-time behaviour synchronized by serializing/deserializing
files was much more complex than I expected. So, all of the settings are enabled
at run-time only. Further on I will explain this a little more. The files are
stored in the directory returned by Application.UserAppDataPath.
Actually the main Tetris control is a kind of a grid. That's why I called it
TetrisGrid. It has columns, rows and cells. Each cells is
represented by a SingleSquare object. This object has the
following fields:
Rectangle rect - represents the rectangle that is
currently occupied by the object (coordinates are relative to the parent tetris
grid).
bool filled - determines whether the rect field is
filled with color or not.
Color color - the Color value to fill the rectangle
with (if filled).
TetrisGrid parent - reference to the TetrisGrid this
object is located on. Now comes the Figure object. Actually each figure might
be considered as a collection of four single square objects. So if we know these
squares we can define the figure completely. And that is the whole thing - the
figure is nothing but an object which takes care of defining those four 'single
squares'. You may think of a figure as a rectangle which is partially filled
with color - in this aspect we may think of some kind of 'location' of the
figure on the Tetris grid. And by knowing this 'location' we will be able to
find out the indexes of the four squares in the Tetris grid's array of
SingleSquare objects. The Figure object declares the following fields:
int xPosition - this is the index of the column the
topleft edge of the figure's rectangle is situated.
int yPosition - this is the index of the row the
topleft edge of the figure's rectangle is situated.
int columns - a cashed value which is equal to the
number of columns in the tetris grid.
int rows - a cashed value which is equal to the number
of rows in the tetris grid.
int width - shows the number of columns the figure
expands onto.
int height - shows the number of rows the figure
expands onto.
RotateAngle angle - the current RotateAngle value of the figure.
RotateDirection direction - the direction in which the
figure is rotated.
Color color - the color value the figure will be filled
with.
TetrisGrid parent - reference to the TetrisGrid the
figure is located on. RotateAngle. We can find the index of the square a figure starts
from (this is always the topleft edge) with the following expression: int startIndex = yPosition * columns + xPosition;
Note that the SingleSquare object this index will return
might not appear as part of the figure. For example take a look at how the
indexes of the Triangle are determined:
..........................
int start = yPosition * columns + xPosition;
switch(angle)
{
case RotateAngle.Deg0:
indexes.Add(start + 1);
indexes.Add(start + columns);
indexes.Add(start + columns + 1);
indexes.Add(start + columns + 2);
width = 3;
height = 2;
break;
..........................
}
..........................
I keep this convention for starting index so all the figures use one
model.
This is the time to say that I am using a convention for fields
and methods to be internal (visible within the assembly) rather that private. It
is more useful for you don't have to write a property to access a field and you
don't have to write a public method which will expose a private one (if you have
to expose fields and methods out of the assembly then you should make properties
and public methods:)).
The control that is exposed to you is an user control which contains the Tetris grid as well as some other panels - for displaying score, lines, level, making preview of the next figure and showing current state of the game - paused or playing.
Figure classvirtual ArrayList GetRectsIndexes - returns the indexes
of the four squares that are part of the figure. This method is virtual and each
one of the seven standard figures simply overrides it.
ArrayList GetDifferentIndexes - return the different
indexes between two locations of a figure.
virtual void DrawPreview - draws e preview of a figure
on a specified graphics surface.
void Move - moves the figure to the desired MoveDirection.
bool CanMove - determines whether a figure can be moved
to the desired MoveDirection.
bool CanDraw - used to determine whether a new figure
can be drawn.
void DrawPreviewSquare - draws a single preview square.
void DrawFigure - performs drawing of the figure. That
is setting the boolean flag filled of all the four SingleSquare objects to true.
void ClearFigure - clears the figure. That is setting
the boolean flag filled of all the four SingleSquare objects to false.
void ChangeRotateAngle - changes the RotateAngle value of a figure. GetRectsIndexes method in the LThunder class: protected override ArrayList GetRectsIndexes()
{
ArrayList indexes = new ArrayList();
//get the top-left index for the figure
int start = yPosition * columns + xPosition;
switch(angle)
{
case RotateAngle.Deg0:
case RotateAngle.Deg180:
indexes.Add(start + 1);
indexes.Add(start + 2);
indexes.Add(start + columns);
indexes.Add(start + columns + 1);
width = 3;
height = 2;
break;
case RotateAngle.Deg90:
case RotateAngle.Deg270:
indexes.Add(start);
indexes.Add(start + columns);
indexes.Add(start + columns + 1);
indexes.Add(start + columns * 2 + 1);
width = 2;
height = 3;
break;
}
return indexes;
}
For a certain X and Y positions an array of four indexes are returned and according to these indexes a figure is drawn properly on the Tetris grid.
internal void Move(MoveDirection dir)
{
if(!CanMove(dir))
{
//if we tried to move figure down and we failed,
//so we need new figure
if(dir == MoveDirection.Down)
parent.newFigure = true;
return;
}
//first clear previous location of the figure
ClearFigure();
switch(dir)
{
case MoveDirection.Left:
xPosition -= 1;
break;
case MoveDirection.Right:
xPosition += 1;
break;
case MoveDirection.Down:
yPosition += 1;
break;
case MoveDirection.Rotate:
ChangeRotateAngle();
break;
}
//draw the figure at the new location
DrawFigure();
//refresh the tetris grid
parent.SmartRefresh();
}
The CanMove method also takes as an argument a MoveDirection value and simply tells the figure whether it can be
moved. Here is how the Move method looks like:
private bool CanMove(MoveDirection dir)
{
ArrayList oldIndexes = GetRectsIndexes();
ArrayList newIndexes = new ArrayList();
switch(dir)
{
case MoveDirection.Down:
//if we have reached the end of the rows - do nothing
if(yPosition + height == rows)
return false;
//perform fake offset
yPosition += 1;
//get the indexes with the new offset
newIndexes = GetRectsIndexes();
//restore previous position
yPosition -= 1;
break;
case MoveDirection.Left:
//if we are at the left side of the grid - do nothing
if(xPosition == 0)
return false;
//perform fake offset
xPosition -= 1;
//get the indexes with the new offset
newIndexes = GetRectsIndexes();
//restore previous location
xPosition += 1;
break;
case MoveDirection.Right:
//if we are at the right side of the grid - do nothing
if(xPosition + width == columns)
return false;
xPosition += 1;
newIndexes = GetRectsIndexes();
xPosition -= 1;
break;
case MoveDirection.Rotate:
RotateAngle oldAngle = angle;
ChangeRotateAngle();
newIndexes = GetRectsIndexes();
angle = oldAngle;
//do not allow rotating if we are going to
//exceed the bounds of the grid
if(yPosition + height > rows || xPosition + width > columns)
return false;
break;
}
//get the different indexes
ArrayList different = GetDifferentIndexes(oldIndexes, newIndexes);
foreach(int i in different)
{
//if we have at least one filled square in the different indexes,
//so we cannot move the figure in the desired location
if(parent.squares[i].filled)
return false;
}
//no other conditions, so return true
return true;
}
What I am using as a method to determine whether a figure can be placed on a new location is the following: I perform a fake changing of the location. Then I get the indexes of the figure with the new fake coordinates. After that I get the different (new) indexes the figure fill occupy if it is moved. Then with those indexes I simply check if they are filled or not - if not we can perform actual moving otherwise we cannot move the figure. I have a method for getting those 'different' indexes which is pretty simple:
private ArrayList GetDifferentIndexes(
ArrayList oldIndexes, ArrayList newIndexes)
{
//the different indexes
ArrayList different = new ArrayList();
foreach(int i in newIndexes)
{
if(oldIndexes.Contains(i))
continue;
different.Add(i);
}
return different;
}
Now we have to find out what the ClearFigure and DrawFigure methods do:
internal void ClearFigure()
{
//get the indexes of the squares thid figure occupies
foreach(int index in GetRectsIndexes())
{
//add the square to the parent's invalid rects
parent.invalidRects.Add(parent.squares[index]);
//drop the flag 'filled'
parent.squares[index].filled = false;
}
}
internal void DrawFigure()
{
//get the indexes of the squares thid figure occupies
foreach(int index in GetRectsIndexes())
{
//add the rect in the invalid rects of the parent grid
parent.invalidRects.Add(parent.squares[index]);
//set the color for the square
parent.squares[index].color = color;
//set it to 'filled'
parent.squares[index].filled = true;
}
}
The parent Tetris grid takes care of refreshing the invalid areas of its
surface and in its OnPaint method the invalid squares are
redrawn. Let's see how the Tetris grid takes care of its painting, causing
figures to move down, creating new ones.
TetrisGrid classI will not cover all the methods in this class, just the more significant ones.
SmartRefresh method internal void SmartRefresh()
{
Rectangle r = new Rectangle();
int counter = 0;
//create a rectangle object that is union of all the invalid rects
foreach(SingleSquare sq in invalidRects)
{
if(counter == 0)
r = sq.rect;
else
r = Rectangle.Union(r, sq.rect);
counter++;
}
//invalidate this rectangle
Invalidate(r);
//clear the invalid rects array
invalidRects.Clear();
}
This method creates an invalid rectangle from all the rects that are marked
as invalid and invalidates that rectangle. Due to this method a significant
improvement in the painting of the grid is performed. For instance you don't
need to re-paint straight after a figure is cleared but you may do this job as
late as the figure needs redraw. Or when you clear a line you don't need to call
OnPaint for every single square.
Now let's see how
the OnPaint method is synchronized with that "Smart
refresh":
protected override void OnPaint(PaintEventArgs e)
{
SolidBrush brush = new SolidBrush(BackColor);
foreach(SingleSquare sq in squares)
{
//do not paint if not within the invalid rect
if(!e.Graphics.ClipBounds.IntersectsWith(sq.rect))
continue;
//if filled - call the single square to draw itself on this graphics
if(sq.filled)
sq.Draw(e.Graphics);
//otherwise just clear the square - fill it
//with its parent's back color
else
e.Graphics.FillRectangle(brush, sq.rect);
}
brush.Dispose();
}
For a test purpose I created a 100 * 100 (10 000 cells) Tetris grid and the painting of the grid took only about 4% of the CPU usage (mine is XP Palomino 1600+). There is further optimization of the paint method but as very little people will play on a larger than 10 * 20 tetris grid I left it for future versions:)) (to be honest it was pure laziness:)).
A few words about moving figures down and levels. As you might guess I am
using a System.Windows.Forms.Timer object and on its every
tick I am telling the current figure to move down. Depending on the current
level the ticks of the timer are calculated - it starts from 1000 milliseconds
at level 1 and ends with 60 at level 12. Every 20 lines made increase the level.
But if you have specified custom start level - let's say 8 - you will get to
level 9 when you have done 160 lines. Take a look at the TimerTick method:
void TimerTick(object sender, EventArgs e)
{
currentFigure.Move(MoveDirection.Down);
//if we need new figure
if(newFigure)
{
int linesNum = 0;
bool refresh = false;
//first check for any completed lines
for(int i=0; i < settings.rows; i++)
{
if(GetLine(i))
{
//clear the line
ClearLine(i);
linesNum++;
lines++;
//scroll the line
ScrollLine(i);
//set the refresh flag to true
refresh = true;
}
}
if(refresh)
SmartRefresh();
//adjust score
if(linesNum==0)
score += 0;
if(linesNum==1)
score += 100;
if(linesNum==2)
score += 300;
if(linesNum==3)
score += 600;
if(linesNum==4)
score += 1000;
//adjust level and ticking
UpdateLevel();
//fire the NewFigure event
if(NewFigure != null)
NewFigure(this, new EventArgs());
InitFigure();
InitNextFigure();
}
}
To initialize a new figure I use the Random class. It generates a number in
the interval of 0 - 6 and depending on that number I create the next figure. For
capturing the keyboard events I use the ProcessDialogKey
method. Let me focus a little bit more on that. At first I tried with the OnKeyDown event and it worked fine except for the so called
"Dialog Keys" - ARROW KEYS, TAB, RETURN and ESCAPE. So to capture those keys in
the OnKeyDown event you may override the IsInputKey method and tell the control that you will handle them
or to do the same as I did - overriding the ProcessDialogKey method. It is called for every keyboard event,
so you cannot miss any input key:)).
GATetris classUserControl with a
TetrisGrid docked on it and some other panels used for
previewing next figure and displaying scores, lines and level. It also exposes a
few new public methods such as ShowOptions, ShowBestPlayers and ShowAbout. A context
menu with all the necessary commands goes with the control. And the new property
is Grid - gives you access to the built-in TetrisGrid object. TetrisGrid. So, I created a class
called Settings and if I want to load an existing state I just call Settings.Restore method. The TetrisGrid has
a field of type Settings and if a specific information is needed the control
simply gets it from its Settings object. The one and only reason for not
synchronizing design-time and run-time options is because the Application object
at design time is the Visual Studio itself and the path returned by Application.UserAppDataPath is different at design and run time.
In other words I couldn't find a way to retrieve the directory of the project at
design time. The other way was to specify a file path explicitly (e.g.
C:\GATetris\Settings.dat) but what about if we have 10 tetris controls -
each one will override previous settings. And yet another way was each control
to create a different file but...Well, it was easier to enhance only run-time
behaviour:)) By the way, if anyone knows how to retrieve the Application.UserAppDataPath path for a project at design time I
would be very grateful to learn it! TetrisGrid does not expose any Tetris related properties but
leaves that task to the settings class. And when the ShowOptions method is called it simply creates a form with a
property grid docked on it and a settings object populated in it. Here comes
another interesting thing - as we want the user to be able to roll back any
changes he has done to the settings object we might want to create another
settings object that is equal to the current and only if the user accepts his
changes the control will be updated. In other words we need cloning of the
settings object.
System.Reflection namespace is a very powerful tool to
investigate and change any object at run-time. Take a look how I clone my
Settings object: public object Clone()
{
//get the type of this instance
Type type = GetType();
//create binding flags object
BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic |
BindingFlags.Public;
//get all the fields of this instance
FieldInfo[] fields = type.GetFields(flags);
//create new settings object
Settings sett = new Settings();
//now copy all the fields to the new object
foreach(FieldInfo fi in fields)
{
fi.SetValue(sett, fi.GetValue(this));
}
return sett;
}
Neat stuff, isn't it?! And much more easier than manually going from field to field like that:
//.......................
Settings sett = new Settings();
sett.squareWidth = this.squareWidth;
sett.lineColor = this.lineColor;
//.......................
Here is how the Options dialog looks like:
And a few words about the 10 best players kept in the Tetris. I have a class
called Player which implements IComparable and a BestPlayersCollection
which takes care of sorting, adding and removing players. Also the collection
provides a method for populating a listview. Whenever a game is finished the
GATetris control checks if the current player should be entered in the "Hall Of
Fame" and if so it asks him to enter his name:
DarkBorder and
LightBorder property of the Settings
object to equal colors you will achieve a flat look and if you specify let's say
DarkBorder = Color.Black
LightBorder =
SystemColors.ControlDark
you will get:
As I said in future updates I will improve further the painting of the TetrisGrid control as well as will synchronize the design and run
time options. And yet another thing I intend to do is to write a custom
drop-down control for selecting keys values - you may have already noticed that
by default not all the members of the Keys enum are
populated in the property grid dropdown combo.
Feel free to make your comments (good or bad:)). I would like to know of any bugs!
| You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||