Lesson 7 - Tetris in MonoGame: Block Generator
In the previous lesson, Tetris in MonoGame: Block, we implemented the Block class for our Tetris game.
Today we're going to create a random block generator, more precisely the generator of block patterns.
Patterns File
We'll store the block patterns in a text file. It's a much more convenient and elegant solution than putting it directly into source code, which should contain the game logic only. All such data, like maps or longer texts, should be stored in external files. The file should be secured with a hash or compiled directly into the .exe file of the game, to prevent players from editing it and using it for cheating (by forcing the game to generate easy bricks only). For the sake of simplicity, we'll just give the file very unusual name, so that no one would search for the block patterns in it
We'll add a new text file into the Robotris
project (by adding a
new item the same way as we always do, see the picture) and name it
netfx.dll
:
We'll put block patterns as 4 lines and leave the 5th line empty. The computer doesn't care about it, but it's important to us to read the file more easily. Paste the following data into the file:
1000 1110 0000 0000 0010 1110 0000 0000 0000 0110 0110 0000 0000 1111 0000 0000 0100 1110 0000 0000 0000 1100 0110 0000 0000 0110 1100 0000 0000 0100 0000 0000 1010 0100 1010 0000 0100 1110 0100 0000 1110 0100 0100 0000 0000 0110 0000 0000
The first 7 block patters are from the classic Tetris game, more precisely the Tetris version A, which is also what we're programming. The next ones are new block patterns that are only present in the file, but we won't be using them in this course. You can then improve the game and use them as you want - in different game mode or start spawning them from a certain level.
The last thing that needs to be done is to check the netfx.dll
file in Solution Explorer and then set the Copy to Output
Directory property to Copy always in the Properties
window:
This will ensure that Visual Studio will add this file to the compiled game folder, from where we'll be able to load it. Everything is now ready for the generator.
Block Generator
The block generator will load all the block patterns from the text file into a pattern collection, and then randomly pick one of those patterns and use it to generate a new block.
We'll add a new class named BlockGenerator
and set its access
modifier to public
:
public class BlockGenerator
We'll add a few fields - the patterns
4x4 int
array, which we're already familiar with, the file
field that'll
specify the patterns file path, and the random number generator instance.
private List<int[,]> patterns; private string file; private Random randomGenerator;
We'll then initialize the fields in a constructor, which will take the patterns file path as a parameter:
public BlockGenerator(string file) { patterns = new List<int[,]>(); randomGenerator = new Random(); this.file = file; }
Parsing the File
Let's move to the file parsing. We'll create a LoadBlocks()
method. Here we'll be opening the text file to read it:
public void LoadBlocks() { using (StreamReader sr = new StreamReader(file)) { } }
Don't forget to add the following using
statement (now I mean to
add it at the very beginning of the source code):
using System.IO;
Into the method's using
block, we'll add several variables
before we start reading the file:
string line; int[,] pattern = new int[4, 4]; int j = 0; int lineNumber = 1;
The line
variable is the last read line of the text file. The
pattern
variable is the usual 4x4 array in which we'll store each
pattern loaded from the file. j
is the vertical coordinate of the
pattern array, while i
is the horizontal one.
lineNumber
indicates which line is currently being read.
Let's start reading the file and add the following code right after the variables initialization:
while ((line = sr.ReadLine()) != null) { // skip every 5th line if ((lineNumber % 5) != 0) { } lineNumber++; }
The while
loop keeps reading every line until it reaches the end
of the file. The further code is executed only if the line number isn't
divisible by five. That's because every 5th line is empty.
Let's go into the condition block and store the current line into the appropriate row of the pattern array:
for (int i = 0; i < 4; i++) pattern[i, j] = int.Parse(line[i].ToString());
We'll convert all the 4 characters in the line to string
, and
then parse them into the int
type. We'll store the value in the
pattern at the current coordinates.
Now we only have to utilize the j
variable, which will be used
to specify the pattern row. However, once it exceeds the value of 3
(the 4th, last line), we must reset it, add the pattern to the patterns
collection, and then reset the pattern as well:
if (j > 3) { patterns.Add(pattern); j = 0; pattern = new int[4, 4]; }
Because this piece of code was a bit more difficult to implement, here's the complete method code:
public void LoadBlocks() { using (StreamReader sr = new StreamReader(file)) { // variables initialization string line; int[,] pattern = new int[4, 4]; int j = 0; int lineNumber = 1; // reading all lines of the text file while ((line = sr.ReadLine()) != null) { // skip every 5th line if ((lineNumber % 5) != 0) { // adding all numbers from the line into the pattern for (int i = 0; i < 4; i++) pattern[i, j] = int.Parse(line[i].ToString()); j++; // last line read? if (j > 3) { // add complete pattern patterns.Add(pattern); // and reset all variables j = 0; pattern = new int[4, 4]; } } lineNumber++; } } }
Generating Random Block
Generating a random block is now trivial - we just have to pick a pattern and
create a block based on this pattern. Let's add the Generate()
method, which will take a number of patterns to generate from as a parameter.
This way, with the parameter value of 7
, we can generate simple
blocks, with the value of 12
the more complex ones, and with a
higher number you can even add some bonus ones.
public Block Generate(int count) { int index = randomGenerator.Next(0, count); return new Block(patterns[index]); }
Let's move to LevelComponent
. We'll add a field that will
contain an instance of our block generator:
private BlockGenerator blockGenerator;
Now let's move to Initialize()
to remove both the test pattern
and the block. Next, we'll create a block generator instance here also load the
patterns:
blockGenerator = new BlockGenerator("netfx.dll"); blockGenerator.LoadBlocks();
After loading the patterns, we'll generate a block:
block = blockGenerator.Generate(7);
Let's run the game several times to see the block will always be random. Again, there is still the problem with the blocks centering, but we won't be solving it yet.
Random Tile Sprites
Although we have 15 different tile sprites, all block tiles are the same now.
We'll want the newly created block instance to assign sprites for its tiles
randomly. So the tiles won't just have the 0
to 1
values, but now it'll be 0
to 15
. The block pattern
that we pass in the constructor will always contain only the 0
and
1
values. The block will then generate these sprite indexes
itself.
Let's move back to the Block
class and add a field with the
random number generator instance:
private Random randomGenerator;
We'll initialize it in the constructor:
randomGenerator = new Random();
We'll add a GenerateSprites()
method that'll find all number one
values in the pattern array and replace them with a random number ranging from
1
to 15
. What may be confusing is that we specify the
parameters 1
and 16
in the Next()
method
call, that's because the upper limit is exclusive.
private void GenerateSprites() { // finding block tiles for (int j = 0; j < 4; j++) for (int i = 0; i < 4; i++) if (Tiles[i, j] > 0) Tiles[i, j] = randomGenerator.Next(1, 16); }
We'll call the method in the constructor right after copying the tiles and creating the random generator instance:
GenerateSprites();
Now we have to edit the Draw()
method to take the entire
textures array, instead of a single sprite, and then render them according to
the tile value:
public void Draw(Vector2 border, LepsiSpriteBatch spriteBatch, Texture2D[] sprites) { for (int j = 0; j < 4; j++) for (int i = 0; i < 4; i++) if (Tiles[i, j] > 0) spriteBatch.Draw(sprites[Tiles[i, j] - 1], new Vector2(border.X + (i + position.X) * sprites[0].Width, border.Y + (j + position.Y) * sprites[0].Height), Color.White); }
The block is done for now, but we still have to change its handling, so let's
go back to LevelComponent
.
We'll change the tileSprite
field declaration to an array
declaration, and we'll also rename it:
private Texture2D[] tileSprites;
Let's move to LoadContent()
and load the whole texture array
using the for
loop, instead of loading a single sprite. We have to
initialize the array first.
// Loading tile sprites tileSprites = new Texture2D[15]; for (int i = 0; i < 15; i++) tileSprites[i] = robotrisGame.Content.Load<Texture2D>("Sprites\\Tiles\\" + (i + 1).ToString());
As the last modification we'll rename the field in the Draw()
method:
block.Draw(gameBoardPosition, robotrisGame.spriteBatch, tileSprites);
The result:
In the next lesson, Tetris in MonoGame: Game Board, the game board awaits us.
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 39x (15.72 MB)
Application includes source codes