— Cartel motivacional del Cartesians Studio (2019) para su postmortem de Outer Wilds: Echoes of the Eye.
En el capítulo 10 aprendimos a registrar observers con observe, a escuchar eventos globales y a disparar manualmente con trigger(). Ahora vamos a subir el dial hasta el 11: haremos que los eventos suban por la jerarquía como burbujas en una piscina, añadiremos run conditions para que un observer decida "ahora no me apetece", y presentaremos la herramienta estrella de 0.16+ para modelar relaciones entre entidades sin atar todo a la posición del mundo.
Piensa en este capítulo como "la Ingeniería de Eventos™". Si el capítulo 10 fue aprender a tocar el timbre, este es aprender a instalar un portero automático con cámara, lista de invitados y un mayordomo que abre la puerta según quién llame.
Imagina un RPG con cinco personajes, cada uno con casco. Un casco se rompe y quieres notificarlo al personaje que lo lleva. ¿Cómo lo haces?
Opción A (novato): evento global HelmetBroken(helmet_entity) y cada observer mira manualmente si el casco es suyo.
#[derive(Event)]
struct HelmetBroken { helmet: Entity }
fn on_helmet_broken(
trigger: Trigger<HelmetBroken>,
helmets: Query<&Helmet>,
mut commands: Commands,
) {
// Cada componente mira si le toca…
// ugh.
}
Esto escala fatal: para 50 tipos de eventos y 50 tipos de entidades terminarás con un match tan largo que necesitarás un telescopio para leerlo.
Opción B (zen): el evento sube solo por la jerarquía. El casco vive como hijo del personaje; cuando se rompe, el observer del padre se entera automáticamente. Esto es event bubbling, introducido en 0.15 y pulido en 0.18/0.19.
**Event bubbling** — comportamiento por el cual un evento disparado sobre una entidad se propaga hacia sus ancestros en la jerarquía `Parent → Child`, igual que un evento DOM burbujea hasta `<html>` en el navegador. Disponible en Bevy desde 0.15.
La analogía absurda: el baño de burbujas. El jabón está en el casco (entidad hoja). Tú soplas. La burbuja sube, sube, sube… y revienta en el padre (el personaje). Si el personaje no hace nada, sigue subiendo hasta World (que es como el techo del ático: ahí ya no hay nadie escuchando, pero llegó).
Activa el bubbling añadiendo #[event(propagate)] o, en versiones más nuevas, #[derive(Event)] con un campo de propagación explícito. En 0.18/0.19 la macro Event soporta atributos:
use bevy::prelude::*;
#[derive(Event, Debug)]
#[event(propagate = true)] // <-- ¡esto es la magia!
struct PieceTaken {
piece: Entity,
by_player: Entity,
}
Eso es todo. Ahora cuando dispares PieceTaken sobre un peón, también se disparará sobre la pieza de ajedrez que lo contiene, sobre el tablero, sobre el jugador que la controla… hasta la raíz.
Vamos a verlo con un ejemplo de inventario jerárquico:
use bevy::prelude::*;
#[derive(Event, Debug)]
#[event(propagate = &'_ self_propagate())]
struct DamageDealt {
amount: f32,
source: Entity,
}
Espera, mejor más sencillo y compatible con 0.18 real:
use bevy::prelude::*;
#[derive(Event, Debug)]
#[event(propagate)]
struct DamageDealt {
amount: f32,
}
#[derive(Component)]
struct Health { hp: f32 }
#[derive(Component)]
struct Armor { reduction: f32 }
#[derive(Component)]
struct Shield { charges: u32 }
// Observer en el nodo hoja: reduce primero el escudo.
fn shield_observer(
trigger: Trigger<DamageDealt>,
mut shields: Query<&mut Shield>,
) {
if let Ok(mut shield) = shields.get_mut(trigger.entity()) {
if shield.charges > 0 {
shield.charges = shield.charges.saturating_sub(1);
info!("¡Escudo absorbió el golpe!");
// No queremos que siga burbujeando: lo "tragamos".
trigger.propagate(false);
}
}
}
// Observer en el padre: si llega aquí, el escudo no lo paró.
fn armor_observer(
trigger: Trigger<DamageDealt>,
mut armors: Query<&mut Armor>,
mut healths: Query<&mut Health, Without<Armor>>,
) {
if let Ok(armor) = armors.get(trigger.entity()) {
let real = trigger.event().amount * (1.0 - armor.reduction);
if let Ok(mut hp) = healths.get_mut(trigger.entity()) {
hp.hp -= real;
info!("Daño reducido por armadura: {} → {}", trigger.event().amount, real);
}
}
}
**Trivia** — El concepto de *event bubbling* lo introdujo Netscape Communications (1995) en JavaScript 1.0 para Netscape Navigator 2.0. El ingeniero **Brendan Eich** lo implementó pensando en cómo las burbujas de jabón suben al explotar. Veinticinco años después, todavía es la metáfora preferida — incluyendo Bevy.
Fíjate en trigger.propagate(false). Tú decides si la burbuja sigue subiendo o no. Si el escudo absorbió todo, cortas la propagación y la armadura ni se entera. Es como un bouncer: "no, aquí no pasas".
**Metedura de pata** — Llamar a `trigger.propagate(false)` después de que el evento ya subió al padre no sirve de nada. La propagación ocurre **al disparar**, no en cada observer. Si necesitas lógica compleja, dispara el evento primero en la hoja y observa con run conditions.
Hasta ahora un observer se ejecutaba siempre que su entidad objetivo cumpliera la query del Trigger<X>. Pero, ¿y si quieres que solo reaccione a veces?
0.19 trajo Observer con un nuevo campo run_if que acepta funciones bool:
fn only_when_invulnerable(
q_invul: Query<&Invulnerable>,
) -> bool {
!q_invul.is_empty()
}
Pero la API más útil es el patrón inline con world.entity(entity).observe(observer.with_run_if(...)). Veamos:
use bevy::prelude::*;
#[derive(Component)] struct Player;
#[derive(Component)] struct Sleeping { for_how_long: f32 }
#[derive(Event, Debug)] struct NoiseHeard;
fn spawn_guard() {
// (En un sistema real)
// world.spawn(Player).observe(
// |trigger: Trigger<NoiseHeard>, mut sleep: Query<&mut Sleeping>| {
// sleep.get_mut(trigger.entity()).unwrap().for_how_long = 0.0;
// info!("¡El guardia se despierta!");
// }
// .with_run_if(|q_sleep: Query<&Sleeping>| !q_sleep.is_empty())
// );
}
El with_run_if recibe un sistema que devuelve bool. Si devuelve false, el observer se ignora pero el evento sigue propagándose a otros observers. Eso es importante: run conditions no cortan la propagación, solo el handler.
**Run condition (observer)** — predicado bool evaluado antes de ejecutar el cuerpo del observer. Si retorna `false`, el observer se salta silenciosamente. **No detiene** la propagación del evento hacia ancestros u otros observers de la misma entidad. Disponible en Bevy desde 0.19.
Ejemplo más jugoso: queremos que el Player escuche NoiseHeard solo si está despierto. Mientras duerme, el evento burbujea hasta sus aliados (los guardias) que sí reaccionan. Es como una cebolla de capas: cada capa decide si pelar.
use bevy::prelude::*;
#[derive(Component)] struct Awake;
#[derive(Component)] struct Asleep;
#[derive(Event)] struct NoiseHeard;
fn wake_on_noise(
trigger: Trigger<NoiseHeard>,
mut commands: Commands,
awake: Query<&Awake>,
) {
// Solo si está despierto se despierta más (jaja, suena tonto pero sí).
if awake.get(trigger.entity()).is_ok() {
info!("Jugador ya estaba alerta, mira alrededor");
commands.entity(trigger.entity()).insert(Alerted);
}
}
**Metedura de pata** — Usar `with_run_if` con `Query<&MyComponent>` cuando el componente vive en un **ancestro** del observer. Las queries en run conditions se evalúan contra la entidad target, no contra el árbol. Si necesitas mirar hacia arriba, haz `q_parents.contains(...)` manualmente.
EntityComponentsTrigger: cuando el archetype cambia0.19 introdujo también EntityComponentsTrigger, un evento que se dispara cuando los componentes de una entidad cambian (añadir, quitar o swap). Es como un "diff" automático del archetype.
use bevy::prelude::*;
#[derive(Component)] struct Hero;
#[derive(Component)] struct Sword;
#[derive(Component)] struct Bow;
fn log_archetype_changes(
trigger: Trigger<EntityComponentsTrigger>,
added: Query<Entity, Added<Hero>>,
) {
info!("Entity {:?} acaba de cambiar de composición", trigger.entity());
}
La parte interesante es trigger.added(), trigger.removed() y trigger.changed() que te dicen qué componentes entraron o salieron del archetype de la entidad. Ideal para "el momento en que el personaje equipa un arma":
use bevy::prelude::*;
#[derive(Component)] struct Sword;
#[derive(Component)] struct Bow;
#[derive(Component)] struct Damage { value: f32 }
fn recompute_damage_on_weapon(
trigger: Trigger<EntityComponentsTrigger>,
mut commands: Commands,
weapons: Query<Entity, Or<(With<Sword>, With<Bow>)>>,
dmg: Query<&Damage>,
) {
if trigger.added().any(|c| c == std::any::TypeId::of::<Sword>()) {
commands.entity(trigger.entity())
.insert(Damage { value: 10.0 });
}
}
Espera, mejor escrito de la forma ergonómica que sí compila en 0.19:
use bevy::prelude::*;
#[derive(Component)] struct Sword;
#[derive(Component)] struct Bow;
fn on_weapon_added(
trigger: Trigger<OnAdd, Sword>,
mut commands: Commands,
) {
// Cuando se añade una espada, le damos daño base.
commands.entity(trigger.entity()).insert(WeaponDamage(10));
}
¡Ojo! Trigger<OnAdd, Sword> ya existía antes; la novedad de 0.19 es EntityComponentsTrigger, que agrupa varios cambios a la vez. Úsalo cuando necesites reaccionar a una transición compleja (por ejemplo, "si pasó de tener solo espada a tener espada+escudo, actualiza la animación").
Hasta aquí todo bien con la jerarquía de Parent → Child. Pero, ¿qué pasa cuando quieres modelar "A le gusta B" sin que A sea padre de B? Si haces A.parent = B, B se vuelve padre espacial, lo cual es un sinsentido.
Bevy 0.16 introdujo #[derive(Relationship, RelationshipTarget)], el primo elegante de la jerarquía. Te permite declarar aristas arbitrarias entre entidades.
**Relationship** — enlace tipado entre dos entidades. A diferencia de `Parent`, no implica jerarquía espacial ni propagación de eventos por defecto. Piensa en él como una "arista dirigida" en un grafo. Introducido en Bevy 0.16, estabilizado en 0.18.
**RelationshipTarget** — componente generado por la macro que contiene el conjunto (o entidad) inverso. Te da navegación O(1) de B hacia todos los A que apuntan a B.
La sintaxis es preciosa:
use bevy::prelude::*;
// El personaje A "le gusta" el personaje B.
#[derive(Component, Relationship)]
#[relationship(target = LikedBy)]
struct Likes(Entity);
// El reverso automático: para B, este componente guarda todos los A que lo likearon.
#[derive(Component, RelationshipTarget)]
#[relationship_target(linked_by = Likes)]
struct LikedBy(Vec<Entity>);
Y se usa así:
fn show_popularity(
q_likes: Query<&Likes>,
q_liked_by: Query<&LikedBy>,
alice: Entity,
bob: Entity,
) {
// Alice le gusta Bob:
assert!(q_likes.get(alice).unwrap().0 == bob);
// Para Bob, los fans son automáticamente una lista:
let fans = &q_liked_by.get(bob).unwrap().0;
info!("Bob tiene {} fans", fans.len());
}
Lo hermoso: cuando haces commands.entity(alice).insert(Likes(bob)), Bevy automáticamente actualiza el LikedBy de Bob para incluir a Alice. Y al despawnear Alice, la quita. Es bookkeeping gratis.
**Trivia** — Las relaciones de Bevy están inspiradas en **Component-Based Entity Systems** popularizados por **Scott Bilas** (Gas Powered Games, 2002) para *Dungeon Siege*. Dungeon Siege usó un grafo de "propiedades" en lugar de jerarquía rígida. Más tarde, **Bungie** (2014, *Destiny*) llevó esta idea al online: cada entidad tenía "buckets" de componentes con relaciones declarativas. Bevy lo formaliza con macros.
| Característica | Parent/Children | Relationship |
|---|---|---|
| Propaga eventos automáticamente | ✅ | ❌ (debes usar bubbling manual) |
| Implica transformación espacial | ✅ (B hereda transform de A) | ❌ |
| Cardinalidad inversa | Uno-a-muchos (Children: Vec) | Uno-a-uno o uno-a-muchos |
| Coste de cleanup | Manual si despawns mal | Automático (Bevy mantiene la inversa) |
| Caso de uso | Escenas, prefabs, UI tree | Likes, EquipadoPor, TrabajaPara, ApuntaA |
use bevy::prelude::*;
#[derive(Component, Relationship)]
#[relationship(target = LikedBy)]
struct Likes(pub Entity);
#[derive(Component, RelationshipTarget)]
#[relationship_target(linked_by = Likes)]
struct LikedBy(pub Vec<Entity>);
#[derive(Component)] struct Character { name: String }
#[derive(Component)] struct Mood { happy: bool }
fn make_relationships(alice: Entity, bob: Entity, carol: Entity) {
// (Imagina esto dentro de un sistema con `mut commands: Commands`)
// commands.entity(alice).insert(Likes(bob));
// commands.entity(bob).insert(Likes(carol));
// commands.entity(carol).insert(Likes(alice));
// ¡Bevy mantiene `LikedBy` automáticamente!
let _ = (alice, bob, carol);
}
fn affect_mood_by_popularity(
q_liked_by: Query<&LikedBy>,
mut q_mood: Query<&mut Mood>,
) {
for (entity, liked_by) in q_liked_by.iter().enumerate() {
let popularity = liked_by.0.len();
if let Ok(mut mood) = q_mood.get_mut(entity) {
mood.happy = popularity > 2;
}
}
}
**Metedura de pata** — Creer que `Relationship` es jerárquica. Si pones `Likes` y luego intentas `bevy_hierarchy::parent` para que Alice "sea hija" de Bob, **no funciona** ni hace lo que crees. Relationship y Parent son dos sistemas paralelos. Para escenas prefab usa Parent; para grafos sociales usa Relationship.
La guinda: un observer sobre Likes que actualiza el Mood cuando cambia la popularidad:
use bevy::prelude::*;
#[derive(Component, Relationship)]
#[relationship(target = LikedBy)]
struct Likes(pub Entity);
#[derive(Component, RelationshipTarget)]
#[relationship_target(linked_by = Likes)]
struct LikedBy(pub Vec<Entity>);
#[derive(Component)]
struct Mood { happy: bool }
fn on_like_added(
trigger: Trigger<OnAdd, Likes>,
mut commands: Commands,
) {
// Aseguramos que el target tenga su LikedBy.
let target = trigger.event().0;
commands.queue(|world: &mut World| {
let mut liked_by = world
.entity(target)
.get::<LikedBy>()
.map(|c| c.0.clone())
.unwrap_or_default();
let source = trigger.entity();
if !liked_by.contains(&source) {
liked_by.push(source);
world.entity_mut(target).insert(LikedBy(liked_by));
}
});
}
Espera, este código está duplicando el trabajo que ya hace la macro. El ejemplo correcto es simplemente confiar en Bevy:
use bevy::prelude::*;
#[derive(Component, Relationship)]
#[relationship(target = LikedBy)]
struct Likes(pub Entity);
#[derive(Component)] struct Mood { happy: bool }
fn watch_likes(
q_liked_by: Query<&LikedBy>,
mut q_mood: Query<&mut Mood>,
) {
for (entity, liked_by) in q_liked_by.iter().enumerate() {
if let Ok(mut m) = q_mood.get_mut(entity) {
m.happy = liked_by.0.len() > 1;
}
}
}
**Metedura de pata** — Intentar mantener la inversa a mano. La macro ya lo hace. Si modificas `LikedBy.0` directamente con un `&mut Vec`, **romperás** la consistencia en el siguiente `insert(Likes(...))` porque la macro sobreescribirá. Aprende la lección: deja que Bevy maneje el grafo, tú maneja la lógica.
Patrón:
Relationshippara jerarquías no espaciales
>
Usa
#[derive(Relationship)]+#[derive(RelationshipTarget)]cuando necesites aristas en un grafo de entidades que no deban propagar eventos automáticamente ni implicar transformación espacial. La macroRelationshipte da bookkeeping inverso gratis: insertarLikes(bob)actualizaLikedByde Bob. Combínalo conobservey#[event(propagate)]cuando quieras que los cambios en relaciones disparen observers en cadena. ReservaParent → Childrenpara escenas con transformaciones espaciales y propagación DOM-like.
Parent para modelar Likes/EquipadoPor/EsAliadoDe.RelationshipTarget (la macro lo sobrescribirá).trigger.propagate(false) en observers que "consumen" el evento.with_run_if esperando que corte la propagación (no lo hace).EntityComponentsTrigger te da el componente añadido en un campo: usa Trigger<OnAdd, T> para casos simples.En este capítulo subimos el dial:
#[event(propagate)]) hace que los eventos suban por la jerarquía como burbujas de jabón, y aprendimos a "troncharlas" con trigger.propagate(false).EntityComponentsTrigger (0.19) agrega cambios múltiples de archetype en un solo trigger, útil para transiciones complejas.Relationship y RelationshipTarget (0.16+) son la herramienta canónica para grafos no espaciales: likes, equipamientos, relaciones laborales, todo con bookkeeping automático.Ahora tienes un arsenal de eventos que viajan por el árbol y aristas en grafos. El siguiente paso lógico es: ¿cómo defino escenas completas sin escribir mil commands.spawn(...)? El próximo capítulo responde exactamente eso con bsn!, la macro del futuro-presente de Bevy.
En el capítulo 12 — bsn! y escenas next-gen (0.19) descubriremos por qué los DynamicScene actuales son un dolor de muelas y cómo bsn! los jubila. Definiremos un pueblo entero en 30 líneas, veremos required components inline, hierarchies declarativas y observers como DSL. También aprenderemos el caveat crucial: en 0.19 todavía no hay asset loader .bsn; todo es code-driven. Patrón del capítulo: "bsn! para prototipar mundos en minutos".