Get up to 80 % extra points for free! More info:

Lesson 9 - Tetris in MonoGame: Functional Game Core

In the previous lesson, Tetris in MonoGame: Game Board, we implemented the Tetris game board.

Today we're going to make our game core functional and playable.

Let's move to LevelComponent. We'll add a gameBoard field which will store an instance of the game board class we created in the previous lesson:

private GameBoard gameBoard;

In Initialize() we'll set the game board dimensions and position:

gameBoard = new GameBoard(10, 20, gameBoardPosition);

We'll add a method that returns us a new block. It'll simply generate a new block and set its position to the top center of the game board:

public void NextBlock()
{
    block = blockGenerator.Generate(7);
    block.position = gameBoard.InsertBlock(block);
}

We can see that we used the game board's method to set the block's spawn position. We'll call this method at the end of Initialize(), instead of generating the block directly:

NextBlock();

We'll definitely want to render the board, so we'll add the following line right below rendering the block in Draw():

gameBoard.Draw(robotrisGame.spriteBatch, tileSprites);

Great. You can try to run the game. A random block is generated at the top center of the game board.

Real-time Logic

We're finally getting to the game's real-time logic. Let's start with making the block falling.

Falling Block

The block will fall down only once in a certain time. Let's begin at 1 second, but we'll speed the game up later. Let's add the speed field of the float type:

private float speed;

In Initialize() we'll set it to 1:

speed = 1;

We're going to need to measure time since the last block fall. So let's add a similar field called timeSinceLastFall:

private float timeSinceLastFall;

In Initialize() we'll set it to 0:

timeSinceLastFall = 0;

We'll move to Update(). Here we'll add a seconds variable of the float type and store the elapsed time since the last update there (in seconds):

float seconds = (float)gameTime.ElapsedGameTime.TotalSeconds;

We'll then add this number of seconds to timeSinceLastFall:

timeSinceLastFall += seconds;

Now we just have to check whether the time specified in speed has elapsed. If so, we'll make the block fall and reset timeSinceLastFall:

if (timeSinceLastFall >= speed)
{
    block.Fall();
    timeSinceLastFall = 0;
}

Let's try it:

Sample Tetris game in MonoGame - Tetris From Scratch

Of course, after a while, the block will fall out of the game board. We have to check for collisions and add the block to the game board if it occurred. We've already implemented methods for this, so it should be no problem to make it work.

The collision occurs when the block is already where it shouldn't be. To fix this problem, we'll simply decrease Y by one. Then we'll add the block to the game board, and find and remove all complete rows. Finally, we'll spawn the next block.

if (gameBoard.Collision(block, block.position))
{
    block.position.Y--;
    gameBoard.Merge(block);
    gameBoard.RemoveRows();
    NextBlock();
}

We'll put this whole condition, including its body, right after calling the Fall() method in Update() because the collision occurs when the block falls either too low or on another block.

Now let's start the game and let it run for a while:

Sample Tetris game in MonoGame - Tetris From Scratch

We can see that collision occurs when the block reaches the bottom of the game board or another block. Collision also occurs when the block is moved outside the game board at its sides.

Block Movement

To have the game actually playable, we should be able to move the block. It shouldn't be difficult, we just have to check for collisions to prevent the block to move out of the game board or hit another block. This is exactly when the position parameter of the Collision() method becomes useful, since we need to ask whether the block's future coordinates are occupied by another block, and if so, we won't move the block there at all.

if ((robotrisGame.keyboardState.IsKeyDown(Keys.Right)) &&
   (!gameBoard.Collision(block, new Vector2(block.position.X + 1, block.position.Y))))
    block.position.X++; // move right
if ((robotrisGame.keyboardState.IsKeyDown(Keys.Left)) &&
    (!gameBoard.Collision(block, new Vector2(block.position.X - 1, block.position.Y))))
    block.position.X--; // move left

We'll put the code into Update(), right after the time update.

The movement is very fast because we check for the key input too often. We've already encountered this problem in one of the first lessons of this course. One option would be to use our NewKey() method and move the block by pressing and releasing the key, but that wouldn't be very user friendly. That's why we'll create a timer, just like we did to make the block fall. The movement will be performed only if a specific time interval has elapsed.

We'll add two more fields to the class - timeSinceLastKeyPress and keyDelay:

private float timeSinceLastKeyPress;
private float keyDelay;

We'll initialize those fields in Initialize():

keyDelay = 0.06f;
timeSinceLastKeyPress = 0;

Now we'll implement behavior exactly the same as we did with the fall. In Update() we'll add the elapsed seconds to timeSinceLastKeyPress. Then we'll check the keyboard input in a condition and eventually reset the elapsed time since the last key press.

timeSinceLastKeyPress += seconds;
if (timeSinceLastKeyPress > keyDelay)
{
    if ((robotrisGame.keyboardState.IsKeyDown(Keys.Right)) &&
       (!gameBoard.Collision(block, new Vector2(block.position.X + 1, block.position.Y))))
        block.position.X++; // move right
    if ((robotrisGame.keyboardState.IsKeyDown(Keys.Left)) &&
        (!gameBoard.Collision(block, new Vector2(block.position.X - 1, block.position.Y))))
        block.position.X--; // move left
    timeSinceLastKeyPress = 0;
}

Try it out. You can regulate the speed by changing the key delay in Initialize().

Let's go back to the rotation. Although it works fine, a situation may occur that the block collides after it's rotated (it may intersect with other blocks or even move out of the game board partially). In that case, we don't want the rotation to occur. We can solve this problem with a small trick - we'll be rotating the block just as we do now, but then if any collision occurs, we'll rotate the block three more times to return it back to its original position, while the player won't notice anything :)

This is how we'll modify the rotation:

if (robotrisGame.NewKey(Keys.Enter) || robotrisGame.NewKey(Keys.Up))
{
    block.Rotate();
    // Rotated block collides - rotating back
    if (gameBoard.Collision(block, block.position))
        for (int i = 0; i < 3; i++)
            block.Rotate();
}

Because the player will certainly be impatient, and the blocks are falling slowly at the beginning, we'll give them an option to accelerate the fall using the right Ctrl key. So the speed won't be constant anymore and the speed field won't be enough for us. Let's go to Update() and add a new variable named timeBetweenFalls, right below the keyboard input check, and set it to speed. If the right Ctrl key is held down, we'll set it to a lower value, let's say 0.15f. Into the key down condition, we'll also have to specify that the speed has to be higher than 0.15f, otherwise the player could use the key to slow down the game to make it easier.

float timeBetweenFalls = speed;
    if (robotrisGame.keyboardState.IsKeyDown(Keys.RightControl) && (speed > 0.15f))
        timeBetweenFalls = 0.15f;

We'll also modify the block fall condition. We'll use the timeBetweenFalls variable, instead of the speed field:

if (timeSinceLastFall >= timeBetweenFalls)

Let's try.

The original Tetris game also had another key that would make the block fall down to the game board's bottom or to the first block it'd collide with. We'll implement this behavior as well and bind it to the down arrow cursor key. All we have to do is to call the Fall() method in a loop until the collision is imminent. Then we'll update the timeSinceLastFall field to invoke the block fall handling below. Insert the following code right below the key input check:

if (robotrisGame.NewKey(Keys.Down))
{
    while (!gameBoard.Kolize(block, new Vector2(block.position.X, block.position.Y + 1)))
        block.Fall();
    timeSinceLastFall += timeBetweenFalls;
}

Let's try.

Sample Tetris game in MonoGame - Tetris From Scratch

That’s all for today.

In the next lesson, Tetris in MonoGame: Score and Level Completing, we'll complete the level :)


 

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 44x (15.73 MB)
Application includes source codes

 

Previous article
Tetris in MonoGame: Game Board
All articles in this section
Tetris From Scratch
Skip article
(not recommended)
Tetris in MonoGame: Score and Level Completing
Article has been written for you by David Capka Hartinger
Avatar
User rating:
4 votes
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 David learned IT at the Unicorn University - a prestigious college providing education on IT and economics.
Activities