— Frase robada de una charla de GDC que no puedo citar sin tomarme tres cafés.
Abre cualquier motor de juegos "moderno" y verás un menú tentador: ECS, behavior trees, observers, eventos, schedule graphs, fixed timestep... La mayoría de las cosas que vas a usar con Bevy ya existían cuando tu bisabuelo llevaba pantalones de campana. Lo que cambió no son las ideas, sino el lenguaje en que se cuentan y el café que tomaban sus autores.
Este capítulo es una visita guiada por cuarenta y pico años de juegos 2D, no para coleccionar lore, sino para mostrar cómo cada problema recurrente —"¿cómo hago que el fantasma decida?", "¿cómo evito que mi código se vuelva un plato de espaguetis?", "¿cómo represento una unidad en un RTS?"— se resolvió una y otra vez con patrones que siguen vivos en Bevy. Vamos a ver FSM, entity-component, observer, event bus, behavior tree y, ya en los años 2010, la versión moderna del entity-component llamada ECS.
Piensa en una cocina de restaurante: cada cocinero hace una sola cosa, pero entre todos sacan el menú. Esa es la idea que vertebra el libro, y la lleva defendiendo la industria desde los arcades, antes de que nadie la llamara "arquitectura".
Antes de hablar de patrones, una línea de tiempo apretada. No es exhaustiva (sería un libro entero), pero ancla cada idea a un juego que ya conoces.
1978 Space Invaders (Taito) ............ primer "objeto" en RAM
1980 Pac-Man (Namco) ................... primera FSM comercial en fantasmas
1985 Super Mario Bros (Nintendo) ....... tiles + colisiones AABB
1989 Prince of Persia (Mechner) ........ animación rotoscópica
1993 Doom (id Software) ................ separación datos/lógica y doors/triggers
1995 Warcraft (Blizzard) ............... unidades como entidades seleccionables
1997 Age of Empires (Microsoft) ........ IA basada en tareas + pathfinding
1997 Dungeon Keeper (Bullfrog) ......... IA basada en scripts
1998 Baldur's Gate (BioWare) ........... código espagueti con guarnición
1998 Thief (Looking Glass) ............. IA stim-response con estados progresivos
1999 RollerCoaster Tycoon (Sawyer) ..... "ECS accidental" escrito en ensamblador
2014 Shovel Knight (Yacht Club) ........ plataformas casi ECS hechas a mano
2016 Stardew Valley (ConcernedApe) ..... UI densa + save local, mod-friendly
2016 Overwatch (Blizzard) .............. ECS en producción AAA
2017 Hollow Knight (Team Cherry) ...... tilemap masivo + luz 2D
2018 Celeste (Maddy Makes Games) ....... fixed timestep para plataformas perfectas
2020 Hades (Supergiant) ................ ECS indie, música adaptativa
2021 Inscryption (Daniel Mullins) ...... save con migración de versiones
2021 Bevy 0.6 (Cartes) ................. el ECS de Rust que une todo esto
Hay una flecha invisible que conecta todas esas viñetas: cada juego aprendió algo del anterior, se equivocó, y dejó un patrón escrito en alguna parte.
El 1980 Namco lanzó Pac-Man. Su director, Toru Iwatani, le pidió a su programador Shigeo Funaki que los fantasmas no se sintieran "tontos". El resultado fue un hito técnico y pedagógico: cada fantasma tenía cuatro estados (House, Scatter, Chase, Frightened) y un ciclo temporal (Scatter 7 s → Chase 20 s → Scatter 7 s → …). Cuando Pac-Man comía una power pellet, se activaba Frightened durante ~6-8 s, el fantasma se ponía azul y huía en línea recta. Si lo tocabas en ese estado, pasaba a Eaten (los clásicos ojos que vuelven a casa por el camino más corto).
Esto es una máquina de estados finitos: un ente con un estado interno, transiciones disparadas por condiciones, y un comportamiento distinto por estado. Es tan vieja como la informática, pero en Pac-Man fue la primera vez que millones de jugadores la sintieron en acción.
FSM / Finite State Machine (patrón): sistema con un número finito de estados y transiciones explícitas entre ellos. Cada estado define un comportamiento; cada transición, una condición.
Estado (concepto): "modo" en el que se encuentra una entidad. Un fantasma está en `Chase` o en `Frightened`, no en ambos.
Transición (concepto): cambio de estado disparado por una condición (timer, evento, colisión).
Componente (concepto): dato que se le "pega" a una entidad. Hoy Bevy lo llama `Component`; en 1980 nadie lo llamaba así, pero la idea ya estaba.
En Space Invaders (Taito, 1978), Tomohiro Nishikado metió en la memoria del Intel 8080 un array de 55 invasores con coordenadas (x, y). Cada fila se movía al unísono y disparaba cuando le tocaba. No había "clase invasor" con herencia, había una tabla en RAM. Cada invasor era, en esencia, un conjunto mínimo de datos (posición, sprite, fila) más una función que actuaba sobre el array.
Eso es entity-component avant la lettre: la entidad (índice en el array) no contiene comportamiento; los datos van sueltos en columnas; el comportamiento vive en sistemas aparte.
Super Mario Bros (Nintendo, 1985) llevó la idea a otro nivel. El mundo se dividía en tiles de 16×16 píxeles: bloques, suelo, tuberías, monedas. Mario tenía una posición, velocidad, estado (Small, Big, Fire) y un sprite. Una seta era un sprite con una velocidad horizontal y un timer de rebote. Un Goomba era un sprite con velocidad y una IA microscópica ("camina hasta chocar con un bloque, entonces date la vuelta").
Nada de eso era herencia. Era composición: el mismo motor recorría tablas de objetos y aplicaba reglas genéricas. ¿Mario es invencible porque heredó de MarioEstrella? No: porque tiene un componente Invencible { restante: 8.0 } y una flag StarPower. Si le quitas la estrella, deja de ser invencible. Esa separación entre lo que eres y lo que tienes es la columna vertebral de ECS hoy.
Saltemos a Prince of Persia (1989), Jordan Mechner. Mechner grabó a su hermano David saltando, gateando, cayendo, y luego pintó frame a frame sobre las fotos. El resultado: animación más fluida que cualquier arcade anterior. Lo importante aquí no es solo el arte: es que los datos de animación se separaron del código de juego. Un archivo de frames por personaje, índices, duraciones, callbacks de evento ("cuando termina la animación de caída, pasa a Idle"). Esto es un patrón de data-driven animation: el comportamiento del personaje lo dicta una tabla, no un switch gigante en C.
Bevy usa exactamente esto en su AnimationPlayer y en sistemas de sprite atlas con índices.
Los 90 fueron la década en que la IA intentó no ser "tonta". Aparecieron tres líneas que perviven:
Idle → Suspicious → Searching → Alarmed). El guardia no "actuaba en consecuencia de la vista": primero evaluaba el estímulo, acumulaba sospecha, y luego decidía. Es stim-response en estado puro. (Ojo: en Thief no usaron el término formal "stim-response"; yo lo uso aquí como nombre del patrón, que coincide con la literatura posterior de IA de juegos.)Hoy Bevy implementa exactamente ese "estimulo → respuesta" con observers: tú lanzas un evento (EntityEvent), y las entidades suscritas ejecutan su lógica. La sospecha del guardia es, literalmente, un Component con un contador que un observer decrementa con el tiempo.
Doom (id Software, 1993) popularizó el bus de eventos: las puertas, los triggers de suelo, los monstruos y las armas se comunicaban con mensajes genéricos (Door::Open, Line::Cross). El motor no sabía qué haría el listener; solo entregaba el mensaje. Hoy esto es un patrón tan ubicuo que casi nadie se detiene a nombrarlo: Bevy lo llama EventReader<T> / EventWriter<T>.
Warcraft (Blizzard, 1995) y Age of Empires (Microsoft, 1997) dieron el salto conceptual: cada unidad (peón, marine, aldeano) era una entidad con atributos (HP, posición, propietario, orden actual) y un conjunto de comportamientos según tipo. La interfaz de selección múltiple era un detalle visual sobre una arquitectura entity-centric.
RollerCoaster Tycoon (1999), de Chris Sawyer, merece una mención especial: está escrito casi enteramente en ensamblador x86 y simula miles de visitantes con datos tabulares que son, sin quererlo, un proto-ECS. Cada visitante tenía estado (posición, hambre, felicidad,bladder), sin métodos propios. Sistemas externos actuaban sobre tablas. Sawyer lo hizo por rendimiento bruto, no por elegancia teórica, pero el resultado es idéntico al que persigue Bevy: datos separados, comportamientos centralizados.
Warcraft: Orcs & Humans (Blizzard, 1995) — uno de los primeros RTS donde grupos de unidades se mueven como uno solo. La selección múltiple obligó a separar "qué soy" (unidad) de "cómo me muevo" (formación).
Thief: The Dark Project (Looking Glass, 1998) — la IA original fue reescrita al 80 % por Tom Leonard tres semanas antes de la fecha de envío. Se sigue enseñando en universidades.
RollerCoaster Tycoon (Chris Sawyer, 1999) — escrito en ensamblador x86 con un editor hexadecimal como IDE. Compilaba en 30 segundos y corría miles de agentes.
A finales de los 90 y principios de los 2000, la orientación a objetos reinaba. Si eras serio, hacías tu class Player : public GameObject : public Entity : public Updatable. Si eras más serio todavía, añadías design patterns: Singleton, Factory, Observer, Visitor... Y todo se fue volviendo un jeroglífico.
El problema con OOP en juegos es triple:
class Zombie : public Enemy : public Actor : public Collidable : public Renderable, ¿qué pasa si quieres un zombi que no es Collidable? ¿Haces class FlyingZombie? ¿Duplicas código? ¿Un mixin? Cada nuevo caso rompe la jerarquía.Player tiene 12 métodos virtuales y un update() que llama a input() → physics() → animation(), la CPU no puede predecir saltos y se pierde entre cachés.OOP no está "mal" en abstracto; pero para un dominio con cientos o miles de entidades semi-independientes, la herencia es la herramienta equivocada. La industria lo aprendió por las malas.
❌ Clasificar por *lo que las cosas son*: clase `Player`, clase `Enemy`, clase `EnemyWithShield`, clase `EnemyWithShieldAndFireAura`...
Terminas con una pirámide de 17 niveles donde añadir un componente nuevo implica editar 4 archivos.
✅ Clasificar por *lo que las cosas tienen*: una entidad tiene `Health`, otra tiene `Position`, otra tiene `Sprite`. El comportamiento vive en sistemas que filtran por presencia de componentes.
Añadir un aura de fuego = un componente nuevo, sin tocar las clases existentes.
💡 Por qué: la herencia favorece jerarquías; los juegos son grafos.
Antes de Bevy hubo Amethyst, un motor ECS en Rust que arrancó como proyecto en 2016. Amethyst sentó las bases: Specs como librería de ECS, plugins como unidades modulares, sistemas tipados. En 2021, Amethyst publicó "Starting fresh": dejaban el proyecto, recomendaban oficialmente a Bevy como heredero espiritual, y donaban lo que tenía sentido.
Bevy arrancó en 2019 de la mano de Carter Anderson (alias "Cartes") como un experimento personal. Su premisa: un ECS sin compromisos, con un render graph moderno, schedules explícitos, y una promesa de compilación rápida. Para 2021 ya era la opción de facto de quien quisiera hacer juegos 2D en Rust. La versión actual del libro, 0.18/0.19, es el resultado de cinco años de iteración despiadada: cada vez que Bevy simplifica una API, otra se deprecia. Hay dolores de cabeza, sí, pero también una coherencia interna que motores más viejos no tienen.
La genealogía completa, entonces, es:
arcade (entidad-en-tabla) → OOP (herencia) → datos-tabulares (RCT, Thief)
↓
ECS "moderno" en Rust: Amethyst (2016-2021)
↓
Bevy (2019-)
Bevy no inventó ECS, lo consolidó, lo puso en Rust con ownership (posesión de memoria gestionada por el compilador) y lo adornó con observers, required components, schedules y un render graph que se explica solo. Cada capítulo de este libro mira un patrón de los que acabamos de ver y muestra cómo Bevy lo implementa hoy.
Para no perder el hilo, los patrones que aparecen en el resto del libro, mapeados a su origen:
| Patrón | Nació (más o menos) | Dónde lo verás en Bevy |
|---|---|---|
| FSM | Pac-Man (1980) | #[derive(States)], SubStates |
| Entity-Component | Space Invaders / SMB (1978-85) | Entity, #[derive(Component)] |
| Animation pipeline | Prince of Persia (1989) | AnimationPlayer, atlas |
| Scripted behavior / BT | Dungeon Keeper (1997) | BT como componentes (cap. 21) |
| Event bus | Doom (1993) | Event<T>, EventReader<T> |
| Stim-response | Thief (1998) | Observer, Trigger<E> |
| ECS moderno | Overwatch (2016 blog/GDC 2017) | World, System, archetype |
| Fixed timestep | Celeste (2018) | FixedUpdate, schedule |
| Composición indie | Hades (2020) | entidades con muchos componentes |
ECS / Entity-Component-System (arquitectura): modelo donde la entidad es un id, los datos van en componentes, y los comportamientos son funciones puras llamadas sistemas.
FSM / Finite State Machine (patrón): ver caja anterior.
OOP / Object-Oriented Programming (paradigma): programar con clases que heredan entre sí. Útil para GUI, incómodo para juegos con muchas entidades.
Stim-response (patrón de IA): la entidad recibe estímulos externos y reacciona según su estado interno (sospecha, alerta, miedo). Patrón precursor de los observers modernos.
Observer (patrón): entidad suscrita a un evento. Cuando el evento se dispara, el observer reacciona sin que el emisor sepa quién escucha.
Behavior tree / BT (patrón de IA): estructura en árbol donde cada nodo decide "éxito / fallo / en curso" y se compone en secuencias, selectores y decoradores.
Rotoscopia (técnica de animación): animar sobre imágenes de referencia filmadas del mundo real.
Event bus (patrón): sistema central que reparte mensajes entre productores y consumidores sin que se conozcan entre sí.
Space Invaders (Taito, 1978) — Tomohiro Nishikado lo programó en un Intel 8080 con 2 KB de RAM. Para acelerar el ritmo conforme caían los invasores, simplemente borraba menos filas en cada frame (los píxeles vacíos dejaban el efecto).
Prince of Persia (Jordan Mechner, 1989) — el 50 % de los *frames* de animación se grabaron a las 4 de la madrugada a su hermano David corriendo por el garaje.
Overwatch (Blizzard, 2016) — la charla *"Overwatch Gameplay Architecture and Netcode"* de Tim Ford en GDC 2017 popularizó el término ECS dentro de la industria AAA. Puedes verla en el GDC Vault.
Hades (Supergiant, 2020) — el ECS casero de Supergiant gestiona más de 300 componentes distintos. Cada arma, cada boon de un dios, cada estado narrativo es un componente separado.
Stardew Valley (ConcernedApe, 2016) — Eric Barone escribió todo el motor desde cero en C# usando MonoGame. Es el ejemplo más puro de "data-driven + UI densa + save local" sin frameworks externos.
Problema: tengo cientos de "entes" distintos (zombis, balas, partículas, pickups) que comparten algunos rasgos pero no todos. Crear una clase por combinación es inviable.
Solución: tratar la entidad como un id y almacenar sus datos en componentes planos. Los sistemas iteran sobre las entidades que tengan los componentes que necesitan.
Cuándo sí:
Health).Cuándo no:
En el próximo capítulo bajamos a tierra: Rust. Sin conocerlo, Bevy te parecerá un muro de ladrillos.
Abrimos el capó de Rust: ownership, borrowing, enums, traits, Result y Option. Lo justo para que el compilador deje de insultarte y Bevy deje de sonar a otro idioma.