Tetris Revisited: Bells & Whistles 1
This is a follow-up from the 8-hour Tetris prototype article.
Now we have a working game prototype, I shall walk you through how to make a series of basic improvements. The full source code is available below (see the original article for other dependencies you need to install to compile the code; the game is based on my Simple2D graphics library).
Download: Source code (.cpp) | Source code (.h) | Executable
Time spent: 2.5 hours.
Graphical Tweaks
Grid
Modern Tetris implementations include a background grid to make piece placement easier. Drawing a grid is very easily achieved with two for loops: one to draw the horizontal lines and one to draw the vertical:
// Draw bucket grid for (int y = 0; y < BucketSizeY; y++) renderer->LineWH(LeftSide, TopSide + y * SimpleTetris::BlockSize + 1, BucketSizeX * SimpleTetris::BlockSize, 0, Colour::DarkGray, 0.5f); for (int x = 1; x < BucketSizeX; x++) renderer->LineWH(LeftSide + x * SimpleTetris::BlockSize, TopSide, 0, BucketSizeY * SimpleTetris::BlockSize, Colour::DarkGray, 0.5f);
BucketSizeX
and BucketSizeY
contain the number of blocks that can fit in the bucket horizontally and vertically. SimpleTetris::BlockSize
is the pixel width and height of each block. LeftSide
and TopSide
contain the top-left x and y positions of the bucket.
Rounded Pieces
To make the pieces look slightly less ugly, we change each block from being plotted as a normal rectangle to a rounded rectangle (a rectangle with curved corners). All we have to do is change the block drawing statement to call FillRoundedRectangleWH instead of FillRectangleWH, and add two arguments before the colour argument to specify the x and y radii of the corners:
renderer->FillRoundedRectangleWH(TetrisBucket::LeftSide + x * SimpleTetris::BlockSize, TetrisBucket::TopSide + y * SimpleTetris::BlockSize, SimpleTetris::BlockSize, SimpleTetris::BlockSize, SimpleTetris::BlockSize / 4, // x radius SimpleTetris::BlockSize / 4, // y radius Piece.ShapeColour);
Piece Silhouette
Modern Tetris implementations include a “silhouette” of the current piece. This is a greyed out rendering of the piece showing where it will land if left to fall in its current position.

The silhouette of a piece shows where it will land if it continues to fall from its current location.
To draw the silhouette in the right place, there are a couple of options. We could visualise the piece at the bottom of the bucket and move it up one row at a time until it is not touching any other pieces. A quicker-to-implement but less resource-efficient method is to simulate the piece falling until it can go no further. This is the approach I have used here. The current position of the piece is remembered, the simulation run, the piece drawn in outline and the original piece position restored.
// Draw a tetris piece's silhouette (at the lowest possible position it could fall) void TetrisShape::DrawSilhouette() { int oldPosY = PosY; MoveToBottom(); Draw(false, true); PosY = oldPosY; }
MoveToBottom()
is exactly the same function that is called when the user presses the Fast Drop button when playing (which sends the piece immediately as far down as it will go). The arguments to Draw
specify that we want the piece drawn in a silhouette outline. DrawSilhouette()
is then called from the main scene drawing function on each frame.
Interface Additions
Pausing The Game
All single-player games should have a pause function. This is also very simple to add. The gameState
variable stores the current game state (Menu, Playing, GameOver) etc. What is drawn and the available user actions depend on the game state. To allow pausing, we just add a new game state called Pause and provide a mechanism to toggle pause on and off in the part of the code which checks for user keypresses:
void SimpleTetris::OnKeyPress(WPARAM key) { // Toggle pause if (gameState == Playing && key == 'P') gameState = Pause; else if (gameState == Pause && key == 'P') gameState = Playing;
The else if
is important here. If we had just used another if
without the else
, the game could never be paused as the pause state would be reverted as soon as it was set.
Note that we only allow pause to be activated when the game is currently in progress – not, for example, on the menu screen. If the game is paused, we must also ignore other keypresses by adding a check for gameState
before the code which handles the arrow keys and so on:
// Only process keypresses if the game is in progress if (gameState != Playing) return; if (key == VK_LEFT) currentShape.MoveLeft(); if (key == VK_RIGHT) currentShape.MoveRight(); if (key == VK_DOWN) currentShape.MoveDown(); ...
Finally, we must provide feedback to the user when the game is paused. We do this by adding some simple code that displays a message if the game is paused in the main drawing function:
// Print pause message if (gameState == Pause) { gameOverFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER); Text(0, 200, "PAUSED", Colour::White, gameOverFormat); }
Note that we still want to render the actual game (the bucket, shapes etc.) even when the game is paused, so we don’t check for the pause state when drawing the rest of the game. Note also that printing the pause message must come after the rest of the scene has been drawn, otherwise it will get partially obscured by other drawn objects.
Main Menu and multiple games
The prototype version of Tetris started the game immediately when you ran the program, and the only way to retry when the game was over was to close the program and re-open it. This is not cool! Games should have menus and game over screens where applicable, and we achieve this by adding two more game states: Menu and GameOver.
When the program starts, the initial game state is set to Menu. The main drawing function chooses via an if
statement whether to draw the main game screen or the menu, depending on the game state. Drawing the menu looks something like this:
// Display menu if (gameState == Menu) { gameOverFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER); Text(0, 50, "SimpleTetris", Colour::White, gameOverFormat); scoreFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER); Text(0, 115, "(c) Katy Coe 2012", Colour::White, scoreFormat); Text(0, 142, "www.djkaty.com", Colour::White, scoreFormat); FillRoundedRectangleWH(menuStartX, menuStartY, menuButtonSizeX, menuButtonSizeY, 20, 20, menuButtonColour); Text(0, menuStartY + 33, "Play", Colour::Yellow, scoreFormat); scoreFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_TRAILING); Text(0, ResolutionY - 30, "v1.1", Colour::White, scoreFormat); }
The first 5 lines draw the title and copyright messages. The middle 2 lines draw a box that the user can click on to start a new game, and the last 2 lines draw the version number in the bottom-right corner of the screen.

Basic clickable button: the background colour of the Play button changes when the mouse hovers over it. Clicking on the button causes the game to start when the left mouse button is released.
The Play button has a colour of menuButtonColour
. We want this colour to depend on whether the mouse is hovering over it or not, so we capture mouse movements using a Simple2D function hook as follows:
void SimpleTetris::OnMouseMove(int x, int y, WPARAM keys) { if (gameState != Menu) return; // Toggle the menu button colour if the mouse is hovering over the rectangle if (x >= menuStartX && x < menuStartX + menuButtonSizeX && y >= menuStartY && y < menuStartY + menuButtonSizeY) menuButtonColour = Colour::BlueViolet; else menuButtonColour = Colour::DarkBlue; }
Note that mouse actions are ignored unless the game state is currently Menu. The code checks if the mouse pointer is within the Play button or not and sets its background colour appropriately.
We also need to capture mouse click events on the button:
void SimpleTetris::OnMouseButton(UINT button, int x, int y, WPARAM keys) { if (gameState != Menu) return; if (x >= menuStartX && x < menuStartX + menuButtonSizeX && y >= menuStartY && y < menuStartY + menuButtonSizeY && button == WM_LBUTTONUP) newGame(); }
Once again the code ignores the mouse unless we are in the menu, checks if it is inside the Play button and calls a new function newGame()
if the left mouse button has been released. Note we prefer to wait for the user to release the mouse button rather than starting the game as soon as they press it for ergonomic reasons.
We didn’t have to worry about re-initializing everything needed for the game when it could only be played once, but now we need to make sure everything is correctly prepared at the start of each new game, so we abstract this behaviour into newGame()
, and remove it from the initial setup of the program, waiting instead until the user clicks on Play.
// Set up a new game // Note this has to be called AFTER the brush resources are made void SimpleTetris::newGame() { int shapeNum; // Assign the first and second shapes (the screen shows what the next shape to drop will be) shapeNum = rand() % numShapeTypes; currentShape.SetShape(Pieces[shapeNum]); currentShape.Activate(); shapeNum = rand() % numShapeTypes; nextShape.SetShape(Pieces[shapeNum]); // You are on level 1 level = 1; // No score yet score = 0; // No lines cleared linesCleared = 0; // Empty the bucket bucket.Reset(); // Set game state gameState = Playing; }
The code should be fairly self-explanatory, except that unlike before we must also ensure the bucket is empty at the start of each game, which requires some new code in TetrisBucket
as follows:
// Empty the bucket void TetrisBucket::Reset() { for (int y = 0; y < BucketSizeY; y++) for (int x = 0; x < BucketSizeX; x++) board[y][x] = -1; }
When updating the game objects (namely, the moving piece), we now only want to do this if the game is in the Playing state, ie. not the menu, not paused and not game over. One if
statement clears this up:
void SimpleTetris::UpdateObjects() { // Do nothing if the game is over or paused if (gameState != Playing) return; if (currentShape.Update(level)) newPiece(); }
Game Over
To test for the game over condition, we have to see if the next block that would be placed at the top of the grid collides with another block already present. This is done in newPiece()
where a new piece is set up after the previous one has landed. Once currentShape
has been updated with the new piece to put in play, we check for collisions with the bucket contents:
// If the new shape is already on another shape, it's game over! if (bucket.IsCollision(¤tShape)) { // Set game over state gameState = GameOver; // We still want to show this shape, to show that the grid is full, // so we have to move it gradually upwards til there are no collisions while (bucket.IsCollision(¤tShape)) currentShape.PosY--; }
Note that we have to provide feedback to the user how the game ended, so to this end we still keep the new piece “in play”, and simply move it upwards beyond the top edge of the bucket until it is no longer colliding with anything. This allows the player to see visually that the bucket has overflowed. Remember, the game board is still drawn even in the game over state.
In the main drawing function, we provide additional feedback to the user when the game is over:
// Print game over message if (gameState == GameOver) { gameOverFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER); Text(0, 200, "EPIC FAIL", Colour::Green, gameOverFormat); scoreFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER); Text(0, 270, "Press space to continue", Colour::White, scoreFormat); scoreFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING); }
Finally, we have to allow a game state transition from the game over state to the main menu via a user key press, which we add to the key press-handling function by checking for the space bar:
// Space pressed in game over state returns to the menu if (gameState == GameOver && key == ' ') gameState = Menu;
Since the game essentially does nothing except draw the game board and game over message without updating any object positions or taking other user input once the game over state is entered, this essentially freezes the game until the user presses space, then causes it to go back to the main menu.
Gameplay Mechanics
Shift / Hold
The Shift / Hold mechanic of modern Tetris allows the player the option to switch the current falling piece for the piece shown in the Next Piece box. They can only do this once at a particular point, then may not use the switch feature again until the current (switched) piece has landed.
First we need to track if the user has switched out the current piece or not. We add a boolean variable held
and set it to false both in newGame()
and newPiece()
(the latter is called when each new piece besides the very first one in a game comes into play).
Second, we accept a user key press (Shift key in this case) to do the switch, if it hasn’t already been used on this piece:
if (key == VK_SHIFT && !held) shiftShapes();
Performing the shift is conceptually simple – just swap which piece is in play – but in reality it is more problematic. First, the new piece must be in the same place in the bucket as the old piece. This not only means it has to be moved down to wherever the old piece was, but collision checking must be done and the piece moved around until it fits.
There are various solutions to the piece alignment problem. Here is what I came up with:
// Align a piece with the bottom-left of another piece (for shift/hold) // Disallow if collision bool TetrisShape::Align(TetrisShape other) { PosX = other.PosX + other.firstUsedColumn() - firstUsedColumn(); PosY = other.PosY + other.lastUsedRow() - lastUsedRow(); // Check for out of bounds collisions and fix // We have aligned to the bottom and left so only the right-hand side // could be past the edge of the bucket if (PosX + lastUsedColumn() > TetrisBucket::BucketSizeX - 1) PosX = TetrisBucket::BucketSizeX - GetWidth(); // Check for collisions with other pieces return !bucket->IsCollision(this); }
TetrisShape::Align
changes its own co-ordinates to be aligned to the shape specified in the argument according to the following rules:
- The first two lines make the left-hand side and bottom side of the two pieces line up in the bucket. This ensures that the switched piece will not be any lower down (but its top edge may be higher up) in the bucket than the original piece. It also ensures that it will not be off the left-hand edge of the bucket.
- This leaves the possibility that the switched piece might be off the right-hand side of the bucket. The next line of code moves the piece left so that its right-hand edge is aligned with the right-hand edge of the bucket, if this is the case.
- Finally we check to see if the switched piece is now sitting on top of any pieces already in the bucket, returning
true
if the piece can be placed andfalse
if there is a collision.
Whether or not this matches how modern Tetris is actually implemented I don’t know, but it seems to work in a fluid way that feels natural.
The actual piece switching code called when the user presses Shift looks like this:
// Implement the shift/hold mechanic void SimpleTetris::shiftShapes() { // Align new current shape to be in the same place as the shifted shape bool ok = nextShape.Align(currentShape); // Disallow the shift if this causes a collision if (!ok) return; // Hold used this turn - don't allow again held = true; // Swap shapes TetrisShape tempShape; tempShape = currentShape; currentShape = nextShape; nextShape = tempShape; // Reset rotation of shifted shape nextShape.CurrentRotation = 0; // Activate new current shape currentShape.Activate(); }
Before swapping the pieces, we try to conceptually move the piece in the Next Piece box to be aligned with the current piece. If this is not possible because of a collision, nothing happens. Otherwise, held
is set so the user can’t switch again until another piece comes into play, the pieces are swapped and the fall timer initialized on the new piece.
This would seem to be everything needed, but in fact we have now introduced a new problem: when a piece gets shifted into the Next Piece box, its previous position and rotation in the bucket is retained. This means that when we bring it into play, we have to move it back to the top of the bucket. We also have to reset the rotation of a shifted piece so it appears in the correct orientation in the Next Piece box. Similarly, if a shift is attempted but refused because of a collision, the refused piece takes on the current piece’s place in the bucket, so it must be moved to the top when it comes into play as well. The solution is simple:
// Set up a new piece void SimpleTetris::newPiece() { // Put the current shape in the bucket bucket.Add(currentShape, score, linesCleared, level); // Bring the next shape into play currentShape = nextShape; currentShape.MoveToTop(); // make sure piece is in the right place currentShape.Activate(); // Shift/hold not used this turn held = false; ...
void TetrisShape::MoveToTop() { // No rotation CurrentRotation = 0; // The X position of the shape will always be centerised in the bucket PosX = (TetrisBucket::BucketSizeX - 4) / 2; // The Y position of the shape must be such that the first solid block in the 4x4 grid appears on the top row of the bucket! PosY = -firstUsedRow(); }
This completes the implementation of the shift/hold mechanic.
Score Multipliers
Basic score multipliers are extremely easy to add to the mix. TetrisBucket::Add
processes shapes that have finished falling and updates your score. By passing in a reference to the level the player is currently on, the score for filling 1, 2, 3 or 4 lines simultaneously can be scaled up relative to the level:
// Update score linesCleared += newFills; if (newFills == 1) score += 10 * level; if (newFills == 2) score += 100 * level; if (newFills == 3) score += 500 * level; if (newFills == 4) score += 1000 * level;
newFills
contains the number of lines that were filled (cleared) when the piece being processed landed.
Closing
I hope this gave some insight into how to add certain basic features to your games. Next time we will look at a modern solution to high score tables that is applicable to most generic games.