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