Capítulo 6. Eventos: la forma clásica de comunicar sistemas

CAP 06 · Bevy 0.18/0.19
"En 1978, en una sala de máquinas de un arcade de Tokio, un tal Shigeru Miyamoto vio cómo un tipo jugaba con una máquina de pinball y se le ocurrió Space Invaders. La idea genial: cada vez que un alien muere, se enviaba una señal al marcador. No era una llamada directa entre el alien y el marcador: era un evento, un 'telegrama'. Tomohiro Nishikado lo llamó 'interrupción' en el código del 8080, pero la idea era la misma: un emisor que no sabe quién escucha, y un receptor que reacciona cuando llega el mensaje. Cuarenta y seis años después, ese patrón sigue siendo el corazón de todo juego, y se llama 'evento'."

En el capítulo 4 mencionamos los events de pasada. Ahora les llega su hora. Los eventos son el mecanismo de Bevy para que los systems se hablen entre sí sin acoplarse. Piensa en ellos como un sistema de correo interno: un system escribe una carta, la tira al buzón, y cualquier otro system puede leer las cartas acumuladas. El emisor no sabe (ni le importa) quién va a leer la carta. El receptor no sabe (ni le importa) quién la escribió. Limpio, desacoplado, elegante.

6.1 La metáfora del telegrama

Antes de tocar código, asienta la idea con una analogía. Porque las analogías, además de hacer el libro más entretenido, son la mejor manera de que tu cerebro "haga click" con un concepto abstracto.

Imagina que un system le quiere decir algo a otro. Las opciones son:

  1. Llamada directa: el system A llama al system B y le dice "oye, haz esto". Acoplamiento máximo. Si B cambia su firma, A se rompe. Si A quiere contarle lo mismo a C, tiene que cambiar A. Un desastre.
  2. Variable compartida: el system A escribe en una variable global "el mensaje". B (o quien sea) la lee. Funciona, pero todos los systems tienen que coordinarse para no leerse a la vez, y no hay historial: solo el último mensaje sobrevive.
  3. Telegrama (evento): el system A escribe una carta, la mete en un buzón, y se olvida. B, C, D, E, F... cualquier system puede ir al buzón cuando quiera y leer las cartas. Si nadie las lee, se quedan ahí. Si hay tres sistemas escuchando, los tres la reciben. Es un bus de mensajes asíncrono y desacoplado.

Obviamente, en ECS queremos la opción 3. Y eso es un evento: un struct que se deriva de Event, se envía con un EventWriter<T>, y se recibe con un EventReader<T>. Tan simple como eso.

- **`Event`**: trait que se le pone a un struct/enum para marcarlo como "tipo de mensaje". Se deriva con `#[derive(Event)]`.
- **`EventWriter<T>`**: handle para enviar eventos de tipo `T`. Se pide como `mut writer: EventWriter<MiEvento>` en la firma de un system.
- **`EventReader<T>`**: handle para leer eventos de tipo `T`. Se pide como `mut reader: EventReader<MiEvento>` y se itera con `reader.read()` o `reader.read_mut()`.
- **`Events<T>`**: el "buzón" en sí, que vive como resource en el world. Bevy lo crea automáticamente al registrar el evento con `add_event::<T>()`.
- **Doble-buffering**: Bevy mantiene dos buffers de eventos. Cuando se llama `Events::update()`, el buffer viejo se descarta y el actual pasa a ser el viejo. Así nadie pierde mensajes por orden de ejecución, pero tampoco se acumulan infinitamente.
- **Trigger (observer)**: variante de evento que NO usa el buffer: se entrega directamente a quien lo escucha, sin pasar por el ciclo de doble-buffering. Llegaron en 0.13 y se estabilizaron en 0.15 como la API de observers basada en Trigger<E> + world.trigger/world.trigger_event. Lo vemos al final del capítulo.

6.2 El struct evento: declaración y registro

Para crear un evento, deriva Event y (casi siempre) añádele campos con datos. Lo que se te ocurra: daño hecho, item recogido, nivel cambiado, sonido a reproducir, lo que sea.

use bevy::prelude::*;

#[derive(Event)]
struct DanoRecibido {
    entidad: Entity,
    cantidad: f32,
}

#[derive(Event)]
struct MonedaRecogida {
    cantidad: u32,
    posicion: (f32, f32),
}

#[derive(Event)]
struct CambioDeNivel {
    nuevo_nivel: usize,
}

Una vez definidos, los registras en la App con add_event::<T>(). Esto le dice a Bevy: "crea un buzón para este tipo de evento y mantenlo vivo".

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_event::<DanoRecibido>()
        .add_event::<MonedaRecogida>()
        .add_event::<CambioDeNivel>()
        .add_systems(Update, (recibir_dano, recoger_moneda))
        .run();
}

Si te olvidas de add_event, no pasa nada catastrófico: Bevy detecta que no hay buzón y te avisa con un error clarísimo del estilo "the resource Events<MiEvento> does not exist". Pero acostumbrarse a registrar todos los eventos al inicio es buena higiene.

★ **¿Event o Message?** Si has estado ojeando el repositorio de Bevy en github habrás visto en some PRs menciones a un nuevo trait Message, un MessageWriter y un add_message. Es un **movimiento en marcha**: la idea a medio plazo es separar conceptualmente los "hechos" (Event, lo que YA pasó: un alien murió) de los "comandos/mensajes" (Message, lo que QUIERES que pase: "reproduce este sonido"). En 0.19 **ambas APIs coexisten**: Event + EventWriter + add_event siguen siendo la API pública, documentada y totalmente vigente. Si en el futuro tu app migra a Message, el patrón es idéntico y la traducción es mecánica. En este libro usamos Event porque es la API estable y la que verás en todos los ejemplos y tutoriales actuales. Cuando Message se estabilice como sustituto, te tocará migrar.

6.3 EventWriter<T>: enviar el telegrama

Para enviar un evento, pide un EventWriter<T> mutable en la firma de tu system, y usa writer.write(...):

fn sistema_de_colision(
    colisiones: Query<(Entity, &Transform, &Hitbox)>,
    mut writer_dano: EventWriter<DanoRecibido>,
) {
    for (entity, transform, hitbox) in &colisiones {
        if hitbox.debe_recibir_dano {
            writer_dano.write(DanoRecibido {
                entidad: entity,
                cantidad: 25.0,
            });
        }
    }
}

writer.write() no ejecuta nada en el momento. Solo añade el evento al buzón del frame actual. Los receivers lo leerán cuando les toque. Desacoplamiento puro: este system no tiene ni idea de quién va a procesar el daño. Ni le importa. (Nota de versiones: en Bevy pre-0.16 este método se llamaba EventWriter::send. Fue renombreado a write en 0.16 para que el nombre cuadre con el tipo (Writer) y, tras un ciclo de deprecación, eliminado del todo en 0.17. En 0.18/0.19 send ya no existe: si lo usas, no compila. Así que si estás copiando código de un tutorial viejo y el compilador te grita "no method named send", ahí tienes la traducción: send(...)write(...).)

¿Múltiples eventos en un solo system? Mezcla writers:

fn sistema_complejo(
    mut w1: EventWriter<MonedaRecogida>,
    mut w2: EventWriter<CambioDeNivel>,
) {
    w1.write(MonedaRecogida { cantidad: 5, posicion: (10.0, 20.0) });
    w2.write(CambioDeNivel { nuevo_nivel: 2 });
}

¿Enviar el mismo evento varias veces en un mismo system? Por supuesto. write() se puede llamar N veces, y el buzón acumula.

6.4 EventReader<T>: leer los telegramas acumulados

Para leer, pide un EventReader<T> mutable y llama a reader.read(). Eso te devuelve un iterador de &T con todos los eventos enviados en este frame:

fn procesar_dano(
    mut reader: EventReader<DanoRecibido>,
    mut query: Query<&mut Health>,
) {
    for evento in reader.read() {
        if let Ok(mut health) = query.get_mut(evento.entidad) {
            health.actual -= evento.cantidad;
        }
    }
}

reader.read() consume los eventos del buffer "actual" (el del frame en curso). Una vez leídos, no se vuelven a leer. La próxima vez que llames a reader.read() (el siguiente frame), el buffer actual será el "viejo" (el del frame anterior) y te dará esos. Y así sucesivamente.

Truco importante: reader.read() solo te da los eventos desde la última vez que los leíste. Si un system tiene un EventReader<T> y no llama a .read() en un frame, los eventos de ese frame se acumularán y se entregarán todos juntos en el siguiente. Esto es bueno: garantiza que no se pierden mensajes. Pero hay que ser consciente.

**La metedura de pata del día**: llamar a `reader.read()` dos veces en el mismo system. La primera vez consume los eventos; la segunda, el iterador está vacío. Si tienes un `for evento in reader.read()` y luego otro `for evento in reader.read()` más abajo esperando "leer los mismos otra vez", el segundo bucle no hace nada. Lección: lee una vez por system por frame, guarda los resultados en una variable local, y trabaja con esa.

6.5 El doble-buffering: cómo no perder mensajes

Vale, aquí viene la parte sutil. Imagina esta secuencia:

  1. Frame 1: system A envía un evento DanoRecibido.
  2. Frame 1: system B no existe todavía o no se ejecuta.
  3. Frame 2: system B se ejecuta y quiere leer eventos. ¿Ve el DanoRecibido del frame 1?

La respuesta es SÍ, gracias al doble-buffering. Bevy mantiene dos buffers: el "A" (eventos del frame actual) y el "B" (eventos del frame anterior). Cuando termina el frame, llama a Events::<T>::update() internamente, que hace:

buffer_viejo = buffer_actual
buffer_actual = vaciar(buffer_viejo)

Así, en cada frame:

Resultado: ningún mensaje se pierde mientras haya un system que lo lea al menos una vez por frame. Y los mensajes antiguos (más de un frame sin leer) sí se descartan, evitando fugas.

Si quieres, puedes llamar a Events::<T>::update() manualmente en un system con mut events: ResMut<Events<T>> para forzar el ciclo. Pero en el 99% de los casos, Bevy lo hace solo. Déjalo en paz.

- **Doble-buffering**: técnica de Bevy para que un evento enviado en el frame N sea visible en el frame N+1 aunque ningún system lo lea en N. Dos buffers: actual y viejo, que se intercambian al final del frame.
- **`Events::<T>::update()`**: función que rota los buffers. Bevy la llama automáticamente; no la llames tú salvo en casos raros (sistemas de pause, replay, etc.).
- **Buffer overflow**: si envías un evento y nadie lo lee en un frame, no pasa nada: lo leerá el siguiente. Pero si nadie lo lee nunca, ahí se queda (hasta que se rote el buffer y se pierda). Por eso los eventos son para "cosas que pasan AHORA MISMO", no para "estado persistente". Para estado, usa resources.

6.6 Eventos buffered vs triggers (observers)

Desde hace ya varios releases de Bevy (la API basada en Trigger<E> es la canónica desde 0.15), hay una distinción importante que tienes que entender:

La API de observers está consolidada (la forma Trigger<E> reemplazó a la vieja On<E> en 0.15) y vale un capítulo aparte (de hecho, tiene el suyo: el cap 10). Aquí solo mencionamos la diferencia para que no te confundas si ves tutoriales que usan Observer, On o trigger. Spoiler: hoy se escribe Trigger<E>; si ves On<E>, el tutorial es pre-0.15.

Regla práctica:

En el resto de este capítulo hablamos solo de eventos buffered. Los observers los vemos en profundidad en el cap 10.

6.7 Ejemplo completo: DamageEvent y DeathEvent

Vale, vamos a hacer un sistema de combate minimalista pero completo. La idea:

  1. El jugador pulsa Espacio: un system "ataca" y envía un DamageEvent.
  2. Otro system recibe el DamageEvent, mira si impacta a un enemigo, le resta vida.
  3. Si la vida del enemigo llega a 0, se envía un DeathEvent.
  4. Un tercer system recibe el DeathEvent y reproduce sonido, suma puntos, etc.
use bevy::prelude::*;

// 1. Definimos los eventos
#[derive(Event)]
struct DamageEvent {
    target: Entity,
    cantidad: f32,
}

#[derive(Event)]
struct DeathEvent {
    victim: Entity,
    killer: Option<Entity>,
}

// 2. Components
#[derive(Component)]
struct Vida {
    actual: f32,
    max: f32,
}

#[derive(Component)]
struct Player;

#[derive(Component)]
struct Enemy;

// 3. Resource de puntuación
#[derive(Resource, Default)]
struct Puntuacion(u32);

// 4. El system que "ataca": al pulsar Espacio, manda daño al primer enemy
fn atacar(
    input: Res<ButtonInput<KeyCode>>,
    enemies: Query<Entity, With<Enemy>>,
    mut writer: EventWriter<DamageEvent>,
) {
    if !input.just_pressed(KeyCode::Space) {
        return;
    }
    if let Some(target) = enemies.iter().next() {
        writer.write(DamageEvent {
            target,
            cantidad: 25.0,
        });
    }
}

// 5. El system que recibe daño y emite muerte
fn procesar_dano(
    mut reader: EventReader<DamageEvent>,
    mut query: Query<&mut Vida>,
    mut death_writer: EventWriter<DeathEvent>,
) {
    for evento in reader.read() {
        if let Ok(mut vida) = query.get_mut(evento.target) {
            vida.actual -= evento.cantidad;
            if vida.actual <= 0.0 {
                death_writer.write(DeathEvent {
                    victim: evento.target,
                    killer: None,
                });
            }
        }
    }
}

// 6. El system que reacciona a la muerte
fn reaccionar_a_muerte(
    mut reader: EventReader<DeathEvent>,
    mut puntos: ResMut<Puntuacion>,
    mut commands: Commands,
) {
    for evento in reader.read() {
        println!("¡Entity {:?} ha muerto!", evento.victim);
        puntos.0 += 100;
        commands.entity(evento.victim).despawn();
    }
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_event::<DamageEvent>()
        .add_event::<DeathEvent>()
        .init_resource::<Puntuacion>()
        .add_systems(Startup, |mut commands: Commands| {
            commands.spawn((Vida { actual: 100.0, max: 100.0 }, Player));
            commands.spawn((Vida { actual: 50.0, max: 50.0 }, Enemy));
        })
        .add_systems(Update, (atacar, procesar_dano, reaccionar_a_muerte))
        .run();
}

Pulsas Espacio, el enemy recibe 25 de daño, su vida baja a 25, no muere. Pulsas otra vez, vida a 0, se envía DeathEvent, y el enemy es despawned. Si pulsas una tercera vez, enemies.iter().next() devuelve None (ya no hay enemies), y no pasa nada.

Fíjate en el flujo:

Cada uno es independiente. Puedes añadir más consumidores de DeathEvent (uno que spawnee partículas, otro que suba el contador de kills, otro que haga temblar la cámara) sin tocar procesar_dano. Magia.

- **1978** — *Space Invaders* (Taito, Japón). Tomohiro Nishikado implementó lo que probablemente fue el primer "sistema de eventos" de la historia: cada alien muerto activaba una interrupción del 8080 que actualizaba el marcador. Sin eventos explícitos, pero con la misma idea: un emisor, un bus, un receptor.
- **1997** — *Final Fantasy VII* (Square, Japón). El sistema de combate de FFVII era esencialmente un bus de eventos: cada acción (ataque, hechizo, item) generaba un evento que múltiples sistemas procesaban (daño, animación, sonido, cámara, UI). Tuvieron que inventar el patrón porque las llamadas directas entre 200+ sistemas de batalla eran inmanejables.
- **2024** — *Bevy 0.14* (Cart, EE.UU.). Introduce los "observers" como contraparte a los eventos buffered. La idea: si quieres que SOLO una entity reaccione, no hace falta buzón. Triggers directos. Mismo concepto, menos overhead.

6.8 Errores comunes (la sección de siempre, pero con eventos)

  1. No registrar el evento con add_event::<T>(): el primer EventWriter o EventReader del frame te explota con "resource does not exist".
  2. Llamar a reader.read() dos veces en el mismo system: el segundo bucle está vacío.
  3. Usar eventos para "estado persistente": si quieres que un dato persista entre frames, no es un evento, es un resource. Los eventos se rotan; los resources no.
  4. Asumir que el evento llega en el mismo system que lo envía: NO. Los eventos se procesan en el siguiente ciclo del doble-buffer. Si envías en atacar y lees en procesar_dano en el mismo frame, en el mismo system, no funciona. Tiene que ser otro system (o el mismo, pero después de un Events::update(), que es raro).
  5. No marcar el evento como #[derive(Event)]: el compilador te avisa, pero a veces se te pasa entre derives largos.

6.9 Eventos como "desacoplador de módulos"

Una de las razones por las que los eventos son TAN importantes en un juego real es que te permiten dividir el juego en módulos independientes. Imagina un sistema de "power-up recogido":

Si todo eso estuviera en un único mega-system, sería un monstruo de 200 líneas imposible de mantener. Con eventos, son 5 systems de 20 líneas, cada uno con su responsabilidad, que se acoplan SOLO a través del evento. Puedes añadir el system F (vibración del mando) sin tocar A-E. Puedes quitar el system C sin que A se entere. Esto es ingeniería de software, no solo Bevy.

6.10 — Sistemas fallibles: Result y error handlers

Hasta Bevy 0.14, un sistema solo podía devolver (). Si algo fallaba dentro, tenías dos opciones: unwrap() y rezar, o expect("mensaje") y rezar con mensaje. No había forma elegante de propagar el error a quien llamó el sistema, porque nadie "llamaba" al sistema en el sentido tradicional: el scheduler lo invocaba.

Desde 0.15 (y consolidado en 0.16), Bevy introdujo un sistema unificado de errores. Si un sistema, observer o command devuelve Result<(), BevyError>, Bevy lo considera "fallible" y el scheduler lo trata especialmente. Esto es el mayor avance de calidad de vida de los últimos releases para quien viene de Rust "puro" y espera propagar errores con ?.

Analogía absurda: Bevy pre-0.15 era como un camarero sordo. Le pedías algo, no lo tenía, y tiraba la bandeja al suelo con un estruendo. Bevy 0.15+ es el mismo camarero con un walkie-talkie: le pides algo, no lo tiene, y te avisa por el pinganillo para que decidas si quieres otra cosa o te quedas con hambre. La diferencia es quién decide qué hacer cuando algo falla: antes Bevy, ahora tú.

//! cap-06 — sección 6.10: sistema fallible básico.
use bevy::prelude::*;

#[derive(Resource)]
struct Config { api_key: String }

fn cargar_config_desde_archivo(
    mut commands: Commands,
) -> Result {
    let texto = std::fs::read_to_string("config.toml")
        .map_err(|e| format!("no se pudo leer config.toml: {e}"))?;

    let config: Config = toml::from_str(&texto)
        .map_err(|e| format!("config.toml malformado: {e}"))?;

    commands.insert_resource(config);
    Ok(())
}

Fíjate en la firma: -> Result (que es Result<(), BevyError>). El operador ? propaga errores; el Ok(()) final cierra. Bevy, al ver la firma, sabe que el sistema es fallible y lo gestiona.

Error handlers built-in: Bevy trae siete formas predefinidas de reaccionar cuando un sistema fallible devuelve Err. Se configuran por App o por World:

//! cap-06 — sección 6.10: configurar el error handler.
use bevy::prelude::*;
use bevy::ecs::error;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // Por defecto Bevy PANICKEA en cualquier error. Para prod, no.
        .set_error_handler(error::warn)   // log warn, sigue adelante.
        // Otros handlers built-in:
        //   .set_error_handler(error::panic)   // <-- default.
        //   .set_error_handler(error::error)   // log error.
        //   .set_error_handler(error::info)    // log info.
        //   .set_error_handler(error::debug)
        //   .set_error_handler(error::trace)
        //   .set_error_handler(error::ignore)  // silencioso total.
        .add_systems(Startup, cargar_config_desde_archivo)
        .run();
}

Regla práctica: en dev, panic (default) te ayuda a encontrar bugs. En release, warn o ignore evita que un error no crítico tire el juego.

Captura individual con pipe(): a veces quieres capturar el Result de un sistema específico sin afectar al resto. Para eso está pipe:

//! cap-06 — sección 6.10: pipe() para captura individual.
use bevy::prelude::*;

fn sistema_que_puede_fallar() -> Result<(), MiError> { todo!() }

fn manejar_resultado_especifico(
    In(result): In<Result<(), MiError>>,
) {
    match result {
        Ok(()) => info!("éxito"),
        Err(e) => warn!("fallo controlado: {e}"),
    }
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(
            Update,
            sistema_que_puede_fallar.pipe(manejar_resultado_especifico),
        )
        .run();
}

pipe convierte el Result en un In<Result> que pasa a la función pipeada. Si el sistema tiró Err, la función pipeada recibe el error y decide.

Comandos fallibles con queue_handled(): los commands también pueden devolver Result:

//! cap-06 — sección 6.10: command con manejo individual.
use bevy::prelude::*;

fn spawn_enemigo_en_tile(
    mut commands: Commands,
    tile: IVec2,
) {
    commands.queue_handled(
        |world: &mut World| -> Result {
            if let Some(mut spawner) = world.get_resource_mut::<EnemySpawner>() {
                spawner.spawn_at(tile)?;
            }
            Ok(())
        },
        |error, context| {
            error!("fallo al spawnear enemigo en {tile:?}: {error}");
        },
    );
}

queue_handled recibe un closure que puede fallar y un handler específico para ese fallo. Si el closure devuelve Err, el handler se ejecuta; si devuelve Ok(()), no pasa nada.

Errores custom y From: si quieres que tu tipo de error se convierta automáticamente en BevyError, implementa From para los tipos que uses con ?:

//! cap-06 — sección 6.10: error custom.
use bevy::prelude::*;
use std::io;

#[derive(Debug)]
enum GameError {
    Io(io::Error),
    Parse(String),
    Asset(String),
}

impl From<io::Error> for GameError {
    fn from(e: io::Error) -> Self { GameError::Io(e) }
}

fn sistema_con_error_custom() -> Result<(), GameError> {
    let f = std::fs::File::open("save.dat")?;   // io::Error -> GameError.
    Ok(())
}

BevyError implementa From<E> para cualquier tipo que tenga std::error::Error, así que en la mayoría de casos no necesitas hacer nada: el ? propaga y Bevy lo acepta.

Cuándo SÍ usar sistemas fallibles:

Cuándo NO usar sistemas fallibles:

**Metedura de pata** — Usar `anyhow::Result` en un sistema Bevy. Entra en conflicto con `bevy::ecs::error::Result` por el glob import de `bevy::prelude::*`. Si necesitas `anyhow` por su `context()`, defínelo detrás de un `type Result<T> = anyhow::Result<T>;` y reexporta con un nombre distinto, o usa `bevy::ecs::error::Result` a secas.

Otro clásico: olvidar que un sistema con `Result` tiene **overhead de error handling**. Si tu sistema es un hot path (física de 100k partículas), devuelve `()` y maneja errores con `unwrap_or_else` internamente. La regla: `Result` para sistemas que **razonablemente pueden fallar**; `()` para sistemas deterministas.
★ **Rust 1.0 (2015)** — el operador `?` se introdujo como feature nightly
  en 2014 y se estabilizó en Rust 1.13 (2016). La idea vino de Swift y
  F#. Bevy adoptó `?` para sistemas en 0.15 (2024), nueve años después.
  El retraso fue deliberado: el equipo quería entender bien cómo
  encajaba con el patrón de inyección de dependencias antes de
  permitir errores propagables.

★ **Bevy 0.15 (septiembre 2024)** — la unificación del sistema de
  errores permitió que `Query::single`, `Commands::queue` y otros
  métodos que antes paniqueaban silenciosamente pasaran a devolver
  `Result`. El PR que lo introdujo (parte del esfuerzo "Result in
  systems") abrió la puerta a los error handlers configurables vía
  App::set_error_handler. Hasta entonces, el programador tenía
  que usar get_single o unwrap_or_else para no paniquear.

6.11 La regla de oro: eventos para "hechos", state para "estado"

Patrón del capítulo: "Los eventos son telegramas, no llamadas"

>

Regla: un evento representa un hecho que ya ocurrió (un alien murió, una moneda se recogió, un nivel se completó). NO es un pedido ni una llamada a procedimiento. NO es "haz esto", es "esto pasó".

>

Por qué importa: si tu evento es "haz X", estás acoplando emisor y receptor. El emisor sabe qué tiene que hacer el receptor. El receptor es sustituible solo si el emisor se acuerda de reescribir el evento. Feo. Si tu evento es "esto pasó", el emisor se desentiende, y cualquier número de receptores puede reaccionar como quiera. Limpio, extensible, mantenible.

>

Ejemplos buenos: DamageRecibido { ... }, MonedaRecogida { ... }, NivelCompletado { ... }, EnemigoSpawneado { ... }.

Ejemplos malos: MataAlEnemigo { ... } (eso es un comando, no un evento), AplicaDanoAlPlayer { ... } (idem), EmpiezaElJuego { ... } (eso es un cambio de estado, no un hecho).

>

Truco: si tu evento contiene verbos en imperativo ("mata", "salta", "ataca"), replantéalo. Si contiene pasado ("ha muerto", "ha saltado", "ha atacado"), vas bien.

★ **Star Wars: Galaxies (Sony Online, 2003)** — el MMO usaba un bus de
  eventos global con prioridad y propagación por zonas. Cuando un cazarrecompensas
  aceptaba un contrato, el evento `BountyAccepted` se disparaba en el servidor y
  llegaba a todos los clientes en un radio de 50 metros. Décadas antes de que
  ECS lo popularizara, los MMOs ya necesitaban un "event bus" arquitectónico.
★ **League of Legends (Riot Games, 2009)** — el motor interno usa un
  sistema de eventos tipados (parecido a `Event` de Bevy) para casi todo:
  kills, items comprados, hechizos lanzados, wards colocados. Riot publica
  un "Riot Games Engineering Blog" donde detallan cómo algunos de esos
  eventos viajan a través de la arquitectura en menos de 50 ms.
★ **Half-Life 2 (Valve, 2004)** — la "entity I/O system" del Source engine
  es, en esencia, un bus de eventos entre entidades del mapa. Un botón
  puede disparar un evento que abre una puerta, que a su vez dispara otro
  que enciende una luz. Valve documentó la mitad en sus SDK; la otra mitad
  se aprende abriendo mapas de la comunidad.

Lo que vimos

En el siguiente

En el cap 7 vamos con los schedules y system sets: el orden en que corren los systems, cómo decir "este system va antes que este otro", cómo agruparlos en "fases" (PreUpdate, Update, PostUpdate, FixedUpdate, Last), y cómo Bevy te avisa (con un warning, no con un panic) cuando dos systems podrían ejecutarse en cualquier orden. Es donde el juego pasa de "código que funciona" a "código que se comporta de forma predecible". Prepara la batuta, que vamos a dirigir una orquesta.