How to Create Tetris Game with C++
Step by Step with Score and Game Over and Video tutorial
Tetris (Russian: Тетрис)[a] is a multimedia franchise originating from a puzzle video game created by Soviet software engineer Alexey Pajitnov in 1984.
Video games with Tetris’ core mechanics have been published by several companies for multiple platforms, most prominently during a dispute over the appropriation of the rights in the late 1980s. After a significant period of publication by Nintendo, the rights reverted to Pajitnov in 1996, who co-founded the Tetris Company with Henk Rogers to manage licensing. The franchise has since expanded into film, television, books, and music singles.
In this article we will create Tetris with C++ and SFML adding score and game over .
At the end of the article there is a video and also the link to the source code on GitHub .
01. DRAW THE SQUARES ON THE SCREEN
Basic start files:
tetris.hpp
tetris.cpp
main.cpp
Makefile
resources/
tetris.hpp
#pragma once
#include <SFML/Graphics.hpp>
#include <memory>
class Tetris {
std::shared_ptr<sf::RenderWindow> window;
sf::Texture tiles;
std::shared_ptr<sf::Sprite> sprite;
protected:
void events();
void draw();
public:
Tetris();
void run();
};
tetris.cpp
#include "tetris.hpp"
Tetris::Tetris(){
window = std::make_shared<sf::RenderWindow>(
sf::VideoMode(360, 720),
"Tetris (remix)",
sf::Style::Titlebar | sf::Style::Close
);
window->setPosition(sf::Vector2i(100, 100));
tiles.loadFromFile("./resources/img/squares.png");
sprite = std::make_shared<sf::Sprite>();
sprite->setTexture(tiles);
}
void Tetris::events(){
auto e = std::make_shared<sf::Event>();
while( window->pollEvent(*e)){
if( e->type == sf::Event::Closed){
window->close();
}
}
}
void Tetris::draw(){
window->clear(sf::Color::Black);
window->draw(*sprite);
window->display();
}
void Tetris::run(){
while( window->isOpen() ){
events();
draw();
}
}
main.cpp
#include "tetris.hpp"
int main( int argc , char **argv ){
auto tetris = std::make_shared<Tetris>();
tetris->run();
return 0;
}
Makefile
TARGET=a.out
CXX=g++
DEBUG=-g
OPT=-O0
WARN=-Wall
SFML=-lsfml-graphics -lsfml-window -lsfml-system
CXXFLAGS=$(DEBUG) $(OPT) $(WARN) $(SFML)
LD=g++
OBJS= main.o tetris.o
all: $(OBJS)
$(LD) -o $(TARGET) $(OBJS) $(CXXFLAGS)
@rm *.o
@./$(TARGET)
main.o: main.cpp
$(CXX) -c $(CXXFLAGS) main.cpp -o main.o
tetris.o: tetris.cpp
$(CXX) -c $(CXXFLAGS) tetris.cpp -o tetris.o
Compile and run:
make
02. DRAW SHAPES ON THE SCREEN
Add to
tetris.hpp
(private
and first of all)
static const std::uint32_t lines {20};
static const std::uint32_t cols {10};
static const std::uint32_t squares {4};
static const std::uint32_t shapes {7};
struct Coords {
std::uint32_t x, y;
} z[squares], k[squares];
std::vector<std::vector<std::uint32_t>> area;
std::vector<std::vector<std::uint32_t>> forms;
Add to
tetris.hpp
the member function:void moveToDown();
Shapes:
]
{1,3,5,7}, // I
{2,4,5,7}, // Z
{3,5,4,6}, // S
{3,5,4,7}, // T
{2,3,5,7}, // L
{3,5,7,6}, // J
{2,3,4,5}, // O
Add
tetris.cpp
to the Constructor:
area.resize(lines);
for (std::size_t i {}; i < area.size(); ++i) {
area[i].resize(cols);
}
forms = {
{1,3,5,7}, // I
{2,4,5,7}, // Z
{3,5,4,6}, // S
{3,5,4,7}, // T
{2,3,5,7}, // L
{3,5,7,6}, // J
{2,3,4,5}, // O
};
Add to
tetris.hpp
the member functionvoid Tetris::moveToDown()
totetris.cpp
:
void Tetris::moveToDown(){
std::uint32_t number = {3};
for (std::size_t i {}; i < squares; ++i) {
z[i].x = forms[number][i] % 2;
z[i].y = forms[number][i] / 2;
}
}
Add to
tetris.cpp
thevoid Tetris::draw()
member function:
for (std::size_t i {}; i < squares; ++i) {
sprite->setPosition(z[i].x * 36, z[i].y * 36);
window->draw(*sprite);
}
Add to
tetris.cpp
thevoid Tetris::run()
member function:
...
events();
moveToDown(); // ADD
...
03. ROTATE AND MOVE TO THE SIDE
Add to
tetris.hpp
:
int dirx;
bool rotate;
void changePosition();
void setRotate();
void resetValues();
Add
tetris.cpp
to the constructor:
dirx = {0};
rotate = {false};
Add
tetris.cpp
to theevents()
loop
if( e->type == sf::Event::KeyPressed ){
if( e->key.code == sf::Keyboard::Up ){
rotate = true;
}else if( e->key.code == sf::Keyboard::Left ){
--dirx;
}else if( e->key.code == sf::Keyboard::Right ){
++dirx;
}
}
Add
tetris.cpp
contents of the 3 created member functions:
void Tetris::changePosition(){
for (std::size_t i {}; i < squares; ++i) {
z[i].x += dirx;
}
}
void Tetris::setRotate(){
if( rotate ){
Coords coord = z[1];
for (std::size_t i {}; i < squares; ++i) {
int x = z[i].y - coord.y; // INVERTIDOS
int y = z[i].x - coord.x; // INVERTIDOS
z[i].x = coord.x - x;
z[i].y = coord.y + y; // AQUI É SOMA
}
}
}
void Tetris::resetValues(){
dirx = 0;
rotate = false;
}
Change the
run()
member function intotetris.cpp
and add the new created member functions in that order:
void Tetris::run(){
while( window->isOpen() ){
events();
changePosition();
setRotate();
moveToDown();
resetValues();
draw();
}
}
TO MOVE YOU NEED TO CHANGE moveToDown()
and add the condition if( z[0].x == 0 ){}
and put the LOOP inside
if( z[0].x == 0 ){ // ADD
for (std::size_t i {}; i < squares; ++i) {
z[i].x = forms[number][i] % 2;
z[i].y = forms[number][i] / 2;
}
}
COMPILE AND DISPLAY ROTATION AND SIDES MOVEMENT!
04. MOVE DOWN
Add to
tetris.hpp
:
sf::Clock clock;
float timercount, delay;
Adicionar ao construtor:
timercount = {0.f};
delay = {0.2f};
Adicionar à
void Tetris::events()
antes deauto e = ...
:
float time = clock.getElapsedTime().asSeconds();
clock.restart();
timercount += time;
Adicionar à
void Tetris::moveToDown()
antes dostd::uint32_t number = {3}
:
if( timercount > delay ){
for (std::size_t i {}; i < squares; ++i) {
++z[i].y;
}
timercount = 0;
}
Add to builder:
timercount = {0.f};
delay = {0.2f};
Add to
void Tetris::events()
beforeauto and = ...
:
float time = clock.getElapsedTime().asSeconds();
clock.restart();
timercount += time;
Add to
void Tetris::moveToDown()
beforestd::uint32_t number = {3}
:
if( timercount > delay ){
for (std::size_t i {}; i < squares; ++i) {
++z[i].y;
}
timercount = 0;
}
05. LIMIT THE PARTS TO THE FLOOR AND FIT ONE ON TOP OF THE OTHER
Add to
tetris.hpp
:
bool maxLimit();
Add to
bool Tetris::maxLimit()
:
bool Tetris::maxLimit(){
for (std::size_t i {}; i < squares; ++i) {
if( z[i].x < 0 ||
z[i].x >= cols ||
z[i].y >= lines ||
area[ z[i].y][ z[i].x ] ){
return true;
}
}
return false;
}
Add the seeder to
main.cpp
:
std::srand( std::time( 0 ));
Change
changePosition()
to store the value of the moving shape(z[i]
) to fix it(k[i]
) and check withmaxLimit()
:
void Tetris::changePosition(){
for (std::size_t i {}; i < squares; ++i) {
k[i] = z[i];
z[i].x += dirx;
if(maxLimit()){
for (std::size_t i {}; i < squares; ++i) {
z[i] = k[i];
}
}
}
}
Copy the condition from
if( maxLimit() )...
and paste inside theif( rotate )...
, but outside thefor
loop invoid Tetris::setRotate()
if( maxLimit() ){
for (std::size_t i {}; i < squares; ++i) {
z[i] = k[i];
}
}
Add to
int color
tetris.hpp
and initialize it to1
:color = {1};
Same thing also in
moveToDown()
, but creating random pieces and changing the position of thenumber
line into theif( maxLimit() )
condition, REMOVE CONDITION:if( z[0 ].x == 0 ){ ...
:IMPORTANT: create the loop to set the color to a fixed position and make it random;
void Tetris::moveToDown(){
if( timercount > delay ){
for (std::size_t i {}; i < squares; ++i) {
k[i] = z[i];
z[i].y += 1;
}
if( maxLimit() ){
for (std::size_t i {}; i < squares; ++i) { // ADD
area[ k[i].y ][ k[i].x ] = color;
}
color = 1 + std::rand() % shapes;
std::uint32_t number = std::rand() % shapes;
for (std::size_t i {}; i < squares; ++i) {
z[i].x = forms[number][i] % 2;
z[i].y = forms[number][i] / 2;
}
}
timercount = 0;
}
}
Add loop in
void Tetris::draw()
:
for (std::size_t i {}; i < lines; ++i) {
for (std::size_t j {}; j < cols; ++j) {
if( area[i][j] != 0 ){
sprite->setPosition( j * 36, i * 36 );
window->draw( *sprite );
}
}
}
Add to
events()
outside the loop:
if(sf::Keyboard::isKeyPressed(sf::Keyboard::Down)){ // ADD
delay = 0.05f;
}
TO MAKE THE FIRST PIECE ALSO RANDOM, add to the constructor:
Copy from
moveToDown()
std::uint32_t number = std::rand() % shapes; // ADD
for (std::size_t i {}; i < squares; ++i) {
z[i].x = forms[number][i] % 2;
z[i].y = forms[number][i] / 2;
}
06. ADD RANDOM COLORS
Add
sprite->setTextureRect(sf::IntRect(color * 36, 0, 36, 36));
to thesquares
loop indraw()
for (std::size_t i {}; i < squares; ++i) {
sprite->setTextureRect(sf::IntRect(color * 36, 0, 36, 36));
sprite->setPosition(z[i].x * 36, z[i].y * 36);
window->draw(*sprite);
}
Note that the fixed part will also change color, so to solve this, just add sprite->setTextureRect(sf::IntRect(area[i][j] * 36, 0, 36, 36));
(COPY the already existing and modify) to the lines cols
loop:
for (std::size_t i {}; i < lines; ++i) {
for (std::size_t j {}; j < cols; ++j) {
if( area[i][j] == 0 ){
continue;
}
sprite->setTextureRect(sf::IntRect(area[i][j] * 36, 0, 36, 36));
sprite->setPosition( j * 36, i * 36 );
window->draw( *sprite );
}
}
07. ADD SCORE
Create a new member function in
tetris.hpp
also create score and add as zero to the constructor:
void setScore();
Add to
tetris.cpp
the execution ofsetScore()
:
void Tetris::setScore(){
int match = {lines - 1};
for (std::size_t i = match; i >= 1; --i) {
std::uint32_t sum {};
for (std::size_t j {}; j < cols; ++j) {
if( area[i][j] ){
++sum;
}
area[match][j] = area[i][j];
}
if( sum < cols ){
--match;
}else{
std::cout << "Score: " << ++score << '\n';
}
}
}
08. ADD BACKGROUNDS AND TEXT FOR SCORING
-
GIMP → 360x720 → Filters → Render → Textures → Grid → 36 1 COLOR - Export → background.png
- Add
sf::Texture tiles, bg;
andstd::shared_ptr<sf::Sprite> sprite, background;
- Constructor:
bg.loadFromFile("./resources/img/background.png");
background = std::make_shared<sf::Sprite>();
background->setTexture(bg);
draw()
→window->draw(*background);
09. ADD SCORE AND GAMEOVER
Add to
tetris.hpp
sf::Font font;
sf::Text txtScore, txtGameOver;
bool rotate, gameover;
Add to
tetris.cpp
score = {0};
gameover = {false};
font.loadFromFile("./font.ttf");
txtScore.setFont(font);
txtScore.setPosition(100.f, 10.f);
txtScore.setString( "SCORE: " + std::to_string( score ));
txtScore.setStyle(sf::Text::Bold);
txtScore.setCharacterSize(30);
txtScore.setOutlineThickness(3);
txtGameOver.setFont(font);
txtGameOver.setPosition( 30.f , 330.f);
txtGameOver.setString("GAME OVER");
txtGameOver.setStyle(sf::Text::Bold);
txtGameOver.setCharacterSize(50);
txtGameOver.setOutlineThickness(3);
Tetris::draw()
:
window->draw( txtScore );
if( gameover ){
window->draw( txtGameOver );
}
Tetris::run()
if( !gameover ){
changePosition();
setRotate();
moveToDown();
setScore();
resetValues();
}
Tetris::setScore
:
void Tetris::setScore(){
int match = {lines - 1};
for (std::size_t i = match; i >= 1; --i) {
std::uint32_t sum {};
for (std::size_t j {}; j < cols; ++j) {
//if( area[i][j] != 0 ){
if( area[i][j] ){
if( i == 1 ){
gameover = true;
}
++sum;
}
area[match][j] = area[i][j]; // FIXA AO CHAO
}
if( sum < cols ){
--match;
}else{
txtScore.setString( "SCORE: " + std::to_string( ++score ));
}
}
}
WATCH THE VIDEO
The video is in Portuguese, but you can follow the steps even if you don’t understand the language. You can also enable subtitles and use Youtube’s automatic translation.
Comments