C# week
Save up to 80 % on our C# e-learning courses. Only this week!
Get up to 80 % extra points for free! More info

Lesson 6 - Tetris in MonoGame: Block

In the previous lesson, Dividing a MonoGame Project into Components, we showed how to divide a game project into several components.

Today we're finally starting with the Tetris logic, more precisely with the falling block.

There will always be only two block instances during gameplay - the one that is currently falling down, and the other one that will be next. Since there's going to be more than one block instance in our game, we'll represent the block by a standard class, instead of a game component. Components usually contain a major part of the game logic. After the block hits the "ground", it becomes part of the game board, which isn't a block collection, but a two-dimensional tile array. There certainly are many other solutions to implement this, but this appears to be the simplest one to me.

Let's add a new class that'll represent the Tetris block into the Robotris project. We'll name it Block and and set its access modifier to public.

public class Block

We'll add a public Tiles property to it, which will be a two-dimensional int type array. This will store the individual tiles that the block is made of. The array size is 4x4 (because of the I-block, which is the longest one). We could imagine the S-block like this:

0110
1100
0000
0000

1 represents a used tile, while 0 represents an empty one. We'll set the property setter to private, but it's not completely necessary, since we'd achieve a similar result using an ordinary field. In games, properties aren't used as much as we're used to. That's because they're slower, and video games usually require maximum performance.

public int[,] Tiles { get; private set; }

Next, we'll add the block's position on the game board, making it a regular public field of the Vector2 type. Since Vector2 is a structure (a value type), it'd be a problem to modify its components if we made it a property. So don't worry to use standard public fields in your game projects. Also, we'll add the necessary using statement:

using Microsoft.Xna.Framework;

and the field itself:

public Vector2 position;

Since we know that arrays in C# are passed by reference, we can't simply assign the tiles of one block into another. The problem would be that both blocks would use the same tiles, and if one of them was rotated, the other block would rotate as well. Because we'll copy the block tiles often, we'll create a method for it. It'll create a new tiles array and assign the values of those we want to copy. The tiles array copy will be then returned. We only need two nested for loops for the whole process.

private int[,] CopyTiles(int[,] tiles)
{
    int [,] newTiles = new int [4, 4];
    for (int j = 0; j < 4; j++)
        for (int i = 0; i < 4; i++)
            newTiles[i, j] = tiles[i, j];
    return newTiles;
}

Now, let's implement a class constructor that will take the tiles as a parameter to create its tiles based on it. We'll also reset the block position:

public Block(int[,] tiles)
{
    Tiles = CopyTiles(tiles);

    position = new Vector2(0, 0);
}

Falling

Let's add a simple method named Fall() that'll move the block one row downward:

public void Fall()
{
    position.Y++;
}

Rotating

That was very easy :D Now let's try something more challenging. We'll write a method that rotates our tiles array clockwise. We'll rotate the block by copying its tiles array and use this copy to replace the original array. The trick is that while doing it, we'll also swap the X and Y coordinates and then subtract the Y coordinate from the number 3. For better understanding, see the illustration below:

Rotation of tetris block

If you're still confused, try to draw it on a piece of paper and rotate it. If that doesn't help, you just have to trust me :) The method looks like this:

public void Rotate()
{
    // temp array
    int[,] a = CopyTiles(Tiles);
    // rotate the array by swapping coordinates, like it was a matrix
    for (int y = 0; y < 4; y++)
        for (int x = 0; x < 4; x++)
            Tiles[x, y] = a[y, 3 - x];
}

The block rotation has a problem that we'll ignore for the sake of tutorial simplicity. Some blocks are 2x2 tiles large (only the O-block in the original version), some are 3x3 tiles large and some even 4x4 (the I-block in the original version). The method above always rotates the block as it was 4x4, so smaller blocks aren't centered properly. This could be solved by adding some additional check. If you demand it, I can add it after the course is completed.

Now we just need to render the block. We've already shown how to render components. The block rendering will be very similar. Let's start by adding the Draw() method to it, with a SpriteBatch and the corresponding tile texture as parameters. In components, we injected dependencies by passing the game instance in the component's constructor. Now we pass references via method parameters, which is another way to deal with dependencies. This way is better for individual game objects (e.g. individual tiles). Components, on the other hand, are much larger logical parts (e.g. a level containing tiles, the game board, ...). Since the block coordinates are the coordinates in the game board (i.e. [6; 10]) and not the coordinates on the screen, we'll add the border parameter that'll allow us to shift the block. The method will loop through all individual tiles and render those that have their value greater than 0. Inside the loops, we'll calculate the render position using the tile size to place the tiles side by side next to each other. We'll get the size from the Width and the Height properties of the texture instance.

public void Draw(Vector2 border, BetterSpriteBatch spriteBatch, Texture2D sprite)
{
    for (int j = 0; j < 4; j++)
        for (int i = 0; i < 4; i++)
            if (Tiles[i, j] > 0)
                spriteBatch.Draw(sprite, new Vector2(border.X + (i + position.X) * sprite.Width,
                border.Y + (j + position.Y) * sprite.Height), Color.White);
}

We'll add the following using statement to use the Texture2D type

using Microsoft.Xna.Framework.Graphics;

Now, let's test the class. Download the tile sprites from the archive below this article. It's a set of 15 tile sprites. Open the Tiles/ folder and drag it into the Sprites/ folder in MonoGame Pipeline Tool. This is how it should look like:

Content after adding the tile sprites

Let's go to the LevelComponent class and add 3 private fields to it:

private Block block;
private Vector2 gameBoardPosition;
private Texture2D tileSprite;

The first one is the currently falling block instance, the second one is the game board position in the level background, and the third one is the tile sprite.

In Initialize(), we'll assign a new Block class instance into the block field. Since we don't have a generator that would create blocks from certain patterns yet, we'll create one such pattern ourselves for testing purposes. Since we know that a new .NET integer array contains only zeros, we just need to set 4 specific values to form the S-block (the block shown in the block rotation picture above):

int[,] pattern = new int[4, 4];
pattern[1, 0] = 1;
pattern[2, 0] = 1;
pattern[0, 1] = 1;
pattern[1, 1] = 1;
block = new Block(pattern);

We'll also set the gameBoardPosition field right after it:

gameBoardPosition = new Vector2(366, 50);

Inside LoadContent() we'll load one of the tile sprites into the respective field we defined:

tileSprite = robotisGame.Content.Load<Texture2D>(@"Sprites\Tiles\5");

Now let's move into Draw() and add the block rendering right after the background rendering:

block.Draw(gameBoardPosition, robotrisGame.spriteBatch, tileSprite);

Although we have to call the Draw() method, the block still handles all the rendering operations itself. This is how it should be with all game objects.

Now let's try:

Sample Robotris game in MonoGame

As the last thing, we'll add the rotation logic. Inside Update() we'll update the rotation when either the Enter key or the up arrow cursor key is pressed:

if (robotrisGame.NewKey(Keys.Enter) || robotrisGame.NewKey(Keys.Up))
    block.Rotate();

You can try again. Except for the problem of rotating 3x3 block as 4x4 block, it works pretty well.

In the next lesson, Tetris in MonoGame: Block Generator, we'll implement a block generator, and make the block render its tile sprites randomly.


 

Download

Downloaded 3x (15.72 MB)
Application includes source codes

 

Previous article
Dividing a MonoGame Project into Components
All articles in this section
Tetris From Scratch
Article has been written for you by David Capka
Avatar
Do you like this article?
No one has rated this quite yet, be the first one!
The author is a programmer, who likes web technologies and being the lead/chief article writer at ICT.social. He shares his knowledge with the community and is always looking to improve. He believes that anyone can do what they set their mind to.
Unicorn university The author learned IT at the Unicorn University - a prestigious college providing education on IT and economics.
Activities (3)

 

 

Comments

To maintain the quality of discussion, we only allow registered members to comment. Sign in. If you're new, Sign up, it's free.

No one has commented yet - be the first!