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/Framework | Tipo | Destaques |
|---|---|---|
| Bevy | Engine completa | ECS, hot-reloading, 2D/3D, multiplataforma |
| ggez | Framework 2D | Simples, inspirado no LOVE2D, ideal para iniciantes |
| macroquad | Framework 2D | Minimalista, compila para Wasm facilmente |
| Fyrox | Engine 3D | Editor visual, cenas, física, UI |
| wgpu | API gráfica | Abstração sobre Vulkan/Metal/DX12/WebGPU |
| winit | Janelamento | Criação de janelas cross-platform |
| gilrs | Input | Suporte a gamepads e joysticks |
| kira | Áudio | Engine 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:
- Cache-friendly: Componentes do mesmo tipo ficam contíguos em memória
- Paralelizável: Systems independentes rodam em paralelo automaticamente
- 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
- Aprenda Rust primeiro: Domine ownership e traits — tutorial de primeiros passos e traits e generics
- Comece com macroquad: API simples, resultado visual rápido, compila para Wasm
- Migre para Bevy: Quando precisar de mais estrutura, ECS e ecossistema de plugins
- Meça performance: Use a receita de medir tempo de execução para profiling
- Explore Wasm: Publique seus jogos no navegador com Rust para WebAssembly
- 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
- Receita: Medir Tempo de Execução — Profiling para otimizar seu game loop
- Tutorial: Structs, Enums e Pattern Matching — Fundamento para ECS
- Tutorial: Traits e Generics — Essencial para entender Bevy
- Rust para WebAssembly — Publique jogos no navegador
- Rust para Data Science — ndarray para cálculos matemáticos em jogos
- Tutorial: Concorrência — Processamento paralelo para engines