Capítulo 14A. Anatomía de un Plugin en Bevy: el bloque LEGO oficial

CAP 14A · Bevy 0.18/0.19
"La arquitectura de software es la articulación de las intenciones de diseño del sistema. Los patrones son las frases de ese lenguaje."

Erich Gamma (1995, Gang of Four — autor de Design Patterns con Helm, Johnson y Vlissides, en la seminal editorial Addison-Wesley)

Imagina tu cocina. Tienes una nevera, una tostadora, una licuadora, una freidora de aire que compraste en un arranque de fe fitness. ¿Qué tienen en común? Tres cosas: son electrodomésticos, hacen una sola cosa (la tostadora tuesta, la licuadora licúa, la freidora friega tu autoestima) y se enchufan a un enchufe estándar y funcionan juntos sin pelearse por el circuito.

Eso, querido lector al borde del colapso, es un Plugin en Bevy. Un Plugin es un electrodoméstico. Lo enchufas, hace su cosa, se integra con la cocina. Y si mañana compras una sandwichera, la enchufas también. Y funciona. Sin tocar el cableado. Sin pedirle permiso a la tostadora.

Vamos a destripar el bloque LEGO oficial de Bevy.


El trait Plugin: el contrato del electrodoméstico

En Bevy 0.18/0.19, todo Plugin implementa el trait Plugin. Es un contrato pequeñito con cuatro métodos, aunque en la práctica usarás uno o dos. Aquí está el desglose, con humor y sin piedad:

// Trait completo del Plugin en Bevy 0.18/0.19.
// Sí, tiene cuatro métodos. No, no tienes que usar los cuatro.
// Como los cubiertos de tu cocina: existen 24, pero usas 4.

use bevy::prelude::*;

pub trait Plugin: Send + Sync + 'static {
    /// Se llama durante la construcción inicial del App.
    /// Aquí registras tipos, recursos, sistemas, eventos.
    /// "Build" = construir. Como IKEA, pero sin las instrucciones.
    fn build(&self, app: &mut App);

    /// Se llama DESPUÉS de que TODOS los plugins han hecho build().
    /// Útil para lógica que depende de que otros plugins ya estén enchufados.
    /// Como cuando llegas a la fiesta y el DJ ya pinchó tu canción favorita.
    fn finish(&self, app: &mut App) {
        // Implementación por defecto: no hace nada.
        // Como tu propósito de año nuevo.
    }

    /// Bevy 0.15+: se llama cada frame para preguntar "¿estoy listo?".
    /// Devuelve true cuando todas las dependencias están satisfechas.
    /// Como cuando el horno pita: "¡Estoy listo, mete la pizza!".
    fn ready(&self, app: &mut App) -> bool {
        // Por defecto: true. Soy un plugin optimista, nací listo.
        let _ = app;
        true
    }

    /// Limpieza al apagar. Disponible en versiones recientes.
    /// Para guardar estado, cerrar conexiones, llorar un poco.
    fn cleanup(&self, app: &mut App) {
        // Por defecto: vacío. El vacío es paz.
        let _ = app;
    }
}
- **Plugin**: trait de Rust que define un módulo enchufable al App. Es el bloque LEGO oficial de Bevy.
- **App**: la aplicación principal. La cocina donde enchufas tus electrodomésticos.
- **build()**: método que se ejecuta al construir el App. Aquí enchufas todo.
- **finish()**: se ejecuta tras el build() de TODOS los plugins. La segunda pasada.
- **ready()**: (Bevy 0.15+) verifica si el plugin está listo para usarse cada frame.
- **cleanup()**: limpieza al cerrar. Para cerrar conexiones, persistir datos.

El Plugin mínimo: cinco líneas que te cambian la vida

Antes de añadirle toppings, veamos la pizza base. El plugin más pequeño posible:

// El plugin más minimalista de la historia.
// Cinco líneas. Ni una más. Ni una menos.
// Si tiene menos líneas, no compila. Si tiene más, ya no es minimalista.

use bevy::prelude::*;

// Implementamos Plugin para nuestra struct vacía.
// Como decir: "soy un electrodoméstico que se enchufa".
pub struct HolaMundoPlugin;

// El método build es donde ocurre la magia.
// Por ahora, solo imprimimos. Pronto enchufaremos sistemas.
impl Plugin for HolaMundoPlugin {
    fn build(&self, app: &mut App) {
        println!("¡Hola desde un plugin enchufable!");
    }
}

// Para usarlo en el main:
fn main() {
    App::new()
        .add_plugins(HolaMundoPlugin)  // Fíjate: por nombre, no por instancia.
        .run();
}

Sí, solo cinco líneas con alma. El plugin se registra con add_plugins() y Bevy llama a su build() automáticamente.


Plugin con estado: cuando el electrodoméstico tiene memoria

Algunos electrodomésticos necesitan saber cosas. La cafetera necesita saber cuánta agua tiene. Tu freidora necesita saber a qué temperatura freír. Los plugins también: a veces necesitan configuración, contadores internos, referencias.

// Plugin con estado: como una cafetera que recuerda tu última receta.
// Implementamos Default para que sea fácil de construir.

use bevy::prelude::*;

// Nuestra struct tiene campos. Es un plugin "con memoria".
// Como una lavadora que recuerda que el otro día metiste un calcetín rojo.
#[derive(Default)]
pub struct ConfiguracionAudio {
    pub volumen_master: f32,           // 0.0 = silencio sepulcral, 1.0 = vecino llamando a la policía.
    pub canales: u8,                   // 2 = estéreo, 8 = cine en casa de tío gamer.
    pub activado: bool,                // True = suena. False = raro silencio.
}

// Implementamos Default manualmente para dar valores razonables.
impl Default for ConfiguracionAudio {
    fn default() -> Self {
        Self {
            volumen_master: 0.8,  // Suave, pero audible. Como tu profesor favorito.
            canales: 2,            // Estéreo. Nada de 7.1 todavía.
            activado: true,        // Nacemos encendidos.
        }
    }
}

// El plugin. Tiene un campo de configuración.
// Como una tostadora con dial de "qué tan quemado lo quieres".
pub struct PluginAudio {
    config: ConfiguracionAudio,
}

impl PluginAudio {
    // Constructor conveniente. Como el "quick start" de tu router.
    pub fn new() -> Self {
        Self {
            config: ConfiguracionAudio::default(),
        }
    }

    // Constructor con config custom. Para los sibaritas.
    pub fn con_config(config: ConfiguracionAudio) -> Self {
        Self { config }
    }
}

// Default para el plugin mismo, delega al default de la config.
impl Default for PluginAudio {
    fn default() -> Self {
        Self::new()
    }
}

// ¡Ahora sí, el trait Plugin!
impl Plugin for PluginAudio {
    fn build(&self, app: &mut App) {
        // Insertamos la configuración como Resource.
        // insert_resource = "guardar esto en la memoria global del App".
        app.insert_resource(self.config.clone());

        // Añadimos un sistema. Sí, podemos hacerlo en build().
        // Como configurar la alarma de la cafetera al enchufarla.
        app.add_systems(Update, sistema_que_imprime_volumen);
    }
}

// Un sistema cualquiera para que el ejemplo sea completo.
fn sistema_que_imprime_volumen(config: Res<ConfiguracionAudio>) {
    if config.activado {
        println!("♪ Tocando a volumen {} ♪", config.volumen_master);
    }
}

// Uso:
fn main() {
    App::new()
        .add_plugins(PluginAudio::con_config(ConfiguracionAudio {
            volumen_master: 0.5,
            canales: 2,
            activado: true,
        }))
        .run();
}
- El concepto de "app" enchufable en frameworks no es nuevo. Django (2005, **Lawrence Journal-World**) introdujo las "apps" como módulos reutilizables con un punto de registro central.
- Flask (2010, **Armin Ronacher**) lo llevó más allá con los **blueprints**: pedazos de aplicación que se registran cuando quieres.
- Ember.js (2011, **Yehuda Katz y Tom Dale**) lo popularizó en frontend con sus **initializers** en el contenedor de DI.
- Bevy 0.1 (2020, **Cartesian Goodwill** o sea, **Carter Anderson**) tomó la idea y la convirtió en un trait de Rust puro. Cero magia, puro tipos.

PluginGroup: la caja de enchufes múltiples

A veces no quieres enchufar un electrodoméstico, sino un kit completo. "El kit de cocina italiano" debería traer espaguetera, rallador, y prensador de ajos, todo a la vez.

Ahí entra PluginGroup. Es un grupo de plugins que se enchufa como un solo bloque.

// PluginGroup: cuando tienes UN plugin que ES VARIOS plugins.
// Como un "combo de desayuno" que trae café, tostada y zumo.

use bevy::prelude::*;

// Primero, definimos cada plugin individual.
// Como los platos del menú antes de ofrecer el "menú del día".

pub struct PluginCafe;
impl Plugin for PluginCafe {
    fn build(&self, app: &mut App) {
        println!("☕ Café listo (aunque no se puede beber, es código).");
        app.add_systems(Startup, || println!("Encendiendo cafetera..."));
    }
}

pub struct PluginTostada;
impl Plugin for PluginTostada {
    fn build(&self, app: &mut App) {
        println!("🍞 Tostadora calibrada.");
    }
}

pub struct PluginZumo;
impl Plugin for PluginZumo {
    fn build(&self, app: &mut App) {
        println!("🍊 Zumo exprimido por un sistema ECS.");
    }
}

// Ahora, el grupo. Es un plugin-que-contiene-plugins.
pub struct GrupoDesayuno;

impl PluginGroup for GrupoDesayuno {
    // Aquí enumeras los plugins que componen el grupo.
    // El orden importa. Café primero, tostada después, zumo al final.
    fn build(self) -> PluginGroupBuilder {
        PluginGroupBuilder::start::<Self>()
            .add(PluginCafe)
            .add(PluginTostada)
            .add(PluginZumo)
    }
}

// Uso: igual que cualquier plugin, pero enchufa tres a la vez.
fn main() {
    App::new()
        .add_plugins(GrupoDesayuno)  // Un solo add_plugins, tres plugins enchufados.
        .run();
}

Dependencias y ordenamiento: "primero el café, luego la tostada"

A veces un plugin NECESITA a otro. Tu plugin de "tostada con mantequilla" no funciona si el plugin de "mantequilla" no está enchufado. Bevy respeta el orden en que los enchufas.

// Orden de enchufado = orden de build().
// Enchufe A, luego enchufe B. B puede usar lo que A registró.

use bevy::prelude::*;

// Plugin A: registra un evento.
pub struct PluginEventos;
impl Plugin for PluginEventos {
    fn build(&self, app: &mut App) {
        // add_event = registrar un tipo de evento en el App.
        app.add_event::<MiEvento>();
        println!("📬 PluginEventos: tipo de evento registrado.");
    }
}

// Plugin B: usa ese evento. NECESITA que A se haya enchufado antes.
pub struct PluginLectorEventos;
impl Plugin for PluginLectorEventos {
    fn build(&self, app: &mut App) {
        // Como ya enchufamos A, podemos leer MiEvento aquí.
        app.add_systems(Update, sistema_que_escucha);
        println!("👂 PluginLectorEventos: escuchando eventos.");
    }
}

#[derive(Event)]
struct MiEvento;

fn sistema_que_escucha(mut eventos: EventReader<MiEvento>) {
    for _ in eventos.read() {
        println!("¡Evento recibido!");
    }
}

fn main() {
    // El orden importa. A antes que B.
    App::new()
        .add_plugins(PluginEventos)       // Primero enchufamos A.
        .add_plugins(PluginLectorEventos) // Luego B, que depende de A.
        .run();
}

¿Y si quieres desactivar un plugin? Algunos plugins vienen con un interruptor. disable() lo apaga sin desenchufarlo del todo. Es como tener un enchufe inteligente: el cable está ahí, pero no pasa corriente.

// disable() para opt-out condicional.
// "No quiero la freidora de aire porque mi cocina es pequeña".

use bevy::prelude::*;

// Un plugin que internamente decide si activarse o no.
pub struct PluginOpcional {
    pub activado: bool,  // El interruptor. False = modo fantasma.
}

impl Plugin for PluginOpcional {
    fn build(&self, app: &mut App) {
        if self.activado {
            // Registramos el sistema solo si está activado.
            app.add_systems(Update, || println!("Estoy vivo y activo."));
        } else {
            // Si no, somos un mueble. Estamos aquí, pero no hacemos nada.
            println!("PluginOpcional enchufado pero en modo silencioso.");
        }
    }
}

fn main() {
    let mi_plugin = PluginOpcional { activado: false };

    App::new()
        .add_plugins(mi_plugin)  // Enchufado, pero en silencio.
        .run();
}
- **Olvidar `app.add_systems` y poner `app.add_event` por distraición**: ambos métodos existen, pero hacen cosas muy distintas. `add_systems` registra funciones que corren cada frame. `add_event` registra un tipo de evento. Si los confundes, tu código compila pero no hace lo que esperabas (el error en tiempo de ejecución es de lo más silencioso). Lección: lee el autocompletado. Es tu amigo, no tu enemigo.
- **Asumir que el orden de build() no importa**: importa. Y mucho. Si enchufas B antes que A y B usa cosas de A, explota en runtime. Bevy no te avisa con un cartelito. Te avisa con un panic.
- **Hacer trabajo pesado en `build()`**: build() debería ser rápido (registrar tipos, sistemas, recursos). Si te pones a leer un archivo de 500MB ahí, tu app tarda 5 minutos en arrancar. Usa `finish()` o el schedule `Startup` para eso.
- **Devolver algo en `ready()` sin lógica**: el método por defecto devuelve `true`, asumiendo que estás listo al enchufarte. Si devuelves `false` siempre, tu plugin nunca será "ready" y los sistemas que dependen de él no correrán. Como llegar a una cita y decir "no estoy listo" perpetuamente.

Feature flags: el electrodoméstico con piezas opcionales

A veces tu plugin tiene funcionalidades que solo quieres en compilación. La cafetera tiene espumador de leche, pero si no te gusta la leche, ¿para qué incluirlo? #[cfg(feature = "...")] al rescate.

// Feature flags con cfg.
// Como un mueble modular de IKEA: viene con piezas opcionales.

use bevy::prelude::*;

pub struct PluginModular;

// Función helper que solo existe si la feature está activada.
#[cfg(feature = "espumador")]
fn sistema_espumador() {
    println!("☁️  Espumando leche. Cappuccino virtual en proceso.");
}

#[cfg(feature = "molinillo")]
fn sistema_molinillo() {
    println!("⚙️  Moliendo granos. El olor a café viene gratis.");
}

impl Plugin for PluginModular {
    fn build(&self, app: &mut App) {
        // Solo añadimos el sistema del espumador si la feature está activada.
        // cfg = "compile-time guard". Decide en compilación, no en runtime.
        #[cfg(feature = "espumador")]
        app.add_systems(Update, sistema_espumador);

        // El molinillo es otra feature independiente.
        #[cfg(feature = "molinillo")]
        app.add_systems(Update, sistema_molinillo);

        // El sistema base SIEMPRE se incluye.
        app.add_systems(Update, || println!("☕ Cafetera operativa (modo básico)."));
    }
}

// En Cargo.toml:
// [features]
// espumador = []
// molinillo = []
// default = ["espumador"]  # Por defecto sí incluye espumador, pero no molinillo.

// Uso:
// $ cargo run                                  → solo sistema base
// $ cargo run --features espumador             → base + espumador
// $ cargo run --features "espumador molinillo" → todo activado
- **Cargo features** fueron introducidas en Rust 1.0 (2015, **Mozilla Research**), inspiradas en el sistema de flags de C/C++ pero con compilación condicional first-class.
- Bevy (2020, **Cartesian Goodwill**) las usa intensivamente. La propia Bevy tiene features como `bevy/bevy_audio`, `bevy/bevy_sprite`, etc. Tú puedes hacer lo mismo con tus plugins.
- La palabra `cfg` viene de "configuration", una reliquia del prelenguaje C. Que siga viva en 2026 es parte del encanto de la programación.

Plugin "API-only": el que solo expone tipos

A veces no quieres un electrodoméstico que haga nada. Solo quieres registrar los TIPOS para que otros puedan usarlos. Como un manual de instrucciones sin aparato. O como el cartel de " salida de emergencia" en un cine: no es una puerta, pero señala dónde está la puerta.

// Plugin "API-only": no enchufa sistemas, solo registra tipos públicos.
// Como el index de una biblioteca: no contiene libros, te dice dónde están.

use bevy::prelude::*;

// Nuestro tipo público. Otros plugins lo van a usar.
#[derive(Event)]
pub struct EventoPublico {
    pub mensaje: String,
}

// Recurso público.
#[derive(Resource, Default)]
pub struct EstadoCompartido {
    pub contador: u32,
}

// Plugin API-only. Su trabajo es SOLO registrar tipos.
pub struct MiAPIPublica;

impl Plugin for MiAPIPublica {
    fn build(&self, app: &mut App) {
        // Registramos el evento en el world del App.
        // Ahora otros plugins pueden hacer app.add_event::<EventoPublico>()
        // o usar EventWriter<EventoPublico> en sus sistemas.
        app.add_event::<EventoPublico>();

        // Inicializamos el recurso con su Default.
        app.init_resource::<EstadoCompartido>();

        // No añadimos ningún sistema. Cero ejecución.
        // Pura infraestructura. El fontanero de tu código.
    }
}

// Otro plugin que USA los tipos registrados por MiAPIPublica.
// Fíjate: no necesita conocer la implementación, solo el contrato.
pub struct PluginConsumidor;

impl Plugin for PluginConsumidor {
    fn build(&self, app: &mut App) {
        app.add_systems(Update, (
            sistema_que_envia_evento,
            sistema_que_lee_recurso,
        ));
    }
}

fn sistema_que_envia_evento(mut writer: EventWriter<EventoPublico>) {
    writer.send(EventoPublico {
        mensaje: "Hola desde el consumidor".to_string(),
    });
}

fn sistema_que_lee_recurso(mut estado: ResMut<EstadoCompartido>) {
    estado.contador += 1;
    println!("Contador: {}", estado.contador);
}

fn main() {
    // El plugin API debe ir ANTES que el consumidor.
    // Si no, el consumidor no encuentra los tipos registrados.
    App::new()
        .add_plugins(MiAPIPublica)      // Primero: registro de tipos.
        .add_plugins(PluginConsumidor)  // Después: el que los usa.
        .run();
}
- **Plugin API-only**: plugin cuyo único trabajo es registrar tipos públicos (recursos, eventos, componentes) sin añadir sistemas. Es el "contrato" o "schema" de tu módulo.
- **SubApp**: una aplicación Bevy anidada dentro de otra App. Útil para servidores con múltiples "mundos" lógicos (cliente, servidor, IA). Se accede con `app.add_sub_app()`.
- **Schedule**: lista ordenada de sistemas que corren en una fase concreta (Startup, Update, FixedUpdate, etc.). Es el "cuándo" de tu código.
- **Resource**: dato global accesible desde cualquier sistema. Como las variables globales, pero con tipos y sin el horror de C.
- **Event**: mensaje asíncrono que un sistema envía y otros reciben. Como un email interno de la empresa.

🎯 Patrón del capítulo: Plugin = responsabilidad única

"Cada Plugin debe tener una sola razón para existir."

Traducido del sagrado texto de los patrones: el principio de responsabilidad única (Single Responsibility Principle). Cada plugin hace UNA cosa y la hace bien.

// ❌ MAL: un plugin que hace de todo. Monolito. La kitchen sink.
//    Como un electrodoméstico que es a la vez nevera, horno y lavavajillas.

pub struct PluginDios {
    pub sonido: bool,
    pub fisicas: bool,
    pub ia: bool,
    pub ui: bool,
    pub guardado: bool,
}

impl Plugin for PluginDios {
    fn build(&self, app: &mut App) {
        if self.sonido { /* ...registra audio... */ }
        if self.fisicas { /* ...registra físicas... */ }
        if self.ia { /* ...registra IA... */ }
        if self.ui { /* ...registra UI... */ }
        if self.guardado { /* ...registra save/load... */ }
        // 500 líneas después, tu plugin es un país.
    }
}

// ✅ BIEN: un plugin por responsabilidad. Componible. Testeable.
//    Como tener cada electrodoméstico por separado en su enchufe.

pub struct PluginSonido;
impl Plugin for PluginSonido {
    fn build(&self, app: &mut App) {
        app.add_event::<EventoSonido>()
           .init_resource::<ConfigAudio>()
           .add_systems(Update, sistema_reproducir_sonido);
    }
}

pub struct PluginFisicas;
impl Plugin for PluginFisicas {
    fn build(&self, app: &mut App) {
        app.add_systems(FixedUpdate, sistema_aplicar_gravedad)
           .add_systems(FixedUpdate, sistema_detectar_colisiones);
    }
}

pub struct PluginIA;
impl Plugin for PluginIA {
    fn build(&self, app: &mut App) {
        app.add_systems(Update, sistema_pensar_enemigos);
    }
}

// Composición limpia en el main:
fn main() {
    App::new()
        .add_plugins((PluginSonido, PluginFisicas, PluginIA))
        .run();
}

La diferencia: el primer caso es una caja negra imposible de testear aisladamente. El segundo son piezas LEGO que puedes sacar, cambiar de orden, o desactivar. Esa es la magia.


Lo que vimos

En este capítulo destripamos el trait Plugin de Bevy 0.18/0.19 pieza a pieza:

Ahora tienes el vocabulario para escribir plugins modulares, testeables y componibles. Y lo más importante: para enchufarlos como si fueran LEGO.

En el siguiente

En el 14B veremos bevy_asset y el AssetServer: cómo Bevy carga imágenes, sonidos y fuentes, y cómo crear assets propios (un sistema de tarjetas, por ejemplo) usando el trait Asset. También hablaremos del Handle<T> y de por qué es como un puntero inteligente a un recurso del mundo.

Y en el 14C entraremos en bevy_ecs al detalle: archetypes, tables, change detection, y por qué moverte por el mundo con Query es 100x más rápido que iterar un Vec<Entity>.