A very mediocre Tetris Clone made from scratch in C++11 and SDL with some Graphic tweaks in Blender 3D and Krita. Just an old University project that I made to prove my skills and to get a kickass grade!

Tetris Clone

A very mediocre Tetris Clone made from scratch in C++11 and SDL with some Graphic tweaks in Blender 3D and Krita. Just an old University project that I made to prove my skills and to get a kickass grade!

Tue Dec 12 2017

16 min read

NaN

I want to make this clear from the beginning, the code you're about to see is pure crap, I'm kinda ashamed of it but it's still something I spent a lot of time back in the 2nd year of university. This was the year when the OOP class was in progress and we needed to make a project to be evaluated in C++, so why not trying to emulate a classic with the help of the SDL (Simple DirectMedia Layer) graphic library, with the collaboration of C++ pointers, my love! This is what happens when you trade beer night with friends for programming.

Just kidding, I was still at the pub those nights...

Theme Choice and Motivation

Living in a rapidly growing IT world I've always been fascinated by videogames and their approach to culture and art to a new level never seen before. Even from their legacy, videogames have been able to provide immersive experiences for the players that no other entertainment media has been able to achieve before just for one simple but essential element that other media was lacking, interaction. I consider video games as the most complex and complete expression of art. This mindset made me focus on such media to explore and try to understand the way that is created and the factors that are involved in it's development process.

The final result of the project during gameplay on GNU/Linux under the KDE 5 Plasma Desktop Environment

The final result of the project during gameplay on GNU/Linux under the KDE 5 Plasma Desktop Environment

Concept and Rules

For some contest, Tetris is a game developed by Alexey Pajitnov back in the USSR, when the cold war was warmer than its name and the fear of nuclear threats was established in the worldwide population. During this time most of the science budget that was allocated on both sides of the superpowers was dedicated to military and defensive programs in order to try to step ahead of the opponents. But even during such hard times, there were passionate developers that would find the time to experiment and innovate in other fields that didn't involve the war itself. In such context borns Tetris.

The Tetris game is based on being able to stack and fit different shapes within a matrix graph to delete rows and keep playing until they are stuck up to the beginning and the game's over. It's such an easy concept but the brain reaction that comes with it makes it addictive and fun to play.

SDL TextureManager Class

The loading of the SDL utilities for each element that is shown on the screen it's also managed by a custom class such as the TextureManager that is going to be used as a helper for loading the images from local files into on-screen pieces and UI.

TextureManager.h

#ifndef TEXTUREMANAGER_H_
#define TEXTUREMANAGER_H_
#include "Game.h"

class TextureManager {
    public:
        static SDL_Texture* LoadTexture(const char* texture);
        static void Draw(SDL_Texture* texture, SDL_Rect &src, SDL_Rect &dest);
};
#endif

TextureManager.cpp

#include "TextureManager.h"

SDL_Texture* TextureManager::LoadTexture(const char* texture) {
    SDL_Surface* temp = IMG_Load(texture);
    SDL_Texture* tex = SDL_CreateTextureFromSurface(Game::renderer, temp);
    SDL_FreeSurface(temp);
    return tex;
}
void TextureManager::Draw(SDL_Texture* texture, SDL_Rect &src, SDL_Rect &dest) {
    SDL_RenderCopy(Game::renderer, texture, &src, &dest);
    return;
}

Piece Class

Since the game logic is based on grids, the most convenient solution I found was to use matrix and their data structure known as double arrays within the programming language. The main grid where all the individual pieces are going to be placed is a 10x18 matrix and each piece is represented by a 5x5 matrix that lives within the main grid. The score is dictated by how many rows are deleted from the main grid's bottom and each time a piece falls down one unit.

To store such information and also commit to the project requirements, the Piece is described by its own class whose constructor dictates the randomness of each next piece as well as its logic and appearance (texture, color)

Piece.h

#ifndef PIECE_H_
#define PIECE_H_
#include "Game.h"
#include "TextureManager.h"
#define N 5

class Piece {
    public:
        Piece(int model,int next, int x, int y, int xN, int yN);
        ~Piece();
        void update();
        void render();
        void renderNext();
        void printPiece();
        void rotatePiece();
        void movePiece(int speed);
        void moveGhost(int speed);
        void strifePiece(int direction);
        int* getMatrixPoints();
        void printMatrixPoints(int* ptr);
        int getType();
        int getNextType();
        void setYGhost(int pos);
        int getYGhost();
        void setXGhost(int pos);
        int getXGhost();
        void setXNext(int pos);
        void setYNext(int pos);
        SDL_Texture* getPieceTexture();
        SDL_Texture* getGhostTexture();
    private:
        int matrix[N][N] =  {
            {0, 0, 0, 0, 0},
            {0, 0, 0, 0, 0},
            {0, 0, 2, 0, 0},    //number 2 is the pivot point for rotation
            {0, 0, 0, 0, 0},
            {0, 0, 0, 0, 0}
        };
        int matrixNext[N][N] = {
            {0, 0, 0, 0, 0},
            {0, 0, 0, 0, 0},
            {0, 0, 2, 0, 0},    //number 2 is the pivot point for rotation
            {0, 0, 0, 0, 0},
            {0, 0, 0, 0, 0}
        };
        int type;
        int nextType;
        SDL_Texture* pieceTexture;
        SDL_Texture* ghostTexture;
        SDL_Texture* nextTexture;
        SDL_Texture* blankTexture = TextureManager::LoadTexture("img/BlockAlpha.png");
        SDL_Rect srcRect, destRect;
        SDL_Rect srcGhost, destGhost;
        SDL_Rect srcNext, destNext;
        int xPos;
        int yPos;
        int xGhost;
        int yGhost;
        int xNext;
        int yNext;
};

#endif

From the properties, you can spot the matrix structure with its middle pivot of rotation and the position of both the current piece and its ghost, which is the piece projection updated in real-time where the piece is going to fall in a specific moment.

The constructor of the Piece class is going to create the piece by assigning the 1 value to the correct position within the piece matrix, this will let know SDL how to map the piece texture to the correct block and form a shape out of it based on the model that it receives as an argument during the object instantiation. The destructor is going to clean the memory and the buffer from the previously allocated resources every time a piece has been hitting the ground bottom.

Piece.cpp

// Constructor
Piece::Piece(int model, int next, int x, int y, int xN, int yN) {
    xPos = x;
    yPos = y;
    xGhost = x;
    yGhost = 0;
    xNext = xN;
    yNext = yN;
    type = model;
    nextType = next;
    switch(model) {
        case 1:
            std::cout << "Random Spawn Nr: " << model << " I" << " Color: Cyan" << std::endl;
            matrix[2][1] = 1;   //
            matrix[2][3] = 1;   //  ####
            matrix[2][4] = 1;   //
            pieceTexture = TextureManager::LoadTexture("img/BlockCyan.png");
            ghostTexture = TextureManager::LoadTexture("img/BlockCyanGhost.png");
            break;
        case 2:
            std::cout << "Random Spawn Nr: " << model << " []" << " Color: Yellow" << std::endl;
            matrix[1][2] = 1;   //  ##
            matrix[1][3] = 1;   //  ##
            matrix[2][3] = 1;   //
            pieceTexture = TextureManager::LoadTexture("img/BlockYellow.png");
            ghostTexture = TextureManager::LoadTexture("img/BlockYellowGhost.png");
            break;
        case 3:
            std::cout << "Random Spawn Nr: " << model << " N" << " Color: Red" << std::endl;
            matrix[1][1] = 1;   //  ##
            matrix[1][2] = 1;   //   ##
            matrix[2][3] = 1;   //
            pieceTexture = TextureManager::LoadTexture("img/BlockRed.png");
            ghostTexture = TextureManager::LoadTexture("img/BlockRedGhost.png");
            break;
        case 4:
            std::cout << "Random Spawn Nr: " << model << " N-" << " Color: Green" << std::endl;
            matrix[1][3] = 1;   //   ##
            matrix[1][2] = 1;   //  ##
            matrix[2][1] = 1;   //
            pieceTexture = TextureManager::LoadTexture("img/BlockGreen.png");
            ghostTexture = TextureManager::LoadTexture("img/BlockGreenGhost.png");
            break;
        case 5:
            std::cout << "Random Spawn Nr: " << model << " T" << " Color: Purple" << std::endl;
            matrix[1][2] = 1;   //   #
            matrix[2][1] = 1;   //  ###
            matrix[2][3] = 1;   //
            pieceTexture = TextureManager::LoadTexture("img/BlockPurple.png");
            ghostTexture = TextureManager::LoadTexture("img/BlockPurpleGhost.png");
            break;
        case 6:
            std::cout << "Random Spawn Nr: " << model << " L" << " Color: Orange" << std::endl;
            matrix[1][3] = 1;   //    #
            matrix[2][1] = 1;   //  ###
            matrix[2][3] = 1;   //
            pieceTexture = TextureManager::LoadTexture("img/BlockOrange.png");
            ghostTexture = TextureManager::LoadTexture("img/BlockOrangeGhost.png");
            break;
        case 7:
            std::cout << "Random Spawn Nr: " << model << " L-" << " Color: Blue" << std::endl;
            matrix[1][1] = 1;   //  #
            matrix[2][3] = 1;   //  ###
            matrix[2][1] = 1;   //
            pieceTexture = TextureManager::LoadTexture("img/BlockBlue.png");
            ghostTexture = TextureManager::LoadTexture("img/BlockBlueGhost.png");
            break;
        default:
            break;
    }
}

// Destructor
Piece::~Piece() {
    SDL_DestroyTexture(pieceTexture);
    SDL_DestroyTexture(ghostTexture);
    SDL_DestroyTexture(nextTexture);
}

Among the other methods such as getters, setters, update and render that are used to interact with private methods and update logic and render the current status, there are others for the piece structure change such as rotation, movement, and strife. These are just raw matrix operators and shifters that are going to change the matrix structure according to the command given as input from the player.

Piece.cpp

void Piece::rotatePiece() {
    int tempMatrix[N][N];
    // Transpose the original matrix
    for(int i = 0; i < N; i++) {
        for(int j = 0; j < N; j++) {
            tempMatrix[j][i] = matrix[i][j];
        }
    }
    // Swap columns
    for(int i = 0; i < N; i++) {
        for(int j = 0; j < N/2; j++) {
            std::swap(tempMatrix[i][j], tempMatrix[i][N - j - 1]);
        }
    }
    // Override original matrix with the clockwise rotated one
    for(int i = 0; i < N; i++) {
        for(int j = 0; j < N; j++) {
            matrix[i][j] = tempMatrix[i][j];
        }
    }
    std::cout << "Matrix Rotated Clockwise:" << std::endl;
    return;
}
void Piece::movePiece(int speed) {
    yPos += speed;
    return;
}
void Piece::moveGhost(int speed) {
    yGhost -= speed;
    return;
}
void Piece::strifePiece(int direction) {
    if(direction == 1) {
        xPos += 32;
        xGhost += 32;
    }
    else if(direction == -1) {
        xPos -= 32;
        xGhost -= 32;
    }
    return;
}
The Piece over the main Board and its Ghost standing below at the maximum lower level. The bounding box of the entire Piece matrix is highlighted

The Piece over the main Board and its Ghost standing below at the maximum lower level. The bounding box of the entire Piece matrix is highlighted

Board Class

The mainboard is described within a class with properties and methods for checking the individual pieces' bounds within the grid and the logic for rotation and movement, as well as the fall mechanism and the check for full lines after each piece, is placed on the bottom. If a line is full the method eerase linesint* lines) will delete the line from the matrix and shift it downwards by one or more units based on how many lines have been filled at the same time.

Board.h

#ifndef BOARD_H_
#define BOARD_H_
#include "Game.h"
#include "Piece.h"
#include "TextureManager.h"
#define N 5
#define W 14    // 10 for the board, 4 for the buffer zone
#define H 23    // 18 for the board, 5 for the buffer zone

class Board {
    public:
        Board();
        ~Board();
        void update();
        void render();
        void printBoard();
        void syncPosition();
        void setInitMatY(int number);
        int getInitMatY();
        void setInitMatX(int number);
        int getInitMatX();
        bool groundCollision(int* positions);
        bool wallCollisionLeft(int* positions);
        bool wallCollisionRight(int* positions);
        int pivotAtWalls();
        int isPieceI(int* positions);
        bool isRotationSafe(int* positions);
        bool isRotationSafeI(int* positions);
        void fillBoard(int* positions, int type);
        bool areLinesFull();
        int* checkLines();
        void eraseLines(int* lines);
        void usdTest();
        void resetMatrixPositions();
        int getCollisionPoint(int* positions);
        int getLines();
        bool gameOver();
    private:
        int matrixBoard[H][W];
        int tempMatrix[N][N];
        // int lines;
        int initMatX = 0;
        int initMatY = 4;
        int initPosX = 0;
        int initPosY = -32;
        // Load Piece Textures
        SDL_Texture* boardTexture = TextureManager::LoadTexture("img/BlockBoard.png");
        SDL_Texture* blankTexture = TextureManager::LoadTexture("img/BlockAlpha.png");
        SDL_Texture* cyanTexture = TextureManager::LoadTexture("img/BlockCyan.png");
        SDL_Texture* yellowTexture = TextureManager::LoadTexture("img/BlockYellow.png");
        SDL_Texture* redTexture = TextureManager::LoadTexture("img/BlockRed.png");
        SDL_Texture* greenTexture = TextureManager::LoadTexture("img/BlockGreen.png");
        SDL_Texture* purpleTexture = TextureManager::LoadTexture("img/BlockPurple.png");
        SDL_Texture* orangeTexture = TextureManager::LoadTexture("img/BlockOrange.png");
        SDL_Texture* blueTexture = TextureManager::LoadTexture("img/BlockBlue.png");
        // Load Ghost Textures
        SDL_Texture* cyanGhostTexture = TextureManager::LoadTexture("img/BlockCyanGhost.png");
        SDL_Texture* yellowGhostTexture = TextureManager::LoadTexture("img/BlockYellowGhost.png");
        SDL_Texture* redGhostTexture = TextureManager::LoadTexture("img/BlockRedGhost.png");
        SDL_Texture* greenGhostTexture = TextureManager::LoadTexture("img/BlockGreenGhost.png");
        SDL_Texture* purpleGhostTexture = TextureManager::LoadTexture("img/BlockPurpleGhost.png");
        SDL_Texture* orangeGhostTexture = TextureManager::LoadTexture("img/BlockOrangeGhost.png");
        SDL_Texture* blueGhostTexture = TextureManager::LoadTexture("img/BlockBlueGhost.png");
        // Rectangle used for render the board
        SDL_Rect srcRect, destRect;
};
#endif

There are a lot of methods in here, most of them check the movement, rotation, and correct positioning of pieces within the board but there are also methods that are dedicated to game events such as the game over check and the full-line checker as well as the line eraser.

Board.cpp

int* Board::checkLines() {
    static int index[4];   // max rows that can be deleted in one
    int n = 0;
    for(int i = 3; i < H - 2; i++) {
        for(int j = 2; j < W - 2; j++) {
            if(matrixBoard[i][j] == 0) {
                break;
            }
            else {
                if(j == 11) {
                    index[n] = i;
                    // lines++;
                    n++;
                }
            }
        }
    }
    std::cout << "Vector Elements:" << std::endl;
    for(int i = 0; i < 4; i++) {
        std::cout << index[i] << " ";
    }
    std::cout << "\n";
    return index;
}

void Board::eraseLines(int* lines)
{
    int n = 0;
    bool fall = false;
    for(int i = 3; i < H - 2; i++) {
        for(int j = 2; j < W - 2; j++) {
            if(lines[n] == 0) {
                break;
            }
            else {
                if(lines[n] == i) {
                    matrixBoard[i][j] = 0;
                    if(j == 11) {
                        SDL_Delay(500);
                        fall = true;
                    }
                }
                if(fall == true) {
                    for(int tempI = i; tempI > 3; tempI--) {
                        for(int tempJ = 2; tempJ < W - 2; tempJ++) {
                            matrixBoard[tempI][tempJ] = matrixBoard[tempI - 1][tempJ];
                        }
                    }
                    fall = false;
                }
            }
        }
    }
    return;
}

bool Board::areLinesFull() {
    for(int i = 3; i < H - 2; i++) {
        for(int j = 2; j < W - 2; j++) {
            if(matrixBoard[i][j] == 0) {
                break;
            }
            else {
                if(j == 11) {
                    return true;
                }
            }
        }
    }
    return false;
}

Notice that the board itself has some buffer zone for pieces to be able to compensate their empty blocks and allowing a correct positioning within the main grid walls, the structure itself results to be a 14x23 but the active playing area is still 10x18. The Game Over function is going to check for pieces stuck within the upper cells of the main board grid matrix. If any is detected the game is over.

Board.cpp

bool Board::gameOver() {
    for(int j = 2; j < W - 2; j++) {
        if(matrixBoard[1][j] != 44 || matrixBoard[2][j] != 44) {
            return true;
        }
    }
    return false;
}

Timer Class

On the right side of the game window, there is a dedicated spot for statistics, basically, the points gained and the number of lines erased from the game beginning. All of these are controlled by the Timer class.

Timer.h

#ifndef TIMER_H_
#define TIMER_H_
#include "Game.h"
#include <string>

class Timer {
    public:
        Timer();
        ~Timer();
        void update();
        void render();
        void setScore(long number);
        long getScore();
        void setLines(int number);
        int getLines();
    private:
        long score;
        int lines;
        TTF_Font* font = TTF_OpenFont("FetteTrumpDeutsch.ttf", 30);
        SDL_Color color = {255, 255, 255, 255};
        // For Score
        SDL_Surface* textSurface;
        SDL_Texture* text;
        SDL_Rect textRect;
        // For lines
        SDL_Surface* textLinesSurface;
        SDL_Texture* textLines;
        SDL_Rect textLinesRect;
};
#endif

Execution Loop

There are many theories on how to implement the most productive and fail-proof game loop, but the most the convenient way I found was to make a cascade system based on 5 methods each with a different purpose that triggers once actions are executed by the player. Some of them are executed just once while others are executed per frame and they're contained within the Game class.

Game.h

#ifndef GAME_H_
#define GAME_H_
#include "SDL2/SDL.h"
#include "SDL2/SDL_image.h"
#include "SDL2/SDL_ttf.h"
#include <iostream>
#include <random> //for random piece spawn
#include <ctime>  //for real random

class Game {
  public:
    Game();
    ~Game();
    void init(const char* title, int xP, int yP, int width, int height, bool fullscreen);
    void update();
    void render();
    void renderPause();
    void events();
    void clean();
    static SDL_Renderer *renderer;
    static SDL_Event event;
    bool isRunning();
    bool isPaused();
  private:
    SDL_Texture* backgroundTexture;
    SDL_Rect srcBackground, destBackground;
    SDL_Texture* pauseTexture;
    SDL_Texture* howToPlayTexture;
    SDL_Texture* aboutTexture;
    SDL_Rect srcPause, destPause;
    int randomChoice;
    int randomNext;
    int pieceI;
    bool lines;
    int speed = 60;
    int rotationGhost = 0;
    int isRotated = false;
    bool newPiece = true;
    int counter = 0;
    bool running;
    bool paused = false;
    int menu = 0;
    SDL_Window *window;
};
#endif
  • init() is triggered just inside the main method to initialize the resources such as objects and graphics as well as the game window
  • events() allows managing the input events during the gameplay such as piece movement, rotation, downfall, and menu interaction
  • update() keeps track of each logical change within the game elements (besides the TextureManager since it's abstract)
  • render() takes care of drawing on screen the elements that change each frame as well as textures
  • clean() is assigned to be executed just when the game window is closed and takes care of buffer cleaning and memory deallocation.

These methods are fired in the main loop of the game, with the following sequence described in the code:

Main.cpp

#include <iostream>
#include "Game.h"
using namespace std;

Game *game = nullptr;

int main(int argc, const char * argv[]) {
  // Frame rate control (locked to 60FPS/s)
  const int FPS = 60;
  const int frameDelay = 1000 / FPS;
  int frameTime;
  Uint32 frameStart;
  // Game init and loop
  game = new Game();
  game->init("Tetris", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 800, 704, false);
  while(game->isRunning()) {
    game->events();
    while(!game->isPaused()) {
      frameStart = SDL_GetTicks();
      game->events();
      game->update();
      game->render();
      frameTime = SDL_GetTicks() - frameStart;
      if(frameDelay > frameTime)
      { SDL_Delay(frameDelay - frameTime); }
    }
    game->renderPause();
  }
  game->clean();
  return 0;
}

Compile and Execute

This project has been tested on GNU/Linux only, to compile the project you have to make sure you also have the SDL2 libraries installed system side, in my case I usually use Debian Like Distros so apt it's my package manager of choice and Bash my default Terminal, you'll need the following:

Terminal.sh

sudo apt update
sudo apt install libsdl2 libsdl2-image-2.0-0 libsdl2-ttf-2.0-0
g++ -std=c++11 Main.cpp TextureManager.cpp Game.cpp Piece.cpp Board.cpp Timer.cpp $(pkg-config --cflags --libs sdl2) -o Tetris.out -lSDL2_image -lSDL2_ttf

The last command is going to compile the repo code with the G++ compiler by targeting the C++11 version of the programming language. At the end, you should get a Tetris.out file that you can execute with ./Tetris.out.

How to Play

Once the output program is executed, the game starts, giving the player the option to pause it anytime with the Esc key, this will however hide the current mainboard to avoid time freeze cheating. The pause menu has the How to Play option that can be selected with the Q key.

The Pause screen appearing by pressing the esc key, during this state the game logic is freezed

The Pause screen appearing by pressing the esc key, during this state the game logic is freezed

Instructions about how to play the game, commands and rules

Instructions about how to play the game, commands and rules

Basically during gameplay, you can use the left and right arrow keys to strafe the currently falling piece, the up key rotates the current piece by 90 degrees clockwise as long as the conditions to do such operation is satisfied, the down key is going to fall the piece one unit faster than the current speed if long pressed it will send the piece to the bottom instantly. The speed of the default falling piece is going to grow directly proportional to the number of points and rows cleaned during the gameplay.

There is also an About page where some information about the author and the project itself is displayed.

A page with some info about the author and the project in general

A page with some info about the author and the project in general

Conclusion

This project was tons of fun for me, it proved back in the day that I can create something functional in a small amount of time and also let me with the satisfaction that I always get from finishing such experiments. In the end, everything went right, besides some segmentation faults that randomly happen because of my bad memory deallocation it's functional and fun to play.

© Șerban Mihai-Ciprian 2022 • All Rights Reserved

Terms and Conditions Privacy Policy