Capítulo 13. Resources-as-components y el nuevo modelo unificado de Bevy 0.19

CAP 13 · Bevy 0.18/0.19
"Llevamos cinco años manteniendo dos sistemas de almacenamiento casi idénticos. Es hora de admitir que siempre fueron el mismo."

Cart (Cartesian Studios) en el Bevy Community Meeting #42 (2024), explicando el RFC que fusionó Resource con Component.

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.


13.1 — El problema histórico: dos sistemas, un mismo alma

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:

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>`.

13.2 — El cambio: Resource: Component

En 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:

**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.

13.3 — Implicaciones prácticas

3.1 — Un tipo, una declaración

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 });
}

3.2 — 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.

3.3 — Acceso unificado

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);
    }
}

13.4 — El trade-off importante

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 } }


13.5 — Patrón de migración desde código antiguo

Si tu proyecto tiene código 0.17/0.18 con register_resource, sigue estos pasos:

Paso 1 — Actualiza dependencias

[dependencies]
bevy = { version = "0.19", features = ["reflect"] }

Paso 2 — Añade 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 { /* ... */ }

Paso 3 — Renombra register_resource a register_component

// Antes
app.register_resource::<OldSettings>();

// Después
app.register_component::<NewSettings>();

Paso 4 — Busca usos ambiguos

grep -rn "derive.*Resource.*Component" src/

Si encuentras #[derive(Resource, Component)] sin un comentario que explique por qué, refactoriza a dos structs.

Paso 5 — Verifica con tests

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`.

13.6 — Cuándo usar cada uno

SituaciónUsa
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 mismoComponent
Lista de jugadores conectadosResource (es global)
Inventario de un PNJ concretoComponent
Métricas del sistemaResource
Stats que el personaje lleva encimaComponent

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.

🎯 13.7 — Patrón del capítulo

Patrón: Resources-as-components para un modelo mental unificado

>

Trata Resource como una cardinalidad específica de Component (cardinalidad = 1). Esto te permite:

1. Compartir lógica entre Res<T> y Query<&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.

Anti-patrones

  1. ❌ Derivar Resource, Component en el mismo struct esperando que "sepan coexistir".
  2. ❌ Migrar todo a Component solo por moda: a veces un singleton es más claro.
  3. ❌ Olvidar #[reflect(Component)] en un recurso migrado: el TypeRegistry no lo reconocerá.
  4. ❌ Asumir que register_resource ya no existe: en 0.19 todavía funciona como deprecado.
  5. ❌ Reimplementar Res<T> como wrapper manual: usa el trait derivado.

Lo que vimos

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.

En el siguiente

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.