O Primeiro Pixel — Janela, Desenho e Input com raylib

O Primeiro Pixel — Janela, Desenho e Input com raylib

A primeira janela traz de volta uma lição antiga em contexto novo: ela é um recurso e precisa ser fechada, o que uma classe resolve sem depender de memória humana. Também aparece a distinção que mais muda o jogo na prática, entre ler uma tecla mantida e uma tecla recém-pressionada.
Linguagem C++

10 min de leitura

No artigo Um Novo Território — Por que Jogos, o Game Loop e as Três Bibliotecas conhecemos o game loop em teoria — o laço input-update-render — e as três bibliotecas do nosso arsenal. Hoje o esqueleto ganha carne: criamos uma janela de verdade com raylib, desenhamos formas, capturamos o teclado, e movemos um retângulo pela tela sob seu comando. E faremos isso do jeito C++: como a raylib é uma biblioteca C (com InitWindow/CloseWindow manuais, exatamente como o fopen/fclose do Nascimento e Morte de um Objeto e o SQLite do Extensão), vamos envolvê-la num objeto RAII (Fase 2), para que a janela se abra no construtor e feche no destrutor, automaticamente. Ao fim desta aula, o game loop deixará de ser um esqueleto conceitual e virá um programa vivo, com um pixel que se move quando você aperta as setas. É o momento em que jogos deixam de ser abstração.

A janela mais simples e o loop real

A raylib reduz a criação de uma janela a uma linha, e o game loop a um while claro. Eis o programa vivo mínimo — uma janela que fica aberta até você fechá-la:

#include "raylib.h"

int main() {
    InitWindow(800, 600, "Meu Primeiro Jogo");   // abre a janela (800x600)
    SetTargetFPS(60);                              // mira em 60 frames por segundo

    // O GAME LOOP: roda até o usuário fechar a janela (X ou ESC).
    while (!WindowShouldClose()) {
        // --- RENDER ---
        BeginDrawing();                 // inicia o desenho do frame
        ClearBackground(RAYWHITE);      // limpa a tela com uma cor de fundo
        DrawText("Pressione as setas!", 250, 280, 20, DARKGRAY);
        EndDrawing();                   // finaliza e mostra o frame
    }

    CloseWindow();   // fecha a janela e libera recursos — MANUAL (perigoso)
    return 0;
}

Compile e rode (numa máquina desktop com raylib instalada — veja as fontes) e você verá uma janela com o texto. O SetTargetFPS(60) pede à raylib para rodar o loop 60 vezes por segundo, cuidando do tempo entre frames por você. O BeginDrawing/EndDrawing delimitam o desenho de cada frame, e ClearBackground apaga o frame anterior (sem isso, os desenhos se acumulariam). Mas repare no CloseWindow() no fim: é um recurso manual a liberar, com o problema de sempre — se algo entre InitWindow e CloseWindow lançar ou retornar cedo, a janela vaza. Você já sabe a cura.

Envolvendo a janela em RAII

Fiel ao curso, envolvemos a API C da raylib numa classe RAII — a janela se abre no construtor, fecha no destrutor, e nunca vaza:

#include "raylib.h"
#include <string>

// Envolve a janela da raylib em RAII: abre no construtor, fecha no destrutor.
class Janela {
public:
    Janela(int largura, int altura, const std::string& titulo) {
        InitWindow(largura, altura, titulo.c_str());   // adquire (Fase 2)
        SetTargetFPS(60);
    }
    ~Janela() {
        CloseWindow();   // libera automaticamente ao sair de escopo
    }

    // Uma janela é um recurso de dono único — proíbe cópia (Fase 2/8).
    Janela(const Janela&) = delete;
    Janela& operator=(const Janela&) = delete;

    bool deve_fechar() const { return WindowShouldClose(); }
};

int main() {
    Janela janela(800, 600, "Jogo com RAII");   // abre aqui

    while (!janela.deve_fechar()) {
        BeginDrawing();
        ClearBackground(RAYWHITE);
        DrawText("Janela gerenciada por RAII", 200, 280, 20, DARKGRAY);
        EndDrawing();
    }
    return 0;
    // Sem CloseWindow() manual! O destrutor de 'janela' fecha tudo, sempre.
}

Note o que ganhamos: nenhum CloseWindow() manual, cópia proibida (a janela tem dono único, como o unique_ptr), e a garantia de que a janela fecha em qualquer caminho de saída, inclusive por exceção — a aliança RAII-exceção do Quando o Contrato se Quebra. É a lição do SQLite (Extensão) repetida num contexto novo: os princípios do curso domam qualquer API C, transformando InitWindow/CloseWindow bruto em código seguro. A partir de agora, toda a infraestrutura da raylib que precisa de liberação (janela, texturas, sons) ganhará esse tratamento.

Desenhando formas e capturando input

Um jogo precisa desenhar mais que texto e reagir ao jogador. A raylib oferece funções de desenho diretas e checagem de teclas simples. Vamos a um retângulo que se move com as setas — o game loop completo, com as três fases:

#include "raylib.h"

int main() {
    Janela janela(800, 600, "Retângulo Móvel");   // (classe RAII do trecho anterior)

    // Estado do jogo: posição do retângulo.
    float x = 400.0f, y = 300.0f;
    const float velocidade = 300.0f;   // pixels por segundo

    while (!janela.deve_fechar()) {
        float dt = GetFrameTime();   // delta time: tempo do último frame (Fase games Um Novo Território)

        // --- 1. INPUT + 2. UPDATE ---
        // IsKeyDown pergunta se a tecla está pressionada AGORA.
        // Move proporcional a dt: velocidade consistente em qualquer máquina.
        if (IsKeyDown(KEY_RIGHT)) x += velocidade * dt;
        if (IsKeyDown(KEY_LEFT))  x -= velocidade * dt;
        if (IsKeyDown(KEY_DOWN))  y += velocidade * dt;
        if (IsKeyDown(KEY_UP))    y -= velocidade * dt;

        // --- 3. RENDER ---
        BeginDrawing();
        ClearBackground(RAYWHITE);
        DrawRectangle((int)x, (int)y, 50, 50, BLUE);   // desenha o retângulo
        DrawText("Setas para mover", 10, 10, 20, GRAY);
        EndDrawing();
    }
    return 0;
}

Rode e mova o retângulo azul com as setas. Contemple o game loop do Um Novo Território agora vivo: IsKeyDown lê o input, a posição é atualizada com velocidade * dt (movimento consistente, delta time), e DrawRectangle renderiza — as três fases, 60 vezes por segundo. O GetFrameTime() da raylib entrega o dt prontinho. Uma nota sobre input: IsKeyDown responde enquanto a tecla está pressionada (bom para movimento contínuo); há também IsKeyPressed, que dispara uma vez quando a tecla é apertada (bom para ações como pular ou atirar) — a distinção importa e voltaremos a ela. Você acabou de escrever seu primeiro programa interativo em tempo real.

A primeira janela já traz de volta uma lição antiga num contexto novo: a janela é um recurso, precisa ser fechada, e envolvê-la numa classe resolve isso sem depender de ninguém lembrar. O detalhe que muda o jogo na prática é a diferença entre tecla pressionada e tecla mantida — uma serve para movimento contínuo, a outra para ação única, e trocá-las produz o personagem que pula cem vezes enquanto o dedo está no botão.

Fontes e leituras recomendadas

  • raylib.com/cheatsheet: a referência rápida de toda a API da raylib — InitWindow, DrawRectangle, IsKeyDown, GetFrameTime e centenas de outras.
  • github.com/raysan5/raylib/wiki: o wiki com instruções de instalação para cada sistema operacional, e via vcpkg (vcpkg install raylib).
  • raylib.com/examples: dezenas de exemplos executáveis que demonstram cada recurso da biblioteca.
  • raylib-games — GitHub — Coleção oficial de jogos completos feitos com a raylib, do Pong ao Asteroids, com o código-fonte inteiro de cada um.
  • Game Programming Patterns — Game Loop — O capítulo de Robert Nystrom sobre o padrão implementado aqui. O livro inteiro está disponível de graça no site.

Exercícios

Exercício 1

Compile e rode o exemplo do retângulo móvel (instale a raylib via vcpkg ou pelo gerenciador do seu sistema). Mova o retângulo com as setas e descreva o que observa. Confirme que o movimento é suave.

Ver resposta

✓ Resposta: Ao compilar (por exemplo, com raylib instalada via vcpkg: g++ jogo.cpp -lraylib mais as flags do sistema, ou usando o CMake com find_package(raylib)) e rodar, aparece uma janela 800x600 com um retângulo azul no centro. Segurando as setas, o retângulo desliza suavemente na direção correspondente, e soltando, ele para. O movimento é suave porque o loop roda a 60fps e o retângulo se move uma pequena fração a cada frame (velocidade * dt), o que o olho percebe como movimento contínuo. Segurar duas setas simultaneamente (ex.: direita + baixo) move o retângulo na diagonal, pois ambos os if de IsKeyDown são verdadeiros no mesmo frame.

Exercício 2

Explique por que a classe Janela RAII é preferível a chamar InitWindow/CloseWindow manualmente. Que problema concreto ela previne, e a qual lição anterior do curso (cite o artigo) isso remete?

Ver resposta

✓ Resposta: A classe Janela RAII é preferível porque garante que CloseWindow() seja chamado automaticamente quando o objeto sai de escopo, em qualquer caminho de saída — inclusive se uma exceção for lançada no meio do jogo. Com as chamadas manuais, se algo entre InitWindow e CloseWindow lançasse ou fizesse um return antecipado, o CloseWindow seria pulado, e a janela (e os recursos gráficos associados) vazaria. O problema concreto que ela previne é justamente esse vazamento de recurso da janela. Isso remete diretamente à lição do RAII, estabelecida na Fase 2 (artigo Nascimento e Morte de um Objeto — Construtores, Destrutores e o RAII na Prática, com o ArquivoTexto, e reforçada no Extensão com o BancoSQLite): envolver um recurso de aquisição/liberação manual num objeto cujo construtor adquire e destrutor libera, amarrando a limpeza ao tempo de vida do objeto, não a uma linha de código.

Exercício 3

Modifique o exemplo para que o retângulo não saia da tela: adicione limites que impeçam x e y de irem além das bordas da janela (0 a 800 na horizontal, 0 a 600 na vertical, considerando o tamanho do retângulo).

Ver resposta

✓ Resposta: Com limites de tela:

// Após atualizar x e y, restringe-os às bordas (retângulo de 50x50):
if (x < 0) x = 0;
if (x > 800 - 50) x = 800 - 50;   // 800 = largura; 50 = largura do retângulo
if (y < 0) y = 0;
if (y > 600 - 50) y = 600 - 50;   // 600 = altura

Isso "prende" o retângulo dentro da janela: se o movimento o levaria além de uma borda, o valor é fixado no limite. Subtraímos o tamanho do retângulo (50) dos limites máximos para que ele não desapareça parcialmente pela borda direita/inferior — a posição x/y é o canto superior esquerdo, então o canto direito está em x+50. É a proteção de invariante ("o retângulo permanece na tela") do artigo Projetando Abstrações — Encapsulamento, Invariantes e o Ponteiro this, aplicada a posição.

Exercício 4

Explique a diferença entre IsKeyDown e IsKeyPressed. Para cada situação, diga qual usaria: (a) mover um personagem enquanto a seta é segurada; (b) fazer o personagem pular uma vez ao apertar espaço; (c) atirar um projétil a cada aperto de tecla.

Ver resposta

✓ Resposta: IsKeyDown retorna verdadeiro continuamente, a cada frame, enquanto a tecla está fisicamente pressionada — ideal para ações contínuas. IsKeyPressed retorna verdadeiro apenas no frame em que a tecla foi apertada (a transição de solta para pressionada), disparando uma única vez por aperto — ideal para ações pontuais. Aplicando: (a) mover enquanto a seta é segurada → IsKeyDown (movimento contínuo, precisa responder a cada frame que a tecla está baixa); (b) pular uma vez ao apertar espaço → IsKeyPressed (um pulo por aperto; IsKeyDown faria o personagem "pular" repetidamente enquanto o espaço estivesse segurado); (c) atirar um projétil a cada aperto → IsKeyPressed (um tiro por aperto; IsKeyDown dispararia uma rajada contínua de projéteis a 60 por segundo enquanto a tecla estivesse baixa — o que às vezes é desejado, mas "a cada aperto" pede IsKeyPressed).

Exercício 5

Adicione ao exemplo uma segunda forma (um círculo, com DrawCircle) controlada por teclas diferentes (W/A/S/D), de modo que dois objetos se movam independentemente na mesma tela. Explique como o game loop acomoda múltiplos objetos.

Ver resposta

✓ Resposta: Duas formas independentes:

float rx = 400, ry = 300;   // retângulo (setas)
float cx = 200, cy = 200;   // círculo (WASD)
const float vel = 300.0f;

while (!janela.deve_fechar()) {
    float dt = GetFrameTime();
    // Retângulo com setas:
    if (IsKeyDown(KEY_RIGHT)) rx += vel*dt;
    if (IsKeyDown(KEY_LEFT))  rx -= vel*dt;
    if (IsKeyDown(KEY_DOWN))  ry += vel*dt;
    if (IsKeyDown(KEY_UP))    ry -= vel*dt;
    // Círculo com WASD:
    if (IsKeyDown(KEY_D)) cx += vel*dt;
    if (IsKeyDown(KEY_A)) cx -= vel*dt;
    if (IsKeyDown(KEY_S)) cy += vel*dt;
    if (IsKeyDown(KEY_W)) cy -= vel*dt;

    BeginDrawing();
    ClearBackground(RAYWHITE);
    DrawRectangle((int)rx, (int)ry, 50, 50, BLUE);
    DrawCircle((int)cx, (int)cy, 25, RED);
    EndDrawing();
}

O game loop acomoda múltiplos objetos naturalmente: cada fase (input, update, render) simplesmente trata todos os objetos. No input/update, checamos as teclas de cada objeto e atualizamos suas posições; no render, desenhamos cada um. Como tudo acontece dentro de um único frame, os dois objetos parecem se mover simultaneamente. Adicionar um terceiro, quarto ou centésimo objeto segue o mesmo padrão — e é justamente essa multiplicação que o próximo artigo (ECS) vai organizar, para não virar um emaranhado de variáveis soltas como este começa a ser.

Comentários

Mais em Linguagem C++

A Filosofia por Trás do C++ e a Saída Elegante do printf
A Filosofia por Trás do C++ e a Saída Elegante do printf

Três detalhes aparecem na primeira linha de qualquer programa C++: o iostream…

A Sabedoria Codificada — Core Guidelines e Código Limpo em C++
A Sabedoria Codificada — Core Guidelines e Código Limpo em C++

As Core Guidelines destilam experiência de quem projetou a linguagem, mas…

A Primeira Pedra — Uma Célula Poliforme com std::variant
A Primeira Pedra — Uma Célula Poliforme com std::variant

A primeira peça do banco precisa guardar inteiro, texto ou real na mesma…