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.
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.
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".
Added<T> y Changed<T>: lo nuevo y lo modificadoEstos 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.
Added, Changed y RefHasta 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ó.
Bevy no guarda un booleano "este componente cambió". Guarda, para cada componente de cada entity, dos enteros llamados ticks:
added_tick: el momento (un contador interno del world) en el que se insertó el componente.changed_tick: el momento de la última mutación, es decir, cuando alguien pidió &mut T y Bevy consideró que se había escrito (realmente, cuando se accedió mut, aunque luego no cambies el valor).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:
Added<T> incluye la entity si added_tick está entre last_run y this_run. O sea: "el componente se insertó desde la última vez que yo miré".Changed<T> incluye la entity si changed_tick está en ese mismo intervalo. Como changed_tick se actualiza también en el insert, Changed incluye a Added: todo lo recién añadido también cuenta como cambiado.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 cambiosA 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".
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.
Or, combinaciones y cuándo liarlasLos 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:
Or puede tener hasta 16 filtros, pero si te pasas probablemente estés modelando mal el problema (considera un componente Combate tag y filtrela por él).Changed/Added dentro de un Or obligan a Bevy a revisar ticks en cada iteración. Es barato, pero no "gratis como un With". En tight loops con cientos de miles de entities, mide antes de asumir.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.
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.
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.
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>Res<T>: acceso inmutable. Puedes leer, no escribir. Si dos systems piden Res<T> del mismo resource, no hay conflicto.ResMut<T>: acceso mutable. Puedes leer Y escribir. Si dos systems piden ResMut<T> del mismo resource al mismo tiempo, Bevy se queja (a menos que estén en schedules distintos o uses .before() / .after() para ordenarlos).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.
Time, AssetServer, Window...Bevy trae un montón de resources ya insertados. Los más importantes:
Time: el reloj del juego. time.delta_secs() para el delta del frame, time.elapsed_secs() para el tiempo total desde el arranque.AssetServer: para cargar imágenes, sonidos, mallas, fuentes. Viene del plugin de assets.Window: el handle a la ventana. Puedes pedirla con Query<&Window, With<PrimaryWindow>> o como resource.Events<X>: el "buzón" de events (lo vemos en el cap 6).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).
FromWorld: inicialización que necesita otros datosA 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.
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
GameSettingsEntityy luego hacerQuery<&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
InventarioDelPlayercomo resource cuando hay 4 players en pantalla con inventarios distintos. Imposible: solo hay unInventarioDelPlayerresource, 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.
Local<T>: el bolsillo del systemHay 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.
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:
init_resource::<PuntuacionGlobal>() inicializa el resource con Default. Si tuvieras un FromWorld, Bevy lo usaría.input.just_pressed(KeyCode::Space) es true solo el frame en que se pulsa (no mientras se mantiene).puntos.is_changed() te dice si el resource ha cambiado este frame. Útil para "solo imprimir si ha pasado algo".With<Player> para limitar a la entity con ese tag. Aunque solo hay una, es buena práctica.&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().&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 sí 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).#[derive(Resource)]: el compilador te dirá que tu struct no implementa Resource, así que es fácil de pillar.App::new() runs: no. Cada App::new() crea un world nuevo. Si quieres persistencia, guarda en disco.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.
With, Without, Added, Changed) y pueden pedir varios components en una tupla.ParamSet resuelve el conflicto "quiero dos vistas del mismo component" cuando no queda otra.Res<T>/ResMut<T>; FromWorld permite inicialización compleja.Local<T> es el bolsillo del system: persiste entre frames pero no se comparte.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).