Se você está procurando uma dupla moderna para desenvolvimento de jogos que combine performance nativa, controle de memória e simplicidade, Zig + Raylib é uma das melhores opções disponíveis em 2026. Zig oferece a velocidade de C com segurança de memória, e Raylib fornece uma API de jogos limpa e sem dependências pesadas.
Neste tutorial, vamos configurar o ambiente, entender o build system e criar um jogo Pong completo do zero. Se você já conhece a linguagem, ótimo. Se não, confira por que aprender Zig antes de continuar.
O que é Raylib?
Raylib é uma biblioteca de programação de jogos inspirada na simplicidade de Borland BGI e XNA. Criada por Ramon Santamaria, ela é escrita em C puro e foi projetada para ser simples, sem dependências externas e multiplataforma.
Por que Raylib combina perfeitamente com Zig:
- Interop C nativo: Zig importa headers C sem FFI — basta
@cImport - Sem runtime pesado: ambos são leves e previsíveis
- Build system integrado: o build.zig substitui CMake, Make e autotools
- Controle de memória: Zig dá controle total sobre alocações, ideal para game loops
Configurando o Projeto
Primeiro, crie um novo projeto Zig:
// Terminal: zig init
// Isso cria a estrutura básica com build.zig e src/main.zig
Adicionando Raylib como Dependência
O ecossistema Zig usa build.zig.zon para gerenciar dependências. Para adicionar Raylib, edite o arquivo build.zig.zon:
.{
.name = "meu-jogo-zig",
.version = "0.1.0",
.dependencies = .{
.raylib = .{
.url = "https://github.com/raysan5/raylib/archive/refs/tags/5.5.tar.gz",
.hash = "...", // Hash gerado pelo zig fetch
},
},
}
Para obter o hash correto, execute zig fetch --save https://github.com/raysan5/raylib/archive/refs/tags/5.5.tar.gz e o sistema de build atualizará o arquivo automaticamente.
Configurando o build.zig
O build.zig precisa vincular Raylib ao seu executável:
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const raylib_dep = b.dependency("raylib", .{
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable(.{
.name = "meu-jogo",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe.linkLibrary(raylib_dep.artifact("raylib"));
exe.addIncludePath(raylib_dep.path("src"));
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
const run_step = b.step("run", "Executar o jogo");
run_step.dependOn(&run_cmd.step);
}
Com essa configuração, zig build run compila o Raylib do source, linka com seu projeto e executa o jogo — tudo em um único comando.
Criando uma Janela
Vamos começar com o básico — abrir uma janela e renderizar um fundo:
const rl = @cImport({
@cInclude("raylib.h");
});
pub fn main() void {
const largura: c_int = 800;
const altura: c_int = 600;
rl.InitWindow(largura, altura, "Meu Jogo em Zig");
defer rl.CloseWindow();
rl.SetTargetFPS(60);
while (!rl.WindowShouldClose()) {
rl.BeginDrawing();
defer rl.EndDrawing();
rl.ClearBackground(rl.RAYWHITE);
rl.DrawText("Olá, Zig + Raylib!", 250, 280, 30, rl.DARKGRAY);
}
}
Note como o defer de Zig se encaixa perfeitamente com a API do Raylib. defer rl.CloseWindow() garante que a janela será fechada quando main terminar, e defer rl.EndDrawing() fecha o frame de desenho automaticamente ao final de cada iteração do loop.
Desenhando Formas e Sprites
Raylib oferece funções simples para renderizar primitivas geométricas:
// Dentro do loop de desenho:
rl.BeginDrawing();
defer rl.EndDrawing();
rl.ClearBackground(rl.BLACK);
// Retângulo preenchido
rl.DrawRectangle(100, 100, 200, 150, rl.BLUE);
// Retângulo com bordas
rl.DrawRectangleLines(350, 100, 200, 150, rl.RED);
// Círculo
rl.DrawCircle(400, 400, 50, rl.GREEN);
// Linha
rl.DrawLine(0, 300, 800, 300, rl.YELLOW);
// Texto com posição e tamanho
rl.DrawText("Score: 42", 10, 10, 20, rl.WHITE);
Para carregar e renderizar sprites (texturas), use:
const textura = rl.LoadTexture("assets/personagem.png");
defer rl.UnloadTexture(textura);
// No loop de desenho:
rl.DrawTexture(textura, pos_x, pos_y, rl.WHITE);
// Com rotação e escala:
rl.DrawTextureEx(textura, .{ .x = 100, .y = 100 }, 45.0, 2.0, rl.WHITE);
Tratando Input do Jogador
Raylib simplifica o tratamento de input com funções diretas:
// Teclado
if (rl.IsKeyDown(rl.KEY_W) or rl.IsKeyDown(rl.KEY_UP)) {
jogador_y -= velocidade;
}
if (rl.IsKeyDown(rl.KEY_S) or rl.IsKeyDown(rl.KEY_DOWN)) {
jogador_y += velocidade;
}
if (rl.IsKeyPressed(rl.KEY_SPACE)) {
// Ação única ao pressionar (não repete enquanto segura)
atirar();
}
// Mouse
const mouse_x = rl.GetMouseX();
const mouse_y = rl.GetMouseY();
if (rl.IsMouseButtonPressed(rl.MOUSE_BUTTON_LEFT)) {
// Clique do mouse
}
// Gamepad
if (rl.IsGamepadAvailable(0)) {
if (rl.IsGamepadButtonDown(0, rl.GAMEPAD_BUTTON_RIGHT_FACE_DOWN)) {
// Botão A do controle
}
}
A diferença entre IsKeyDown (mantém enquanto pressionado) e IsKeyPressed (dispara uma vez) é crucial para jogos. Movimentação usa IsKeyDown, ações como pular ou atirar usam IsKeyPressed.
Game Loop: O Coração do Jogo
Todo jogo segue o padrão Update → Draw. Em Zig com Raylib, isso fica limpo e organizado:
const Estado = struct {
bola_x: f32,
bola_y: f32,
vel_x: f32,
vel_y: f32,
jogador_y: f32,
cpu_y: f32,
score_jogador: u32,
score_cpu: u32,
};
fn atualizar(estado: *Estado) void {
const dt = rl.GetFrameTime();
// Mover bola
estado.bola_x += estado.vel_x * dt;
estado.bola_y += estado.vel_y * dt;
// Colisão com bordas superior/inferior
if (estado.bola_y <= 0 or estado.bola_y >= 600) {
estado.vel_y = -estado.vel_y;
}
// Mover jogador com teclado
if (rl.IsKeyDown(rl.KEY_UP)) estado.jogador_y -= 300 * dt;
if (rl.IsKeyDown(rl.KEY_DOWN)) estado.jogador_y += 300 * dt;
}
fn desenhar(estado: *const Estado) void {
rl.BeginDrawing();
defer rl.EndDrawing();
rl.ClearBackground(rl.BLACK);
// Linha central
rl.DrawLine(400, 0, 400, 600, rl.GRAY);
// Raquetes
rl.DrawRectangle(20, @intFromFloat(estado.jogador_y), 10, 80, rl.WHITE);
rl.DrawRectangle(770, @intFromFloat(estado.cpu_y), 10, 80, rl.WHITE);
// Bola
rl.DrawCircle(@intFromFloat(estado.bola_x), @intFromFloat(estado.bola_y), 8, rl.WHITE);
// Score
const buf_jogador = std.fmt.bufPrint(&[_]u8{0} ** 16, "{d}", .{estado.score_jogador}) catch "0";
rl.DrawText(@ptrCast(buf_jogador), 200, 20, 40, rl.WHITE);
}
Detecção de Colisão
Raylib inclui funções prontas para colisão, mas entender a lógica é importante:
fn verificarColisao(bola_x: f32, bola_y: f32, raio: f32, rect_x: f32, rect_y: f32, rect_w: f32, rect_h: f32) bool {
// Colisão círculo-retângulo (AABB)
const ponto_mais_proximo_x = std.math.clamp(bola_x, rect_x, rect_x + rect_w);
const ponto_mais_proximo_y = std.math.clamp(bola_y, rect_y, rect_y + rect_h);
const dist_x = bola_x - ponto_mais_proximo_x;
const dist_y = bola_y - ponto_mais_proximo_y;
return (dist_x * dist_x + dist_y * dist_y) <= (raio * raio);
}
// Ou usando a função built-in do Raylib:
const colisao = rl.CheckCollisionCircleRec(
.{ .x = bola_x, .y = bola_y },
raio,
.{ .x = rect_x, .y = rect_y, .width = rect_w, .height = rect_h },
);
Construindo o Pong Completo
Com todos os conceitos acima, o fluxo completo do jogo fica assim:
- Inicialização: criar janela, definir estado inicial
- Loop principal: ler input → atualizar estado → verificar colisões → desenhar
- IA do oponente: a raquete do CPU segue a bola com velocidade limitada
- Pontuação: quando a bola passa de uma raquete, o oponente marca ponto
- Reset: bola volta ao centro com direção aleatória após cada ponto
A IA do oponente pode ser simples — basta mover a raquete na direção da bola com uma velocidade máxima:
fn atualizarCPU(estado: *Estado, dt: f32) void {
const centro_raquete = estado.cpu_y + 40; // metade da altura da raquete
if (centro_raquete < estado.bola_y - 10) {
estado.cpu_y += 250 * dt; // velocidade da CPU
} else if (centro_raquete > estado.bola_y + 10) {
estado.cpu_y -= 250 * dt;
}
}
Por que Zig para Game Dev?
Zig traz vantagens únicas para desenvolvimento de jogos:
- Sem garbage collector: frame times previsíveis, sem stuttering
- Comptime para otimização: tabelas de lookup, código gerado em compilação
- Cross-compilation trivial:
zig build -Dtarget=x86_64-windowsgera executável Windows a partir do Linux/Mac - Interop C perfeito: use qualquer biblioteca C de jogos sem wrappers
Para quem busca uma engine mais completa, o Mach Engine é construído inteiramente em Zig e oferece um framework de game dev mais opinado. Se você está considerando uma carreira em game dev com Zig, veja nosso guia sobre Zig para game dev.
Próximos Passos
Depois de dominar o básico com Raylib, você pode expandir para:
- Áudio:
rl.InitAudioDevice()erl.PlaySound()para efeitos sonoros - Animações: spritesheet com
DrawTextureRecpara frames individuais - Física: implementar gravidade, fricção e velocidade angular
- UI in-game: Raylib inclui
rayguipara interfaces simples - Multiplayer: sockets com a stdlib de Zig para jogos em rede
Se quiser explorar interfaces gráficas além de jogos, confira nosso artigo sobre Zig e interfaces gráficas. E para se aprofundar no build system, veja o glossário sobre build.zig e build.zig.zon.
Conclusão
A combinação Zig + Raylib é ideal para quem quer criar jogos com controle total sobre performance e memória, sem a complexidade de engines como Unity ou Unreal. Zig oferece a ergonomia que C nunca teve, e Raylib oferece a simplicidade que SDL e OpenGL não entregam sozinhos.
Com o que aprendeu neste tutorial, você já pode criar jogos 2D completos. O ecossistema de game dev em Zig está crescendo rapidamente, e dominar essas ferramentas agora é um investimento que vai valer a pena nos próximos anos.