O desenvolvimento de jogos exige o máximo de performance e controle sobre o hardware, e é exatamente isso que a zig lang oferece. A linguagem Zig combina velocidade comparável ao C, gerenciamento de memória sem garbage collector e excelente interoperabilidade com bibliotecas C, tornando-a uma escolha cada vez mais popular entre game devs. Neste guia completo, vamos explorar como construir jogos com Zig na prática.
Por Que Zig para Game Development
Game devs enfrentam desafios únicos que fazem de Zig uma escolha atraente:
- Sem pausas de GC: Garbage collectors podem causar stuttering visível em jogos. Zig não tem GC, dando controle total sobre quando e como a memória é alocada e liberada.
- Performance previsível: Cada operação em Zig tem custo previsível. Não há alocações ocultas, funções virtuais escondidas ou boxing automático.
- Cross-compilation embutida: Compile para Windows, Linux, macOS e até consoles a partir de uma única máquina.
- Interoperabilidade C zero-cost: Use bibliotecas C de gamedev (raylib, SDL2, OpenGL, Vulkan) diretamente, sem wrapper ou overhead.
- Comptime: Gere lookup tables, bake dados de níveis e otimize estruturas de dados em tempo de compilação.
- Alocadores customizáveis: Use arena allocators para frames, pool allocators para entidades e allocators fixos para recursos que vivem por toda a partida.
Engines e Bibliotecas do Ecossistema
O ecossistema de gamedev em Zig está crescendo rapidamente. Aqui estão as principais opções:
Mach Engine
O Mach Engine é uma game engine escrita inteiramente em Zig, projetada para ser modular e de alta performance. Ela fornece:
- Rendering via WebGPU (compatível com Vulkan, Metal, DX12)
- Sistema de áudio integrado
- ECS embutido
- Build system integrado com Zig
raylib-zig
Bindings Zig para raylib, uma das bibliotecas de gamedev mais acessíveis. Raylib é perfeita para protótipos rápidos e jogos 2D/3D simples.
SDL2 Bindings
SDL2 é a biblioteca de mídia mais usada em gamedev. Os bindings Zig permitem usar SDL2 com a ergonomia da linguagem Zig.
Outras Opções
- Sokol bindings: Abstração gráfica cross-platform minimalista.
- GLFW bindings: Para quem quer trabalhar diretamente com OpenGL/Vulkan.
- Box2D bindings: Física 2D.
Configurando raylib com Zig
Vamos configurar um projeto de jogo usando raylib, que é a forma mais rápida de começar.
Estrutura do Projeto
meu-jogo/
├── build.zig
├── build.zig.zon
└── src/
└── main.zig
build.zig.zon
.{
.name = "meu-jogo",
.version = "0.1.0",
.dependencies = .{
.raylib = .{
.url = "https://github.com/raysan5/raylib/archive/refs/tags/5.0.tar.gz",
.hash = "...", // Hash será fornecido pelo zig build na primeira execução
},
},
}
build.zig
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"));
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
const run_step = b.step("run", "Rodar o jogo");
run_step.dependOn(&run_cmd.step);
}
Criando uma Janela e Game Loop
O game loop é o coração de qualquer jogo. Vamos criar um com raylib:
const std = @import("std");
const rl = @cImport({
@cInclude("raylib.h");
});
const LARGURA = 800;
const ALTURA = 600;
pub fn main() void {
// Inicializar janela
rl.InitWindow(LARGURA, ALTURA, "Meu Jogo em Zig");
defer rl.CloseWindow();
rl.SetTargetFPS(60);
// Game Loop principal
while (!rl.WindowShouldClose()) {
// === ATUALIZAÇÃO ===
atualizar();
// === RENDERIZAÇÃO ===
rl.BeginDrawing();
defer rl.EndDrawing();
rl.ClearBackground(rl.RAYWHITE);
rl.DrawText("Olá, Game Dev em Zig!", 190, 200, 30, rl.DARKGRAY);
rl.DrawFPS(10, 10);
}
}
fn atualizar() void {
// Lógica de atualização aqui
}
Compile e execute:
zig build run
Movendo um Personagem com Input
Vamos criar um personagem que se move com as teclas de seta:
const std = @import("std");
const rl = @cImport({
@cInclude("raylib.h");
});
const LARGURA = 800;
const ALTURA = 600;
const VELOCIDADE = 4.0;
const TAMANHO_JOGADOR = 40;
const Jogador = struct {
x: f32,
y: f32,
cor: rl.Color,
pub fn atualizar(self: *Jogador) void {
if (rl.IsKeyDown(rl.KEY_RIGHT)) self.x += VELOCIDADE;
if (rl.IsKeyDown(rl.KEY_LEFT)) self.x -= VELOCIDADE;
if (rl.IsKeyDown(rl.KEY_DOWN)) self.y += VELOCIDADE;
if (rl.IsKeyDown(rl.KEY_UP)) self.y -= VELOCIDADE;
// Manter dentro da tela
self.x = std.math.clamp(self.x, 0, LARGURA - TAMANHO_JOGADOR);
self.y = std.math.clamp(self.y, 0, ALTURA - TAMANHO_JOGADOR);
}
pub fn desenhar(self: Jogador) void {
rl.DrawRectangle(
@intFromFloat(self.x),
@intFromFloat(self.y),
TAMANHO_JOGADOR,
TAMANHO_JOGADOR,
self.cor,
);
}
};
pub fn main() void {
rl.InitWindow(LARGURA, ALTURA, "Jogador com Movimento");
defer rl.CloseWindow();
rl.SetTargetFPS(60);
var jogador = Jogador{
.x = @as(f32, LARGURA) / 2.0,
.y = @as(f32, ALTURA) / 2.0,
.cor = rl.BLUE,
};
while (!rl.WindowShouldClose()) {
jogador.atualizar();
rl.BeginDrawing();
defer rl.EndDrawing();
rl.ClearBackground(rl.RAYWHITE);
jogador.desenhar();
rl.DrawText("Use as setas para mover", 10, 10, 20, rl.GRAY);
}
}
Sprites e Texturas
Para jogos reais, precisamos carregar e exibir imagens:
const rl = @cImport({
@cInclude("raylib.h");
});
const Sprite = struct {
textura: rl.Texture2D,
x: f32,
y: f32,
escala: f32 = 1.0,
rotacao: f32 = 0.0,
pub fn carregar(caminho: [*:0]const u8) Sprite {
return Sprite{
.textura = rl.LoadTexture(caminho),
.x = 0,
.y = 0,
};
}
pub fn descarregar(self: *Sprite) void {
rl.UnloadTexture(self.textura);
}
pub fn desenhar(self: Sprite) void {
rl.DrawTextureEx(
self.textura,
rl.Vector2{ .x = self.x, .y = self.y },
self.rotacao,
self.escala,
rl.WHITE,
);
}
};
pub fn main() void {
rl.InitWindow(800, 600, "Sprites em Zig");
defer rl.CloseWindow();
rl.SetTargetFPS(60);
var heroi = Sprite.carregar("assets/heroi.png");
defer heroi.descarregar();
heroi.x = 400;
heroi.y = 300;
heroi.escala = 2.0;
while (!rl.WindowShouldClose()) {
rl.BeginDrawing();
defer rl.EndDrawing();
rl.ClearBackground(rl.BLACK);
heroi.desenhar();
}
}
Gerenciamento de Memória para Jogos: Arena Allocators
Em jogos, a alocação de memória precisa ser rápida e previsível. Zig oferece arena allocators que são perfeitos para dados temporários de cada frame.
const std = @import("std");
const Particula = struct {
x: f32,
y: f32,
vx: f32,
vy: f32,
vida: f32,
};
pub fn main() !void {
// Allocator de arena para dados por frame
// Toda memória alocada é liberada de uma vez no final do frame
var arena_buffer: [1024 * 1024]u8 = undefined; // 1MB para o frame
var frame_allocator = std.heap.FixedBufferAllocator.init(&arena_buffer);
// Allocator de longa duração para recursos persistentes
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const persistent_alloc = gpa.allocator();
// Lista persistente de entidades (vive por toda a partida)
var entidades = std.ArrayList(Particula).init(persistent_alloc);
defer entidades.deinit();
// Simulação de 60 frames
var frame: u32 = 0;
while (frame < 60) : (frame += 1) {
// Resetar arena no início de cada frame
frame_allocator.reset();
const frame_alloc = frame_allocator.allocator();
// Dados temporários deste frame (colisões, cálculos, etc.)
var colisoes = std.ArrayList([2]usize).init(frame_alloc);
// Não precisa de defer deinit! Arena vai resetar tudo
// Detectar colisões (dado temporário do frame)
for (entidades.items, 0..) |a, i| {
for (entidades.items[i + 1 ..], 0..) |b, j_offset| {
const j = i + 1 + j_offset;
const dx = a.x - b.x;
const dy = a.y - b.y;
if (dx * dx + dy * dy < 100.0) {
try colisoes.append(.{ i, j });
}
}
}
// Processar colisões...
_ = colisoes; // Uso simplificado para o exemplo
}
}
A vantagem da arena é que a liberação de memória é instantânea: basta resetar o ponteiro. Isso evita a fragmentação de memória e elimina o overhead de liberações individuais.
Padrão ECS (Entity Component System)
O ECS é o padrão de arquitetura mais popular em game development moderno. Zig é particularmente bom para implementar ECS graças ao comptime e aos generics.
const std = @import("std");
// === COMPONENTES ===
const Posicao = struct {
x: f32 = 0,
y: f32 = 0,
};
const Velocidade = struct {
vx: f32 = 0,
vy: f32 = 0,
};
const Sprite = struct {
largura: u32,
altura: u32,
cor: u32 = 0xFFFFFFFF,
};
const Vida = struct {
atual: i32,
maxima: i32,
};
// === MUNDO ECS SIMPLIFICADO ===
const MAX_ENTIDADES = 1000;
const Mundo = struct {
// Cada componente tem seu próprio array (SoA - Structure of Arrays)
posicoes: [MAX_ENTIDADES]?Posicao = [_]?Posicao{null} ** MAX_ENTIDADES,
velocidades: [MAX_ENTIDADES]?Velocidade = [_]?Velocidade{null} ** MAX_ENTIDADES,
sprites: [MAX_ENTIDADES]?Sprite = [_]?Sprite{null} ** MAX_ENTIDADES,
vidas: [MAX_ENTIDADES]?Vida = [_]?Vida{null} ** MAX_ENTIDADES,
proxima_entidade: usize = 0,
pub fn criarEntidade(self: *Mundo) usize {
const id = self.proxima_entidade;
self.proxima_entidade += 1;
return id;
}
pub fn adicionarPosicao(self: *Mundo, id: usize, pos: Posicao) void {
self.posicoes[id] = pos;
}
pub fn adicionarVelocidade(self: *Mundo, id: usize, vel: Velocidade) void {
self.velocidades[id] = vel;
}
pub fn adicionarSprite(self: *Mundo, id: usize, spr: Sprite) void {
self.sprites[id] = spr;
}
pub fn adicionarVida(self: *Mundo, id: usize, vida: Vida) void {
self.vidas[id] = vida;
}
};
// === SISTEMAS ===
fn sistemaMovimento(mundo: *Mundo, delta: f32) void {
for (0..mundo.proxima_entidade) |id| {
if (mundo.posicoes[id]) |*pos| {
if (mundo.velocidades[id]) |vel| {
pos.x += vel.vx * delta;
pos.y += vel.vy * delta;
}
}
}
}
fn sistemaRenderizacao(mundo: *Mundo) void {
for (0..mundo.proxima_entidade) |id| {
if (mundo.posicoes[id]) |pos| {
if (mundo.sprites[id]) |spr| {
// Aqui você chamaria rl.DrawRectangle ou similar
_ = pos;
_ = spr;
}
}
}
}
pub fn main() void {
var mundo = Mundo{};
// Criar jogador
const jogador = mundo.criarEntidade();
mundo.adicionarPosicao(jogador, .{ .x = 400, .y = 300 });
mundo.adicionarVelocidade(jogador, .{ .vx = 0, .vy = 0 });
mundo.adicionarSprite(jogador, .{ .largura = 32, .altura = 32, .cor = 0xFF0000FF });
mundo.adicionarVida(jogador, .{ .atual = 100, .maxima = 100 });
// Criar inimigos
for (0..10) |i| {
const inimigo = mundo.criarEntidade();
mundo.adicionarPosicao(inimigo, .{
.x = @as(f32, @floatFromInt(i)) * 60.0,
.y = 100,
});
mundo.adicionarVelocidade(inimigo, .{ .vx = 1, .vy = 0 });
mundo.adicionarSprite(inimigo, .{ .largura = 24, .altura = 24, .cor = 0x00FF00FF });
mundo.adicionarVida(inimigo, .{ .atual = 50, .maxima = 50 });
}
// Game loop
const delta: f32 = 1.0 / 60.0;
var frame: u32 = 0;
while (frame < 3600) : (frame += 1) { // 60 segundos a 60fps
sistemaMovimento(&mundo, delta);
sistemaRenderizacao(&mundo);
}
}
O layout SoA (Structure of Arrays) usado aqui é amigável ao cache da CPU, o que é crucial para performance em jogos com muitas entidades. Para aprender a medir e validar esses ganhos, veja o tutorial de profiling e benchmarks em Zig.
Detecção de Colisão
Implementação básica de AABB (Axis-Aligned Bounding Box):
const Retangulo = struct {
x: f32,
y: f32,
largura: f32,
altura: f32,
pub fn colide(self: Retangulo, outro: Retangulo) bool {
return self.x < outro.x + outro.largura and
self.x + self.largura > outro.x and
self.y < outro.y + outro.altura and
self.y + self.altura > outro.y;
}
pub fn contem(self: Retangulo, px: f32, py: f32) bool {
return px >= self.x and
px <= self.x + self.largura and
py >= self.y and
py <= self.y + self.altura;
}
};
// Uso
const jogador_rect = Retangulo{ .x = 100, .y = 200, .largura = 32, .altura = 32 };
const inimigo_rect = Retangulo{ .x = 110, .y = 210, .largura = 24, .altura = 24 };
if (jogador_rect.colide(inimigo_rect)) {
// Processar colisão!
}
Timer e Animação
Controle de tempo para animações de sprites:
const AnimacaoSprite = struct {
frames: []const rl.Rectangle,
frame_atual: usize = 0,
tempo_por_frame: f32,
tempo_acumulado: f32 = 0,
pub fn atualizar(self: *AnimacaoSprite, delta: f32) void {
self.tempo_acumulado += delta;
if (self.tempo_acumulado >= self.tempo_por_frame) {
self.tempo_acumulado -= self.tempo_por_frame;
self.frame_atual = (self.frame_atual + 1) % self.frames.len;
}
}
pub fn frameAtual(self: AnimacaoSprite) rl.Rectangle {
return self.frames[self.frame_atual];
}
};
Jogos e Engines Notáveis em Zig
O ecossistema de gamedev em Zig está crescendo. Alguns projetos notáveis:
- Mach Engine: Engine completa escrita em Zig, com rendering WebGPU e ECS integrado.
- Zig Gamedev: Coleção de bibliotecas de gamedev para Zig, incluindo math, physics e rendering.
- Pacman.zig: Implementação completa de Pac-Man em Zig como projeto educacional.
- Cosmic: Motor de busca semântico que demonstra a capacidade de Zig para projetos de alta performance.
- Bun: Embora não seja um jogo, demonstra como Zig pode ser usado para construir software de altíssima performance.
A comunidade de gamedev em Zig é ativa e acolhedora, com projetos open-source que servem como excelentes referências de aprendizado.
Dicas de Performance para Game Devs
- Use arena allocators: Aloque dados temporários de cada frame em uma arena e resete no final.
- Prefira SoA sobre AoS: Structure of Arrays é melhor para performance de cache.
- Aproveite SIMD: Zig tem suporte embutido a operações vetoriais SIMD para cálculos de física e rendering.
- Comptime para lookup tables: Gere tabelas de seno/cosseno em tempo de compilação.
- Evite alocações no hot path: Pré-aloque buffers e reutilize memória.
// Tabela de seno gerada em tempo de compilação
const SIN_TABLE_SIZE = 360;
const sin_table = blk: {
var table: [SIN_TABLE_SIZE]f32 = undefined;
for (0..SIN_TABLE_SIZE) |i| {
const angulo = @as(f32, @floatFromInt(i)) * std.math.pi / 180.0;
table[i] = @sin(angulo);
}
break :blk table;
};
// Uso: sin_table[angulo_em_graus] é instantâneo, sem cálculo em runtime
Conclusão
Zig está se posicionando como uma alternativa moderna ao C para desenvolvimento de jogos. Com performance nativa, gerenciamento de memória flexível, cross-compilation embutida e excelente interoperabilidade com bibliotecas C, Zig oferece tudo que um game dev precisa. O ecossistema ainda é jovem comparado ao C++ ou C#, mas está crescendo rapidamente com projetos como Mach Engine e a adoção por projetos de alta visibilidade como o Bun.