Click here to Skip to main content
Click here to Skip to main content

Silverlight Tetris

, 2 Dec 2008
Rate this:
Please Sign up or sign in to vote.
Build a tetris game using Silverlight 2.

Introduction

While it has not happened yet, Silverlight will change the development world much in the way that .NET did years ago. The fusion of .NET power with a lightweight web footprint is truly going to take your everyday, run of the mill ASP.NET site to new heights. Not to mention the powerful implications on desktop software, as well. (By this, I mean that with some tweaking, Silverlight can also be distributed as a desktop application.)

Now, I love all the fancy animations and super transformations of which I’ve seen plenty of samples. Lord knows I enjoy watching colors fade, and circles grow and shrink, and all that general interactive design goodness. But, that’s not what really excites me about the technology. Call me crazy, but it’s the fact that I have a standardized, .NET based interface for delivering applications to the client. Yes, the web is standardized, but browsers don’t seem to be. Yes, you can use JavaScript, or VBScript, or whatever else is out there, but I like C#, and I like .NET, and I love the fact that I can now deliver a solution entirely developed in that space. We’ve had C# in the database for some time now (even in Oracle!), and for a while, we had those ActiveX documents… oops, I mean XBAPs that brought the power of WPF to the web. It’s not been since the advent of SL2, however, that we’ve really seen a technology that has such far reaching scenarios, yet is still highly lightweight (2MB). Years ago, I wrote a Tetris clone - Blox - that illustrated the power of GDI+ and some of what can be accomplished with it. I thought it would be cool to rewrite the entire app from scratch using Silverlight 2.

Click here to play.

The Rules of the Game

For those of you who spent the Eighties and Nineties in a cave, Tetris is a puzzle video game, originally designed and programmed by Alexey Pajitnov, in June of 1985, while working for the Dorodnicyn Computing Centre of the Academy of Science of the USSR in Moscow. (I would suggest reading an article on Wikipedia to gather more information on the subject, as describing it in detail is beyond the scope of this article.) It features a configuration of four blocks into seven shapes: I, L, J, S, Z, T, O. These shapes fall from the sky, and must be placed in such a way as to fill a line completely, leaving no gaps. The shapes may also be rotated to assist in attaining the best placement. When a level is filled using the variously shaped falling blocks, the line disappears and points are awarded. That’s basically it.

Developing Blox

As with any game, before actually diving into the semantics of points and even forms, it is important to understand the physics of the game. These are the factors we must consider that affect the artifacts on the screen. They typically do not require user interaction, but can. In the case of our Blox API for building Tetris games, there are five primary considerations:

  • Gravity - As soon as a shape materializes in the Tetris world, it immediately begins falling towards the floor of the game. Over time, the force of gravity can increase to make the game more difficult. This means that a timer is required to process the block’s descent.
  • Rotation - Shapes in the Tetris world can be rotated in a clockwise manner to 90, 180, 270, and 360/0 degrees. The rotation must obey all other rules.
  • Boundaries - Shapes cannot move through other shapes, or move past the boundaries of the game.
  • Alignment - Since shapes cannot pass though each other, it stands to reason that shapes can pile up atop one another. In fact, the game is lost when there are so many shapes piled up that it is no longer possible for a new block to be dropped.
  • Motion - Shapes can be moved sideways, left and right, at will (provided their paths are not blocked by any other shape or the boundaries of the game).

There are obviously more things to consider, but these five will do to explain the basic functionality.

Key Components

The primary classes of the Blox API are GameField, Shape, Block, and LineManager (depicted below):

primary_classes.jpg

The GameField is a Silverlight User Control which represents the surface area of the game. It provides the background colors, size, boundaries, and the speed of the game. It also provides a hosting surface for Shapes and LineManagers. Visually, GameField contains only one element, a grid. The general pattern is as follows. At load time, GameField populates its internal grid with enough Block objects to fill the entire gaming surface (determined by GameHeight and GameWidth). The parts of GameField’s Loaded method that are relevant to this discussion are listed below:

_blocks = new BlockCollection (this.GameWidth, this.GameHeight);

//populate width
foreach (int game_width in Enumerable.Range(0, this.GameWidth))
{
 ColumnDefinition col_def = new ColumnDefinition();
 //col_def.Width = new GridLength(25);
 LayoutRoot.ColumnDefinitions.Add(col_def);
}

//populate height
foreach (int game_height in Enumerable.Range(0, this.GameHeight))
{
   RowDefinition row_def = new RowDefinition();
   //row_def.Height = new GridLength(25);
   LayoutRoot.RowDefinitions.Add(row_def);
}

////populate controls
foreach (int game_height in Enumerable.Range(0, this.GameHeight))
{

  LineManagers.Add(new LineManager(this.GameWidth, game_height));

  foreach (int game_width in Enumerable.Range(0, this.GameWidth))
  {
      //add a block to that area
      Block block = new Block();
      block.SetValue(Grid.ColumnProperty, game_width);
      block.SetValue(Grid.RowProperty, game_height);
      LayoutRoot.Children.Add(block);
      Blocks.Add(block, game_width, game_height);
  }    
}

As you can see from the sample, GameHeight and GameWidth determine how many rows and columns the internal grid of GameField will have. Next, a Block object is added to each cell of the grid. The block is also added to a BlockCollection represented by the Blocks property. BlockCollection a simple class that encapsulates a two dimensional array of Block objects, the code for it is seen below.

public class BlockCollection
{
    Block[,] _blocks;
    int _width, _height;

    public BlockCollection(int width, int height)
    {
        this._height = height;
        this._width = width;
        _blocks = new Block[width, height];
    }

    public Block this[int left, int top]
    {
        get
        {
            //dont throw error, just return 
            if(left >= _width )
                left = _width - 1;
            if(top >= _height)
                top = _height -1;
            return _blocks[left, top];
        }
    }

    internal void Add(Block block, int left, int top)
    {
        _blocks[left, top] = block;
    }
}

The Blox API does not use WPF animation – it did not seem necessary; rather, the appearance of motion is achieved by turning individual Block objects within the grid ‘on’ or ‘off’. The Block class provides the Occupy and Clear objects for doing this.

public void Occupy(Shape shape)
{
    LayoutRoot.Background = shape.Background;
    _isoccupied = true;
}

public void Occupy(Shape shape, Thickness borders, CornerRadius corners)
{
    LayoutRoot.Background = shape.Background;
    LayoutRoot.BorderThickness = borders;
    LayoutRoot.CornerRadius = corners;
    _isoccupied = true;
}

public void Occupy(Brush background)
{
    LayoutRoot.Background = background;
    _isoccupied = true;
}

public void Clear()
{
    LayoutRoot.Background = GameField.Singleton.FieldBackground;
    _isoccupied = false;
}

This process is managed in two ways. The LineManager can do this on a line by line basis, moving every block on a given line down one notch. The Shape can also do this, in which case, it is coordinating the re-configuration of blocks in response to gravity or rotation. Let’s look at the Shape first.

Shape

Shape is an abstract base class for all the possible shapes in the Tetris universe. As mentioned earlier, this can be I, J, L, S, Z, T, or O. Knock yourself out with some new shapes if you so desire. Adding a new shape to the GameField is as simple as the following:

SquareShape square = new SquareShape();
square.Background = new SolidColorBrush(Colors.Purple);
square.Left = 10;
square.Top = 0;
control_gamefield.AddShape(square);

Inside the GameField, AddShape looks like this:

public void AddShape(Shape shape)
{
    if (Blocks[shape.Left, shape.Top].IsOccupied)
    {
        if (GameOver != null)
            GameOver();
        }
        else
        {
            ActiveShape = shape;
            ActiveShape.Wedged += (target_shape) =>
            {
                ActiveShape = null;
                if (ShapeWedged != null)
                    ShapeWedged();
            };                
            shape.Draw(this);
            shape.Initialize(this);
        }
    }

From this sample, you can see some of the key mechanics of the game. First is the condition by which the game ends; this is when there is no space for a newly added shape to be placed (indicated be checking the IsOccupied property of the block at the shape’s top left corner). Note that this is not necessarily at the top of the GameField. In the previous listing, we saw that the SquareShape was placed at the top, but this does not have to be the case. IsOccupied exposes the _isoccupied private field of Block. If there is indeed space, the new shape is set as the ActiveShape of the GameField, a Wedged event is set on the shape, the shape is actually drawn on the screen, and finally, the shape is initialized. We will start the discussion with Draw.

Draw/Clear

Draw is one of the abstract methods of the Shape class. For the Square class, Draw looks like this:

public override void Draw(IShapeRenderer field)
{
    field.Blocks[Left, Top].Occupy(this);
    field.Blocks[Left + 1, Top].Occupy(this);
    field.Blocks[Left, Bottom].Occupy(this);
    field.Blocks[Left + 1, Bottom].Occupy(this);
}

As you can see, the general idea here is to call Occupy on the appropriate blocks for the given shape. Based on this revelation, it should be easy to see why the subsequent Clear for Square would be defined as follows:

field.Blocks[Left, Top].Clear();
field.Blocks[Left + 1, Top].Clear();
field.Blocks[Left, Bottom].Clear();
field.Blocks[Left + 1, Bottom].Clear();

Given that some shapes will occupy different blocks when rotated, for any shape other than Square, the shape object has a ShapeAxis property (which indicates the degree of rotation a shape is presently in). For the I shape (Line class), which has only two distinct representations, the Draw method looks like this:

switch (this.ShapeAxis)
{
    case 0: case 180:
        field.Blocks[Left , Top].Occupy(this);
        field.Blocks[Left - 1, Top].Occupy(this);
        field.Blocks[Left + 1, Top].Occupy(this);
        field.Blocks[Left + 2, Top].Occupy(this);
        break;
    case 90: case 270:
        field.Blocks[Left, Top].Occupy(this);
        field.Blocks[Left, Top + 1].Occupy(this);
        field.Blocks[Left, Top + 2].Occupy(this);
        field.Blocks[Left, Top + 3].Occupy(this);
        break;
}

For the L shape which has a representation for each of its axes, Draw looks like this:

switch (this.ShapeAxis)
{
    case 0:
        field.Blocks[Left, Top].Occupy(this);
        field.Blocks[Left - 1, Top].Occupy(this);
        field.Blocks[Left + 1, Top].Occupy(this);
        field.Blocks[Left + 1, Top - 1].Occupy(this);
        break;
    case 90:
        field.Blocks[Left, Top].Occupy(this);
        field.Blocks[Left, Top - 1].Occupy(this);
        field.Blocks[Left, Top + 1].Occupy(this);
        field.Blocks[Left + 1, Top + 1].Occupy(this);
        break;
    case 180:
        field.Blocks[Left, Top].Occupy(this);
        field.Blocks[Left - 1, Top ].Occupy(this);
        field.Blocks[Left - 1, Top + 1 ].Occupy(this);
        field.Blocks[Left + 1, Top].Occupy(this);
        break;
    case 270:
        field.Blocks[Left, Top].Occupy(this);
        field.Blocks[Left , Top - 1].Occupy(this);
        field.Blocks[Left - 1, Top - 1].Occupy(this);
        field.Blocks[Left, Top + 1].Occupy(this);
        break;                
}

Initialize

Initialize on the base abstract class looks like this:

public virtual void Initialize(GameField field)
{
    _timer_descent = new DispatcherTimer();
    _timer_descent.Interval = TimeSpan.FromSeconds(field.GameSpeed);
    _timer_descent.Tick += (sender, args) =>
    {
        Decend(field);
    };
    _timer_descent.Start();
}

This means that the primary purpose of Initialize is to start a timer associated with each Shape, which allows the shape to fall. It makes much more sense to have this defined as part of GameField, since for the moment, each shape uses the general game speed defined by GameField; however, I chose to do it this way as an extensibility mechanism, should one intent to provide for mass in the game, for example. After all, a new shape could be devised which fell faster (or slower) than GameSpeed by some factor. Descend, the function called by every GameSpeed, is defined in the base class Shape, and looks like this:

public virtual void Descend(GameField field)
{
    if (CanDescend(field))
    {
        ClearShape(field);
        Top += 1;
        DrawShape(field);
    }
    else
    {
        //stop the decent timer for now
        _timer_descent.Stop();
        Wedge(field);

        if (Wedged != null)
            Wedged(this);
    }
}

As you can see from the sample above, this is where the Wedge event is fired. Basically, when the shape can’t move down any further, it falls into the wedged state, which fires the Wedged event. Descend always calls the abstract method CanDescend, which has a unique definition for every shape type. For instance, the Z shape has a CanDescend defined as follows:

if (_timer == null)
{
    _timer = new DispatcherTimer();
    _timer.Interval = TimeSpan.FromSeconds(GameField.Singleton.GameSpeed);
    _timer.Tick += (A, B) =>
    {
        for (int line = GameHeight - 1; line >= 0; line--)
        {
            LineManager manager = LineManagers[line];
            if (manager.IsFull())
            {
                manager.ClearBlocks();
                if (Score != null)
                    Score(ScoreIncrement);
                manager.ShiftDown();
            }
        }
    };
    _timer.Start();
}

Line Managers

If you re-examine the AddShape listing from earlier, you will notice that once the shape object is wedged, it is set to null (via the ActiveShape property). No more shape object as far as the GameField is concerned. The blocks remain (in fact, since Clear is not called on the shape, the individual blocks that constitute the shape remain, meaning you will still see the shape on screen). This behavior is consistent with Tetris. The question is, what manages the continued descent of blocks after the blocks beneath disappear? The answer is the LineManager class. If you remember the GameField’s Loaded listing above, a LineManager is created for each row in the GameField's internal grid. The parts of GameField’s Loaded method that are relevant to this discussion are listed below:

if (_timer == null)
{
    _timer = new DispatcherTimer();
    _timer.Interval = TimeSpan.FromSeconds(GameField.Singleton.GameSpeed);
    _timer.Tick += (A, B) =>
    {
        for (int line = GameHeight - 1; line >= 0; line--)
        {
            LineManager manager = LineManagers[line];
            if (manager.IsFull())
            {
                manager.ClearBlocks();
                if (Score != null)
                    Score(ScoreIncrement);
                manager.ShiftDown();
            }
        }
    };

    _timer.Start();
}

Besides initializing the boundaries of the game surface, Loaded also initializes and starts a timer which, based on the GameSpeed property, loops though each line, calling IsFull. If the line is indeed full, the manager for that line clears all the blocks on the line, fires a Score event, passing in the GameField’s ScoreIncrement property. Once that is completed, the ShiftDown is called on the manager. The purpose of ShiftDown is to copy all the blocks from the LineManager above to this current LineManager, then redraw each block on the new line, effectively shifting the line above down one line.

public bool ShiftDown()
{
    if (_blocks.Count == 0)
        return false;

    //copy the line above me
    if (LineManagers[_line_number - 1]._blocks.Count > 0)
    {
        ClearBlocks(); //suspect

        _blocks = new List<BlockInfo>(LineManagers[_line_number - 1]._blocks);

        //populate this line with the item above's blocks
        foreach (BlockInfo block_info in _blocks)
        {
            GameField.Singleton.Blocks[block_info.Left, 
                      _line_number].Occupy(block_info.BlockColor);
        }
    }
    else
    {               
        //clear all blocks
        foreach (BlockInfo block_info in _blocks)
        {
            GameField.Singleton.Blocks[block_info.Left, _line_number].Clear();
        }
        _blocks = new List<BlockInfo>( LineManagers[_line_number - 1]._blocks);
    }

    return LineManagers[_line_number - 1].ShiftDown();
}

As you can see from the sample, this is recursive, starting from the cleared line, and moving upwards.

Registering Blocks to LineManagers

The last thing to note is the abstract Wedge method on Shape. For the LineManager functionality to work, it must have pre-existing knowledge of what blocks are on any given line and what positions these blocks occupy. Since each shape is different and occupies different configurations of the blocks during its descent, when it finally stops, this functionality is deferred to it. We already showed that this happens in the Descend function. When there is no space to draw a shape, the following code executes:

//stop the decent timer for now
_timer_descent.Stop();

Wedge(field);

if (Wedged != null)
    Wedged(this);

As you can see, before the Wedged event is fired, Wedge is called on the Shape (causing the actual shape’s unique wedge implementation to be called). Here is how Wedge is defined in the Square class:

field.LineManagers[Top].AddBlock(new BlockInfo
{
    Left = this.Left,
    BlockColor = this.Background,
});

field.LineManagers[Top].AddBlock(new BlockInfo
{
    Left = this.Right,
    BlockColor = this.Background,
});

field.LineManagers[Bottom].AddBlock(new BlockInfo
{
    Left = this.Left,
    BlockColor = this.Background,
});

field.LineManagers[Bottom].AddBlock(new BlockInfo
{
    Left = this.Right,
    BlockColor = this.Background,
});

Putting it all Together

There are really two ways to get started relatively quickly with this library. The first is very simple. Open a Silverlight application project in Visual Studio (see the References for how to get access to Visual Studio and get the Silverlight toolkit). Once you have it open, add the library from this article, Block.Silverlight.Library, to your project. Now, open Page.xaml. At this point, it should look like this:

<UserControl x:Class="Blox.Silverlight.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
       >
    <Grid x:Name="LayoutRoot" Background="White">
    </Grid>
</UserControl>

Next, add a XAML reference to the Page.xaml markup:

xmlns:blox="clr-namespace:Blox.Silverlight;assembly=Blox.Silverlight"

I use blox as the namespace, but you can use anything.

Now, add the following into the Grid:

<blox:SimpleTetrisGame />

Your final listing should look like this:

<UserControl x:Class="Blox.Silverlight.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:blox="clr-namespace:Blox.Silverlight;assembly=Blox.Silverlight.Library"         
    >
    <Grid x:Name="LayoutRoot" Background="White">
        <blox:SimpleTetrisGame />
    </Grid>
</UserControl>

With this completed, build and hit F5. You should see a screen that looks like the following:

This is the screen for the simple Tetris game. It represents a basic Tetris implementation, complete with levels (achieved by passing multiples of 100). Of course, this is based on my preferences as far as what score you get for filling a line. You might want a different score, or perhaps give more points for consecutive scores, for getting back down to the first line, etc. To get more ‘jiggy’ with it, you can use the GameField control directly.

<Border BorderThickness="2" BorderBrush="White" 
                            Margin="10,10,10,100" Grid.Column="0" >
    <blox:GameField x:Name="control_gamefield" GameHeight="20" 
          GameWidth="20"  GameSpeed=".5" ScoreIncrement="10" >
        <blox:GameField.FieldBackground>
            <SolidColorBrush Color="Black" Opacity=".85" />
        </blox:GameField.FieldBackground>
    </blox:GameField>
</Border>

If you choose to do this, you will need to handle the appropriate events to get the basic game functionality working properly. Here is the way the game field is utilized in the SimpleTetrisControl constructor:

control_gamefield.ShapeWedged += () =>
{
    //control_gamefield.AddShape(control_shape_preview.NextShape);
    LoadShape();
};

control_gamefield.GameOver += () =>
{
    txt_game_over_message.Text = "Game Over!";
    txt_final_score.Text = "Final Score:" +  Score.ToString();
    border_game_over.Visibility = Visibility.Visible;
};

control_gamefield.Score += (points) =>
{
    Score += points;
    txt_score.Text = Score.ToString();

    if (Score % 100 == 0)
    {
        switch (Score){
            case 100:
                control_gamefield.FieldStopDark.Color = Colors.DarkGray;
                break;
            case 200:
                control_gamefield.FieldStopDark.Color = Colors.LightGray;
                break;
            case 300:
                control_gamefield.FieldStopDark.Color = Colors.Yellow;
                break;
        }

        control_gamefield.GameSpeed -= .1;
    }
};

Please explore the included code for further details.

Conclusion

Hey, I hope you enjoy dissecting the code and creating new and exciting Tetris implementations.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Edward Moemeka

United States United States
Hi I'm Edward Moemeka,
For more interesting articles about stuff check out my blog at http://moemeka.blogspot.com
To correspond, email me at edward.moemeka@synertry.com
To support my company, thus help me feed my family, check out our awesome online preview at www.synertry.com. Remember, its in alpha Wink | ;-)

Comments and Discussions

 
GeneralHelp with mindfactorial PinmemberPrabhoo6-Jan-10 20:21 
GeneralNeed source code of this game Pinmembermirza asim18-May-09 1:01 
GeneralReference to MindFactorial.Silverlight.Library is missing Pinmemberjesper112-May-09 4:58 
GeneralMy vote of 1 Pinmemberamx30009-Dec-08 1:46 
GeneralGame play bug PinmemberRTate5-Dec-08 4:41 
GeneralRe: Game play bug PinmemberEdward Moemeka6-Dec-08 21:14 
GeneralMy vote of 2 PinprotectorMarc Clifton3-Dec-08 1:36 
GeneralMy vote of 1 PinmvpLuc Pattyn2-Dec-08 21:31 
GeneralRe: My vote of 1 PinmemberEdward Moemeka2-Dec-08 22:11 
GeneralMy vote of 2 PinmemberAxelM2-Dec-08 20:41 
GeneralRe: My vote of 2 PinmemberEdward Moemeka2-Dec-08 22:16 
GeneralPlease re-format this article ! PinmemberThomas Weller2-Dec-08 20:21 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140709.1 | Last Updated 3 Dec 2008
Article Copyright 2008 by Edward Moemeka
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid