Rust para Jogos: Bevy, ECS e Game Engines | Rust Brasil

Guia completo de Rust para jogos em 2026: Bevy engine, ggez, macroquad, wgpu e arquitetura ECS com exemplos práticos de game loop.

Introdução

O desenvolvimento de jogos é uma das fronteiras mais emocionantes para Rust. A linguagem oferece exatamente o que game engines precisam: performance previsível sem garbage collector (eliminando stutters causados por pausas de GC), abstrações zero-cost (generics e traits não adicionam overhead em runtime), segurança de memória (reduzindo crashes em produção) e excelente suporte a concorrência (fundamental para jogos modernos que aproveitam múltiplos cores).

O Bevy se estabeleceu como a engine de jogos mais popular do ecossistema Rust, implementando uma arquitetura Entity Component System (ECS) que é simultaneamente ergonômica e de alta performance. Além do Bevy, frameworks como ggez, macroquad e wgpu cobrem desde protótipos 2D até renderização 3D avançada. Neste artigo, vamos explorar o ecossistema, construir um jogo funcional e entender como empresas estão adotando Rust para gamedev.

Ecossistema de Desenvolvimento de Jogos

Engines e Frameworks

Engine/FrameworkTipoDestaques
BevyEngine completaECS, hot-reloading, 2D/3D, multiplataforma
ggezFramework 2DSimples, inspirado no LOVE2D, ideal para iniciantes
macroquadFramework 2DMinimalista, compila para Wasm facilmente
FyroxEngine 3DEditor visual, cenas, física, UI
wgpuAPI gráficaAbstração sobre Vulkan/Metal/DX12/WebGPU
winitJanelamentoCriação de janelas cross-platform
gilrsInputSuporte a gamepads e joysticks
kiraÁudioEngine de áudio com mixagem e efeitos

Cargo.toml para um Projeto Bevy

[package]
name = "meu-jogo"
version = "0.1.0"
edition = "2021"

[dependencies]
bevy = "0.15"
rand = "0.8"

# Otimização: compilar dependências com otimizações,
# mas manter debug info no nosso código
[profile.dev.package."*"]
opt-level = 3

[profile.dev]
opt-level = 1

Arquitetura ECS (Entity Component System)

Antes de mergulhar no código, é importante entender a arquitetura ECS, que é o coração do Bevy:

  • Entity: Um identificador único (apenas um número). Não contém dados nem lógica
  • Component: Dados puros associados a uma entidade (posição, velocidade, sprite, vida)
  • System: Funções que operam sobre entidades que possuem determinados componentes

Esta arquitetura é ideal para jogos porque:

  1. Cache-friendly: Componentes do mesmo tipo ficam contíguos em memória
  2. Paralelizável: Systems independentes rodam em paralelo automaticamente
  3. Composicional: Comportamentos emergem da combinação de componentes simples

Exemplo Prático: Jogo de Esquiva Espacial com Bevy

Vamos construir um jogo completo onde o jogador controla uma nave que precisa esquivar de asteroides.

use bevy::prelude::*;
use rand::Rng;

// === Componentes ===

#[derive(Component)]
struct Jogador {
    velocidade: f32,
}

#[derive(Component)]
struct Asteroide {
    velocidade: f32,
}

#[derive(Component)]
struct Pontuacao(u32);

#[derive(Resource)]
struct PontuacaoGlobal {
    valor: u32,
    timer_spawn: Timer,
}

// === Setup ===

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    // Câmera 2D
    commands.spawn(Camera2d);

    // Jogador (nave)
    commands.spawn((
        Sprite {
            color: Color::srgb(0.2, 0.7, 1.0),
            custom_size: Some(Vec2::new(40.0, 50.0)),
            ..default()
        },
        Transform::from_xyz(0.0, -250.0, 0.0),
        Jogador { velocidade: 400.0 },
    ));

    // Recurso de pontuação
    commands.insert_resource(PontuacaoGlobal {
        valor: 0,
        timer_spawn: Timer::from_seconds(0.8, TimerMode::Repeating),
    });

    // Texto de pontuação
    commands.spawn((
        Text::new("Pontos: 0"),
        TextFont {
            font_size: 30.0,
            ..default()
        },
        TextColor(Color::WHITE),
        Node {
            position_type: PositionType::Absolute,
            top: Val::Px(10.0),
            left: Val::Px(10.0),
            ..default()
        },
        Pontuacao(0),
    ));
}

// === Systems ===

/// Movimenta o jogador com as setas do teclado.
fn mover_jogador(
    keyboard: Res<ButtonInput<KeyCode>>,
    time: Res<Time>,
    mut query: Query<(&mut Transform, &Jogador)>,
) {
    let (mut transform, jogador) = query.single_mut();
    let mut direcao = Vec3::ZERO;

    if keyboard.pressed(KeyCode::ArrowLeft) || keyboard.pressed(KeyCode::KeyA) {
        direcao.x -= 1.0;
    }
    if keyboard.pressed(KeyCode::ArrowRight) || keyboard.pressed(KeyCode::KeyD) {
        direcao.x += 1.0;
    }
    if keyboard.pressed(KeyCode::ArrowUp) || keyboard.pressed(KeyCode::KeyW) {
        direcao.y += 1.0;
    }
    if keyboard.pressed(KeyCode::ArrowDown) || keyboard.pressed(KeyCode::KeyS) {
        direcao.y -= 1.0;
    }

    if direcao.length() > 0.0 {
        direcao = direcao.normalize();
    }

    transform.translation += direcao * jogador.velocidade * time.delta_secs();

    // Limitar aos bounds da tela
    transform.translation.x = transform.translation.x.clamp(-600.0, 600.0);
    transform.translation.y = transform.translation.y.clamp(-350.0, 350.0);
}

/// Gera novos asteroides periodicamente.
fn gerar_asteroides(
    mut commands: Commands,
    time: Res<Time>,
    mut pontuacao: ResMut<PontuacaoGlobal>,
) {
    pontuacao.timer_spawn.tick(time.delta());

    if pontuacao.timer_spawn.just_finished() {
        let mut rng = rand::thread_rng();
        let x = rng.gen_range(-500.0..500.0);
        let tamanho = rng.gen_range(20.0..60.0);
        let velocidade = rng.gen_range(100.0..350.0);

        commands.spawn((
            Sprite {
                color: Color::srgb(0.9, 0.3, 0.2),
                custom_size: Some(Vec2::splat(tamanho)),
                ..default()
            },
            Transform::from_xyz(x, 400.0, 0.0),
            Asteroide { velocidade },
        ));
    }
}

/// Movimenta asteroides para baixo e remove os que saíram da tela.
fn mover_asteroides(
    mut commands: Commands,
    time: Res<Time>,
    mut query: Query<(Entity, &mut Transform, &Asteroide)>,
    mut pontuacao: ResMut<PontuacaoGlobal>,
) {
    for (entity, mut transform, asteroide) in &mut query {
        transform.translation.y -= asteroide.velocidade * time.delta_secs();

        // Rotação visual
        transform.rotate_z(2.0 * time.delta_secs());

        // Remover asteroides que saíram da tela (e contar pontos)
        if transform.translation.y < -450.0 {
            commands.entity(entity).despawn();
            pontuacao.valor += 10;
        }
    }
}

/// Detecta colisão entre jogador e asteroides.
fn detectar_colisao(
    mut commands: Commands,
    jogador_query: Query<&Transform, With<Jogador>>,
    asteroide_query: Query<(Entity, &Transform, &Sprite), With<Asteroide>>,
    mut pontuacao: ResMut<PontuacaoGlobal>,
) {
    let jogador_pos = jogador_query.single().translation;

    for (entity, asteroide_transform, sprite) in &asteroide_query {
        let tamanho = sprite.custom_size.unwrap_or(Vec2::splat(30.0)).x;
        let distancia = jogador_pos.distance(asteroide_transform.translation);

        if distancia < (tamanho / 2.0 + 20.0) {
            // Colisão detectada — remover asteroide e penalizar
            commands.entity(entity).despawn();
            pontuacao.valor = pontuacao.valor.saturating_sub(50);
        }
    }
}

/// Atualiza o texto de pontuação na tela.
fn atualizar_pontuacao(
    pontuacao: Res<PontuacaoGlobal>,
    mut query: Query<(&mut Text, &Pontuacao)>,
) {
    if pontuacao.is_changed() {
        for (mut texto, _) in &mut query {
            **texto = format!("Pontos: {}", pontuacao.valor);
        }
    }
}

// === Main ===

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(WindowPlugin {
            primary_window: Some(Window {
                title: "Esquiva Espacial — Rust com Bevy".to_string(),
                resolution: (1280.0, 720.0).into(),
                ..default()
            }),
            ..default()
        }))
        .add_systems(Startup, setup)
        .add_systems(Update, (
            mover_jogador,
            gerar_asteroides,
            mover_asteroides,
            detectar_colisao,
            atualizar_pontuacao,
        ))
        .run();
}

Jogo 2D Simples com macroquad

Para protótipos rápidos, macroquad oferece uma API minimalista:

[dependencies]
macroquad = "0.4"
use macroquad::prelude::*;

struct Bola {
    x: f32,
    y: f32,
    vx: f32,
    vy: f32,
    raio: f32,
}

#[macroquad::main("Pong — Rust")]
async fn main() {
    let mut jogador_y: f32 = screen_height() / 2.0;
    let largura_raquete = 15.0;
    let altura_raquete = 100.0;

    let mut bola = Bola {
        x: screen_width() / 2.0,
        y: screen_height() / 2.0,
        vx: 300.0,
        vy: 200.0,
        raio: 10.0,
    };

    let mut pontos: u32 = 0;

    loop {
        clear_background(Color::from_rgba(20, 20, 40, 255));

        let dt = get_frame_time();

        // Input do jogador
        if is_key_down(KeyCode::Up) {
            jogador_y -= 400.0 * dt;
        }
        if is_key_down(KeyCode::Down) {
            jogador_y += 400.0 * dt;
        }
        jogador_y = jogador_y.clamp(0.0, screen_height() - altura_raquete);

        // Mover bola
        bola.x += bola.vx * dt;
        bola.y += bola.vy * dt;

        // Colisão com paredes
        if bola.y - bola.raio < 0.0 || bola.y + bola.raio > screen_height() {
            bola.vy *= -1.0;
        }

        // Colisão com raquete
        if bola.x - bola.raio < 30.0 + largura_raquete
            && bola.y > jogador_y
            && bola.y < jogador_y + altura_raquete
        {
            bola.vx = bola.vx.abs();
            bola.vx *= 1.05; // Acelerar a cada rebatida
            pontos += 1;
        }

        // Bola saiu pela direita — rebater de volta
        if bola.x + bola.raio > screen_width() {
            bola.vx *= -1.0;
        }

        // Bola saiu pela esquerda — reiniciar
        if bola.x < 0.0 {
            bola.x = screen_width() / 2.0;
            bola.y = screen_height() / 2.0;
            bola.vx = 300.0;
            bola.vy = 200.0;
            pontos = 0;
        }

        // Desenhar
        draw_rectangle(30.0, jogador_y, largura_raquete, altura_raquete, BLUE);
        draw_circle(bola.x, bola.y, bola.raio, WHITE);

        // Linha central
        for i in (0..screen_height() as i32).step_by(20) {
            draw_line(
                screen_width() / 2.0, i as f32,
                screen_width() / 2.0, (i + 10) as f32,
                1.0, GRAY,
            );
        }

        // Pontuação
        draw_text(
            &format!("Pontos: {}", pontos),
            screen_width() / 2.0 - 60.0,
            40.0,
            30.0,
            WHITE,
        );

        next_frame().await;
    }
}

wgpu: Renderização de Baixo Nível

Para quem precisa de controle total sobre a GPU, wgpu fornece uma API segura sobre Vulkan, Metal e DirectX 12:

[dependencies]
wgpu = "24"
winit = "0.30"
pollster = "0.4"

wgpu é usado internamente pelo Bevy e é a base para renderização cross-platform em Rust, incluindo suporte a WebGPU no navegador.

Empresas e Projetos Usando Rust em Gamedev

  • Embark Studios: Estúdio de jogos AAA (fundado por ex-DICE/EA) que usa Rust como linguagem principal
  • Ready at Dawn: Componentes de engine em Rust
  • Veloren: MMORPG voxel open-source escrito inteiramente em Rust
  • Fish Fight: Jogo multiplayer 2D construído com macroquad
  • A/B Street: Simulador de tráfego urbano open-source em Rust
  • Tiny Glade: Jogo relaxante de construção que usa Bevy para ferramentas internas
  • Ambient: Runtime de jogos multiplayer em Rust + Wasm

O Game Loop em Rust

Entender o game loop é fundamental. Em Bevy, ele é gerenciado automaticamente, mas o conceito é:

// Conceitual — o Bevy implementa isso internamente
loop {
    // 1. Processar input (teclado, mouse, gamepad)
    processar_input();

    // 2. Atualizar lógica do jogo (física, IA, gameplay)
    // Usa delta_time para independência de framerate
    atualizar_logica(delta_time);

    // 3. Renderizar frame
    renderizar();

    // 4. Apresentar na tela
    apresentar_frame();
}

Bevy divide isso em schedules: PreUpdate, Update, PostUpdate, FixedUpdate (para física), e cada system é automaticamente paralelizado quando possível.

Como Começar

  1. Aprenda Rust primeiro: Domine ownership e traits — tutorial de primeiros passos e traits e generics
  2. Comece com macroquad: API simples, resultado visual rápido, compila para Wasm
  3. Migre para Bevy: Quando precisar de mais estrutura, ECS e ecossistema de plugins
  4. Meça performance: Use a receita de medir tempo de execução para profiling
  5. Explore Wasm: Publique seus jogos no navegador com Rust para WebAssembly
  6. Estude ECS: Entenda Entity Component System — é o padrão da indústria moderna

Conclusão

Rust está se tornando uma opção cada vez mais viável para desenvolvimento de jogos. Bevy oferece uma experiência de desenvolvimento moderna com ECS, hot-reloading e uma comunidade ativa. Para jogos 2D, protótipos e game jams, frameworks como macroquad e ggez permitem começar em minutos. E para quem precisa de controle total, wgpu fornece acesso direto à GPU com segurança de memória. O futuro do gamedev em Rust é promissor.


Veja Também