Capítulo 2. Rust para Bevy en 30 minutos (lo justo para no morir)

CAP 02 · Bevy 0.18/0.19
"El compilador de Rust no te odia. Te conoce mejor que tu terapeuta."

— Dicho popular en el Discord oficial de Bevy, probablemente.

2.1 El compilador como editor de pareja

Antes de tocar Bevy necesitas el vocabulario mínimo de Rust. No necesitas ser experto: si sabes lo que es un enum, un trait, un Result y un Option, tienes el 80 % de lo que vas a leer. Este capítulo es esa capa mínima: ownership, borrowing, tipos algebraicos, traits, errores y colecciones.

Rust es un lenguaje con un compilador quejica. Te avisa de cada uso indebido de memoria antes de que ejecutes el programa. Es como tener al aire de un revisor de tren revisándote cada billete: al principio es agotador; cuando te acostumbras, viajar es un placer. Y en un motor de juegos, donde cada frame cuenta, *no pagar un garbage collector** (recolector de basura que libera memoria automáticamente) significa frames* estables y sin pausas misteriosas.

Piensa en Rust como una cocina con un chef que no te deja salir de la cocina con el cuchillo hasta que lo devuelves. Te obliga a devolverlo. Suena pesado, pero cuando lo entiendes, no hay accidentes con los dedos.

Una analogía absurda antes de empezar: ownership es como adoptar una planta. Solo hay una persona dueña del tiesto (la variable dueña). Si quieres regarla tú, le pides a la dueña que te la preste (una referencia inmutable); si quieres trasplantarla, le pides permiso para moverla (referencia mutable) y, mientras tanto, nadie más puede tocarla. Si la planta se muere, solo la dueña llora.

2.2 Ownership en tres actos

Rust tiene tres reglas de oro:

  1. Cada valor tiene un dueño (una variable).
  2. Solo puede haber un dueño a la vez.
  3. Cuando el dueño sale del ámbito (la llave }), el valor se libera.
fn main() {
    // Acto 1: nace el dueño.
    let s = String::from("hola"); // s es la única dueña del string.
    // Acto 2: la propiedad se mueve.
    let t = s; // s ya no es válido. La memoria ahora pertenece a t.
    println!("{}", t); // OK
    // println!("{}", s); // ERROR: s ya no existe.
} // t se libera aquí. La memoria vuelve al sistema.

El acto 2 es la "movida" (move). Cuando asignas una estructura que vive en el heap (memoria dinámica, no stack) —como un String o un Vec— no se copia, se transfiere. Esto evita copias accidentales y libera al programador de pensar en cuándo limpiar.

fn main() {
    let s = String::from("hola");
    saludar(s);            // s se mueve dentro de la función.
    // println!("{}", s);  // ERROR: s ya no es nuestro.
}

fn saludar(nombre: String) {
    println!("Hola, {}!", nombre);
}

Para evitar el dolor, hay dos salidas:

fn main() {
    let s = String::from("hola");
    saludar(&s);   // prestamos s, no lo regalamos.
    println!("{}", s); // OK: sigue siendo nuestro.
}

fn saludar(nombre: &String) {
    println!("Hola, {}!", nombre);
}

2.3 Borrowing y referencias

Una referencia es un puntero con camisa de gala: te deja mirar sin apropiarte. Hay dos tipos:

Regla de oro: o tienes muchas referencias inmutables, o tienes una mutable. Nunca ambas al mismo tiempo. Si lo intentas, el compilador te dirá algo como "cannot borrow as mutable because also borrowed as immutable".

fn main() {
    let mut s = String::from("hola");

    let r1 = &s;     // prestamos para leer.
    let r2 = &s;     // y otra vez.
    println!("{} {}", r1, r2); // OK mientras r1 y r2 vivan.

    let r3 = &mut s; // ahora prestamos para escribir.
    r3.push_str(" mundo");
    // println!("{}", r1); // ERROR: r1 todavía vive y r3 quiere escribir.
    println!("{}", r3);
}

Bevy usa esto a saco: Query<&Position> pide referencias inmutables a todos los Position, y Query<&mut Position> pide una mutable exclusiva. Si tu sistema pide ambas a la vez sobre la misma entidad, el compilador te lo explica antes de que explote en producción.

Ownership (concepto): cada valor tiene exactamente un dueño en un momento dado.
Move (concepto): transferencia de propiedad; el dueño anterior deja de tener el valor.
Borrow (concepto): préstamo temporal de una referencia, sin mover la propiedad.
Referencia inmutable `&T` (tipo): puntero de solo lectura; puede haber varias a la vez.
Referencia mutable `&mut T` (tipo): puntero de escritura exclusivo; solo una a la vez, sin otras lecturas vivas.
Lifetime (concepto): cuánto vive una referencia. El compilador lo deduce casi siempre; cuando no puede, lo pides con `'a` o `'static`. En este libro casi no lo verás.

2.4 enum y match: el arma secreta

Los enum de Rust son sum types: un valor puede ser una de varias variantes, y cada variante puede llevar datos. Nada que ver con los enum de C, que son solo constantes.

enum Item {
    Nada,
    Moneda(u32),
    Pocion { cura: u32, usos: u8 },
    Espada { daño: u32, manos: u8 },
}

Para tratarlos, match es obligatorio y exhaustivo:

fn describir(item: &Item) -> String {
    match item {
        Item::Nada            => "vacío".to_string(),
        Item::Moneda(v)       => format!("moneda de {}", v),
        Item::Pocion { cura, usos } => format!("poción {} con {} usos", cura, usos),
        Item::Espada { daño, manos } => format!("espada de {}, {} manos", daño, manos),
    }
}

Si te olvidas un caso, el compilador te avisa. Esta es la razón por la que Bevy puede modelar estados (enum AppState { Menu, Playing, Paused }), componentes variantes y eventos sin un null en ningún rincón.

Option<T> y Result<T, E> son enum de la librería estándar:

pub enum Option<T> {
    None,
    Some(T),
}

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

Option<T> significa "puede que haya algo o no". Result<T, E> significa "esto puede fallar y, si falla, te explico por qué". No hay null, no hay exceptions: si algo puede no estar, es Option; si algo puede fallar, es Result.

fn cargar_mapa(ruta: &str) -> Result<Mapa, std::io::Error> {
    // ...
}

fn main() {
    let mapa = cargar_mapa("mapa.json"); // mapa: Result<Mapa, io::Error>
    match mapa {
        Ok(m) => println!("{}x{}", m.ancho, m.alto),
        Err(e) => eprintln!("No pude: {}", e),
    }
}

2.5 Traits y derive

Un trait es un contrato: "este tipo sabe hacer X". Es la versión Rust de las interfaces en otros lenguajes, pero con esteroides: puede tener métodos por defecto, métodos estáticos, y tipos asociados.

trait Saludable {
    fn saludar(&self) -> String;

    fn gritar(&self) -> String {  // método con implementación por defecto.
        self.saludar() + "!!!"
    }
}

struct Zombie { nombre: String }

impl Saludable for Zombie {
    fn saludar(&self) -> String {
        format!("gruñe {}", self.nombre)
    }
}

derive es macro que genera implementaciones boilerplate (código repetitivo y mecánico) por ti. Lo verás todo el rato en Bevy:

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Pos { x: i32, y: i32 }

#[derive(Debug, Clone, PartialEq)]
enum Modo { Solo, Coop(u8) }

Los derives más comunes en Bevy: Component, Resource, Event, States, Bundle, Default, Reflect, Serialize, Deserialize. Cuando un snippet diga #[derive(Component)], ya sabes que es "esto es un componente, por favor, genera el código que Bevy necesita para meterlo en el mundo".

2.6 Errores con Result y ?

El operador ? propaga errores de forma concisa. Si una función devuelve Result<T, E>, puedes hacer:

use std::fs;
use std::io;

fn leer_config(ruta: &str) -> Result<String, io::Error> {
    let bytes = fs::read(ruta)?;       // si falla, devolvemos io::Error.
    let texto = String::from_utf8(bytes)
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    Ok(texto)
}

? es syntactic sugar (azúcar sintáctica, una forma más compacta de escribir lo mismo) para "si es Err, devuelve; si es Ok, desenvuelve". En Bevy verás ? dentro de sistemas que devuelven Result<(), BevyError>.

2.7 Colecciones: Vec y HashMap

Las dos colecciones del 90 % de los casos:

use std::collections::HashMap;

fn main() {
    let mut enemigos: Vec<String> = vec!["zombi".into(), "esqueleto".into()];
    enemigos.push("fantasma".into());

    let mut daño: HashMap<&str, u32> = HashMap::new();
    daño.insert("zombi", 10);
    daño.insert("esqueleto", 15);
    println!("zombi hace {} pts", daño["zombi"]);
}

Bevy usa mucho Vec por debajo (sus entidades, sus queries) y HashMap cuando necesitas índices. Pero recuerda: en ECS rara vez guardas entidades en un HashMap; las entidades viven en el World y se accede a ellas con Query. Lo veremos en el cap. 4.

Trait (concepto): contrato que un tipo puede implementar. Parecido a interface, más flexible.
Derive (macro): genera implementaciones de traits marcados con `#[derive(...)]`.
Struct (tipo): producto de campos. Como una clase sin herencia ni métodos por defecto.
Enum (tipo): suma de variantes. Cada variante puede llevar datos distintos.
Option<T> (tipo): "puede haber algo o nada". Reemplaza al null.
Result<T, E> (tipo): "puede tener un valor o un error". Reemplaza a las excepciones.
`?` (operador): propaga el error hacia arriba si la función devuelve Result.
Match (expresión): pattern matching (coincidencia de patrones) exhaustivo sobre enums y otras estructuras.
Vec<T> (colección): array dinámico, pila de cosas del mismo tipo.
HashMap<K, V> (colección): diccionario hash, acceso por clave.

2.8 Snippet compilable: un mini-juego sin Bevy

Para cerrar, un programa completo y compilable que junta todo. No usa Bevy: solo Rust puro, para que veas el lenguaje sin el motor de por medio. Luego en el cap. 4 lo traducimos a Bevy.

//! cap-02 — sección 2.8: mini-juego de inventario en Rust puro.

#[derive(Debug, Clone, PartialEq)]
enum Item {
    Moneda(u32),
    Pocion { cura: u32, usos: u8 },
}

#[derive(Debug, Default)]
struct Inventario {
    items: Vec<Item>,
}

impl Inventario {
    fn agregar(&mut self, item: Item) {
        self.items.push(item);
    }

    fn usar_pocion(&mut self) -> Option<u32> {
        // Busca la primera poción con usos > 0.
        for item in self.items.iter_mut() {
            if let Item::Pocion { cura, usos } = item {
                if *usos > 0 {
                    *usos -= 1;
                    return Some(*cura);
                }
            }
        }
        None
    }

    fn contar_monedas(&self) -> u32 {
        self.items.iter().filter_map(|i| {
            if let Item::Moneda(v) = i { Some(*v) } else { None }
        }).sum()
    }
}

fn main() {
    let mut inv = Inventario::default();
    inv.agregar(Item::Moneda(10));
    inv.agregar(Item::Pocion { cura: 25, usos: 2 });
    inv.agregar(Item::Moneda(5));

    println!("monedas: {}", inv.contar_monedas());          // 15
    println!("bebo poción: {:?}", inv.usar_pocion());        // Some(25)
    println!("bebo otra: {:?}", inv.usar_pocion());         // Some(25)
    println!("bebo otra: {:?}", inv.usar_pocion());         // None
    println!("queda: {:?}", inv);
}

Compílalo con cargo new inventario && cd inventario y luego pega el código. Verás que cargo run lo ejecuta sin warnings. Cada línea tiene una intención: Vec para acumular, match para tratar el enum, if let para casos puntuales, filter_map para sumar monedas, Option para decir "no hay poción".

❌ let referencia = &String::from("hola");
   referencia.push_str(" mundo"); // ERROR: `&String` no permite mutar.
✅ let mut s = String::from("hola");
   let r = &mut s;            // préstamo mutable exclusivo.
   r.push_str(" mundo");
💡 Por qué: `&T` es solo lectura. Para escribir, necesitas `&mut T`. Y solo puede haber un prestatario mutable a la vez.

2.9 Glosario del capítulo

Cargo (herramienta): gestor de paquetes y compilación de Rust. `cargo new`, `cargo run`, `cargo check` son tus nuevos mejores amigos.
Crate (concepto): librería o ejecutable en Rust. Bevy es un crate; tu juego es otro.
`#[derive(...)]` (macro): genera implementaciones de traits automáticamente. `#[derive(Component)]` aparece cientos de veces en un proyecto Bevy.
`Trait` (concepto): contrato de métodos que un tipo implementa.
`Struct` (tipo): agrupación de campos con nombre. Como una clase sin métodos ni herencia.
`Enum` (tipo): valor que es exactamente una de varias variantes; cada variante puede llevar datos.
`Option<T>` (tipo): "algo o nada". Evita el null.
`Result<T, E>` (tipo): "valor o error". Evita las excepciones.
`Match` (expresión): pattern matching exhaustivo. Si falta un caso, no compila.
Lifetime `'a` (concepto): región de tiempo durante la cual una referencia es válida. Casi siempre lo deduce el compilador; cuando no, lo verás en funciones genéricas avanzadas. En este libro no lo usamos.
`Send` y `Sync` (traits): un tipo es `Send` si puede moverse entre hilos; es `Sync` si varios hilos pueden leerlo a la vez. Bevy se basa en ellos para su *scheduler* paralelo.

2.10 Trivia histórica

Rust 1.0 (Mozilla Research, 2015) — se lanzó el 15 de mayo. Su diseñador principal, Graydon Hoare, llevaba trabajando en él desde 2006 como proyecto personal.
Bevy (Carter "Cartes" Anderson, 2019) — la primera versión pública, 0.1, era un *toy engine* (motor experimental de prueba) con un ECS casero. Cinco años y miles de PRs después, es el motor de juegos Rust con más momentum.
Bevy 0.18 (marzo 2026) — la línea base del libro. Introduce `bsn!`, required components estables, observers sobre relaciones.
Bevy 0.19 (línea LTS) — unifica `Resource` y `Component`, simplifica `Query` con change detection y permite relaciones first-class.
Graydon Hoare (Mozilla, 2006) — el nombre "Rust" viene de un hongo parásito (*rust fungi*), porque los sistemas de *plumbing* de Mozilla estaban "oxidados".

🎯 2.11 Patrón del capítulo: "trait-driven design"

Problema: quiero que varias partes de mi código compartan un comportamiento sin atarlas a una jerarquía. Por ejemplo, "todo lo que se puede dañar" debería tener un método recibir_daño, pero Player, Enemy y Crate no comparten clase.

Solución: define un trait Dañable con un método recibir_daño(&mut self, cantidad: u32). Implementa el trait para cada tipo. Ahora cualquier función puede aceptar &mut dyn Dañable (un puntero a "cualquier cosa que sepa dañarse") y trabajar polimórficamente sin herencia.

trait Dañable {
    fn recibir_daño(&mut self, cantidad: u32) -> bool; // true si muere.
}

struct Player { hp: i32 }
impl Dañable for Player {
    fn recibir_daño(&mut self, cantidad: u32) -> bool {
        self.hp -= cantidad as i32;
        self.hp <= 0
    }
}

fn explosion(objetos: &mut Vec<Box<dyn Dañable>>, dmg: u32) {
    for o in objetos.iter_mut() {
        if o.recibir_daño(dmg) { println!("uno cayó"); }
    }
}

Cuándo sí:

Cuándo no:

En Bevy, el patrón "trait-driven" se traduce casi siempre a "componente + sistema": defines Component, escribes System que opera sobre Query<&T>, y la polimorfía sale gratis por composición.

Lo que vimos

En el siguiente

Cap. 3: qué pasa entre frames. Game loop, delta time, fixed timestep, entidad como número. Sin Bevy todavía: solo pseudocódigo llano, para que la cabeza asimile antes de meterle crates encima.