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:
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:
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:
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:
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