Click here to Skip to main content
Email Password   helpLost your password?

This is how the Tetris looks in a sample app

The figures

These are the standard seven figures which I conditionally named:

Introduction

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.

Explaining the object model

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:

So, the TetrisGrid stores an array of such 'single squares' and the length of that array is the product of the columns and the rows. So far we have a grid of cells each one of which knows its coordinates, color and whether it is filled or not.

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:

All we have do when we want to move or rotate a figure is to change its X and Y positions or its 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.

Implementing the Figure class

The Figure class defines the following methods: Let's take a look at the implementation of the 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.

How the moving of a figure is implemented

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.

Implementing the TetrisGrid class

I will not cover all the methods in this class, just the more significant ones.

The 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:)).

Putting it all together - the GATetris class

As I said the control that is exposed to you is an UserControl 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.

Now is the time to talk more about the options that goes with the control. I thought long how to persist the state of the control and finally decided that it is much more useful to have a separate serializable class that stores the needed information and values than to serialize the entire 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!

So, the 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.

Performing cloning of an object through Reflection

The 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:

Points of Interest

By setting the 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!

History

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
Generalsource/demo
Pyro Joe
5:22 7 Apr '05  
your demo does not contain the executable. I also tried opening (importing) with C# 2005 and executing, but it showed many errors. Anyway, can you fix the executable (put it in there!).
GeneralGreat
Wender Oliveira
10:25 3 Aug '04  
Great game... I did a tetris game too but isn't good like this one. I used jpeg images to drown my blocks and not drawing libs...

Wender Oliveira
.NET Programmer
GeneralObject Rotation
Himmett
16:42 14 Mar '04  
Hi,

I wonder if you know a certain method that would allow the rotation of an object.

I searched C# control class and I only found it allows moving an object control by changing the location of the (Top/Left) point. This performs a translational motion. I wanted to see how to enable a control a rotational motion.

Thanks
Himmett
GeneralCool Game
sumitkm
10:46 5 Mar '04  
Hey Georgi this is a really cool job. I just downloaded it and played 2 hrs before I took a first look. I did some minor modifications to bring up gradient cells for the blocks. Looks cool. Haven't tested for the performance hit.

Anyway good Job.
Sumit.Smile
GeneralRe: Cool Game
Georgi Atanasov
23:11 7 Mar '04  
Hello, sumitkm!

Thanks for your message!

Regards,
Georgi
GeneralDirectX, what do i need to start?
dmartins
6:11 3 Feb '04  
SighI want to start using DirectX9 to build a game.
I started reading some things on MSDN and some examples,but i dont know how to compile the code,threre are so many errors... I do not know how to startFrown I think i dont have the libraries,where do i get them? How can i use them? Please help me!!!!

Deniz Martins
GeneralRe: DirectX, what do i need to start?
Titiger
22:13 4 Feb '04  
Go to microsoft.com and search for DirectX. Download the SDK and install.
In Visual Studio, you can add refrences to your project.
Why not just search google "C# directX"? you can download some games to learn.

for fun
GeneralRe: DirectX, what do i need to start?
Anonymous
2:31 3 Apr '04  
To begin you have too download the DirectX Sdk. for the rest I am olso lost in DirectX programming.Good Luck Wink
GeneralGreat game! Feature request
soichih
13:55 29 Jan '04  
I loved your tetris~! Great work!

If you are going to upgrade this game, I'd like to vote for adding sound to this game! I loved the clicky sound on tetris when I drop the block or got my row cleared, etc..!

Also, do you think you can improve on how blocks are coming down? Right now, it simply moved maybe 32 dots (or whatever the height of the block) at time, but I think it will be neat if you can make it to move smoothly but quickly for the 32 dots and stay for second on the air, and then again smoothly and quickly goes down by 32 dots!

Well, I just wanted to post my request. Maybe I will work on it.
GeneralRe: Great game! Feature request
Profox Jase
0:29 2 Mar '04  
Hiya

This is a great game eh?

If you want to add some sound, you could take a look at (shameless plug Red faced) my article which I wrote some time ago, when I was learning C#. Its not too bad and shows you how to use an array of activex media players to play a number of sounds. If you do decide to use it, make sure you read the article first, or finish reading this sentence...the player assumes the files you pass it are temporary and may be deleted, so you should make a copy of them first.





Jase

jason.king@profox.co.uk
Feel the love at www.profox.co.uk
GeneralTerms ...
Bart Goossens
10:52 29 Jan '04  
Like written on the next site http://www.tetris.com/building_blocks/glossary2.html
the terminology is a little bit different. But otherwise nice coding ...

And an other small but imortant detail:
Q. Who do I contact for Tetris® Licensing information?
A. Please contact licenses@tetris.com for information regarding licensing. Please do not use this email address for other questions ...
(Copyright 2003 The Tetris Company, LLC. Tetris®; © Elorg 1987-2003;Tetris® Logo by Roger Dean; © The Tetris Company 1997. Original concept and design by Alexey Pajitnov. Tetris® licensed to The Tetris Company, LLC. All rights reserved.
)
Tetris is a registrated trademark Unsure so be carefull with what you're writing or doing

GeneralIt's cool
vgzbg
12:55 28 Jan '04  
It's a very nice tetris. I played it for hours expecting a naked girl to appear, but well, it did not!!!!!! Where is she?!? It will be nice to see a naked girl's in teris. I want naked girls, or men! I am desperate! Red faced

Gogo's Opponent
GeneralYep! You're quite right!
joro_23
13:17 28 Jan '04  
I like it too, but I have some advise 'bout the game speed - during changing levels it becomes very difficult /suddenly the speed increases to the extend of IMPOSSIBLE!!/ Please correct this, Georgi. And of course - the question with the naked girls /or men instead/ - I hardly motivate... Well, at least include some animals, also naked of course! Some shaved sheeps will do! WTF


Forever your's!
GeneralA.D.H
Anonymous
12:20 27 Jan '04  
dragi gospodin Atanasov.Napravili ste raznovidnost na tetris bez da si platite licenza za tazi si deynost.za sajalenie tryabva da vi saobshtim che shte tryabva da ponesete globata koyato e v razmer na 8 leva koeto pravi po4ti 2 kokteyla v broadway.taka 4e sha se razberem kato se varnesh
(ot kompaniyata)
GeneralRe: A.D.H
Georgi Atanasov
2:30 28 Jan '04  
He-heBig Grin

Tova mi haresa!
6te se razberem po vaprosaCool

Georgi
GeneralNo C#2002 - No way!!!
Jeff S Davis
11:52 27 Jan '04  
The source apparantly requires C# 2003 or better. That is a pain for me because I only have C#2002 (Visual Studio 2002, actually).

Trying to open the source on my machine therefore gives me the $#@%$#@%#$% "newer version required" garbage.

Is there any way this can be backported?
GeneralRe: No C#2002 - No way!!!
Anonymous
14:48 27 Jan '04  
There are projects on codeproject for that - but it's only the project file that is incompatible, not the c# code. So if worst comes to it, just create another project and add the cs files and whatever.
GeneralRe: No C#2002 - No way!!!
pascode
21:22 7 Mar '04  
Could you please provide some detailed instructions for this? Have to build a .DLL first or not?
Thanks in advance

GeneralRe: No C#2002 - No way!!!
Agent 86
20:39 9 Mar '04  
Find the file with a ".csproj" extextion, and open it in wordpad. look for the line that says:

SchemaVersion = "2.0"

and replace with:

SchemaVersion = "1.0"

then open the file with VS 2002. Hope this helps!

Agent 86
GeneralRe: No C#2002 - No way!!!
pascode
23:36 16 Mar '04  
Yes, it works!!
Thank you very much

GeneralFreaky Zip File
stano
5:09 24 Jan '04  
The zip file doesn't appear to work with XP's zip utility, but it does work with Winzip.

Just thought you should know.
GeneralRe: Freaky Zip File
Cristoff
22:29 24 Jan '04  
it does
GeneralRe: Freaky Zip File
Georgi Atanasov
23:21 25 Jan '04  
Hi,
The zip file has been made with WinRAR and on my PC it works with the XP archive tool.

A, be, Hristo, ti li tovaSmile)

Regards,
Georgi
GeneralRe: Freaky Zip File
kromozom
5:11 27 Jan '04  
Stano, I'm using Windows Server 2003 Enterprise Ed. and the zip file also works with default Windows' zip program.

-
When in doubt, push a pawn!
-
GeneralSettings
Mike Ellison
15:20 21 Jan '04  
Hi Georgi. I like what you've done here. I think the Options window you have makes a nice example of how to use the PropertiesGrid for application or object settings.


Last Updated 21 Jan 2004 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010