— Lema no oficial del equipo Bevy, circa 2024.
Llevamos seis capítulos definiendo entidades pieza a pieza, y todo ha funcionado razonablemente bien en los ejemplos pequeños. Pero el día que te sientas a programar tu primer RPG serio, abres Player::spawn y ves algo así:
//! cap-08 — sección 8.1: el spawn vulnerable (versión "antes").
commands.spawn((
Player,
Nombre("Aragorn".into()),
Position { x: 0.0, y: 0.0 },
Velocity { x: 0.0, y: 0.0 },
Health { actual: 100, max: 100 },
Hitbox { radius: 12.0 },
Sprite::default(),
AnimationState::default(),
Inventory::default(),
// ... y faltan tres más que aún no te acuerdas.
));
Y te das cuenta de tres problemas simultáneos:
Player necesita un componente nuevo (digamos Mana), tienes que ir por todos los spawn de tu juego y añadirlo. Si se te olvida uno, el sistema que itera Query<&Mana, With<Player>> simplemente lo ignora silenciosamente, y el bug aparece tres meses después como "el mago a veces no lanza hechizos".Health { actual, max } y Mana { actual, max }, pero nadie garantiza que actual <= max. Hoy, un system hace health.actual += 999 y se queda en 999/100. El componente es un dato, no un contrato.Enemy debe llevar LootTable, tienes que buscar cada Enemy::spawn, Boss::spawn, Minion::spawn... y rezar para no haber dejado ninguno por ahí.La solución obvia es meter un constructor Player::new(commands) que centralice el spawn. Eso funciona, pero deja la responsabilidad al código de usuario. Si en algún punto spawnas un Player con world.spawn_empty().insert(Player), el constructor no se ejecuta y vuelves al problema uno.
Analogía absurda: un required component es como pedir un sándwich en una cafetería. Si pides "jamón y queso", el sándwich llega con pan. No tienes que decir "con pan": el sándwich requiere pan por contrato. Si pides solo "jamón", el dependiente te mira raro y te lo añade igualmente porque la receta exige pan, mostaza y bandeja. Lo mismo pasa con #[require(Velocity)] sobre Position: si spawnas Position, Bevy te añade Velocity automáticamente, sin que lo pidas.
Bevy 0.17 introdujo una mejora que pasó bastante desapercibida fuera del equipo: el orden de inserción pasó de ser breadth-first (hermano antes que hijo) a depth-first (hijo antes que hermano). Si en 0.16 el orden de los componentes derivados era difícil de predecir, en 0.18/0.19 es determinista y respeta la jerarquía de dependencias. Lo verás en acción en la sección 8.3.
#[require(T)] simple y múltipleLa sintaxis canónica es una atributo más sobre el derive:
//! cap-08 — sección 8.2: require simple y múltiple.
use bevy::prelude::*;
#[derive(Component)]
#[require(Position)] // <-- siempre que exista Position, añade Velocity.
struct Velocity {
x: f32,
y: f32,
}
#[derive(Component, Default)]
#[require(Position, Velocity, Health)] // <-- múltiples requires.
struct Player;
¿Notas el matiz? El atributo va en el componente que requiere, no en el que es requerido. La dirección de la flecha es "A necesita que también exista B". Cuando spawneas A, Bevy mira A's requires e inserta B (y recursivamente, los requires de B).
Si el componente derivado no implementa Default, Bevy no puede insertarlo "vacío": necesitas o bien Default, o bien especificar el valor en el propio atributo:
//! cap-08 — sección 8.2: require con datos iniciales.
#[derive(Component)]
#[require(
Velocity { x: 0.0, y: 0.0 }, // <-- valores literales.
Health { actual: 100, max: 100 }, // <-- struct-like con campos.
)]
struct Player;
Esto es especialmente útil cuando el dato por defecto no es "vacío" sino "el estado inicial de un personaje recién creado". Lo verás mucho en componentes como Hitbox { radius: 8.0 } o Lives(u32).
Sintaxis de la forma con expresión. Si quieres reutilizar Default de un componente que no es Default para el required automático, o pasar una función constructora cualquiera, la forma es #[require(Tipo = expr)], donde expr es cualquier expresión que evalúe al valor (sin paréntesis extra):
//! cap-08 — sección 8.2: require con expresión constructora.
// Forma correcta: `Tipo = expr` (sin paréntesis sobre `default`).
#[derive(Component, Default)]
#[require(Living, Inventory = Inventory::default)]
struct Character;
// ❌ NO compila: `Tipo::default()` con paréntesis dentro del atributo.
// #[require(Inventory::default())]
//
// ✅ Equivalente válido:
// #[require(Inventory = Inventory::default)]
Cuidado. Escribir
#[require(Inventory::default())](con paréntesis) no compila: la macro del atributo esperaTipoa secas,Tipo { ... }con literales oTipo = exprcon una expresión sin invocación colgante. Si necesitas invocarDefault, escribeInventory = Inventory::default(function item, sin()).
world.register_required_components::<A, B>()Hay un caso incómodo: cuando tu lib define Player, y una bin quiere que todo Player lleve MyCustomUI (de otro crate). No puedes editar Player para añadir el #[require]. Para eso existe el registro explícito en el App:
//! cap-08 — sección 8.2.1: registro explícito en runtime.
fn main() {
App::new()
.add_plugins(DefaultPlugins)
// A partir de esta línea, cualquier spawn de Player
// garantiza que también exista MyCustomUI.
.register_required_components::<Player, MyCustomUI>()
.add_systems(Startup, spawn_player)
.run();
}
register_required_components se llama típicamente en build de un Plugin. Lo usaremos en el cap. 27 cuando extendamos el juego base con un plugin de "save state" que añade Dirty a todo Player.
PowerUp no debería ser required de Player.Player puede ser Human o Ghost, y el required es distinto en cada caso, no uses requires: usa dos entidades diferentes con Bundle (cap. 24).En 0.16, el orden de inserción de los componentes derivados era breadth-first: primero todos los requires directos, después los requires de los requires. Era intuitivo, pero rompía una invariante útil: el orden de inserción no reflejaba la cadena de dependencias.
A partir de 0.17, el orden es depth-first:
// Pseudocódigo de la regla.
insertar(A):
si A ya está en la entidad: parar.
marcar A como "en curso".
para cada require R de A (en orden de declaración):
insertar(R) // <-- recursión primero.
marcar A como "completo".
poner A en la entidad.
Ejemplo concreto:
//! cap-08 — sección 8.3: cadena de requires depth-first.
#[derive(Component, Default)] struct Position;
#[derive(Component, Default)] struct Velocity;
#[derive(Component, Default)] struct Health { actual: u32, max: u32 }
#[derive(Component, Default)] struct Sprite;
#[derive(Component, Default)] struct AnimationState;
#[derive(Component, Default)]
#[require(Position, Velocity, Sprite)]
struct Character;
#[derive(Component, Default)]
#[require(Character, Health, AnimationState)]
struct Player;
Cuando spawneas Player, Bevy ejecuta las inserciones en este orden:
Position (required por Character, declarado el primero).Character (una vez sus requires están dentro).Health (siguiente required de Player).AnimationState (último required de Player).Sprite (required de Character; se inserta después de Character por depth-first).Velocity (otro required de Character).Player (al final, cuando todo lo demás está dentro).Es importante entender esto por dos razones:
on_insert(Position) actualiza Velocity, el hook ve a Position ya insertada, pero la Velocity aún no. En depth-first eso funciona como esperas; en breadth-first daba sorpresas.Added<T>, Changed<T>) considera "added" a un componente en el frame de su primera inserción. Saber el orden te ayuda a razonar sobre queries como Added<Health>.Truco de depuración. Si quieres ver exactamente qué orden siguió Bevy en un spawn, añade un hook
on_inserttrivial que imprimainfo!("inserted {:?}", std::any::type_name::<T>()). Lo verás enlog.
Los Relationship y RelationshipTarget (cap. 11) también aceptan #[require]. Hay un caso de uso típico: ChildOf como required de Enemy para garantizar que todo enemigo tiene un padre (Spawner).
//! cap-08 — sección 8.4: required + relationship.
use bevy::prelude::*;
#[derive(Component, Default)]
struct Spawner; // La entidad que "posee" un conjunto de enemigos.
// Un componente marcador de Enemy puede requerir otros componentes suyos.
// (La relación padre-hijo con un Spawner concreto se modela mejor con
// `ChildOf` insertada en runtime; ver cap. 9B.6 y cap. 11.)
#[derive(Component, Default)]
#[require(Spawner)] // <-- aquí Spawner es solo un componente marcador, NO la arista de grafo.
struct Enemy;
Ojo: la arista de relación ChildOf necesita un Entity válido como padre, algo que no puedes construir en el atributo #[require(...)] (no existe un SpawnerEntity global). Para "todo enemigo cuelga de un spawner", lo correcto es spawnear el enemigo y luego insertarle ChildOf(spawner_entity) en runtime (cap. 11). Required components te da el marcador; la relación te da el grafo. Verifica la combinación exacta #[require] + ChildOf en la versión de Bevy que uses; el soporte de requires sobre relaciones ha evolucionado entre 0.17 y 0.19.
Otro uso habitual en este libro: en el cap. 14 haremos #[require(Camera2d, OrthographicProjection, RenderTarget)] para que toda cámara 2D venga con sus piezas por contrato, sin que el programador tenga que acordarse.
Required component (concepto): componente que Bevy inserta automáticamente al añadir otro que lo declara con `#[require(...)]`.
Default (Rust): trait que indica cómo construir un valor "vacío" o inicial; necesario para que Bevy cree el componente derivado sin datos.
Depth-first ordering (concepto): orden de inserción que sigue la cadena de dependencias hasta el fondo antes de volver; introducido en Bevy 0.17.
Breadth-first ordering (concepto): orden de inserción por niveles; era el comportamiento pre-0.17, ahora legado.
`register_required_components::<A, B>` (API): método de `App` que añade `B` como required de `A` en runtime, útil cuando no puedes editar `A`.
Relationship + ChildOf (concepto): relación jerárquica padre-hijo nativa de Bevy (0.16+). `ChildOf(Entity)` va en el hijo; `Children(Vec<Entity>)` se mantiene automáticamente en el padre. Se inserta en runtime, no suele mezclarse con `#[require]` porque el `Entity` padre no es conocible en tiempo de compilación.
`#[require(...)]` (atributo): atributo de Rust sobre `#[derive(Component)]` que declara dependencias automáticas; acepta tipos o valores literales.
Player y sus amigos invisiblesVamos a cerrar el capítulo con un ejemplo end-to-end. Imagina un RPG 2D con esta jerarquía:
Player
├─ Nameplate (UI flotante)
├─ Inventory (slots, peso)
└─ StatusEffectBundle (buffs/debuffs activos)
Si la implementas "a mano" en cada spawn, te olvidarás de algo. Versión con requires:
//! cap-08 — sección 8.5: Player compuesto con requires.
use bevy::prelude::*;
// --- Componentes hoja (sin requires propios salvo trivial) -----------
#[derive(Component, Default)]
struct Position { x: f32, y: f32 }
#[derive(Component, Default)]
struct Velocity { x: f32, y: f32 }
#[derive(Component, Default)]
struct Sprite; // Marcamos el componente como existe; lo gestionaremos en cap. 14.
#[derive(Component, Default)]
struct Health {
actual: u32,
max: u32,
}
// --- Paquetes semánticos ---------------------------------------------
#[derive(Component, Default)]
#[require(Position, Velocity, Sprite)]
struct Movable; // Cualquier cosa que se mueve por la pantalla.
#[derive(Component, Default)]
#[require(Movable, Health)]
struct Living; // Cualquier cosa con vida.
#[derive(Component, Default)]
#[require(Living, Inventory = Inventory::default)]
struct Character; // Personaje de RPG (player o NPC).
#[derive(Component, Default)]
struct Inventory {
slots: Vec<ItemId>,
peso_actual: u32,
peso_max: u32,
}
#[derive(Copy, Clone)]
struct ItemId(u32);
// --- Player final ----------------------------------------------------
#[derive(Component, Default)]
#[require(Character, Nameplate = Nameplate::default)]
struct Player;
#[derive(Component, Default)]
struct Nameplate {
text: String,
}
Ahora un spawn de Player produce, en orden depth-first:
Position, Velocity, Sprite (requires de Movable).Movable.Health, Inventory (requires de Character).Character.Nameplate (require explícito).Player.Y los sistemas que iteran Query<&Health, With<Character>> o Query<&Velocity, With<Movable>> siempre encuentran datos. No hay forma de olvidarse.
//! cap-08 — sección 8.5: spawn minimal y sistemas felices.
fn spawn_player(mut commands: Commands) {
// Solo spawneamos Player. El resto viene por contrato.
commands.spawn(Player::default());
}
fn loggear_inventarios(
query: Query<(Entity, &Inventory), With<Character>>,
) {
for (e, inv) in &query {
info!("personaje {:?} lleva {} items", e, inv.slots.len());
}
}
fn main() {
App::new()
.add_plugins(MinimalPlugins)
.register_required_components::<Player, MyOptionalModComponent>()
.add_systems(Startup, spawn_player)
.add_systems(Update, loggear_inventarios)
.run();
}
// Componente de un mod hipotético.
#[derive(Component, Default)]
struct MyOptionalModComponent;
❌ Derivar `Component` sin `Default` y usar `#[require(MiComponente)]` sin datos:
#[derive(Component)]
#[require(Velocity)] // <-- Bevy no sabe qué valores poner.
struct Player;
✅ Dos opciones:
1. Añade `Default` a `Velocity` (si tiene sentido el "cero" como estado inicial):
#[derive(Component, Default)]
#[require(Velocity)] struct Player;
2. O especifica el valor literal:
#[derive(Component)]
#[require(Velocity { x: 0.0, y: 0.0 })] struct Player;
💡 Por qué: Bevy, en el momento del spawn, no tiene contexto para inventarse los
datos. O le das un "punto de partida válido" (Default) o le das el dato exacto.
★ Baldur's Gate (BioWare, 1998) — cada compañero del juego tenía un conjunto fijo
de estadísticas, hechizos y equipo inicial. Si moddeabas el juego y olvidabas
inicializar uno de esos campos, el personaje quedaba "muerto en pie" (vivo pero
sin IA). Required components es, en cierto sentido, la versión mecanografiada
de esa promesa: "este personaje SIEMPRE tiene hechizos; si no los tiene, algo
va mal en el spawn".
★ Shovel Knight (Yacht Club Games, 2014) — los enemigos comparten un "esqueleto"
común (vida, colisión, sprite, comportamiento). Si el programador olvidaba
añadir uno, el enemigo era invisible pero seguía ahí, ejecutando IA sobre un
fantasma. Required components previene ese fantasma.
★ Age of Empires II (Ensemble Studios, 1999) — las unidades tenían "componentes
必" como ataque y armadura. El motor Intern explícitamente rechazaba spawnear
una unidad sin ellos, retornando un error al editor.
"Si tu Personaje siempre tiene Posición y Velocidad, que el compilador te lo recuerde, no tu memoria de pez."
— Frase tatuada en la taza del equipo Bevy, 2024.
El problema: tus entidades están compuestas por cinco, ocho, doce componentes. El programador que las crea tiene que recordar el conjunto exacto; si omite uno, los sistemas fallan en silencio (queries vacías) o, peor, en voz alta (panics varios). El refactor de añadir un componente nuevo es un rastreo por toda la base de código.
La solución: define componentes-paquete semánticos que agrupan sus dependencias con #[require(...)]. Los componentes hoja son Default (o aceptan datos en el atributo); los paquetes los incluyen; los específicos del juego (Player, Enemy, Boss) requieren el paquete más un par de retoques propios.
// ❌ MAL: el "spawn vulnerable". Un día te olvidas de Health y el mago
// se queda sin maná para siempre. Sin error de compilación. Sin warning.
// Solo silencio y tres meses de "¿por qué el hechizo no sale?".
commands.spawn((
Player,
Position::default(),
Velocity::default(),
Health::default(),
Mana::default(), // <-- algún día esto falta.
Sprite::default(),
));
// ✅ BIEN: el contrato lo carga el compilador. Si spawneas Player, Bevy
// garantiza Position, Velocity, Health, Mana, Sprite. Sin excusas.
#[derive(Component, Default)]
#[require(Position, Velocity, Health, Mana, Sprite)]
struct Player;
// Plantilla reusable para componentes-paquete.
#[derive(Component, Default)]
#[require(A, B, C)]
struct Paquete;
#[derive(Component, Default)]
#[require(Paquete, Extra)]
struct EntidadDeNegocio;
Cuándo sí.
Health, Mana, Faction) esté presente en todas las entidades de un tipo.Plugin que añade un required extra al vuelo: app.register_required_components::<Enemy, LootTable>().Cuándo no.
ensure con Commands o spawn manual).Bundle con uno de varios sub-bundles).#[require(T)]) convierten la composición de entidades de "elección manual cada vez" a "contrato verificable por el compilador".Default es el camino fácil: si tu componente tiene un estado inicial "vacío" razonable, derivar Default y olvidarte. Si no, pasar un valor literal en el atributo (#[require(Velocity { x: 0.0, y: 0.0 })]).register_required_components::<A, B>() en el App permite añadir un required en runtime, útil cuando defines Player en una lib y un mod quiere garantizar su propio componente.on_insert) y la detección de cambios (Added<T>).ChildOf se insertan en runtime con un Entity padre concreto (cap. 9B.6 y cap. 11); no son el caso típico de #[require], que no puede construir un Entity válido en tiempo de compilación.Default y no poner valor literal. Bevy no va a inventarse los datos por ti.En el cap 9 subimos un peldaño: los component hooks (on_add, on_insert, on_discard, on_remove) te dejan no solo garantizar la presencia, sino reaccionar a la inserción, descarte y remoción para mantener invariantes automáticamente. Imagina: cada vez que un Player se inserta, un hook rellena su Health con los puntos iniciales; cada vez que se remueve, suelta el LootTable al mundo. Programación dirigida por eventos, pero a nivel de componente. Agárrate, que esto se pone jugoso.