— Carter Anderson, en el blog oficial de Bevy (2024), anunciando la macro
bsn!.
Si has llegado hasta aquí desde el capítulo 1, has visto cómo Bevy pasó de un motor experimental a un sistema con ECS, scenes, observers, relations y hooks. Pero faltaba una pieza que todo el mundo pedía a gritos: una forma declarativa y legible de definir escenas. Hasta 0.18, escribir una escena grande era como rellenar el papeleo del médico a mano: muchos commands.spawn(...), muchos with_children, muchos insert. La macro bsn! (Bevy Scene Notation, pronunciado "beeson") cambia eso para siempre.
Piensa en este capítulo como el día que le dieron a Bevy un diccionario ilustrado en vez de un manual de instrucciones. Misma información, otra accesibilidad.
Para apreciar bsn!, primero hay que sufrir con lo que había. Supongamos que quieres una escena de "casa" con:
House y Transform.Con commands.spawn a la antigua:
use bevy::prelude::*;
#[derive(Component)] struct House;
#[derive(Component)] struct Mailbox;
#[derive(Component)] struct Door { locked: bool }
#[derive(Component)] struct Lock;
fn spawn_house(mut commands: Commands) {
commands
.spawn((
Name::new("House"),
House,
Transform::from_xyz(0.0, 0.0, 0.0),
))
.with_children(|parent| {
parent.spawn((Name::new("Mailbox"), Mailbox, Transform::from_xyz(2.0, 0.0, 0.0)));
parent
.spawn((
Name::new("Door"),
Door { locked: true },
Transform::from_xyz(0.0, 0.0, 1.0),
))
.with_children(|door| {
door.spawn((Name::new("Lock"), Lock));
});
});
}
Cuenta las líneas. Cuenta los with_children anidados. Cuenta los Transform::from_xyz que vas a tener que escribir para 50 objetos. Es repetitivo y propenso a errores. Olvidas un paréntesis y media casa flota en el éter.
DynamicScene + .scn (archivo binario) es más compacto en disco pero requiere un loader y un round-trip a través de serialización. No es rápido para prototipar.
bsn! aparece como respuesta: una macro que te deja escribir la estructura como un literal estructurado, directamente en código Rust.
**bsn! (Bevy Scene Notation)** — macro procedural de Bevy 0.19 que produce un `DynamicScene` (o un bundle para `spawn`) a partir de una sintaxis declarativa inspirada en JSON. Soporta hierarchies, required components, observers inline y componentes reflejados (Reflect).
La forma más simple:
use bevy::prelude::*;
use bevy::scene::bsn;
let scene = bsn! {
Name("Casa") : {
House,
Transform::from_xyz(0.0, 0.0, 0.0),
Mailbox : {
Name: "Buzón",
Transform::from_xyz(2.0, 0.0, 0.0),
},
Door(locked: true) : {
Transform::from_xyz(0.0, 0.0, 1.0),
Lock,
},
}
};
Compilando esto obtienes un valor que se puede pasar a DynamicSceneBuilder o usar como bundle. La sintaxis es casi autoexplicativa: Component(value) para un componente con datos, Component solo para tag, y : seguido de { } para abrir children.
**Trivia** — El nombre `bsn` es un guiño a JSON (JavaScript Object Notation), pero con la B de Bevy. Carter Anderson lo bautizó así en una reunión del equipo core el 14 de junio de 2024, según el commit que introdujo la macro. Lo pronuncian como "beeson" en Discord.
Una expresión bsn! tiene tres bloques principales:
Reflect o Component.#[require]), bsn! los añade automáticamente a menos que los listes tú.{ ... } después de : define las entidades hijas, propagando el Parent.use bevy::prelude::*;
#[derive(Component, Reflect, Default)]
#[reflect(Component)]
struct House;
#[derive(Component, Reflect)]
#[reflect(Component)]
#[require(MailboxFlag)] // <-- required component
struct Mailbox;
#[derive(Component, Reflect, Default)]
#[reflect(Component)]
struct MailboxFlag;
Al usar bsn! con Mailbox, el MailboxFlag se inserta solo. Puedes sobreescribirlo explícitamente si quieres.
| Forma | Significado |
|---|---|
Name | Tag component sin datos |
Name("texto") | Componente con un solo campo (azucarado) |
Name(value1, value2) | Tupla de campos en orden |
Transform::from_xyz(...) | Llamada a constructor |
Color::srgb(0.5, 0.5, 0.5) | Cualquier expresión evaluada en scope |
42_i32 | Literal (interpretado como Reflect-friendly) |
Component : { ... } | Abre children de esta entidad |
**Metedura de pata** — Asumir que `bsn!` te deja llamar funciones externas con lógica compleja. Solo acepta expresiones `const` o que el compilador pueda resolver. Para computación pesada, construye un `DynamicScene` manualmente o usa `commands.spawn`.
Vamos a por el ejemplo jugoso: un pueblo con tres casas, un pozo, un árbol y un cartel. Antes y después.
Antes (sin bsn!): ~80 líneas de commands.spawn.
Después (con bsn!):
use bevy::prelude::*;
use bevy::scene::bsn;
#[derive(Component, Reflect, Default)] #[reflect(Component)] struct House { owner: &'static str }
#[derive(Component, Reflect, Default)] #[reflect(Component)] struct Well;
#[derive(Component, Reflect, Default)] #[reflect(Component)] struct Tree;
#[derive(Component, Reflect, Default)] #[reflect(Component)] struct Sign { text: &'static str }
#[derive(Component, Reflect, Default)] #[reflect(Component)] struct Roof;
let pueblo = bsn! {
Name: "Pueblo de Sprout"
: {
House(owner: "Aldara") : {
Transform::from_xyz(-4.0, 0.0, 0.0),
Roof : { Transform::from_xyz(0.0, 2.0, 0.0) },
},
House(owner: "Bram") : {
Transform::from_xyz(0.0, 0.0, 0.0),
Roof : { Transform::from_xyz(0.0, 2.0, 0.0) },
},
House(owner: "Cyra") : {
Transform::from_xyz(4.0, 0.0, 0.0),
Roof : { Transform::from_xyz(0.0, 2.0, 0.0) },
},
Well : { Transform::from_xyz(2.0, 0.0, -3.0) },
Tree : { Transform::from_xyz(-3.0, 0.0, -4.0) },
Sign(text: "Bienvenidos a Sprout") : {
Transform::from_xyz(0.0, 0.0, 5.0),
},
}
};
Compacto, jerárquico, legible. Y lo mejor: cuando lo compiles y mires el AST, Bevy detectará errores tipográficos en componentes que no existen, gracias a #[reflect(Component)].
**Reflect + Component** — el atributo `#[reflect(Component)]` registra el tipo en el `TypeRegistry`, permitiendo que `bsn!` lo reconozca y valide. Sin él, `bsn!` no sabe qué componentes existen y los rechaza en tiempo de compilación (o runtime, según el modo).
0.19 permite definir observers dentro de bsn! para que se adjunten automáticamente a la entidad:
use bevy::prelude::*;
use bevy::scene::bsn;
#[derive(Component, Reflect, Default)] #[reflect(Component)] struct Door;
#[derive(Event, Reflect, Default)] #[reflect(Event)] struct DoorOpened;
let casa_con_sensor = bsn! {
House : {
Door : {
// observer inline: cuando se dispare DoorOpened, loguea
observe(|trigger: Trigger<DoorOpened>, names: Query<&Name>| {
info!("Puerta {:?} abierta", trigger.entity());
}),
},
}
};
Esa sintaxis exacta puede variar entre 0.19.0 y 0.19.x porque la macro todavía está en estabilización. Lo importante es la intención: el observer vive al lado de la entidad que escucha, no en un sistema distante.
**Trivia** — La sintaxis `observe(|...| { ... })` se inspiró en las **"trait aliases"** que el equipo de Rust experimentó en 2018 y descartó. El RFC 1733 (sinmerged) proponía bloques similares. La idea resucitó en Bevy porque los observers son closures con estado implícito.
bsn! vs DynamicScene vs SceneBundleLas tres vías de definir escenas:
| Método | Pros | Contras | Cuándo usarlo |
|---|---|---|---|
commands.spawn | Tipado fuerte, runtime | Verboso, mucha boilerplate | Lógica generada dinámicamente |
DynamicScene + .scn | Binario compacto, asset-loadable | Reflection en runtime, sin tipos | Datos de diseñadores, save files |
bsn! | Declarativo, checked, jerárquico | Solo en código, no asset todavía | Prototipado rápido, prefabs en código |
SceneBundle (loader) | Carga .scn desde disco | Asset pipeline | Producción con assets |
bsn! no reemplaza a DynamicScene: lo envuelve. Internamente, expande a una estructura que DynamicSceneBuilder puede serializar. La diferencia es la ergonomía en el código fuente.
**Metedura de pata** — Creer que `bsn!` genera un asset `.scn` que puedes cargar con `AssetServer`. **No lo hace en 0.19.** Todavía no hay loader oficial para sintaxis `.bsn` en disco. Es code-driven only. Si necesitas un asset, exporta el `DynamicScene` resultante con `DynamicSceneBuilder`.
Repetimos esto porque la confusión es frecuente:
bsn! { ... } en tu código y obtener un bundle o un DynamicScene.let pueblo: Handle<DynamicScene> = asset_server.load("pueblo.bsn");.bsn! y guárdalo con DynamicSceneBuilder::build + SceneSerializer → .scn. Después cárgalo normalmente.use bevy::prelude::*;
use bevy::scene::{bsn, DynamicSceneBuilder};
fn save_bsn_as_asset(world: &mut World, path: &str) {
let scene = bsn! {
House : { Roof }
};
let mut builder = DynamicSceneBuilder::from_world(world);
builder.extract_entity(scene.entity()).unwrap();
let dynamic = builder.build();
let serialized = dynamic.serialize_ron(world.resource::<AppTypeRegistry>()).unwrap();
std::fs::write(path, serialized).unwrap();
}
**DynamicScene** — estructura intermedia de Bevy que describe una escena como una lista de entities + componentes, sin instanciar en el `World`. Permite serializar, transferir entre worlds y aplicar selectivamente.
Cuando algo en bsn! falla, el compilador te lanza mensajes a veces crípticos. Algunos patrones típicos:
Foo does not implement Reflect": te falta #[derive(Reflect)] o #[reflect(Component)].Foo(a: 1) en un struct sin campo público a.**Trivia** — Bevy usa `bevy_reflect` (desarrollado originalmente por **Justin Moll** y **Nikita Pekin** desde 2021) para hacer posible todo esto. Sin reflection, ninguna macro podría validar tipos en tiempo de compilación. Es la pieza invisible que hace que `bsn!` funcione.
bsn_list![]Hasta ahora cada bsn! producía una sola entidad (con sus children). Pero muchas veces quieres spawnear varias entidades sueltas de una vez, sin que sean hijas unas de otras. Para eso existe bsn_list![]:
//! cap-12 — sección 12.9: bsn_list![].
use bevy::prelude::*;
use bevy::scene::bsn;
let pickups_del_nivel = bsn_list![
Moneda : { Transform::from_xyz(2.0, 0.0, 0.0) },
Moneda : { Transform::from_xyz(4.0, 0.0, 0.0) },
Moneda : { Transform::from_xyz(6.0, 0.0, 0.0) },
Pocion : { Transform::from_xyz(10.0, 0.0, 1.0) },
];
Cada elemento de la lista es una entidad independiente que se spawnea al ejecutar el código. El resultado es un array o Vec de entidades; las puedes iterar o pasar a un sistema.
Caso de uso típico: oleadas de enemigos, pickups, decoraciones, efectos. En vez de un bsn! con 50 hijos (que te obliga a tener un padre artificial), una bsn_list![] te da 50 entidades sueltas con sintaxis mínima.
Combinando bsn! y bsn_list![]:
//! cap-12 — sección 12.9: bsn anidado en bsn_list.
let nivel_completo = bsn! {
Name: "Nivel 1"
: {
SpawnPoint : { Transform::from_xyz(0.0, 0.0, 0.0) },
Meta : { Transform::from_xyz(50.0, 0.0, 0.0) },
}
};
// Luego, en otro sistema, spawnea la lista de pickups:
let pickups = bsn_list![
Moneda : { Transform::from_xyz(2.0, 0.0, 0.0) },
Moneda : { Transform::from_xyz(4.0, 0.0, 0.0) },
];
bsn_list![] es especialmente útil para datos generados proceduralmente: el contenido exacto de la lista puede venir de un loop (for i in 0..50 { bsn_list!(...) }) o de un fichero de configuración. La macro solo se queja de la sintaxis, no del origen de los datos.
**Trivia** — `bsn_list![]` se inspiró en las **"array comprehensions"** que muchos lenguajes funcionales (Haskell, F#, Elixir) tienen desde siempre. Rust no las tiene, pero la macro de Bevy las simula con sintaxis familiar. El RFC 4574 sobre array comprehensions (2024) podría traerlas nativas a Rust 2026.
SceneComponent para tus tipos customEl cap 12.3 te mostró que tus componentes necesitan #[reflect(Component)] para aparecer en bsn!. Pero hay un paso más: derivar SceneComponent te garantiza que si tu componente está presente, la escena completa que lo define también lo está. Sin esto, nada impide que alguien spawnee Player::default() sin su bsn! escena asociada.
//! cap-12 — sección 12.10: SceneComponent derive.
use bevy::prelude::*;
use bevy::scene::SceneComponent;
#[derive(Component, Reflect, Default, SceneComponent)]
#[reflect(Component)]
struct Player {
hp: u32,
max_hp: u32,
speed: f32,
}
Ahora, si en tu juego alguien hace world.spawn(Player::default()), Bevy loguea un error en lugar de spawear la entidad vacía. Para spawnear un Player correctamente, hay que pasar por la escena:
//! cap-12 — sección 12.10: spawn correcto via bsn!.
let p = world.spawn_scene(bsn! {
Player(hp: 100, max_hp: 100, speed: 120.0) : {
Transform::default(),
Sprite::default(),
}
});
El método spawn_scene valida que el SceneComponent derive esté presente y aplica la escena completa (Player + Transform + Sprite + sus required components). Si lo intentas spawnear "a mano" con world.spawn(Player::default()), Bevy te grita.
Cuándo usarlo:
Player, Enemy, Boss, LevelGeometry.Cuándo NO usarlo:
Marker, DebugFlag, Dead).Position, Velocity, Health).Comparación rápida:
| Forma de spawnear | Garantías | Cuándo |
|---|---|---|
world.spawn(MyComp::default()) | Solo el componente. Required pueden faltar. | Tags, datos puros |
world.spawn(MyComp + bundle) | Tú recuerdas el bundle. | Componentes con datos, no críticos |
world.spawn_scene(bsn! { MyComp : {...} }) | Componente + escena + required + observers. | Componentes con lógica de inicialización |
El SceneComponent derive es la red de seguridad para "no se te olvide la inicialización". Úsalo en componentes que tienen un setup complejo, y deja los tags y datos puros sin él.
**Trivia** — La idea de `SceneComponent` viene de un patrón llamado **"Always-Initialized Entity"** que los autores de Bevy vieron en los motores de Naughty Dog (presentaciones de GDC 2014-2018). Un personaje en Uncharted nunca se spawnea como "el esqueleto vacío"; se spawnea como "la entidad con todo su setup". `SceneComponent` lo hace explícito en tipos.
Patrón:
bsn!para prototipar mundos en minutos
>
Cuando necesites una escena con jerarquía clara y muchos componentes, escribe un bloque
bsn!antes de tocarcommands.spawn. Te dará:
1. Validación en compilación (vía
Reflect).
2. Estructura visual que cualquier colaborador entiende sin leer Rust.
3. Refactorización segura: renombrar un componente rompe el build, no en runtime.
4. Migración futura trivial a un asset loader cuando 0.20+ lo introduzca.
>
Combínalo con
DynamicSceneBuilder::extract_entitysi necesitas persistir. Para datos generados en runtime (procedural, save files), sigue concommands.spawn.
bsn! para una sola entidad. El overhead sintáctico no compensa..bsn desde disco: todavía no es un formato asset.#[reflect(Component)]: la macro fallará silenciosamente con errores raros.rand() dentro de bsn!: no es const y revienta.bsn!, una macro declarativa que jerarquiza, valida y resume..bsn; todo es code-driven.bsn! para prototipado rápido, exportar a .scn si necesitas persistir.Con bsn! puedes definir un nivel entero en una sola función Rust. Pero todavía hay una asimetría en el lenguaje: a veces quieres que un tipo sea Recurso y a la vez Componente. La respuesta elegante a eso es lo que veremos a continuación.
En el capítulo 13 — Resources-as-components (0.19) veremos el cambio conceptual más grande de 0.19: Resource ahora es un subtrait de Component. Esto unifica los modelos y elimina duplicación, pero trae trade-offs importantes (un tipo no puede ser ambos a la vez). Aprenderemos a migrar código antiguo, cuándo usar uno u otro, y por qué este cambio estaba cantado desde 0.10. Patrón de migración incluido.