Audio e Fisica em Jogos com Zig: Som, Colisoes e Gravidade

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:

  1. Setup e Janela Grafica
  2. Game Loop e Rendering
  3. Input Handling
  4. ECS Pattern
  5. Audio e Fisica (este artigo)

Proximos Passos


Compartilhe seus jogos feitos em Zig com a comunidade Zig Brasil!

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.