Audio e fisica sao os pilares que dao vida a um jogo. Neste artigo final da serie de Game Dev, implementamos um sistema de audio para efeitos sonoros e musica, e um motor de fisica 2D com deteccao de colisao, gravidade e resposta a colisao. Ao final, integramos tudo em um pequeno jogo funcional.
Este artigo conclui a serie Desenvolvimento de Jogos com Zig. Recomendamos ter concluido os artigos anteriores, especialmente o ECS.
Sistema de Audio
Inicializacao e Reproducao Basica
Raylib inclui um subsistema de audio completo:
const rl = @cImport(@cInclude("raylib.h"));
pub fn main() void {
rl.InitWindow(800, 600, "Audio");
defer rl.CloseWindow();
rl.InitAudioDevice();
defer rl.CloseAudioDevice();
// Carregar efeitos sonoros (arquivos curtos)
const som_pulo = rl.LoadSound("assets/sounds/pulo.wav");
defer rl.UnloadSound(som_pulo);
const som_tiro = rl.LoadSound("assets/sounds/tiro.wav");
defer rl.UnloadSound(som_tiro);
// Carregar musica (streaming, nao carrega tudo na memoria)
var musica = rl.LoadMusicStream("assets/sounds/trilha.ogg");
defer rl.UnloadMusicStream(musica);
rl.PlayMusicStream(musica);
rl.SetMusicVolume(musica, 0.5);
rl.SetTargetFPS(60);
while (!rl.WindowShouldClose()) {
rl.UpdateMusicStream(musica); // Importante: atualizar stream
if (rl.IsKeyPressed(rl.KEY_SPACE)) {
rl.PlaySound(som_pulo);
}
if (rl.IsMouseButtonPressed(rl.MOUSE_BUTTON_LEFT)) {
rl.PlaySound(som_tiro);
}
if (rl.IsKeyPressed(rl.KEY_M)) {
if (rl.IsMusicStreamPlaying(musica)) {
rl.PauseMusicStream(musica);
} else {
rl.ResumeMusicStream(musica);
}
}
rl.BeginDrawing();
rl.ClearBackground(rl.BLACK);
rl.DrawText("ESPACO: pulo | CLICK: tiro | M: musica", 150, 290, 16, rl.WHITE);
rl.EndDrawing();
}
}
Gerenciador de Audio
Para projetos maiores, centralize o gerenciamento de audio:
const std = @import("std");
const rl = @cImport(@cInclude("raylib.h"));
pub const AudioManager = struct {
sons: std.StringHashMap(rl.Sound),
musicas: std.StringHashMap(rl.Music),
volume_master: f32 = 1.0,
volume_efeitos: f32 = 1.0,
volume_musica: f32 = 0.7,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) AudioManager {
rl.InitAudioDevice();
return .{
.sons = std.StringHashMap(rl.Sound).init(allocator),
.musicas = std.StringHashMap(rl.Music).init(allocator),
.allocator = allocator,
};
}
pub fn deinit(self: *AudioManager) void {
var iter_sons = self.sons.valueIterator();
while (iter_sons.next()) |som| rl.UnloadSound(som.*);
self.sons.deinit();
var iter_mus = self.musicas.valueIterator();
while (iter_mus.next()) |mus| rl.UnloadMusicStream(mus.*);
self.musicas.deinit();
rl.CloseAudioDevice();
}
pub fn carregarSom(self: *AudioManager, nome: []const u8, caminho: [*:0]const u8) !void {
const som = rl.LoadSound(caminho);
try self.sons.put(nome, som);
}
pub fn tocarSom(self: *AudioManager, nome: []const u8) void {
if (self.sons.get(nome)) |som| {
rl.SetSoundVolume(som, self.volume_efeitos * self.volume_master);
rl.PlaySound(som);
}
}
pub fn tocarSomPosicional(self: *AudioManager, nome: []const u8, x: f32, listener_x: f32) void {
if (self.sons.get(nome)) |som| {
const distancia = @abs(x - listener_x);
const max_dist: f32 = 800;
const volume = @max(0, 1.0 - distancia / max_dist);
const pan = (x - listener_x) / max_dist; // -1 a 1
rl.SetSoundVolume(som, volume * self.volume_efeitos * self.volume_master);
rl.SetSoundPan(som, 0.5 + pan * 0.5);
rl.PlaySound(som);
}
}
};
Fisica 2D
Vetores
A base de qualquer motor de fisica:
const std = @import("std");
pub const Vec2 = struct {
x: f32 = 0,
y: f32 = 0,
pub fn add(a: Vec2, b: Vec2) Vec2 {
return .{ .x = a.x + b.x, .y = a.y + b.y };
}
pub fn sub(a: Vec2, b: Vec2) Vec2 {
return .{ .x = a.x - b.x, .y = a.y - b.y };
}
pub fn scale(v: Vec2, s: f32) Vec2 {
return .{ .x = v.x * s, .y = v.y * s };
}
pub fn dot(a: Vec2, b: Vec2) f32 {
return a.x * b.x + a.y * b.y;
}
pub fn length(v: Vec2) f32 {
return @sqrt(v.x * v.x + v.y * v.y);
}
pub fn normalize(v: Vec2) Vec2 {
const len = v.length();
if (len == 0) return .{};
return .{ .x = v.x / len, .y = v.y / len };
}
};
Corpo Rigido
pub const CorpoRigido = struct {
posicao: Vec2 = .{},
velocidade: Vec2 = .{},
aceleracao: Vec2 = .{},
massa: f32 = 1.0,
restituicao: f32 = 0.5, // Elasticidade (0 = sem bounce, 1 = bounce perfeito)
atrito: f32 = 0.1,
estatico: bool = false, // Corpos estaticos nao se movem
pub fn aplicarForca(self: *CorpoRigido, forca: Vec2) void {
if (self.estatico) return;
// F = m * a => a = F / m
const acc = Vec2.scale(forca, 1.0 / self.massa);
self.aceleracao = Vec2.add(self.aceleracao, acc);
}
pub fn integrar(self: *CorpoRigido, dt: f32) void {
if (self.estatico) return;
// Integracao de Euler semi-implicita
self.velocidade = Vec2.add(self.velocidade, Vec2.scale(self.aceleracao, dt));
// Aplicar atrito
self.velocidade = Vec2.scale(self.velocidade, 1.0 - self.atrito * dt);
self.posicao = Vec2.add(self.posicao, Vec2.scale(self.velocidade, dt));
// Resetar aceleracao para o proximo frame
self.aceleracao = .{};
}
};
Deteccao de Colisao AABB
pub const AABB = struct {
min: Vec2,
max: Vec2,
pub fn fromCorpo(corpo: *const CorpoRigido, largura: f32, altura: f32) AABB {
return .{
.min = corpo.posicao,
.max = .{
.x = corpo.posicao.x + largura,
.y = corpo.posicao.y + altura,
},
};
}
pub fn intersecta(a: AABB, b: AABB) bool {
return a.min.x < b.max.x and
a.max.x > b.min.x and
a.min.y < b.max.y and
a.max.y > b.min.y;
}
pub fn penetracao(a: AABB, b: AABB) Vec2 {
// Calcular sobreposicao em cada eixo
const overlap_x = @min(a.max.x - b.min.x, b.max.x - a.min.x);
const overlap_y = @min(a.max.y - b.min.y, b.max.y - a.min.y);
if (overlap_x < overlap_y) {
// Resolver no eixo X
const centro_a = (a.min.x + a.max.x) / 2;
const centro_b = (b.min.x + b.max.x) / 2;
if (centro_a < centro_b) {
return .{ .x = -overlap_x, .y = 0 };
} else {
return .{ .x = overlap_x, .y = 0 };
}
} else {
// Resolver no eixo Y
const centro_a = (a.min.y + a.max.y) / 2;
const centro_b = (b.min.y + b.max.y) / 2;
if (centro_a < centro_b) {
return .{ .x = 0, .y = -overlap_y };
} else {
return .{ .x = 0, .y = overlap_y };
}
}
}
};
Motor de Fisica Completo
pub const FisicaMundo = struct {
gravidade: Vec2 = .{ .x = 0, .y = 980 }, // pixels/s^2
corpos: std.ArrayList(*CorpoRigido),
larguras: std.ArrayList(f32),
alturas: std.ArrayList(f32),
pub fn init(allocator: std.mem.Allocator) FisicaMundo {
return .{
.corpos = std.ArrayList(*CorpoRigido).init(allocator),
.larguras = std.ArrayList(f32).init(allocator),
.alturas = std.ArrayList(f32).init(allocator),
};
}
pub fn deinit(self: *FisicaMundo) void {
self.corpos.deinit();
self.larguras.deinit();
self.alturas.deinit();
}
pub fn adicionarCorpo(self: *FisicaMundo, corpo: *CorpoRigido, w: f32, h: f32) !void {
try self.corpos.append(corpo);
try self.larguras.append(w);
try self.alturas.append(h);
}
pub fn step(self: *FisicaMundo, dt: f32) void {
// 1. Aplicar gravidade
for (self.corpos.items) |corpo| {
if (!corpo.estatico) {
corpo.aplicarForca(Vec2.scale(self.gravidade, corpo.massa));
}
}
// 2. Integrar posicoes
for (self.corpos.items) |corpo| {
corpo.integrar(dt);
}
// 3. Detectar e resolver colisoes
for (0..self.corpos.items.len) |i| {
for (i + 1..self.corpos.items.len) |j| {
const a = AABB.fromCorpo(self.corpos.items[i], self.larguras.items[i], self.alturas.items[i]);
const b = AABB.fromCorpo(self.corpos.items[j], self.larguras.items[j], self.alturas.items[j]);
if (AABB.intersecta(a, b)) {
self.resolverColisao(i, j, a, b);
}
}
}
}
fn resolverColisao(self: *FisicaMundo, i: usize, j: usize, a: AABB, b: AABB) void {
const pen = AABB.penetracao(a, b);
const corpo_a = self.corpos.items[i];
const corpo_b = self.corpos.items[j];
if (corpo_a.estatico and corpo_b.estatico) return;
if (corpo_a.estatico) {
corpo_b.posicao = Vec2.sub(corpo_b.posicao, pen);
// Inverter velocidade no eixo de colisao
if (pen.x != 0) corpo_b.velocidade.x *= -corpo_b.restituicao;
if (pen.y != 0) corpo_b.velocidade.y *= -corpo_b.restituicao;
} else if (corpo_b.estatico) {
corpo_a.posicao = Vec2.add(corpo_a.posicao, pen);
if (pen.x != 0) corpo_a.velocidade.x *= -corpo_a.restituicao;
if (pen.y != 0) corpo_a.velocidade.y *= -corpo_a.restituicao;
} else {
const half_pen = Vec2.scale(pen, 0.5);
corpo_a.posicao = Vec2.add(corpo_a.posicao, half_pen);
corpo_b.posicao = Vec2.sub(corpo_b.posicao, half_pen);
}
}
};
Jogo Completo: Integrando Tudo
Aqui esta um exemplo minimo integrando rendering, input, fisica e audio:
const std = @import("std");
const rl = @cImport(@cInclude("raylib.h"));
pub fn main() void {
rl.InitWindow(800, 600, "Jogo Completo - Zig");
defer rl.CloseWindow();
rl.InitAudioDevice();
defer rl.CloseAudioDevice();
rl.SetTargetFPS(60);
// Estado do jogador
var jogador_x: f32 = 400;
var jogador_y: f32 = 500;
var vel_y: f32 = 0;
var no_chao = false;
const gravidade: f32 = 1200;
const chao_y: f32 = 550;
var pontuacao: u32 = 0;
// Plataformas
const Plataforma = struct { x: f32, y: f32, w: f32 };
const plataformas = [_]Plataforma{
.{ .x = 0, .y = chao_y, .w = 800 },
.{ .x = 200, .y = 420, .w = 150 },
.{ .x = 500, .y = 340, .w = 150 },
.{ .x = 100, .y = 260, .w = 150 },
.{ .x = 400, .y = 180, .w = 150 },
};
while (!rl.WindowShouldClose()) {
const dt = rl.GetFrameTime();
// Input
const velocidade: f32 = 300;
if (rl.IsKeyDown(rl.KEY_LEFT) or rl.IsKeyDown(rl.KEY_A)) {
jogador_x -= velocidade * dt;
}
if (rl.IsKeyDown(rl.KEY_RIGHT) or rl.IsKeyDown(rl.KEY_D)) {
jogador_x += velocidade * dt;
}
if ((rl.IsKeyPressed(rl.KEY_SPACE) or rl.IsKeyPressed(rl.KEY_UP)) and no_chao) {
vel_y = -500;
no_chao = false;
}
// Fisica
vel_y += gravidade * dt;
jogador_y += vel_y * dt;
// Colisao com plataformas
no_chao = false;
for (plataformas) |plat| {
if (jogador_x + 24 > plat.x and jogador_x + 8 < plat.x + plat.w and
jogador_y + 32 > plat.y and jogador_y + 32 < plat.y + 20 and
vel_y > 0)
{
jogador_y = plat.y - 32;
vel_y = 0;
no_chao = true;
}
}
// Limites
if (jogador_x < 0) jogador_x = 0;
if (jogador_x > 768) jogador_x = 768;
if (jogador_y > 600) {
jogador_y = 500;
vel_y = 0;
jogador_x = 400;
}
// Render
rl.BeginDrawing();
rl.ClearBackground(.{ .r = 20, .g = 20, .b = 40, .a = 255 });
// Plataformas
for (plataformas) |plat| {
rl.DrawRectangle(
@intFromFloat(plat.x),
@intFromFloat(plat.y),
@intFromFloat(plat.w),
12,
rl.GREEN,
);
}
// Jogador
rl.DrawRectangle(
@intFromFloat(jogador_x),
@intFromFloat(jogador_y),
32,
32,
rl.BLUE,
);
// UI
var buf: [64]u8 = undefined;
const score_text = std.fmt.bufPrint(&buf, "Pontos: {d}", .{pontuacao}) catch "???";
rl.DrawText(@ptrCast(score_text.ptr), 10, 10, 20, rl.WHITE);
rl.DrawFPS(700, 10);
rl.EndDrawing();
}
}
Conclusao da Serie
Esta serie cobriu os fundamentos do desenvolvimento de jogos com Zig:
Proximos Passos
- Otimizacao de Performance — Torne seu jogo mais rapido
- SIMD e Vetorizacao — Acelere calculos de fisica
- Zig WebAssembly — Publique seu jogo na web
- Zig Cross Compilation — Compile para multiplas plataformas
Compartilhe seus jogos feitos em Zig com a comunidade Zig Brasil!