Rust

Rust para GameDev — Motores de Jogo, ECS e Bevy Já leu

14 min de leitura

Rust para GameDev — Motores de Jogo, ECS e Bevy
Rust — Artigo #42 Rust para GameDev — Motores de Jogo, ECS e Bevy Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano Desenvolvimento de jogos é um do

Rust — Artigo #42

Rust para GameDev — Motores de Jogo, ECS e Bevy

Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano


Desenvolvimento de jogos é um dos domínios mais exigentes da computação. Você precisa de performance determinística para manter 60 frames por segundo, segurança de memória para não vazar recursos em sessões de jogo longas, e uma arquitetura que gerencie centenas ou milhares de entidades interagindo simultaneamente.

Rust está se tornando uma escolha séria para gamedev. Bevy — o motor de jogo mais popular escrito em Rust — demonstra como o sistema de tipos pode expressar uma arquitetura ECS (Entity-Component-System) com ergonomia surpreendente. Este artigo explora os conceitos fundamentais e constrói um jogo completo do zero.


O padrão ECS — Entity-Component-System

ECS é a arquitetura dominante em jogos modernos. Em vez de hierarquias de objetos com herança, tudo é decomposto em três conceitos:

Entity:    Um ID único — apenas um número inteiro
Component: Dados puros — posição, velocidade, saúde, sprite
System:    Lógica — processa todas as entidades que têm certos componentes

Exemplo tradicional (OOP):
  class Personagem {
    posicao: Vec2
    velocidade: Vec2
    saude: i32
    fn atualizar() { ... }
    fn renderizar() { ... }
  }

Exemplo ECS:
  Entidade 1: [Posicao(0,0), Velocidade(1,0), Saude(100), Sprite("player")]
  Entidade 2: [Posicao(5,3), Velocidade(-1,0), Saude(50), Sprite("inimigo")]
  Entidade 3: [Posicao(2,7), Luz(255,200,0), Raio(5.0)]

  Sistema Movimento: para cada entidade com (Posicao, Velocidade) → posicao += velocidade
  Sistema Render:    para cada entidade com (Posicao, Sprite) → desenha sprite na posicao
  Sistema Luz:       para cada entidade com (Posicao, Luz) → aplica iluminação

A vantagem sobre OOP: entidades com apenas os componentes que precisam, cache-friendly por design, paralelismo natural entre sistemas independentes.


ECS minimalista do zero

Antes de usar Bevy, vamos implementar um ECS simples para entender os fundamentos:

use std::any::{Any, TypeId};
use std::collections::HashMap;

// Identificador de entidade
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct EntidadeId(u64);

// Componentes são qualquer tipo que implemente Any + Send + Sync
pub trait Componente: Any + Send + Sync {}
impl<T: Any + Send + Sync> Componente for T {}

// Armazenamento de componentes — um Vec por tipo
pub struct ArmazenamentoComponentes {
    dados: HashMap<TypeId, Box<dyn Any>>,
}

impl ArmazenamentoComponentes {
    fn novo() -> Self {
        ArmazenamentoComponentes { dados: HashMap::new() }
    }

    fn inserir<T: Componente>(&mut self, entidade: EntidadeId, componente: T) {
        let tipo = TypeId::of::<T>();
        let mapa = self.dados
            .entry(tipo)
            .or_insert_with(|| Box::new(HashMap::<EntidadeId, T>::new()));

        let mapa_tipado = mapa
            .downcast_mut::<HashMap<EntidadeId, T>>()
            .unwrap();

        mapa_tipado.insert(entidade, componente);
    }

    fn obter<T: Componente>(&self, entidade: EntidadeId) -> Option<&T> {
        let tipo = TypeId::of::<T>();
        self.dados.get(&tipo)?
            .downcast_ref::<HashMap<EntidadeId, T>>()?
            .get(&entidade)
    }

    fn obter_mut<T: Componente>(&mut self, entidade: EntidadeId) -> Option<&mut T> {
        let tipo = TypeId::of::<T>();
        self.dados.get_mut(&tipo)?
            .downcast_mut::<HashMap<EntidadeId, T>>()?
            .get_mut(&entidade)
    }

    fn todos<T: Componente>(&self) -> impl Iterator<Item = (EntidadeId, &T)> {
        let tipo = TypeId::of::<T>();
        self.dados.get(&tipo)
            .and_then(|d| d.downcast_ref::<HashMap<EntidadeId, T>>())
            .map(|m| m.iter().map(|(&id, c)| (id, c)))
            .into_iter()
            .flatten()
    }
}

// Mundo — contém todas as entidades e componentes
pub struct Mundo {
    proximo_id: u64,
    componentes: ArmazenamentoComponentes,
    entidades_vivas: Vec<EntidadeId>,
}

impl Mundo {
    pub fn novo() -> Self {
        Mundo {
            proximo_id: 0,
            componentes: ArmazenamentoComponentes::novo(),
            entidades_vivas: Vec::new(),
        }
    }

    pub fn criar_entidade(&mut self) -> EntidadeId {
        let id = EntidadeId(self.proximo_id);
        self.proximo_id += 1;
        self.entidades_vivas.push(id);
        id
    }

    pub fn adicionar<T: Componente>(&mut self, entidade: EntidadeId, c: T) -> &mut Self {
        self.componentes.inserir(entidade, c);
        self
    }

    pub fn obter<T: Componente>(&self, entidade: EntidadeId) -> Option<&T> {
        self.componentes.obter(entidade)
    }

    pub fn obter_mut<T: Componente>(&mut self, entidade: EntidadeId) -> Option<&mut T> {
        self.componentes.obter_mut(entidade)
    }

    pub fn consultar<T: Componente>(&self) -> impl Iterator<Item = (EntidadeId, &T)> {
        self.componentes.todos()
    }
}

// Componentes do jogo
#[derive(Debug, Clone, Copy)]
struct Posicao { x: f32, y: f32 }

#[derive(Debug, Clone, Copy)]
struct Velocidade { dx: f32, dy: f32 }

#[derive(Debug, Clone)]
struct Saude { atual: i32, maxima: i32 }

#[derive(Debug, Clone)]
struct Nome(String);

#[derive(Debug, Clone, Copy)]
struct Colisao { raio: f32 }

// Sistemas
fn sistema_movimento(mundo: &mut Mundo, dt: f32) {
    let entidades_com_vel: Vec<(EntidadeId, Velocidade)> = mundo
        .consultar::<Velocidade>()
        .map(|(id, v)| (id, *v))
        .collect();

    for (id, vel) in entidades_com_vel {
        if let Some(pos) = mundo.obter_mut::<Posicao>(id) {
            pos.x += vel.dx * dt;
            pos.y += vel.dy * dt;
        }
    }
}

fn sistema_limites(mundo: &mut Mundo, largura: f32, altura: f32) {
    let entidades: Vec<EntidadeId> = mundo
        .consultar::<Posicao>()
        .map(|(id, _)| id)
        .collect();

    for id in entidades {
        if let (Some(pos), Some(vel)) = (
            mundo.obter_mut::<Posicao>(id),
            mundo.obter_mut::<Velocidade>(id),
        ) {
            // Reflexão nas bordas
            if pos.x < 0.0 || pos.x > largura { vel.dx = -vel.dx; }
            if pos.y < 0.0 || pos.y > altura  { vel.dy = -vel.dy; }
            pos.x = pos.x.clamp(0.0, largura);
            pos.y = pos.y.clamp(0.0, altura);
        }
    }
}

fn sistema_render(mundo: &Mundo) {
    println!("── Frame ──────────────────");
    for (id, pos) in mundo.consultar::<Posicao>() {
        let nome = mundo.obter::<Nome>(id)
            .map(|n| n.0.as_str())
            .unwrap_or("?");
        let saude = mundo.obter::<Saude>(id)
            .map(|s| format!("[{}/{}]", s.atual, s.maxima))
            .unwrap_or_default();
        println!("  {:10} ({:6.1}, {:6.1}) {}", nome, pos.x, pos.y, saude);
    }
}

fn main() {
    let mut mundo = Mundo::novo();

    // Cria entidades
    let jogador = mundo.criar_entidade();
    mundo
        .adicionar(jogador, Posicao { x: 50.0, y: 50.0 })
        .adicionar(jogador, Velocidade { dx: 10.0, dy: 5.0 })
        .adicionar(jogador, Saude { atual: 100, maxima: 100 })
        .adicionar(jogador, Colisao { raio: 1.5 })
        .adicionar(jogador, Nome("Jogador".to_string()));

    let inimigo = mundo.criar_entidade();
    mundo
        .adicionar(inimigo, Posicao { x: 80.0, y: 20.0 })
        .adicionar(inimigo, Velocidade { dx: -7.0, dy: 3.0 })
        .adicionar(inimigo, Saude { atual: 50, maxima: 50 })
        .adicionar(inimigo, Colisao { raio: 1.0 })
        .adicionar(inimigo, Nome("Inimigo".to_string()));

    // Luz — entidade sem saúde nem velocidade
    let luz = mundo.criar_entidade();
    mundo
        .adicionar(luz, Posicao { x: 50.0, y: 50.0 })
        .adicionar(luz, Nome("Luz".to_string()));

    println!("Simulação ECS — 5 frames
");

    for frame in 0..5 {
        println!("Frame {frame}:");
        sistema_render(&mundo);
        sistema_movimento(&mut mundo, 0.016); // ~60fps
        sistema_limites(&mut mundo, 100.0, 100.0);
        println!();
    }
}

Bevy — o motor ECS completo

Bevy é o motor de jogo mais ativo do ecossistema Rust. Usa um ECS com ergonomia excepcional graças ao sistema de tipos:

[dependencies]
bevy = "0.13"

Jogo completo: Asteroides

use bevy::prelude::*;
use std::f32::consts::PI;

// ── Componentes ───────────────────────────────────────

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

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

#[derive(Component)]
struct Projetil {
    velocidade: Vec2,
    vida: f32, // segundos até sumir
}

#[derive(Component)]
struct Saude(i32);

#[derive(Component)]
struct Colisao {
    raio: f32,
}

// Marcadores — componentes sem dados
#[derive(Component)] struct VidaUtil;

// ── Recursos ──────────────────────────────────────────

#[derive(Resource, Default)]
struct Placar {
    pontos: u32,
}

#[derive(Resource)]
struct ConfigJogo {
    largura: f32,
    altura: f32,
    vel_projetil: f32,
    cooldown_tiro: f32,
}

impl Default for ConfigJogo {
    fn default() -> Self {
        ConfigJogo {
            largura: 800.0,
            altura: 600.0,
            vel_projetil: 500.0,
            cooldown_tiro: 0.25,
        }
    }
}

#[derive(Resource, Default)]
struct TimerTiro(f32);

// ── Eventos ───────────────────────────────────────────

#[derive(Event)]
struct EventoColisao {
    entidade_a: Entity,
    entidade_b: Entity,
}

#[derive(Event)]
struct EventoPontuou {
    pontos: u32,
}

// ── Setup ─────────────────────────────────────────────

fn setup(
    mut commands: Commands,
    config: Res<ConfigJogo>,
) {
    // Câmera 2D
    commands.spawn(Camera2dBundle::default());

    // Jogador — triângulo no centro
    commands.spawn((
        SpriteBundle {
            sprite: Sprite {
                color: Color::rgb(0.3, 0.8, 1.0),
                custom_size: Some(Vec2::new(30.0, 40.0)),
                ..default()
            },
            transform: Transform::from_xyz(0.0, 0.0, 0.0),
            ..default()
        },
        Jogador { velocidade: 300.0, rotacao_vel: 3.0 },
        Saude(3),
        Colisao { raio: 15.0 },
    ));

    // Asteroides iniciais
    for i in 0..5 {
        let angulo = (i as f32) * (2.0 * PI / 5.0);
        let distancia = 200.0;
        let x = angulo.cos() * distancia;
        let y = angulo.sin() * distancia;

        let vel_x = (angulo + PI / 2.0).cos() * 80.0;
        let vel_y = (angulo + PI / 2.0).sin() * 80.0;

        commands.spawn((
            SpriteBundle {
                sprite: Sprite {
                    color: Color::rgb(0.7, 0.5, 0.3),
                    custom_size: Some(Vec2::new(60.0, 60.0)),
                    ..default()
                },
                transform: Transform::from_xyz(x, y, 0.0),
                ..default()
            },
            Asteroide {
                velocidade: Vec2::new(vel_x, vel_y),
                tamanho: 60.0,
            },
            Colisao { raio: 30.0 },
        ));
    }
}

// ── Sistemas de Input ─────────────────────────────────

fn sistema_input_jogador(
    teclado: Res<ButtonInput<KeyCode>>,
    tempo: Res<Time>,
    config: Res<ConfigJogo>,
    mut timer_tiro: ResMut<TimerTiro>,
    mut comandos: Commands,
    mut query: Query<(&Jogador, &mut Transform)>,
) {
    let Ok((jogador, mut transform)) = query.get_single_mut() else {
        return;
    };

    let dt = tempo.delta_seconds();

    // Rotação
    if teclado.pressed(KeyCode::ArrowLeft) || teclado.pressed(KeyCode::KeyA) {
        transform.rotate_z(jogador.rotacao_vel * dt);
    }
    if teclado.pressed(KeyCode::ArrowRight) || teclado.pressed(KeyCode::KeyD) {
        transform.rotate_z(-jogador.rotacao_vel * dt);
    }

    // Movimento (aceleração na direção que o jogador aponta)
    if teclado.pressed(KeyCode::ArrowUp) || teclado.pressed(KeyCode::KeyW) {
        let direcao = transform.rotation * Vec3::Y;
        transform.translation += direcao * jogador.velocidade * dt;
    }

    // Tiro
    timer_tiro.0 -= dt;
    if (teclado.pressed(KeyCode::Space) || teclado.pressed(KeyCode::KeyZ))
        && timer_tiro.0 <= 0.0
    {
        timer_tiro.0 = config.cooldown_tiro;

        let direcao = transform.rotation * Vec3::Y;
        let vel = Vec2::new(direcao.x, direcao.y) * config.vel_projetil;

        comandos.spawn((
            SpriteBundle {
                sprite: Sprite {
                    color: Color::rgb(1.0, 1.0, 0.3),
                    custom_size: Some(Vec2::new(4.0, 12.0)),
                    ..default()
                },
                transform: Transform {
                    translation: transform.translation + direcao * 25.0,
                    rotation: transform.rotation,
                    ..default()
                },
                ..default()
            },
            Projetil { velocidade: vel, vida: 2.0 },
            Colisao { raio: 4.0 },
        ));
    }
}

// ── Sistemas de Física ────────────────────────────────

fn sistema_mover_asteroides(
    tempo: Res<Time>,
    config: Res<ConfigJogo>,
    mut query: Query<(&Asteroide, &mut Transform)>,
) {
    let dt = tempo.delta_seconds();
    let hw = config.largura / 2.0;
    let hh = config.altura / 2.0;

    for (ast, mut transform) in &mut query {
        transform.translation.x += ast.velocidade.x * dt;
        transform.translation.y += ast.velocidade.y * dt;

        // Wrapping — sai de um lado, aparece do outro
        if transform.translation.x > hw  { transform.translation.x = -hw; }
        if transform.translation.x < -hw { transform.translation.x =  hw; }
        if transform.translation.y > hh  { transform.translation.y = -hh; }
        if transform.translation.y < -hh { transform.translation.y =  hh; }
    }
}

fn sistema_mover_projeteis(
    tempo: Res<Time>,
    mut comandos: Commands,
    mut query: Query<(Entity, &mut Projetil, &mut Transform)>,
) {
    let dt = tempo.delta_seconds();

    for (entidade, mut proj, mut transform) in &mut query {
        transform.translation.x += proj.velocidade.x * dt;
        transform.translation.y += proj.velocidade.y * dt;
        proj.vida -= dt;

        if proj.vida <= 0.0 {
            comandos.entity(entidade).despawn();
        }
    }
}

// ── Sistema de Colisão ────────────────────────────────

fn sistema_colisao_projetil_asteroide(
    mut comandos: Commands,
    mut placar: ResMut<Placar>,
    projeteis: Query<(Entity, &Transform, &Colisao), With<Projetil>>,
    asteroides: Query<(Entity, &Transform, &Colisao, &Asteroide)>,
    mut ev_pontuou: EventWriter<EventoPontuou>,
) {
    for (ep, tp, cp) in &projeteis {
        for (ea, ta, ca, ast) in &asteroides {
            let distancia = tp.translation.distance(ta.translation);

            if distancia < cp.raio + ca.raio {
                // Colisão!
                comandos.entity(ep).despawn();
                comandos.entity(ea).despawn();

                let pontos = (100.0 / ast.tamanho * 60.0) as u32;
                placar.pontos += pontos;
                ev_pontuou.send(EventoPontuou { pontos });

                // Fragmenta asteroide maior em menores
                if ast.tamanho > 25.0 {
                    let novo_tamanho = ast.tamanho / 2.0;
                    for angulo_offset in [-PI/4.0, PI/4.0] {
                        let vel_nova = Vec2::new(
                            ast.velocidade.x * angulo_offset.cos()
                                - ast.velocidade.y * angulo_offset.sin(),
                            ast.velocidade.x * angulo_offset.sin()
                                + ast.velocidade.y * angulo_offset.cos(),
                        ) * 1.5;

                        comandos.spawn((
                            SpriteBundle {
                                sprite: Sprite {
                                    color: Color::rgb(0.6, 0.4, 0.2),
                                    custom_size: Some(Vec2::splat(novo_tamanho)),
                                    ..default()
                                },
                                transform: Transform::from_translation(ta.translation),
                                ..default()
                            },
                            Asteroide {
                                velocidade: vel_nova,
                                tamanho: novo_tamanho,
                            },
                            Colisao { raio: novo_tamanho / 2.0 },
                        ));
                    }
                }
            }
        }
    }
}

// ── UI de Placar ─────────────────────────────────────

#[derive(Component)]
struct TextoPlacar;

fn setup_ui(mut comandos: Commands) {
    comandos.spawn((
        TextBundle::from_section(
            "Pontos: 0",
            TextStyle {
                font_size: 32.0,
                color: Color::WHITE,
                ..default()
            },
        )
        .with_style(Style {
            position_type: PositionType::Absolute,
            top: Val::Px(10.0),
            left: Val::Px(10.0),
            ..default()
        }),
        TextoPlacar,
    ));
}

fn sistema_atualizar_placar(
    placar: Res<Placar>,
    mut query: Query<&mut Text, With<TextoPlacar>>,
) {
    if placar.is_changed() {
        for mut texto in &mut query {
            texto.sections[0].value = format!("Pontos: {}", placar.pontos);
        }
    }
}

// ── Plugin e App ──────────────────────────────────────

pub struct PluginAsteroid;

impl Plugin for PluginAsteroid {
    fn build(&self, app: &mut App) {
        app
            .init_resource::<Placar>()
            .init_resource::<ConfigJogo>()
            .init_resource::<TimerTiro>()
            .add_event::<EventoColisao>()
            .add_event::<EventoPontuou>()
            .add_systems(Startup, (setup, setup_ui))
            .add_systems(Update, (
                sistema_input_jogador,
                sistema_mover_asteroides,
                sistema_mover_projeteis,
                sistema_colisao_projetil_asteroide,
                sistema_atualizar_placar,
            ));
    }
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(WindowPlugin {
            primary_window: Some(Window {
                title: "Asteroides — Bevy ECS".to_string(),
                resolution: (800.0, 600.0).into(),
                ..default()
            }),
            ..default()
        }))
        .add_plugins(PluginAsteroid)
        .run();
}

Estados de jogo com Bevy States

use bevy::prelude::*;

#[derive(States, Debug, Clone, PartialEq, Eq, Hash, Default)]
enum EstadoJogo {
    #[default]
    MenuPrincipal,
    Jogando,
    Pausado,
    GameOver,
}

fn sistema_menu(
    teclado: Res<ButtonInput<KeyCode>>,
    mut prox_estado: ResMut<NextState<EstadoJogo>>,
) {
    if teclado.just_pressed(KeyCode::Enter) {
        prox_estado.set(EstadoJogo::Jogando);
    }
}

fn sistema_pausar(
    teclado: Res<ButtonInput<KeyCode>>,
    estado: Res<State<EstadoJogo>>,
    mut prox_estado: ResMut<NextState<EstadoJogo>>,
) {
    if teclado.just_pressed(KeyCode::Escape) {
        match estado.get() {
            EstadoJogo::Jogando => prox_estado.set(EstadoJogo::Pausado),
            EstadoJogo::Pausado => prox_estado.set(EstadoJogo::Jogando),
            _ => {}
        }
    }
}

pub struct PluginEstados;

impl Plugin for PluginEstados {
    fn build(&self, app: &mut App) {
        app
            .init_state::<EstadoJogo>()
            // Sistemas que rodam apenas em estados específicos
            .add_systems(
                Update,
                sistema_menu.run_if(in_state(EstadoJogo::MenuPrincipal))
            )
            .add_systems(
                Update,
                (
                    sistema_input_jogador,
                    sistema_mover_asteroides,
                ).run_if(in_state(EstadoJogo::Jogando))
            )
            .add_systems(
                Update,
                sistema_pausar.run_if(in_state(EstadoJogo::Jogando)
                    .or_else(in_state(EstadoJogo::Pausado)))
            )
            // Cleanup ao sair de um estado
            .add_systems(
                OnExit(EstadoJogo::Jogando),
                limpar_entidades_jogo
            );
    }
}

fn limpar_entidades_jogo(
    mut comandos: Commands,
    entidades: Query<Entity, Or<(With<Asteroide>, With<Projetil>)>>,
) {
    for entidade in &entidades {
        comandos.entity(entidade).despawn_recursive();
    }
}

Assets e recursos

use bevy::prelude::*;

// Gerenciamento de assets tipado
#[derive(Resource)]
struct AssetsJogo {
    sprite_jogador: Handle<Image>,
    sprite_asteroide: Handle<Image>,
    som_explosao: Handle<AudioSource>,
    som_tiro: Handle<AudioSource>,
    fonte_ui: Handle<Font>,
}

fn carregar_assets(
    mut comandos: Commands,
    servidor_assets: Res<AssetServer>,
) {
    let assets = AssetsJogo {
        sprite_jogador:   servidor_assets.load("sprites/jogador.png"),
        sprite_asteroide: servidor_assets.load("sprites/asteroide.png"),
        som_explosao:     servidor_assets.load("sons/explosao.ogg"),
        som_tiro:         servidor_assets.load("sons/tiro.ogg"),
        fonte_ui:         servidor_assets.load("fontes/principal.ttf"),
    };
    comandos.insert_resource(assets);
}

// Reproduz som ao pontuar
fn sistema_som_pontuacao(
    mut eventos: EventReader<EventoPontuou>,
    assets: Res<AssetsJogo>,
    mut comandos: Commands,
) {
    for _ in eventos.read() {
        comandos.spawn(AudioBundle {
            source: assets.som_explosao.clone(),
            settings: PlaybackSettings::DESPAWN,
        });
    }
}

Bevy para WebAssembly

Uma das características mais atraentes do Bevy: o mesmo código roda no desktop e no browser:

# Instala o target WASM
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli

# Compila para WASM
cargo build --release --target wasm32-unknown-unknown

# Gera os arquivos JavaScript
wasm-bindgen 
    --out-dir ./web/out 
    --target web 
    target/wasm32-unknown-unknown/release/meu_jogo.wasm

index.html:

<!DOCTYPE html>
<html>
<head>
    <title>Asteroides em Rust + Bevy</title>
    <style>
        body { margin: 0; background: #000; display: flex;
               justify-content: center; align-items: center;
               height: 100vh; }
        canvas { display: block; }
    </style>
</head>
<body>
    <script type="module">
        import init from './out/meu_jogo.js';
        await init();
    </script>
</body>
</html>

Performance: sistemas paralelos

Bevy executa sistemas em paralelo automaticamente quando não há conflitos de acesso:

use bevy::prelude::*;

// Estes sistemas rodam em paralelo — acessam componentes diferentes
fn sistema_a(query: Query<&mut Transform, With<Jogador>>) { /* ... */ }
fn sistema_b(query: Query<&mut Saude, With<Asteroide>>) { /* ... */ }

// Este sistema é serial com sistema_a — ambos acessam Transform
fn sistema_c(query: Query<&Transform>) { /* ... */ }

// Ordem explícita quando necessário
fn plugin_com_ordem(app: &mut App) {
    app.add_systems(Update, (
        sistema_input_jogador,
        sistema_mover_asteroides.after(sistema_input_jogador),
        sistema_colisao_projetil_asteroide
            .after(sistema_mover_asteroides),
        sistema_atualizar_placar
            .after(sistema_colisao_projetil_asteroide),
    ));
}

Fontes e leituras recomendadas

  • Bevy Book — guia oficial — https://bevyengine.org/learn/book/introduction/
  • Bevy Examples — exemplos oficiais — https://bevyengine.org/examples/
  • "Unofficial Bevy Cheat Book" — referência prática — https://bevy-cheatbook.github.io
  • hecs crate — ECS minimalista — https://docs.rs/hecs
  • legion crate — ECS de alta performance — https://docs.rs/legion
  • "Game Programming Patterns" — Robert Nystrom — livro gratuito, fundamental — https://gameprogrammingpatterns.com
  • "Data-Oriented Design" — Richard Fabian — filosofia por trás do ECS
  • Bevy Discord — comunidade ativa — https://discord.gg/bevy
  • macroquad — alternativa simples ao Bevy — https://docs.rs/macroquad

Artigo #42 de 52 | Série: Dominando Rust em 1 Ano Próximo → Artigo #43: Distributed Systems — Consenso, mensageria e tolerância a falhas


Comentários

Mais em Rust

Sistemas Distribuídos — Consenso, Mensageria e Tolerância a Falhas
Sistemas Distribuídos — Consenso, Mensageria e Tolerância a Falhas

Rust — Artigo #43 Sistemas Distribuídos — Consenso, Mensageria e Tolerância a...

Variáveis, Tipos e a Arte da Imutabilidade
Variáveis, Tipos e a Arte da Imutabilidade

Em um artigo anterior, instalamos o Rust e entendemos por que ele existe. Hoj...

Módulos e Organização de Código — Estruturando Projetos Reais
Módulos e Organização de Código — Estruturando Projetos Reais

Até agora todos os nossos exemplos viveram em um único arquivo main.rs. Isso...