— Cart (Cartesian Studios) en el Bevy Community Meeting #42 (2024), explicando el RFC que fusionó
ResourceconComponent.
Si has leído capítulos anteriores, recordarás que Bevy tiene dos tipos de almacenamiento global: Resource (singleton del World) y Component (datos por entidad). Parecen primos hermanos: ambos viven en el World, ambos usan #[derive(...)] para anotarse, ambos se acceden con sistemas. La diferencia práctica era... ¿única? ¿una macro? El equipo de Bevy llegó a la misma conclusión que tú: eran el mismo concepto con distinto nombre. 0.19 lo hizo oficial.
Piensa en este capítulo como el momento en que un reality show de cocina junta dos bandejas separadas de galletas en una sola bandeja. Las galletas son las mismas, solo cambia el organizador.
Hasta 0.18, Resource y Component eran dos traits distintos:
// Bevy 0.18
pub trait Resource: 'static + Send + Sync { /* marker */ }
pub trait Component: 'static + Send + Sync { /* marker */ }
La consecuencia práctica:
Resource: una sola instancia por World. Se accede con Res<T> o ResMut<T>.Component: una instancia por entidad. Se accede con Query<&T>.Si querías ambas, escribías dos structs casi idénticos:
#[derive(Resource)]
struct GameSettings { difficulty: f32 }
#[derive(Component)]
struct PerEntitySetting { difficulty: f32 }
Y duplicabas todo: el register_resource y el register_component, el FromReflect, las validaciones, el Debug. Si cambiabas un campo, lo cambiabas en dos sitios.
**Resource (pre-0.19)** — trait marker que marca un tipo como singleton global del `World`. Accesible vía `Res<T>` / `ResMut<T>`.
**Component (pre-0.19)** — trait marker que marca un tipo como dato adjuntable a entidades. Accesible vía `Query<&T>`.
Resource: ComponentEn 0.19, Resource pasó a ser un subtrait de Component:
// Bevy 0.19 (pseudo)
pub trait Component: 'static + Send + Sync {
const STORAGE_TYPE: StorageType = StorageType::Table;
const MUTABILITY: Mutability = Mutability::Mutable;
}
pub trait Resource: Component { /* ahora marca "soy singleton" */ }
La consecuencia inmediata:
Resource exige ser Component, pero con un flag interno que dice "solo una instancia por world".Component (reflect, observers, hooks, relations) también se aplica a Resource.Component sigue siendo el requisito común; Resource es solo un alias conveniente.**Trivia** — La unificación se inspiró en **Flecs** (Sandbox Studios, 2017), un ECS en C donde los "singletons" son simplemente componentes con cardinalidad 1. Flecs añadió `EcsSingleton` en 2019; Bevy tomó nota. La conversación interna entre Cart y **Aleksander Nikitin** sobre Flecs ocurrió en GitHub issue #8421.
use bevy::prelude::*;
#[derive(Component, Resource, Reflect, Debug)]
#[reflect(Component, Resource)]
struct GameSettings {
pub difficulty: f32,
pub music_volume: f32,
}
Una sola declaración, ambos usos:
fn read_settings(res: Res<GameSettings>) {
info!("Dificultad: {}", res.difficulty);
}
// Y si en algún momento decides adjuntarlo a una entidad:
fn attach_settings(mut commands: Commands) {
commands.spawn(GameSettings { difficulty: 1.0, music_volume: 0.8 });
}
ReflectResource se vuelve ReflectComponent (ZST)Antes, registrabas un recurso con:
app.register_resource::<GameSettings>();
Ahora (0.19) solo existe register_component:
app.register_component::<GameSettings>();
ReflectResource sigue existiendo como un ZST (zero-sized type) para retrocompatibilidad, pero su función ahora es marcar que ese componente específico debe comportarse como singleton. Es decir, al registrar un Resource, Bevy internamente configura la cardinalidad a 1.
**ZST (Zero-Sized Type)** — tipo que ocupa 0 bytes en memoria. Se usa como marcador puro. `ReflectResource` en 0.19 es un ZST: su valor no importa, su presencia en el `#[reflect(...)]` activa el modo singleton.
Como Resource: Component, ahora puedes pasar un recurso a un sistema que espera Query<&T> filtrando por una entidad específica. Bevy sabe "este componente vive en el singleton resource":
fn modify_settings(
res: ResMut<GameSettings>,
// Si quisieras tratarlo como componente "real", podrías:
// query: Query<&mut GameSettings, Singleton>,
) {
res.difficulty = (res.difficulty + 0.1).min(2.0);
}
Y al revés, componentes tratados como recursos:
fn log_first_entity(
q: Query<&GameSettings, With<MarkedSingleton>>,
) {
if let Some(s) = q.iter().next() {
info!("Dificultad: {}", s.difficulty);
}
}
Un mismo tipo ya no puede ser simultáneamente Resource y Component обычный (es decir, múltiples instancias en entidades).
El motivo: Bevy necesita saber en compile-time si el almacenamiento es singleton o per-entity. Mezclar ambos en el mismo tipo causaría bugs sutiles (¿a quién modifica ResMut?).
// ❌ Antes era legal (con un poco de hack) — ahora explota:
#[derive(Component, Resource)] // Conflicto en 0.19
struct AmbiguousThing;
El compilador te dirá algo como:
error[E0277]: the trait bound `AmbiguousThing: Resource` is not satisfied
= note: `Resource` requires unique storage, but `Component` allows per-entity
**Metedura de pata** — Intentar derivar ambos traits sin entender el conflicto. Si necesitas "lo mismo" en singleton y en entidad, **declara dos structs distintos** y un trait común:
trait HasDifficulty { fn difficulty(&self) -> f32; }
#[derive(Resource)] struct Settings { difficulty: f32 }
#[derive(Component)] struct LocalSettings { difficulty: f32 }
impl HasDifficulty for Settings { fn difficulty(&self) -> f32 { self.difficulty } }
impl HasDifficulty for LocalSettings { fn difficulty(&self) -> f32 { self.difficulty } }
Si tu proyecto tiene código 0.17/0.18 con register_resource, sigue estos pasos:
[dependencies]
bevy = { version = "0.19", features = ["reflect"] }
Component y Reflect a tus recursos// Antes
#[derive(Resource, Reflect)]
#[reflect(Resource)]
struct OldSettings { /* ... */ }
// Después
#[derive(Resource, Component, Reflect)]
#[reflect(Resource, Component)]
struct NewSettings { /* ... */ }
register_resource a register_component// Antes
app.register_resource::<OldSettings>();
// Después
app.register_component::<NewSettings>();
grep -rn "derive.*Resource.*Component" src/
Si encuentras #[derive(Resource, Component)] sin un comentario que explique por qué, refactoriza a dos structs.
Los Res<T> y ResMut<T> siguen funcionando idéntico. Lo que cambia es el modelo conceptual; los tests no deberían romperse.
**Trivia** — La migración fue más fácil de lo esperado porque el equipo mantuvo **alias deprecados** durante todo el ciclo 0.19. `register_resource` siguió funcionando como wrapper de `register_component` hasta 0.20, dando un ciclo entero para migrar. Es el mismo enfoque que **Rust 1.0** usó en 2015 para deprecar cosas como `std::old_io`.
| Situación | Usa |
|---|---|
| Configuración global (volumen, dificultad) | Resource |
| Datos por entidad (HP, posición) | Component |
| Flag de estado del juego (pausado, game over) | Resource |
| Buff que el jugador tiene ahora mismo | Component |
| Lista de jugadores conectados | Resource (es global) |
| Inventario de un PNJ concreto | Component |
| Métricas del sistema | Resource |
| Stats que el personaje lleva encima | Component |
La regla mental: si es uno solo, Resource. Si es uno por cosa, Component. Si dudan, Component (puedes promoverlo a Resource después envolviéndolo en singleton storage).
**Singleton storage** — patrón que mantiene un componente en una "slot" fija del world, accesible como si fuera un recurso pero tratable como componente. Útil cuando migras lentamente de singleton a per-entity.
Patrón: Resources-as-components para un modelo mental unificado
>
Trata
Resourcecomo una cardinalidad específica deComponent(cardinalidad = 1). Esto te permite:
1. Compartir lógica entre
Res<T>yQuery<&T>.
2. Reusar hooks, observers y relations en recursos.
3. Migrar gradualmente de singleton a per-entity sin reescribir.
4. Evitar duplicación de structs.
>
Trade-off a recordar: no puedes derivar ambos traits en el mismo struct. Si necesitas ambos comportamientos, usa dos structs y un trait común.
Resource, Component en el mismo struct esperando que "sepan coexistir".Component solo por moda: a veces un singleton es más claro.#[reflect(Component)] en un recurso migrado: el TypeRegistry no lo reconocerá.register_resource ya no existe: en 0.19 todavía funciona como deprecado.Res<T> como wrapper manual: usa el trait derivado.Resource con Component haciendo a Resource un subtrait.ReflectResource quedó como ZST marcador; register_resource es wrapper deprecado de register_component.Component, renombrar registros, separar structs ambiguos.Esta unificación cierra una de las asimetrías más viejas del ECS de Bevy. Lo que viene después es terreno abonado para optimizaciones globales: el runtime puede tratar singletons y componentes con el mismo código de archetype lookup, ahorrando cientos de líneas en bevy_ecs.
El capítulo 14 — Rendering 2D con sprites y cámaras cambia de tercio: pasamos del modelo de datos a la imagen final. Aprenderemos a montar un SpriteBundle, configurar la Camera2d con proyección ortográfica, manejar capas de Z, y por qué 0.19 introdujo un nuevo sistema de extracción más rápido. Volvemos a las pantallas, pero con todo el arsenal ECS fresco en la mochila.