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 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:
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:
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:
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.
Did you have a problem with anything? Download the sample application below and compare it with your project, you will find the error easily.
Download
By downloading the following file, you agree to the license terms
Downloaded 110x (15.72 MB)
Application includes source codes