Capítulo 5. Queries y Resources: pidiendo lo que necesitas

CAP 05 · Bevy 0.18/0.19
"En 1993, id Software publicó Doom. El truco no era solo el motor: era que cada monstruo del juego, cada bala, cada pickup, era un set de datos en arrays planos. John Carmack lo llamaba 'cache coherence': poner los datos juntos para que la CPU no se fuera a por café cada vez que necesitaba un enemigo. Casi nadie en la industria le hizo caso. Hoy, treinta años después, todo juego triple-A usa ese mismo truco. Solo que ahora se llama ECS, y lo lleva puesto incluso la consola de tu salón."

En el capítulo 4 vimos el trío entities-components-systems y conseguimos mover un par de cubos. Bien. Ahora viene la parte que separa a quien "usa Bevy" de quien "piensa en Bevy": las queries (los binoculares que filtran el mundo) y los resources (los datos globales del juego). Si los components son los papelitos pegados a los comensales, las queries son los gritos del mesonero, y los resources son la pizarra detrás de la barra.

5.1 Anatomía de una query: binoculares con filtro

Una query es un parámetro de system que dice "dame todas las entities que tengan estos components y, opcionalmente, cumplan estas condiciones". La forma más simple es:

fn listar_todo(query: Query<&Transform>) {
    for transform in &query {
        println!("Posición: {:?}", transform.translation);
    }
}

Eso le pide a Bevy: "todas las entities que tengan &Transform, dámelas". Bevy recorre su almacén, encuentra las que cumplen, y te las entrega en un iterador. Cero magia, cero sorpresas.

Pero claro, pedir "todo" es como pedir en un restaurante "toda la comida del menú". Poco útil. Lo normal es filtrar: "dame los Transform de las entities que tengan un Player". Y para eso usamos los filtros de Bevy.

Filtro With<T> y Without<T>

Los dos filtros más básicos. With<T> significa "que tenga este component". Without<T> significa "que NO lo tenga". Se ponen como segundo parámetro de tipo del Query:

// Solo entities que tengan el component Player.
fn mover_jugador(mut query: Query<&mut Transform, With<Player>>) {
    for mut transform in &mut query {
        transform.translation.x += 1.0;
    }
}

// Solo entities que NO tengan Enemy.
fn mover_no_enemigos(mut query: Query<&mut Transform, Without<Enemy>>) {
    for mut transform in &mut query {
        transform.translation.x += 1.0;
    }
}

// Las dos a la vez: que tengan Player Y NO tengan Enemy.
fn mover_players_puros(
    mut query: Query<&mut Transform, With<Player>, Without<Enemy>>,
) {
    for mut transform in &mut query {
        transform.translation.x += 1.0;
    }
}
- **`With<T>`**: filtro que exige la presencia de un component. Como un portero que dice "solo dejo pasar a quien lleve esta acreditación".
- **`Without<T>`**: filtro que prohíbe un component. "Solo dejo pasar a quien NO lleve esta otra".
- **`Added<T>`**: filtro que solo incluye entities en las que `T` se haya añadido *este frame*. Útil para "cosas nuevas".
- **`Changed<T>`**: filtro que solo incluye entities en las que `T` se haya modificado *este frame*. Útil para "cosas que han cambiado".

Filtro Added<T> y Changed<T>: lo nuevo y lo modificado

Estos dos son los más sutiles y los más potentes. Added<T> significa "este component se ha añadido a la entity en este frame". Changed<T> significa "este component se ha modificado en este frame (incluido Added)".

¿Para qué sirven? Imagina que quieres reaccionar solo cuando algo cambia, no cada frame. Por ejemplo: "cuando un enemigo entre en modo alerta, reproduce un sonido". O "cuando un player coja un item nuevo, actualiza el HUD".

fn reaccionar_a_nuevos_enemigos(
    query: Query<Entity, Added<Enemy>>,
    mut commands: Commands,
) {
    for entity in &query {
        println!("¡Enemy nuevo aparecido: {:?}!", entity);
        // Aquí podrías spawnear partículas, tocar sonido, etc.
        commands.entity(entity).insert(Alerta(true));
    }
}

Changed<T> es parecido pero más amplio: incluye cualquier entity cuyo T haya sido tocado este frame, sea por add, insert o set. Se usa muchísimo en combinación con &mut T:

fn loggear_cambios_de_vida(query: Query<&Health, Changed<Health>>) {
    for health in &query {
        println!("Vida cambió a {}", health.actual);
    }
}

Truco práctico: si quieres "Added + Changed" (cualquier modificación) y nada más, usa Changed<T>. Si quieres "solo lo recién spawneado", usa Added<T>. Si quieres "todo, siempre", no pongas filtro.

- **1993** — *Doom* (id Software, EE.UU.). John Carmack diseñó el motor en C usando "thing_t" structs con flags de bits para indicar tipo de monstruo, tamaño, reacción al sonido. Cada filtro de `With<Enemy>` de Bevy es, en espíritu, un `if (thing->flags & MF_ENEMY)`.
- **2015** — *Overwatch* (Blizzard, EE.UU.). El equipo publicó una charla legendaria sobre cómo reescribieron su ECS para que los filtros de tipo "Soldado aliado sin ultimate" se resolvieran en arrays SIMD. El "QuerySet" de Bevy viene de la misma idea: vista multidimensional sobre los mismos datos.

5.2 Change detection en serio: ticks, Added, Changed y Ref

Hasta aquí hemos mencionado Added<T> y Changed<T> como "el filtro de lo nuevo y de lo cambiado", pero se merecen una sección propia, porque son de las herramientas más potentes —y más fáciles de usar mal— de todo Bevy. Si vienes de Unity, esto es tu "OnPropertyChanged" sin boileplate. Si vienes de React, piensa en ello como useMemo: solo recalculas cuando el input cambió.

Cómo funciona por dentro: los ticks de cambio

Bevy no guarda un booleano "este componente cambió". Guarda, para cada componente de cada entity, dos enteros llamados ticks:

Cada vez que tu sistema se ejecuta, Bevy compara esos ticks con dos referencias globales: this_run (el tick del arranque del system en este frame) y last_run (el tick del arranque anterior de ESTE system, no del frame). Las reglas son:

Consecuencia importante: los ticks son por system. Si el sistema A pidió &mut Transform, solo marca como cambiado lo que ese mismo frame verán los sistemas que corran después de A. Si otro sistema B lee Changed<Transform> pero corre antes que A en el mismo frame, no verá el cambio hasta el siguiente frame. El orden del schedule (Cap. 7) importa aquí más de lo que parece.

Ref<T>: la lectura que detecta cambios

A veces no quieres filtrar por cambio, sino "leer y, además, saber si lo que leí cambió". Ahí entra Ref<T>. Es como &T pero envuelto en un tipo que recuerda si el componente cambió este frame:

fn depurar_transforms(query: Query<Ref<Transform>>) {
    for transform in &query {
        if transform.is_changed() {
            println!("La entity se movió a {:?}", transform.translation);
        }
    }
}

El método .is_added() también existe y te dice si el componente fue recién insertado. Útil para "inicialización perezosa": haces la query normal, y dentro del bucle distingas entre "acaba de aparecer" y "ya existía".

Anti-patrón: ensuciar el detector

El error clásico de change detection es pedir &mut T en un sistema que "solo lee" —por ejemplo, porque copy-pasteaste la firma—. En cuanto pides &mut, Bevy marca el tick de cambio tan pronto como accedes, aunque no escribas nada. Resultado: todos los sistemas que corran después y usen Changed<T> lo verán como "cambió" cada frame, y tu optimización se va al garete. Si solo lees, pide &T o Ref<T>. Nunca &mut "por si acaso".

**Metedura de pata**: un sistema "depurador" que pide `Query<&mut Transform>` solo para imprimir el valor. Como ha pedido `&mut`, todos los `Transform` se marcan como cambiados cada frame. Otro sistema que use `Changed<Transform>` para "solo reaccionar cuando algo se mueve" pasa a ejecutarse en TODAS las entities TODOS los frames. Tu "optimización" acaba siendo un bucle O(N) disfrazado de event-driven. Pedid `Ref<T>` o `&T` para solo-lectura.

5.2bis Filtros avanzados: Or, combinaciones y cuándo liarlas

Los filtros que vimos (With, Without, Added, Changed) son como && implícitos: todos deben cumplirse a la vez. ¿Y si quieres "esto O aquello"? Para eso está Or<(...)>, un filtro que envuelve una tupla de filtros y se cumple si cualquiera de ellos se cumple:

// Entities que acaban de spawnear (Added<Enemy>)
// O cuya vida cambió (Changed<Vida>). Útil para "refresca el HUD".
fn refrescar_barra_de_vida(
    query: Query<&Vida, Or<(Added<Enemy>, Changed<Vida>)>>,
) {
    for vida in &query {
        // Solo se ejecuta si la entity cumple uno de los dos criterios.
        println!("Refresco el HUD de {:?} con vida {}", vida, vida.actual);
    }
}

Combinar Or con filtros normales funciona como esperarías: cada filtro de la tupla externa (los que van fuera del Or) se AND-an con el Or:

// Jugadores Y enemigos, pero que no estén muertos.
// Equivale a: (With<Player> OR With<Enemy>) AND Without<Muerto>
fn listar_combatientes(
    query: Query<Entity, (Or<(With<Player>, With<Enemy>)>, Without<Muerto>)>,
) {
    for e in &query {
        println!("Combate: {:?}", e);
    }
}

Dos detalles finos:

5.2ter Pedir varios components a la vez: tuplas y filtros múltiples

Hasta ahora hemos pedido un component y filtrado por otro. Pero lo más común es pedir varios components simultáneamente y hacer algo con todos juntos. Para eso, el primer parámetro de tipo del Query puede ser una tupla:

fn mover_y_girar(
    mut query: Query<(&mut Transform, &Velocidad)>,
    time: Res<Time>,
) {
    let dt = time.delta_secs();
    for (mut transform, vel) in &mut query {
        transform.translation.x += vel.x * dt;
        transform.translation.y += vel.y * dt;
        transform.rotate_z(vel.giro * dt);
    }
}

Cada entity del iterador tiene un &mut Transform y un &Velocidad. Bevy se encarga de que ambos sean accesibles al mismo tiempo (es una de las maravillas del borrow-checker de Bevy: entiende qué components pides, valida que no se pisen, y aborte el system si lo hacen).

¿Y si quiero pedir tres? Una tupla de tres:

Query<(&mut Transform, &Velocidad, &Player)>

¿Y si quiero pedir un &mut y un &? Mezclando:

Query<(&mut Health, &Player)>

¿Y si quiero pedir un component opcional (que esté o no esté)? Option<T>:

Query<(&mut Transform, Option<&Velocidad>)>

Para cada entity, Bevy te da Some(velocidad) si lo tiene, o None si no. Perfecto para el clásico "si tiene velocidad, aplicarla; si no, quedarse quieto".

**La metedura de pata del día**: pedir `&mut Transform` y `&mut Transform` en la misma query. Sí, suena absurdo, pero cuando refactorizas un system y de repente tienes dos parámetros con `&mut`, no te das cuenta. Bevy, generoso, te compila. Y al ejecutar, te explota en la cara con un error de "conflicting mutable borrow". Lección: si ves que tu query tiene el mismo component mutable dos veces, casi siempre puedes fusionar las dos en una sola tupla.

5.3 QuerySet y ParamSet: cuando quieres DOS vistas del mismo mundo

Imagina este system (aparentemente inocente):

fn imposible(query: Query<&mut Transform>, otra: Query<&mut Transform>) {
    for a in &query {
        for b in &otra {
            // Quiero leer `a` y `b` al mismo tiempo.
            // Pero Bevy dice: "no puedo, los dos son &mut Transform".
        }
    }
}

Bevy no te deja. Y tiene razón: si pides dos &mut Transform, no puede garantizar que no apunten a la misma entity. Solución: QuerySet/ParamSet.

Un ParamSet te da "slots" de parámetros, y solo puedes tener un slot en uso a la vez. Es como un "interruptor" para parámetros en conflicto:

use bevy::ecs::system::ParamSet;

fn comparar_posiciones(
    mut q: ParamSet<(
        Query<&Transform, With<Player>>,
        Query<&Transform, With<Enemy>>,
    )>,
) {
    // Accedes a cada Query por separado, y solo una a la vez.
    let player_pos = q.p0().single().translation;
    for enemy in q.p1().iter() {
        let d = (enemy.translation - player_pos).length();
        println!("Distancia al enemy: {d}");
    }
}

ParamSet se reserva los permisos a nivel global, y te garantiza que solo un slot está vivo en cada momento. Es feo de leer, pero a veces es la única forma. Úsalo poco: en la mayoría de casos, separar la lógica en dos systems es más limpio.

En Bevy 0.18/0.19, ParamSet es la API preferida; QuerySet quedó deprecated. Si encuentras tutoriales viejos que dicen QuerySet, sabes que son pre-0.15. Ignóralos.

5.4 Entity y la pregunta "¿cuál es?"

A veces un system necesita no solo los components, sino también el Entity (el id) de cada resultado. Para eso, el primer elemento de la tupla puede ser Entity:

fn matar_a_todo_lo_que_tenga_bandage(
    query: Query<(Entity, &Health)>,
    mut commands: Commands,
) {
    for (entity, health) in &query {
        if health.actual <= 0 {
            commands.entity(entity).despawn();
        }
    }
}

Entity no es un component: es el id puro y duro de la entity. Te sirve para commands.entity(e), para commands.get_entity(e), para pasarlo como argumento, para guardarlo en otra struct, etc.

5.5 Resources: la pizarra del restaurante

Ya los vimos en el cap 4. Ahora vamos en serio. Un resource es un dato global del world. No pertenece a una entity concreta; pertenece al juego entero. Piensa en él como la pizarra que hay detrás de la barra del bar: el "PUNTUACIÓN: 1240", el "TURNO: 3", el "MÚSICA ACTUAL: boss_theme.ogg".

Se define con #[derive(Resource)] (o se inserta cualquier struct que implemente Resource manualmente):

use bevy::prelude::*;

#[derive(Resource, Default)]
struct PuntuacionGlobal {
    puntos: u32,
    record: u32,
}

#[derive(Resource)]
struct ConfiguracionJuego {
    volumen: f32,
    musica: bool,
    efectos: bool,
}

Se inserta una vez (en Startup o donde sea) y se accede desde cualquier system:

fn setup(mut commands: Commands) {
    commands.insert_resource(PuntuacionGlobal::default());
    commands.insert_resource(ConfiguracionJuego {
        volumen: 0.8,
        musica: true,
        efectos: true,
    });
}

fn subir_puntuacion(mut puntos: ResMut<PuntuacionGlobal>) {
    puntos.puntos += 10;
}

fn mostrar_volumen(config: Res<ConfiguracionJuego>) {
    println!("Volumen: {}", config.volumen);
}

Res<T> vs ResMut<T>

fn sistema_a(mut a: ResMut<PuntuacionGlobal>) { a.puntos += 1; }
fn sistema_b(mut a: ResMut<PuntuacionGlobal>) { a.puntos *= 2; }

// Si los dos están en Update sin orden, Bevy entra en pánico:
// "mutable borrow conflict".

La forma de evitarlo es ordenarlos con .before() / .after() (lo vemos en el cap 7), o dejar que uno solo sea mutable y el otro solo lea.

Resources built-in: Time, AssetServer, Window...

Bevy trae un montón de resources ya insertados. Los más importantes:

Y todos se piden en la firma del system como cualquier otro resource.

- **`Res<T>`**: handle inmutable a un resource. Solo lectura. Se puede compartir entre systems sin conflicto.
- **`ResMut<T>`**: handle mutable a un resource. Lectura y escritura. Solo un system a la vez puede tener un `ResMut<T>` activo, salvo que uses orden explícito.
- **`Local<T>`**: dato "de bolsillo" del system. Persiste entre frames, pero solo para ESE system. No es global. Se inicializa con `FromWorld` o con un closure `default`.
- **`FromWorld`**: trait que te permite inicializar un resource a partir del `&World` (por ejemplo, pidiendo otros resources).

5.6 FromWorld: inicialización que necesita otros datos

A veces quieres inicializar un resource y para ello necesitas leer otros resources o entities. Por ejemplo, un resource "Puntuación más alta" que se inicializa con la puntuación guardada en disco. O un "Atlas de sprites" que se construye pidiendo el AssetServer.

El trait FromWorld es para eso. Lo implementas y Bevy lo llama automáticamente cuando hace falta:

use bevy::prelude::*;

struct MejorPuntuacion {
    valor: u32,
}

impl FromWorld for MejorPuntuacion {
    fn from_world(world: &mut World) -> Self {
        // Podrías leer de disco, de otro resource, etc.
        let cargada_de_disco = leer_de_disco().unwrap_or(0);
        Self { valor: cargada_de_disco }
    }
}

fn leer_de_disco() -> Option<u32> { /* ... */ None }

Bevy llamará a from_world la primera vez que pidas el resource con Res<MejorPuntuacion> o ResMut<MejorPuntuacion>. Es un patrón limpio, mucho más limpio que inicializar el resource "a mano" en Startup.

5.7 El patrón "resource para config, component para estado de instancia"

Aquí viene la regla de oro de la separación de responsabilidades. Memorízala, tatúatela, ponla en un pósit:

Patrón del capítulo: "Resource para configuración, Component para estado de instancia"

>

Regla: si un dato es el mismo para todas las entities (o para el juego entero), va como resource. Si un dato es distinto para cada entity, va como component.

>

Ejemplos:

- ConfiguracionJuego { volumen: 0.8 } → resource (un solo volumen para todo el juego).

- Vida { actual: 50, max: 100 } → component (cada entity tiene su vida).

- Dificultad { nivel: 3 } → resource (un solo ajuste global).

- Patrulla { origen: (x, y), radio: 100.0 } → component (cada enemigo tiene su patrulla).

- MapaDelNivel → resource (un solo mapa a la vez).

- InventarioDelPlayer { items: [...] } → component (solo el player lo tiene).

>

Antipatrón: meter "volumen global" como component en una entity fantasma GameSettingsEntity y luego hacer Query<&Volumen, With<GameSettingsTag>>. Funciona, pero rompe la simetría y obliga a queries raras. Mejor: commands.insert_resource(Volumen(0.8)).

>

Antipatrón 2: meter InventarioDelPlayer como resource cuando hay 4 players en pantalla con inventarios distintos. Imposible: solo hay un InventarioDelPlayer resource, y los 4 players se lo pelearían a mordiscos. Va como component en cada player.

La regla es tan simple que parece tonta. Pero créeme: en un juego mediano, te la vas a saltar cinco veces y vas a refactorizar cinco veces. Mejor aprenderla antes.

5.8 Local<T>: el bolsillo del system

Hay un tercer tipo de dato "no global, no en entity, no compartido": el que vive dentro de UN system y persiste entre frames. Para eso existe Local<T>.

fn contador_de_frames(mut contador: Local<u32>) {
    *contador += 1;
    if *contador % 60 == 0 {
        println!("Han pasado {} frames", *contador);
    }
}

Local<T> se inicializa con T::default() la primera vez. Si necesitas algo más complejo, implementa FromWorld o usa Local::<MiStruct>::from_world(...).

Local es ideal para: "cuántas veces ha corrido este system", "el último valor que vi", "el delta-time acumulado de los últimos N frames", "el cursor de spawn del próximo enemigo". Cosas chiquititas, locales, de bolsillo.

5.9 Ejemplo completo: el marcador y el player

Para rematar, un ejemplo que une todo: un PuntuacionGlobal (resource), un Player (component), y dos systems (uno que sube puntos al pulsar espacio, otro que muestra el marcador).

use bevy::prelude::*;

#[derive(Resource, Default)]
struct PuntuacionGlobal {
    puntos: u32,
}

#[derive(Component)]
struct Player;

#[derive(Component)]
struct Velocidad(f32);

fn input_y_subir(
    input: Res<ButtonInput<KeyCode>>,
    mut puntos: ResMut<PuntuacionGlobal>,
) {
    if input.just_pressed(KeyCode::Space) {
        puntos.puntos += 10;
    }
}

fn mover_player(
    time: Res<Time>,
    input: Res<ButtonInput<KeyCode>>,
    mut query: Query<&mut Transform, With<Player>>,
) {
    let mut dir = 0.0;
    if input.pressed(KeyCode::KeyA) { dir -= 1.0; }
    if input.pressed(KeyCode::KeyD) { dir += 1.0; }
    for mut t in &mut query {
        t.translation.x += dir * 100.0 * time.delta_secs();
    }
}

fn mostrar_puntos(puntos: Res<PuntuacionGlobal>) {
    if puntos.puntos > 0 && puntos.is_changed() {
        println!("Puntos: {}", puntos.puntos);
    }
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .init_resource::<PuntuacionGlobal>()
        .add_systems(Startup, |mut commands: Commands| {
            commands.spawn((Transform::default(), Player, Velocidad(0.0)));
        })
        .add_systems(Update, (input_y_subir, mover_player, mostrar_puntos))
        .run();
}

Lee despacio. Fíjate en:

5.10 Errores comunes (la sección donde te ahorro horas)

  1. Pedir &mut Resource y &mut Resource del mismo tipo en dos systems sin ordenarlos: Bevy entra en pánico. Solución: uno solo mutable, o .before()/.after().
  2. Dos queries que piden &mut Transform a la vez en el mismo system: aquí sí hay conflicto real. Si escribes fn f(a: Query<&mut Transform>, b: Query<&mut Transform>), Bevy no puede probar que a y b no apuntan a la misma entity, y te lo rechaza en compile time. Ojo: la trampa solo aplica cuando los accesos se solapan. Si los filtros son disjuntos (a: Query<&mut Transform, With<Player>> y b: Query<&mut Transform, With<Enemy>>), Bevy lo permite sin problema, porque ningún id puede cumplir los dos filtros a la vez. Y por supuesto, una sola query con filtro Query<&mut Transform, With<Player>> funciona perfectamente: filtros y mutabilidad combinan sin rozadura. Cuando los filtros NO son disjuntos y necesitas dos vistas, ahí entra ParamSet (Apartado 5.3).
  3. Olvidar #[derive(Resource)]: el compilador te dirá que tu struct no implementa Resource, así que es fácil de pillar.
  4. Asumir que los resources se persisten entre App::new() runs: no. Cada App::new() crea un world nuevo. Si quieres persistencia, guarda en disco.
  5. Usar ResMut<T> cuando solo lees: a veces funciona, pero a veces Bevy detecta que el system no escribe nada y se queja (en versiones recientes). Usa Res<T> para solo-lectura.
- **2007** — *Crysis* (Crytek, Alemania). El motor CryEngine 2 introducía un sistema de "entity attributes" como resource global, y cada entity leía del resource por nombre. La comunidad lo odiaba por lo lento, pero sentó las bases para separar config (resource) de estado (component).
- **2018** — *Bevy 0.1* (Cart, EE.UU.). La primera versión pública. Los resources se llamaban "Resources" desde el día uno, y el patrón "resource-config / component-state" ya estaba en la charla inaugural. Ocho años y sigue siendo la regla de oro.

Lo que vimos

En el siguiente

En el cap 6 nos metemos con los events: el buzón del mundo. Verás EventReader/EventWriter, el doble-buffering, los eventos buffered vs los triggers, y montaremos un ejemplo de DamageEvent/DeathEvent que se parezca sospechosamente a un sistema de combate de RPG. Tráete una espada, que esto se va a poner violento (en el buen sentido).