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

Lesson 10 - Tetris in MonoGame: Score and Level Completing

In the previous lesson, Tetris in MonoGame: Functional Game Core, we made the game core functional and playable.

Today we're going to complete the level.

Next Block

As is typical to Tetris, the player can see the next block, so they can place the current one strategically. Our game won't be different. We'll add a nextBlock field to LevelComponent that will store the next block instance:

private Block nextBlock;

In Initialize(), we'll create the next block instance and store it in the field, right before calling NextBlock():

nextBlock = blockGenerator.Generate(7);
NextBlock();

Let's move into the NextBlock() method, where we'll assign the next block to block and also generate a new one. The code will now look like this:

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

Finally, we'll render the next block. There's the robot's arm in the background image ready for it, radiating blue light. That's the spot we'll render the next block at. We'll add the following line into Draw(), right below rendering the current block:

nextBlock.Draw(new Vector2(930, 200), robotrisGame.spriteBatch, tileSprites);

The result:

Sample Tetris game in MonoGame - Tetris From Scratch

Score

The game should, of course, challenge players to play it. Tetris has the score and the highest reached level for this purpose. In relation to the player, we're also interested in the number of completed rows and the player's nickname. In the following lessons we'll show how to upload the score to the Internet so players would appear in the hi-score table along with their nicknames.

We'll add a new class named Player to the Robotris project, and put the mentioned fields in it. The class will only serve to store the player data, and all its fields will be public. Be sure to give the class the public access modifier.

public class Player
{
    public long score;
    public int rows;
    public int level;
    public string nickname;

    public Player()
    {
        score = 0;
        rows = 0;
        level = 1;
    }
}

This class isn't very interesting, except that score is of the long type. That's because a really good player might reach score that is greater than the int range :) The constructor then initializes the fields.

The Player instance will be managed by the RobotrisGame class itself. That's because we'll need to share it between other components (such as the score table) in the following lessons. Let's move to RobotrisGame.cs and add a player field:

public Player player;

That's all we do in RobotrisGame.cs, now let's go back to LevelComponent. There, in Initialize(), we'll create a player instance:

robotrisGame.player = new Player();

Rendering HUD

Now we'll implement rendering of a HUD (an information panel with the player's data) that'll be displaying the current score and level. Let's go into the Draw() method and simply render the player's instance data there. We'll need a smaller font for this. We already have one prepared. The font will be used in this particular component only, so we'll load it here. As another option would be placing it into RobotrisGame.cs and set it as public. First, let's create a private font field:

private SpriteFont font;

Inside LoadContent(), we'll load the font from Content:

font = robotrisGame.Content.Load<SpriteFont>("Fonts/font_blox_small");

Now in Draw(), we'll render the values at their respective positions:

robotrisGame.spriteBatch.TextWithShadow(font, "score\n " + robotrisGame.player.score.ToString(), new Vector2(30, 390), Color.Red);
robotrisGame.spriteBatch.TextWithShadow(font, "level\n " + robotrisGame.player.level.ToString(), new Vector2(215, 390), Color.Red);

The result:

Sample Tetris game in MonoGame - Tetris From Scratch

Score points

Let's talk about when add points and how many. We'll, of course, reward complete rows. Let's stick with the original Tetris version A. Given points are based on the number of rows completed by placing a single block:

Number of completed rows Formula
1 level * 40 + 40
2 level * 100 + 100
3 level * 300 + 300
4 level * 1200 + 1200

Completing four rows at once is called "tetris", and can be only achieved using the I-block.

If any row is completed and removed, we'll play a sound effect. Let's prepare it first. We'll add the following field to the class:

private SoundEffect rowSound;

Now load the sound in LoadContent():

rowSound = robotrisGame.Content.Load<SoundEffect>(@"Sounds\sound_row");

Let's add the score. We'll move to Update(), more specifically to the condition that checks the block collision and merges the block into the game board. The condition body will now look like this:

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

We'll set the return value of RemoveRows() to the rows variable (it's the number of rows that have been removed). Let's edit the code:

int rows = gameBoard.RemoveRows();

If we removed at least one row, we'll play the sound effect:

if (rows > 0)
    soundRow.Play();

We'll update the player's rows count:

robotrisGame.player.rows += rows;

And we'll also reward the player with points calculated using the formula:

switch (rows)
{
    case 1: robotrisGame.player.score += robotrisGame.player.level * 40 + 40; break;
    case 2: robotrisGame.player.score += robotrisGame.player.level * 100 + 100; break;
    case 3: robotrisGame.player.score += robotrisGame.player.level * 300 + 300; break;
    case 4: robotrisGame.player.score += robotrisGame.player.level * 1200 + 1200; break;
}

The level will be increased every time 10 rows are removed. At the beginning (0 rows), the level will be 1. We'll calculate it as follows:

int level = robotrisGame.player.rows / 10 + 1;

If the player has different level than the one we've just calculated, we'll update it, and at the same time we'll increase the game speed by 5/6:

if (robotrisGame.player.level != level)
{
    robotrisGame.player.level = level;
    speed = speed * 5 / 6;
}

Now you can try.

Game Over

Let's stay in the block collision code for a while. We'll implement game over. The player loses when the fallen blocks reach the top edge of the game board. In other words, if there's no space for a new block to spawn, the game is over. Therefore, immediately after calling NextBlock(), we'll check whether the spawned block collides. In case of collision, we'll call the game over. For now, this only means we'll exit the application. We'll show the appropriate screen with the score and return back to menu in the following lessons.

Let's add a simple condition right below the NextBlock() call:

if (gameBoard.Collision(block, block.position))
    robotrisGame.Exit();

We exit the game using the Exit() method of the game instance. The level is now playable.

Pause

It's certainly a good idea to give the player the option to pause the game. There are many ways to do this. We could, for example, create another component for the pause and then stop LevelComponent. However, we'll implement a much simpler solution - we'll add states to LevelComponent. There will be two different states: Playing and Pause. In the class, we'll declare a GameState enum and a field of this type:

public enum eGameState
{
    Playing,
    Pause,
}
public eGameState gameState;

In Initialize() we'll set the state to Playing:

gameState = eGameState.Playing;

During the pause the game won't be active and a gray sprite with a text message will be rendered over the screen. Here's the sprite to download:

The pause sprite - Tetris From Scratch

Add it into the Sprites/ folder of RobotrisContent.

As always, we have to create a new field for the sprite:

private Texture2D sprPause;

And load it in LoadContent():

sprPause = robotrisGame.Content.Load<Texture2D>(@"Sprites\spr_pause");

The state will determine the Update() method behavior. Let's divide the method into two branches, one for each state. We'll put the current method body into a condition that checks whether the game state is Playing. Part of the condition body will also be changing the state to pause and playing the pause music when the Escape key is pressed.

// Game
if (gameState == eGameState.Playing)
{
    . . .

    if (robotrisGame.keyboardState.IsKeyDown(Keys.Escape))
    {
        MediaPlayer.Pause();
        gameState = eGameState.Pause;
    }
}

Next, we'll add the pause state branch. In case of the Pause state, the game will be checking whether the Y key (Yes, exit the game) or the N key (No, continue playing) is pressed.

if (gameState == eGameState.Pause)
{
    if (robotrisGame.keyboardState.IsKeyDown(Keys.Y))
        robotrisGame.Exit(); // Exit

    if (robotrisGame.keyboardState.IsKeyDown(Keys.N))
    {
        gameState = eGameState.Playing; // Continue
        MediaPlayer.Resume();
    }
}

Now we need to modify the rendering method to respond to the pause state too. At the end of the Draw() method, we'll render the pause sprite and the text message:

if (gameState == eGameState.Pause)
{
    robotrisGame.spriteBatch.Draw(sprPause, new Rectangle(0, 0, robotrisGame.windowWidth, robotrisGame.windowHeight), Color.White);
    robotrisGame.spriteBatch.TextWithShadow(font, "game paused", new Vector2(480, 260), Color.Red);
    robotrisGame.spriteBatch.TextWithShadow(robotrisGame.fontCourierNew, "Do you really want to exit the game?\n\n\'Y\' - exit the game \n\n\'N\' - continue playing", new Vector2(440, 340), Color.Red);
}

Now let's try:

Sample Tetris game in MonoGame - Tetris From Scratch

In the next lesson, Tetris in MonoGame: Level Features, we'll add even more features into our game.


 

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

 

Previous article
Tetris in MonoGame: Functional Game Core
All articles in this section
Tetris From Scratch
Skip article
(not recommended)
Tetris in MonoGame: Level Features
Article has been written for you by David Capka Hartinger
Avatar
User rating:
3 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