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,GetFrameTimee 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.