O game loop e o coracao de qualquer jogo. Ele e responsavel por atualizar a logica do jogo, processar input e renderizar a tela em uma sequencia continua. Um game loop bem implementado garante que o jogo rode de forma suave e consistente em diferentes hardwares. Neste artigo, construimos um game loop profissional em Zig e exploramos tecnicas de renderizacao.
Este artigo continua do setup e janela grafica. Certifique-se de ter o ambiente configurado.
O Game Loop Classico
O game loop mais simples segue o padrao:
while (rodando) {
processar_input();
atualizar_logica();
renderizar();
}
Mas essa abordagem tem um problema: a velocidade do jogo depende da velocidade do hardware. Em um computador rapido, o jogo roda mais rapido; em um lento, roda devagar.
Variable Timestep
A primeira solucao e usar delta time — o tempo entre frames:
const rl = @cImport(@cInclude("raylib.h"));
const Jogador = struct {
x: f32 = 400,
y: f32 = 300,
velocidade: f32 = 200, // pixels por segundo
pub fn update(self: *Jogador, dt: f32) void {
if (rl.IsKeyDown(rl.KEY_RIGHT)) self.x += self.velocidade * dt;
if (rl.IsKeyDown(rl.KEY_LEFT)) self.x -= self.velocidade * dt;
if (rl.IsKeyDown(rl.KEY_DOWN)) self.y += self.velocidade * dt;
if (rl.IsKeyDown(rl.KEY_UP)) self.y -= self.velocidade * dt;
}
pub fn draw(self: *const Jogador) void {
rl.DrawRectangle(
@intFromFloat(self.x),
@intFromFloat(self.y),
32,
32,
rl.BLUE,
);
}
};
pub fn main() void {
rl.InitWindow(800, 600, "Variable Timestep");
defer rl.CloseWindow();
rl.SetTargetFPS(60);
var jogador = Jogador{};
while (!rl.WindowShouldClose()) {
const dt: f32 = rl.GetFrameTime();
jogador.update(dt);
rl.BeginDrawing();
rl.ClearBackground(rl.BLACK);
jogador.draw();
rl.EndDrawing();
}
}
Fixed Timestep (Recomendado)
Para fisica deterministica e reproducibilidade, use fixed timestep com interpolacao:
const std = @import("std");
const rl = @cImport(@cInclude("raylib.h"));
const TICK_RATE: f32 = 1.0 / 60.0; // 60 updates por segundo
const Estado = struct {
x: f32 = 400,
y: f32 = 300,
vel_x: f32 = 0,
vel_y: f32 = 0,
};
pub fn lerp(a: f32, b: f32, t: f32) f32 {
return a + (b - a) * t;
}
pub fn main() void {
rl.InitWindow(800, 600, "Fixed Timestep");
defer rl.CloseWindow();
var estado_atual = Estado{};
var estado_anterior = Estado{};
var acumulador: f32 = 0;
const velocidade: f32 = 200;
while (!rl.WindowShouldClose()) {
const dt: f32 = rl.GetFrameTime();
acumulador += dt;
// Processar input
var input_x: f32 = 0;
var input_y: f32 = 0;
if (rl.IsKeyDown(rl.KEY_RIGHT)) input_x = 1;
if (rl.IsKeyDown(rl.KEY_LEFT)) input_x = -1;
if (rl.IsKeyDown(rl.KEY_DOWN)) input_y = 1;
if (rl.IsKeyDown(rl.KEY_UP)) input_y = -1;
// Fixed timestep updates
while (acumulador >= TICK_RATE) {
estado_anterior = estado_atual;
estado_atual.vel_x = input_x * velocidade;
estado_atual.vel_y = input_y * velocidade;
estado_atual.x += estado_atual.vel_x * TICK_RATE;
estado_atual.y += estado_atual.vel_y * TICK_RATE;
acumulador -= TICK_RATE;
}
// Interpolacao para rendering suave
const alpha = acumulador / TICK_RATE;
const render_x = lerp(estado_anterior.x, estado_atual.x, alpha);
const render_y = lerp(estado_anterior.y, estado_atual.y, alpha);
// Render
rl.BeginDrawing();
rl.ClearBackground(rl.BLACK);
rl.DrawRectangle(
@intFromFloat(render_x),
@intFromFloat(render_y),
32,
32,
rl.BLUE,
);
rl.DrawFPS(10, 10);
rl.EndDrawing();
}
}
A interpolacao entre estado_anterior e estado_atual garante rendering suave mesmo quando a taxa de update e a taxa de render sao diferentes.
Renderizacao de Sprites
Carregando e Desenhando Texturas
const rl = @cImport(@cInclude("raylib.h"));
pub fn main() void {
rl.InitWindow(800, 600, "Sprites");
defer rl.CloseWindow();
rl.SetTargetFPS(60);
// Carregar textura
const sprite = rl.LoadTexture("assets/sprites/heroi.png");
defer rl.UnloadTexture(sprite);
var pos_x: f32 = 400;
var pos_y: f32 = 300;
var escala: f32 = 2.0;
var rotacao: f32 = 0;
while (!rl.WindowShouldClose()) {
const dt = rl.GetFrameTime();
// Mover
if (rl.IsKeyDown(rl.KEY_RIGHT)) pos_x += 200 * dt;
if (rl.IsKeyDown(rl.KEY_LEFT)) pos_x -= 200 * dt;
// Rotacionar
if (rl.IsKeyDown(rl.KEY_Q)) rotacao -= 90 * dt;
if (rl.IsKeyDown(rl.KEY_E)) rotacao += 90 * dt;
rl.BeginDrawing();
rl.ClearBackground(rl.DARKGRAY);
// Desenhar sprite com rotacao e escala
rl.DrawTextureEx(
sprite,
.{ .x = pos_x, .y = pos_y },
rotacao,
escala,
rl.WHITE,
);
rl.EndDrawing();
}
}
Sprite Sheet e Animacao
Para animacoes, usamos sprite sheets — imagens com varios frames:
const rl = @cImport(@cInclude("raylib.h"));
const AnimacaoSprite = struct {
textura: rl.Texture2D,
frame_largura: f32,
frame_altura: f32,
frames_total: u32,
frame_atual: u32 = 0,
timer: f32 = 0,
velocidade: f32 = 0.1, // segundos por frame
pub fn update(self: *AnimacaoSprite, dt: f32) void {
self.timer += dt;
if (self.timer >= self.velocidade) {
self.timer = 0;
self.frame_atual = (self.frame_atual + 1) % self.frames_total;
}
}
pub fn draw(self: *const AnimacaoSprite, x: f32, y: f32) void {
const source = rl.Rectangle{
.x = @as(f32, @floatFromInt(self.frame_atual)) * self.frame_largura,
.y = 0,
.width = self.frame_largura,
.height = self.frame_altura,
};
const dest = rl.Rectangle{
.x = x,
.y = y,
.width = self.frame_largura * 2,
.height = self.frame_altura * 2,
};
rl.DrawTexturePro(self.textura, source, dest, .{ .x = 0, .y = 0 }, 0, rl.WHITE);
}
};
pub fn main() void {
rl.InitWindow(800, 600, "Animacao");
defer rl.CloseWindow();
rl.SetTargetFPS(60);
const sheet = rl.LoadTexture("assets/sprites/heroi_walk.png");
defer rl.UnloadTexture(sheet);
var anim = AnimacaoSprite{
.textura = sheet,
.frame_largura = 32,
.frame_altura = 32,
.frames_total = 8,
.velocidade = 0.08,
};
while (!rl.WindowShouldClose()) {
const dt = rl.GetFrameTime();
anim.update(dt);
rl.BeginDrawing();
rl.ClearBackground(rl.BLACK);
anim.draw(384, 284);
rl.EndDrawing();
}
}
Gerenciador de Assets
Para projetos maiores, um gerenciador de assets centralizado evita carregar recursos duplicados:
const std = @import("std");
const rl = @cImport(@cInclude("raylib.h"));
pub const AssetManager = struct {
texturas: std.StringHashMap(rl.Texture2D),
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) AssetManager {
return .{
.texturas = std.StringHashMap(rl.Texture2D).init(allocator),
.allocator = allocator,
};
}
pub fn deinit(self: *AssetManager) void {
// Descarregar todas as texturas
var iter = self.texturas.valueIterator();
while (iter.next()) |tex| {
rl.UnloadTexture(tex.*);
}
self.texturas.deinit();
}
pub fn carregarTextura(self: *AssetManager, nome: []const u8, caminho: [*:0]const u8) !rl.Texture2D {
// Verificar cache
if (self.texturas.get(nome)) |tex| {
return tex;
}
// Carregar nova textura
const tex = rl.LoadTexture(caminho);
if (tex.id == 0) {
return error.TexturaInvalida;
}
try self.texturas.put(nome, tex);
return tex;
}
pub fn obterTextura(self: *AssetManager, nome: []const u8) ?rl.Texture2D {
return self.texturas.get(nome);
}
};
Camadas de Renderizacao
Jogos 2D tipicamente usam camadas (layers) para controlar a ordem de desenho:
const std = @import("std");
const rl = @cImport(@cInclude("raylib.h"));
const Camada = enum(u8) {
fundo = 0,
cenario = 1,
entidades = 2,
efeitos = 3,
ui = 4,
};
const ComandoDesenho = struct {
camada: Camada,
textura: rl.Texture2D,
origem: rl.Rectangle,
destino: rl.Rectangle,
cor: rl.Color = rl.WHITE,
};
const Renderer = struct {
comandos: std.ArrayList(ComandoDesenho),
pub fn init(allocator: std.mem.Allocator) Renderer {
return .{
.comandos = std.ArrayList(ComandoDesenho).init(allocator),
};
}
pub fn deinit(self: *Renderer) void {
self.comandos.deinit();
}
pub fn submeter(self: *Renderer, cmd: ComandoDesenho) !void {
try self.comandos.append(cmd);
}
pub fn renderizar(self: *Renderer) void {
// Ordenar por camada
std.sort.insertion(ComandoDesenho, self.comandos.items, {}, struct {
fn cmp(_: void, a: ComandoDesenho, b: ComandoDesenho) bool {
return @intFromEnum(a.camada) < @intFromEnum(b.camada);
}
}.cmp);
// Desenhar tudo
for (self.comandos.items) |cmd| {
rl.DrawTexturePro(
cmd.textura,
cmd.origem,
cmd.destino,
.{ .x = 0, .y = 0 },
0,
cmd.cor,
);
}
// Limpar para o proximo frame
self.comandos.clearRetainingCapacity();
}
};
Camera 2D
Uma camera permite seguir o jogador e criar efeitos de parallax:
const rl = @cImport(@cInclude("raylib.h"));
pub fn main() void {
rl.InitWindow(800, 600, "Camera 2D");
defer rl.CloseWindow();
rl.SetTargetFPS(60);
var jogador_x: f32 = 400;
var jogador_y: f32 = 300;
var camera = rl.Camera2D{
.target = .{ .x = jogador_x, .y = jogador_y },
.offset = .{ .x = 400, .y = 300 }, // Centro da tela
.rotation = 0,
.zoom = 1,
};
while (!rl.WindowShouldClose()) {
const dt = rl.GetFrameTime();
// Mover jogador
if (rl.IsKeyDown(rl.KEY_RIGHT)) jogador_x += 300 * dt;
if (rl.IsKeyDown(rl.KEY_LEFT)) jogador_x -= 300 * dt;
if (rl.IsKeyDown(rl.KEY_DOWN)) jogador_y += 300 * dt;
if (rl.IsKeyDown(rl.KEY_UP)) jogador_y -= 300 * dt;
// Zoom
camera.zoom += rl.GetMouseWheelMove() * 0.1;
if (camera.zoom < 0.25) camera.zoom = 0.25;
// Camera segue jogador suavemente
camera.target.x += (jogador_x - camera.target.x) * 5 * dt;
camera.target.y += (jogador_y - camera.target.y) * 5 * dt;
rl.BeginDrawing();
rl.ClearBackground(rl.BLACK);
rl.BeginMode2D(camera);
{
// Desenhar mundo (relativo a camera)
var i: c_int = -10;
while (i <= 10) : (i += 1) {
var j: c_int = -10;
while (j <= 10) : (j += 1) {
rl.DrawRectangle(i * 100, j * 100, 90, 90, rl.DARKGRAY);
}
}
// Desenhar jogador
rl.DrawRectangle(
@intFromFloat(jogador_x - 16),
@intFromFloat(jogador_y - 16),
32,
32,
rl.RED,
);
}
rl.EndMode2D();
// UI (nao afetada pela camera)
rl.DrawFPS(10, 10);
rl.EndDrawing();
}
}
Exercicios
Tilemap renderer: Crie um sistema que carregue um mapa de tiles a partir de um array 2D e os renderize na tela.
Parallax scrolling: Implemente um fundo com multiplas camadas que se movem em velocidades diferentes para criar efeito de profundidade.
Particle system: Crie um sistema de particulas simples que emita e anime particulas na posicao do mouse.
Proximo Artigo
No proximo artigo, exploramos input handling em profundidade, cobrindo teclado, mouse, gamepad e sistemas de input abstratos.
Conteudo Relacionado
- Artigo anterior: Setup e Janela Grafica
- Otimizacao de Performance — Cache-friendly rendering
- Zig Data Structures — Estruturas para game objects
Compartilhe seus jogos feitos em Zig com a comunidade Zig Brasil!