Retro Games: How to Make a Tetris-like Game

Introduction

Who hasn't played that classic game before to the point where they can close their eyes and still see lines of tetrominoes? For the beginning game developer it's a nice challenge project to tackle and introduces some fundamentally important concepts for making games.

Draw the game screen using data stored in a multi-dimensional array "map"

React to keyboard input : Allow players to rotate tetrominoes

Detect collisions with both the side of the game board as well as other pieces

Detect and remove cleared lines (and possibly update a score table)

Note: While this project utilizes C# and XNA it is certainly possible to extend the logic shown in the code sections to other platforms

Making the Game

The Pieces

There are seven tetrominoes in the game as shown in the diagram below. For some die-hard fans the treatment of these pieces borders on religion. The colors themselves and the way they rotate all have a particular standard that they must follow.

Representing the pieces in game can be done a number of ways. One of the most obvious ways would seem to rely on having seven separate pictures of each of the pieces that we will rotate and draw on screen. However, the blocks are based on one particular square shape which can come in any one of seven colors.

I quick loaded up my favorite paint program and created a small 32 pixel by 32 pixel beveled square that looks like this:

The shapes themselves are built up from some combination of this single block.

This..

.. can be represented like this using a multidimensional array

int[,] block = new int[3, 3] {
{0, 1, 0},
{1, 1, 1},
{0, 0, 0}
};

Once you realize that you can store each of the shapes in a multidimensional array all we need to do is figure out a way to get those shapes into our game. Since the shapes themselves never change it's perfectly fine to hard code each of the basic shapes directly.

To do this we can create a list of multidimensional arrays. You'll notice that the arrays themselves can be 2x3, 3x3, and even 4x4. This makes it easy to accomodate each of the tetromino shapes and works well for performing super rotations.

The Game Board (aka Playing Field, Matrix, Grid)

The standard game board is 10 cells tall and 22 cells tall, where the top two cells are obstructed from view by some type of graphical frame. For the sake of simplicity we are going to chop our game down to 22 cells in height.

For part one of this tutorial we can focus on two major components that we need to keep track of. The first is the board itself which contains all the blocks that have already fallen down into a fixed position. The second is the block that is falling that we will want to potentially apply rotations to based off of user input.

Our game board can actually be stored in another multidimensional array. The beauty of storing the board in a multidimensional array is that it will allow us to "play" the game in our own miniaturized representation of what you see on screen. We can then create a small chunk of code to actually draw the game board based off of our behind-the-scenes representation.

As we begin to fill in static blocks, we will update individual cell locations on the board with the color cells contained within them. After a while we'll start to see a game board that looks like this:

The Falling Piece

Last, but not least, is the currently falling piece. The currently falling piece (aka Spawned Piece) needs to exist separate from the game board. While the game board represents static / non-moving blocks, the falling piece moves downward at a particular pace, can be rotated, and even dropped quickly in response to keyboard input.

To get started we can use a variable to store a randomly generated piece shape and a second to store it's location:

int[,] SpawnedPiece;
Vector2 SpawnedPieceLocation;

Then, we can create a method to quickly choose a tetromino piece from the pieces list at random and colorize it.

// Create a new piece
public void SpawnPiece()
{
int colr = rand.Next(0, pieces.Count); // rand is initialized earlier and has the type "Random"
SpawnedPiece = (int[,])pieces[colr].Clone(); // Make a copy of the piece
int dim = SpawnedPiece.GetLength(0); // Get the dimension of the first row
// Colorize the piece by multiplying it by a scalar representing the color
// In this case, the index for the pieces List is also the same index used for the tintColors
// array
for (int x = 0; x < dim; x++)
for (int y = 0; y < dim; y++)
SpawnedPiece[x, y] *= (colr + 1);
SpawnedPieceLocation = Vector2.Zero; // Temporary
}

Once we have a piece spawned we can perform some limited operations on it (rotate left/right, move left/right, and drop quickly). The first thing we need to ensure is that before we can commit to any operation it must be actually possible to perform that operation.

So to make a move we will do the following:

Accept user input

Determine a new block orientation and location based off of user input

Check to see if we can perform the operation

If yes, commit it

If no, then do nothing

Shown below are some situations we're going to need to be able to detect and handle appropriately:

Our game will use a method called "CanPlace" that will check to see if a piece can be located at a particular board position. It can also tell us if the piece is being blocked by something else or if it is presently offscreen (technically off of the board) and out of bounds.

// Defined outside of the current class
public enum PlaceStates
{
CAN_PLACE,
BLOCKED,
OFFSCREEN
}
...
// Checks to see if piece can be placed at location x,y on the board
// Returns PlaceStates.CAN_PLACE if it can exist there, otherwise reports a reason why it cannot
public PlaceStates CanPlace(int[,] board, int[,] piece, int x, int y)
{
// First we'll need to know the dimensions of the piece
// Since they are square it is sufficient to just get the dimension of the first row
int dim = piece.GetLength(0);
// All pieces are square, so let's use a nested loop to iterate through all the cells of the piece
for (int px = 0; px < dim; px++)
for (int py = 0; py < dim; py++)
{
// Calculate where on the game board this segment should be placed
int coordx = x + px;
int coordy = y + py;
// Is this space empty?
if (piece[px, py] != 0)
{
// If the board location would be too far to the left or right then
// we are hitting a wall
if (coordx < 0 || coordx >= BoardWidth)
return PlaceStates.OFFSCREEN;
// If even one segment can't be placed because it is being blocked then
// we need to return the BLOCKED state
if (coordy >= BoardHeight || board[coordx, coordy] != 0)
{
return PlaceStates.BLOCKED;
}
}
}
// If we get this far we can place the piece!
return PlaceStates.CAN_PLACE;
}

When we reach a point where the block piece is "BLOCKED", then it will be necessary to permanently write it to the game board where it will no longer be moveable.

To accomodate this part of the game we are also going to need to be able to check for and remove any lines that are completed. This is typically where we also need to update some type of score counter.

Lastly, we need a way to rotate a particular piece. The way that this Rotate method works is to perform the actual rotation and give us a new array based off of the rotated block. Since the blocks are all square it is pretty straightforward to swap some of the cell values to perform the rotations.

Let the Blocks Fall!

In this game the blocks move one space down for every given time interval. We're going to need to keep track of how much time has elapsed since the last update.

We will add the following variables to the beginning of your game class:

int StepTime = 300; // Time step between updates in ms
int ElapsedTime = 0; // Total elapsed time since the last update
int KeyBoardElapsedTime = 0; // Total elapsed time since handling the last keypress

From there we just need to add some code in our Update method to adjust the block location based off of time:

protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
ElapsedTime += gameTime.ElapsedGameTime.Milliseconds;
KeyBoardElapsedTime += gameTime.ElapsedGameTime.Milliseconds;
/* SNIP! Keyboard handling code removed */
// If the accumulated time over the last couple Update() method calls exceeds our StepTime variable
if (ElapsedTime > StepTime)
{
// Create a new location for this spawned piece to go to on the next update
Vector2 NewSpawnedPieceLocation = SpawnedPieceLocation + new Vector2(0, 1);
// Now check to see if we can place the piece at that new location
PlaceStates ps = CanPlace(Board, SpawnedPiece, (int)NewSpawnedPieceLocation.X, (int)NewSpawnedPieceLocation.Y);
if (ps != PlaceStates.CAN_PLACE)
{
// We can't move down any further, so place the piece where it is currently
Place(Board, SpawnedPiece, (int)SpawnedPieceLocation.X, (int)SpawnedPieceLocation.Y);
SpawnPiece();
// This is just a check to see if the newly spawned piece is already blocked, in which case the
// game is over
ps = CanPlace(Board, SpawnedPiece, (int)SpawnedPieceLocation.X, (int)SpawnedPieceLocation.Y);
if (ps == PlaceStates.BLOCKED)
{
// Game over.. normally we would change a game state variable but for this tutorial we're just
// going to exit the app
this.Exit();
}
}
else
{
// We can move our piece into the new location, so update the existing piece location
SpawnedPieceLocation = NewSpawnedPieceLocation;
}
ElapsedTime = 0;
}
base.Update(gameTime);
}

Simple enough? Now let's insert some additional code to deal with user keypresses:

Conclusion

This concludes part 1 of "Retro Games: How to Make a Tetromino Game". If you run the attached game sample you will notice that we only have the basic gameplay for the game working. The purpose of omitting these items was to keep the game relatively compact for part 1. What is missing is some type of score counter, showing of the "on deck" or next piece that will be dropped, and adding any type of special effects for removing lines.

In part 2 we will look to tackle polishing up the game a bit more. I hope you enjoyed this tutorial and if you manage to jump ahead of me and complete a full tetromino game, please post a link to your game in the comments section for this article.

This is nice, but isn't it also rather dangerous? The organization that owns the rights to Tetris vigorously pursues anyone caught copying the game.

Why so?

Aren't we allowed to educate our selves?

+ I wonder about all those remakes, did they buy the patent or...

There are a lot of clones out there, and I, for one want to make my own. However, after doing research on this a few years ago I found that The Tetris Company is rather vigilant in their threats against clones.

I believe that tutorials fall under the fair use clause. It would be the same if we used copyrighted material for a school report or thesis. Basically if it's for the use of education, it's cool. What we can't do is sell or distribute the game.

Note: I'm not a lawyer.

Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.