DEV Community

Cover image for Destrua a complexidade: Tetris do zero em C++ com SFML
Mr Punk da Silva
Mr Punk da Silva

Posted on

Destrua a complexidade: Tetris do zero em C++ com SFML

Repo: https://github.com/mrpunkdasilva/16Games-in-Cpp

Este tutorial ensina como criar o jogo Tetris do zero usando C++ e SFML. Vamos começar com conceitos básicos e construir o conhecimento passo a passo, explicando cada parte de forma clara e detalhada.

O que é Tetris

Imagine um jogo onde peças de diferentes formatos caem do céu como chuva, e você precisa organizá-las de forma inteligente para que se encaixem perfeitamente. É como um quebra-cabeças em movimento, onde:

  • Peças geométricas (chamadas de "tetrominós") caem de cima para baixo
  • Você pode mover as peças para esquerda e direita
  • Você pode girar as peças para encaixá-las melhor
  • Quando uma linha horizontal fica completamente preenchida, ela desaparece
  • O objetivo é durar o máximo de tempo possível sem deixar as peças chegarem ao topo

Este jogo é perfeito para aprender conceitos fundamentais de programação como arrays bidimensionais, rotação de objetos, detecção de colisões e manipulação de dados.

A Matemática Secreta dos Tetrominós

O que são Tetrominós?

Tetrominós são formas geométricas feitas de exatamente 4 quadrados conectados. Existem apenas 7 formas diferentes possíveis:

graph TD
    A[I - Linha] --> B[████]
    C[Z - Zigue-zague] --> D[██<br/>  ██]
    E[S - Zigue-zague reverso] --> F[  ██<br/>██]
    G[T - Formato T] --> H[███<br/> █]
    I[L - Formato L] --> J[█<br/>█<br/>██]
    K[J - L reverso] --> L[  █<br/>  █<br/>██]
    M[O - Quadrado] --> N[██<br/>██]
Enter fullscreen mode Exit fullscreen mode

Como Representar Formas com Números

A parte mais inteligente do nosso Tetris é como representamos cada peça usando apenas números. Em vez de desenhar cada forma, usamos um sistema de coordenadas em uma grade 4x4:

graph TB
    subgraph "Grade 4x4"
        A[0,1,2,3<br/>4,5,6,7<br/>8,9,10,11<br/>12,13,14,15]
    end

    subgraph "Peça I"
        B[1,3,5,7]
    end

    subgraph "Peça O"
        C[2,3,4,5]
    end
Enter fullscreen mode Exit fullscreen mode

Vamos entender como isso funciona:

int figures[7][4] = {
    1, 3, 5, 7,  // I - Linha vertical
    2, 4, 5, 7,  // Z - Zigue-zague
    3, 5, 4, 6,  // S - Zigue-zague reverso
    3, 5, 4, 7,  // T - Formato T
    2, 3, 5, 7,  // L - Formato L
    3, 5, 7, 6,  // J - L reverso
    2, 3, 4, 5,  // O - Quadrado
};
Enter fullscreen mode Exit fullscreen mode

Como converter números em posições:

Para transformar um número em coordenadas (x, y):

int x = numero % 2;  // Resto da divisão por 2 = coluna
int y = numero / 2;  // Divisão inteira por 2 = linha
Enter fullscreen mode Exit fullscreen mode

Por exemplo, o número 5:

  • x = 5 % 2 = 1 (coluna 1)
  • y = 5 / 2 = 2 (linha 2)

Isso significa que a posição 5 está na coluna 1, linha 2 da nossa grade 4x4.

A Estrutura do Jogo

O Campo de Jogo - Uma Grade Inteligente

O campo de jogo é como uma folha de papel quadriculado, onde cada quadradinho pode estar vazio (0) ou preenchido com uma cor (1 a 7):

const int M = 20;  // 20 linhas de altura
const int N = 10;  // 10 colunas de largura
int field[M][N] = {0};  // Inicializa tudo com 0 (vazio)
Enter fullscreen mode Exit fullscreen mode

Por que 20x10?

  • É o tamanho clássico do Tetris original
  • Oferece desafio suficiente sem ser impossível
  • Permite que as peças tenham espaço para manobrar

Representando as Peças Ativas

Cada peça tem 4 blocos, então usamos duas estruturas de dados:

struct Point {
    int x, y;    // Coordenadas de cada bloco
} a[4], b[4];   // a = posição atual, b = posição anterior
Enter fullscreen mode Exit fullscreen mode

Por que duas arrays?

  • a[4]: Posição atual da peça (onde ela está agora)
  • b[4]: Posição anterior da peça (onde ela estava antes)
  • Se um movimento for inválido, copiamos b para a (desfazemos o movimento)

As Principais Mecânicas do Jogo

1. Verificação de Colisões - A Função Mais Importante

Esta é a função que decide se um movimento é válido ou não:

bool check() {
    for (int i = 0; i < 4; i++) {
        // Verifica se a peça saiu dos limites da tela
        if (a[i].x < 0 || a[i].x >= N || a[i].y >= M) return false;

        // Verifica se a peça colidiu com algo já no campo
        else if (field[a[i].y][a[i].x]) return false;
    }
    return true;  // Movimento é válido
}
Enter fullscreen mode Exit fullscreen mode

O que esta função verifica:

flowchart TD
    A[Verificar cada bloco da peça] --> B{Está fora dos limites?}
    B -->|Sim| C[Movimento INVÁLIDO]
    B -->|Não| D{Colidiu com peça existente?}
    D -->|Sim| C
    D -->|Não| E[Continuar verificando]
    E --> F{Todos os 4 blocos OK?}
    F -->|Sim| G[Movimento VÁLIDO]
    F -->|Não| A
Enter fullscreen mode Exit fullscreen mode

2. Movimento Horizontal - Esquerda e Direita

O movimento horizontal é simples mas usa um truque inteligente:

// Salvar posição atual
for (int i = 0; i < 4; i++) {
    b[i] = a[i];        // Guardar posição antiga
    a[i].x += dx;       // Mover para nova posição
}

// Se o movimento for inválido, desfazer
if (!check()) {
    for (int i = 0; i < 4; i++) {
        a[i] = b[i];    // Voltar para posição anterior
    }
}
Enter fullscreen mode Exit fullscreen mode

A estratégia "Tentar e Desfazer":

  1. Salvar a posição atual em b
  2. Mover a peça para a nova posição em a
  3. Verificar se a nova posição é válida
  4. Se não for válida, copiar b de volta para a

3. Rotação - A Parte Mais Matemática

A rotação é baseada em matemática de transformação de coordenadas. Giramos cada bloco 90 graus ao redor do segundo bloco da peça:

if (rotate) {
    Point p = a[1];  // Centro de rotação (segundo bloco)

    for (int i = 0; i < 4; i++) {
        // Calcular posição relativa ao centro
        int x = a[i].y - p.y;
        int y = a[i].x - p.x;

        // Aplicar rotação de 90 graus
        a[i].x = p.x - x;
        a[i].y = p.y + y;
    }

    // Se a rotação for inválida, desfazer
    if (!check()) {
        for (int i = 0; i < 4; i++) {
            a[i] = b[i];
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Como funciona a rotação matemática:

graph TD
    A[Ponto Original: x,y] --> B[Relativo ao Centro: x-cx, y-cy]
    B --> C[Rotação 90°: -y, x]
    C --> D[Volta ao Sistema: cx-y, cy+x]

    E[Exemplo: 3,1 com centro 2,2] --> F[Relativo: 1,-1]
    F --> G[Rotacionado: 1,1]
    G --> H[Final: 1,3]
Enter fullscreen mode Exit fullscreen mode

4. Queda Automática - O Tick do Jogo

As peças caem sozinhas seguindo um cronômetro:

float timer = 0, delay = 0.3;  // 0.3 segundos entre cada queda

if (timer > delay) {
    // Salvar posição atual
    for (int i = 0; i < 4; i++) {
        b[i] = a[i];
        a[i].y += 1;    // Mover para baixo
    }

    // Se não conseguir mover para baixo, fixar a peça
    if (!check()) {
        for (int i = 0; i < 4; i++) {
            field[b[i].y][b[i].x] = colorNum;  // Fixar no campo
        }

        // Gerar nova peça
        colorNum = 1 + rand() % 7;
        int n = rand() % 7;
        for (int i = 0; i < 4; i++) {
            a[i].x = figures[n][i] % 2;
            a[i].y = figures[n][i] / 2;
        }
    }

    timer = 0;  // Resetar cronômetro
}
Enter fullscreen mode Exit fullscreen mode

A Lógica de Eliminar Linhas

Detectando Linhas Completas

A parte mais inteligente do Tetris é como removemos linhas completas. Usamos um algoritmo de "compactação":

int k = M - 1;  // Ponteiro para onde escrever
for (int i = M - 1; i > 0; i--) {  // Ler de baixo para cima
    int count = 0;

    // Contar quantos blocos há nesta linha
    for (int j = 0; j < N; j++) {
        if (field[i][j]) count++;
        field[k][j] = field[i][j];  // Copiar linha
    }

    // Se a linha não está completa, manter ela
    if (count < N) k--;
}
Enter fullscreen mode Exit fullscreen mode

Como Funciona o Algoritmo

sequenceDiagram
    participant Leitura as Linha de Leitura (i)
    participant Escrita as Linha de Escrita (k)
    participant Campo as Campo de Jogo

    Leitura->>Campo: Ler linha de baixo para cima
    Campo->>Leitura: Contar blocos na linha
    Leitura->>Escrita: Copiar linha para posição k

    alt Linha incompleta
        Escrita->>Escrita: k-- (mover ponteiro para cima)
    else Linha completa
        Escrita->>Escrita: k permanece (linha será sobrescrita)
    end
Enter fullscreen mode Exit fullscreen mode

Por que isso funciona:

  1. Começamos de baixo para cima
  2. Copiamos cada linha para a posição k
  3. Se a linha não está completa, movemos k para cima
  4. Se a linha está completa, k não se move (linha será sobrescrita)
  5. Linhas completas "desaparecem" naturalmente

Exemplo prático:

Antes:           Depois:
████████████     ████████████
██████████       ██████████
████████████     ████████████
████████████     ████████
████████         ████████
Enter fullscreen mode Exit fullscreen mode

Gerando Peças Aleatórias

Como Criar Peças Novas

Quando uma peça é fixada, criamos uma nova peça aleatória:

colorNum = 1 + rand() % 7;    // Cor aleatória de 1 a 7
int n = rand() % 7;           // Forma aleatória de 0 a 6

for (int i = 0; i < 4; i++) {
    a[i].x = figures[n][i] % 2;  // Posição X do bloco
    a[i].y = figures[n][i] / 2;  // Posição Y do bloco
}
Enter fullscreen mode Exit fullscreen mode

Por que Começar no Topo?

As peças novas sempre aparecem no topo da tela porque:

  • figures[n][i] representa posições de 0 a 15
  • figures[n][i] / 2 dá valores de 0 a 7 (linhas do topo)
  • Isso garante que as peças sempre começam visíveis

O Sistema de Cores

Cada Peça Tem Sua Cor

int colorNum = 1;  // Cor da peça atual (1 a 7)
Enter fullscreen mode Exit fullscreen mode

Mapeamento de cores:

  • 0: Vazio (preto)
  • 1-7: Diferentes cores para cada tipo de peça

Como Desenhar com Cores

// Para peças já fixadas no campo
s.setTextureRect(IntRect(field[i][j] * 18, 0, 18, 18));

// Para a peça atual em movimento
s.setTextureRect(IntRect(colorNum * 18, 0, 18, 18));
Enter fullscreen mode Exit fullscreen mode

Cada cor é uma seção de 18x18 pixels na textura.

Otimizações Inteligentes

Por que Usar Arrays de Tamanho Fixo?

Point a[4], b[4];  // Sempre exatamente 4 pontos
Enter fullscreen mode Exit fullscreen mode

Vantagens:

  • Cada tetrominó tem exatamente 4 blocos
  • Arrays de tamanho fixo são mais rápidos
  • Menos uso de memória
  • Código mais simples

Por que Não Usar Vetores Dinâmicos?

Para um jogo simples como Tetris:

  • A complexidade extra não vale a pena
  • Arrays fixos são mais eficientes
  • O código fica mais fácil de entender

A Matemática do Timing

Controlando a Velocidade

float timer = 0, delay = 0.3;
Enter fullscreen mode Exit fullscreen mode

Como funciona:

  • timer acumula o tempo passado
  • delay define quantos segundos entre cada queda
  • Quando timer > delay, a peça desce um nível

Calculando FPS

Se o jogo roda a 60 FPS:

  • delay = 0.3 segundos
  • Peça cai a cada 0.3 × 60 = 18 frames
  • Isso dá tempo suficiente para o jogador pensar e agir

Detecção de Game Over

Quando o Jogo Termina?

O jogo termina quando uma nova peça não consegue ser colocada no topo:

// Após gerar nova peça
if (!check()) {
    // Game Over - peça não cabe no topo
}
Enter fullscreen mode Exit fullscreen mode

Embora o código atual não implemente explicitamente o game over, a lógica está lá: se check() retornar false para uma peça recém-criada, significa que o campo está cheio.

Conceitos Avançados

Por que Usar Coordenadas Relativas?

Quando geramos peças, usamos coordenadas relativas (0-15) que depois convertemos para coordenadas absolutas da tela. Isso permite:

  1. Fácil mudança de tamanho: Alterar o tamanho dos blocos não quebra o jogo
  2. Rotação simples: Matemática de rotação funciona melhor com coordenadas relativas
  3. Reutilização: O mesmo código serve para diferentes posições na tela

A Beleza da Simplicidade

O Tetris prova que jogos incríveis podem ter código simples:

  • Apenas 154 linhas de código
  • Usa conceitos básicos: arrays, loops, condicionais
  • Sem classes complexas ou padrões de design rebuscados
  • Foca na lógica do jogo, não em arquitetura

Possíveis Melhorias

1. Sistema de Pontuação

int score = 0;
int linesCleared = 0;

// Após eliminar linha
score += 100 * level;
linesCleared++;
Enter fullscreen mode Exit fullscreen mode

2. Níveis de Dificuldade

int level = 1;
delay = 0.5 - (level * 0.05);  // Fica mais rápido a cada nível
Enter fullscreen mode Exit fullscreen mode

3. Previsão da Próxima Peça

int nextPiece = rand() % 7;
// Mostrar nextPiece na interface
Enter fullscreen mode Exit fullscreen mode

4. Sistema de Pausa

bool paused = false;
if (Keyboard::isKeyPressed(Keyboard::P)) paused = !paused;
Enter fullscreen mode Exit fullscreen mode

Por que este Código Funciona Tão Bem

1. Separação Clara de Responsabilidades

  • check(): Valida movimentos
  • Loop principal: Gerencia input e timing
  • Algoritmo de linha: Remove linhas completas

2. Uso Inteligente de Dados

  • Arrays 2D para o campo
  • Arrays 1D para peças
  • Números simples para representar formas

3. Algoritmos Eficientes

  • Verificação de colisão: O(1) por bloco
  • Remoção de linhas: O(n) onde n é o número de linhas
  • Rotação: O(1) por bloco

4. Matemática Simples mas Poderosa

  • Módulo e divisão para conversão de coordenadas
  • Rotação usando transformação linear
  • Timing baseado em acumulação de tempo

O Tetris Como Ferramenta de Aprendizado

Este jogo é perfeito para iniciantes porque ensina:

  1. Arrays bidimensionais: O campo de jogo
  2. Estruturas de dados: Points para representar blocos
  3. Algoritmos: Detecção de colisão, remoção de linhas
  4. Matemática básica: Rotação, conversão de coordenadas
  5. Game loop: Timing, input, renderização
  6. Lógica booleana: Verificações de validade

Cada conceito é usado de forma prática e imediata, tornando o aprendizado mais efetivo do que estudar teoria abstrata.

Conclusão

O Tetris é um exemplo perfeito de como um jogo simples pode ensinar conceitos profundos de programação. Sua beleza está na simplicidade elegante: com menos de 200 linhas de código, temos um jogo completo e divertido que demonstra arrays, algoritmos, matemática e lógica de jogos.

O código não é apenas funcional - é educativo. Cada linha tem um propósito claro, cada algoritmo resolve um problema específico, e o resultado final é um jogo que diverte e ensina ao mesmo tempo.

Top comments (2)

Collapse
 
primetarget profile image
Ethan Anderson

Bem didático e direto, especialmente na parte de colisões e limpeza de linhas. Para um Tetris em C++/SFML, a simplicidade aqui parece acertar o alvo, embora eu ficaria de olho em alguns casos de borda (rotação perto das paredes, game over explícito). No geral, um ponto de partida sólido.

Collapse
 
mrpunkdasilva profile image
Mr Punk da Silva • Edited

Muito massa teu comentário, vou anotar aqui e quem sabe até postar uma parte dois com os pontos que tu abordou e mais algumas outras melhorias