ECS híbrido para la arquitectura de un videojuego

En este artículo explicaremos cómo crear un ECS híbrido para la arquitectura de un videojuego, integrando todos los sistemas anteriores.

A lo largo de esta serie hemos ido construyendo diferentes piezas de una arquitectura ECS híbrida aplicada al desarrollo de videojuegos. Hemos visto qué son las entidades, los componentes y los sistemas, cómo podemos combinar ECS con POO y cómo utilizar esta arquitectura para gestionar enemigos, máquinas de estados, armas, proyectiles, colisiones y partículas.

Llegados a este punto podemos dar un paso más y observar cómo todas estas piezas pueden convivir dentro de un videojuego completo. El objetivo no es convertir absolutamente todo el código en ECS. Al contrario, la arquitectura que utilizo en mis videojuegos combina diferentes técnicas dependiendo de la responsabilidad de cada elemento.

Por un lado tenemos controladores y clases especializadas que encapsulan determinadas funcionalidades y, por otro, sistemas ECS encargados de procesar grandes conjuntos de datos.

El resultado es una arquitectura ECS híbrida donde cada herramienta se utiliza allí donde realmente aporta valor.

La arquitectura ECS general

Una forma sencilla de representar la arquitectura ECS híbrida general de mi videojuego sería:

                VIDEOJUEGO
│
gameClass
│
┌──────────┴───────────┐
│ │
pageClass levelClass
│ │
Configuración Estados del juego
Información │
┌─────┴─────┐
│ │
Sistemas ECS ...
│
┌─────────────┼─────────────┐
│ │ │
playerSystem enemySystem weaponSystem
│ │ │
shootSystem shootSystem ...

Esta estructura permite separar claramente las diferentes responsabilidades del videojuego. Pero antes de entrar en los sistemas ECS conviene diferenciar otro grupo de elementos que también forman parte de la arquitectura.

Controladores globales

Un videojuego necesita determinadas funcionalidades que son utilizadas prácticamente por todo el código. En mi caso dispongo de diferentes controladores globales encargados de tareas como:

  • teclado
  • ratón
  • sonido
  • gráficos
  • obtención de plantillas mediante Factory.

Estos elementos no forman parte de la jerarquía del juego. Su responsabilidad consiste en proporcionar servicios que pueden ser utilizados por las diferentes clases y sistemas.

Por ejemplo, el sistema de enemigos puede utilizar el controlador gráfico para representar un enemigo, mientras que el sistema de disparos puede utilizarlo para dibujar sus proyectiles.

Estos controladores están disponibles para el resto de la arquitectura, pero no representan el juego ni contienen su lógica principal. Esta distinción resulta importante porque evita convertir una clase global en un enorme objeto encargado de controlar todo el videojuego.

La arquitectura del juego

En el ECS híbrido que utilizo para la arquitectura de mis videojuegos, hay tres clases especializadas en organizar el flujo general del videojuego, antes del juego en sí mismo. Podemos representarlo conceptualmente así:

 CONTROLADORES GLOBALES (sin jerarquías)

gameClass
    │
    ├── pageClass
    │
    └── levelClass
            │
            ├── playerSystem
            ├── enemySystem
            ├── weaponSystem
            ├── shootSystem
            ├── bonusSystem
            └── ...

En esta arquitectura ECS híbrida cada clase tiene una responsabilidad concreta:

  • gameClass: Representa el nivel superior de la aplicación y decide qué parte del videojuego debe ejecutarse
  • pageClass: Se utiliza para mostrar las diferentes páginas de configuración, información, menús u otras pantallas que no forman parte directamente de la partida
  • levelClass: Es la clase que realmente ejecuta el videojuego

Esta separación permite diferenciar claramente la aplicación que contiene el juego de la propia partida.

gameClass: controlando el flujo general

La clase gameClass actúa como coordinador principal. Una de sus responsabilidades es decidir qué página debe mostrarse o qué parte de la aplicación debe ejecutarse. Para ello, utilizo un método run() que puede utilizar un switch para seleccionar el estado correspondiente. De forma simplificada:

run() {

    switch (this.page) {

        case PAGE.MENU:
            // Mostrar menú
            break;

        case PAGE.CONFIG:
            // Mostrar configuración
            break;

        case PAGE.INFO:
            // Mostrar información
            break;

        case PAGE.GAME:
            this.level.run();
            break;
    }
}

La idea importante es que gameClass no necesita conocer cómo funcionan internamente los enemigos, las armas o los proyectiles. Su responsabilidad consiste en controlar el flujo general de la aplicación. Cuando llega el momento de ejecutar una partida, delega esa responsabilidad en levelClass.

pageClass: las páginas del videojuego

Las diferentes páginas que puede mostrar el videojuego tampoco necesitan formar parte de los sistemas ECS. Por ejemplo, podemos tener páginas para:

  • menú principal
  • configuración
  • información
  • selección de opciones
  • pantalla de pausa
  • puntuaciones

Para ello podemos utilizar una clase especializada como pageClass. Esto es un buen ejemplo de dónde una solución orientada a objetos puede resultar más sencilla que intentar convertir cada pantalla en una colección de entidades y sistemas ECS.

El objetivo de ECS no es obligarnos a utilizar entidades para absolutamente todo. Si una clase sencilla resuelve correctamente una responsabilidad concreta, no existe ninguna razón para complicarla artificialmente.

levelClass: donde realmente se ejecuta el juego

Cuando gameClass determina que debemos ejecutar una partida, entra en funcionamiento levelClass. Esta clase representa el nivel que estamos ejecutando y contiene los diferentes sistemas necesarios para procesarlo.

Además, al igual que gameClass, puede utilizar un switch para controlar los diferentes estados de la partida. Por ejemplo:

run() {

    switch (this.state) {

        case STATE.START:
            // Preparar nivel
            break;

        case STATE.PLAY:
            // Ejecutar partida
            break;

        case STATE.CRASH:
            // Jugador destruido
            break;

        case STATE.GAMEOVER:
            // Fin de partida
            break;
    }
}

Aquí encontramos una separación muy importante. gameClass decide qué parte de la aplicación estamos ejecutando, mientras que levelClass decide qué está ocurriendo dentro de la partida.

En el ECS híbrido para la arquitectura de un videojuego que utilizo, los sistemas ECS trabajan por debajo de esta capa.

Los sistemas ECS

Dentro de levelClass encontramos los diferentes sistemas ECS especializados y descritos en artículos anteriores. Por ejemplo:

levelClass
    │
    ├── playerSystem
    ├── enemySystem
    ├── weaponSystem
    ├── shootSystem
    ├── bonusSystem
    ├── particleSystem
    └── collisionSystem

Cada sistema administra una responsabilidad concreta:

  • El sistema de enemigos almacena y actualiza los enemigos
  • El sistema de jugadores administra los datos correspondientes a los jugadores
  • El sistema de armas administra las armas asociadas a las entidades que las utilizan
  • El sistema de disparos procesa los proyectiles activos
  • El sistema de partículas procesa grandes cantidades de efectos visuales
  • El sistema de colisiones se encarga de detectar los contactos entre los elementos que pueden interactuar.

De esta forma, el nivel no necesita contener toda la lógica del videojuego. Simplemente coordina los diferentes sistemas.

Entidades y componentes

Como explicamos en artículos anteriores, los sistemas trabajan con entidades que mantienen estructuras de datos sencillas. Por ejemplo, un jugador puede estar representado mediante:

let player = {
    id: ID.PLAYER,
    type: TYPE.SHIP,
    icon: Factory.create(ICON.SHIP1),
    shield: Factory.create(ICON.SHIELD),
    orb: Factory.create(ICON.ORB),
    weapons: new weaponSystemClass(...)
};

El jugador puede contener diferentes componentes y sistemas asociados, pero cada uno mantiene una responsabilidad concreta.

Otros elementos del videojuego pueden seguir exactamente el mismo principio:

PLAYER
    ├── datos
    ├── componentes gráficos
    ├── armas
    └── ...

ENEMY
    ├── datos
    ├── componentes gráficos
    ├── FSM
    └── ...

PROJECTILE
    ├── posición
    ├── velocidad
    ├── daño
    └── ...

PARTICLE
    ├── posición
    ├── velocidad
    ├── vida
    └── ...

No todas las entidades necesitan exactamente los mismos componentes. Precisamente esa es una de las ventajas de ECS: podemos construir entidades diferentes combinando los datos que realmente necesitan.

El bucle principal

Toda esta arquitectura necesita finalmente un punto de entrada que ejecute el videojuego. En mi caso, el HTML inicia los controladores globales, carga las imágenes del videojuego y, cuando ya se han cargado, saltamos al inicio de la aplicación mediante una función init():

// Controladores globales del juego
let Game = null;     
let Factory = null;             
let Sound = new audioClass();  
let Keyboard = new keyboardClass();
let Mouse = new mouseClass("canvasID");
let Graphics = new canvasAPI("canvasID");   

// Carga de imágenes
...
Graphics.load('ship', 'sprites/player_ship.png', init);		

function init() {
    Factory = new factoryClass();
    Game = new gameClass();
    animateScene();
}

Esta inicialización es básica para que todo esté correctamente creado antes de saltar al bucle principal y ejecutar el juego. Una vez inicializado el juego comienza el bucle principal:

function animateScene() {
    Game.run();
    requestAnimationFrame(animateScene);
}

Aquí podemos observar algo muy interesante. El bucle principal no necesita conocer ningún sistema ECS. No necesita saber cómo funcionan los enemigos, las armas, los proyectiles o las partículas.

Únicamente ejecuta:

Game.run();

A partir de ahí, cada nivel y cada sistema se encargan de realizar su propia responsabilidad. La arquitectura completa queda así:

HTML
 │
 └── animateScene()
          │
          ▼
      Game.run()
          │
          ▼
      gameClass
          │
          ├── pageClass
          │
          └── levelClass
                  │
                  ├── playerSystem
                  ├── enemySystem
                  ├── weaponSystem
                  ├── shootSystem
                  ├── bonusSystem
                  ├── particleSystem
                  └── collisionSystem

Esta separación hace que el bucle principal permanezca extremadamente sencillo, aunque el videojuego que existe debajo pueda ser mucho más complejo.

Videojuegos con ECS híbrido

Llegados a este punto puede parecer que nuestra arquitectura tiene muchas clases y que estamos alejándonos de ECS. En realidad ocurre justo lo contrario. El ECS no tiene por qué ocupar toda la arquitectura del videojuego.

En nuestro caso se encuentra principalmente en la forma en que organizamos y procesamos las entidades y sus datos. Los sistemas especializados trabajan con conjuntos de datos:

playerSystem   → jugadores
enemySystem  → enemigos
shootSystem    → proyectiles
bonusSystem   → bonus
particleSystem → partículas

Cada sistema procesa sus propias unidades y aplica sobre ellas la lógica correspondiente. Por encima de estos sistemas utilizamos clases POO para resolver responsabilidades que se benefician de la encapsulación.

Esto es precisamente lo que hemos denominado a lo largo de esta serie ECS híbrido.

POO + ECS

Una de las principales conclusiones de esta serie es que POO y ECS no tienen por qué competir entre sí.

Podemos utilizar POO para construir la estructura general del videojuego y ECS para organizar y procesar los datos que necesitan ser actualizados masivamente. En mi arquitectura podemos encontrar ambos enfoques:

POO
 │
 ├── gameClass
 ├── pageClass
 ├── levelClass
 ├── weaponSystemClass
 └── controladores globales
          │
          ▼
        ECS
          │
 ├── entidades
 ├── componentes
 └── sistemas
          │
          ├── jugadores
          ├── enemigos
          ├── proyectiles
          ├── partículas
          └── ...

No existe una frontera rígida entre ambos modelos. Una clase puede encapsular un sistema cuando existen dependencias fuertes que hacen que esa solución sea más sencilla.

Es exactamente lo que vimos con weaponSystem.

Por otro lado, cuando tenemos grandes cantidades de datos homogéneos, como ocurre con los proyectiles o las partículas, podemos mantener estructuras mucho más sencillas y procesarlas directamente desde un sistema.

Una arquitectura híbrida que puede crecer

Otra ventaja importante de esta arquitectura es que podemos ampliar el videojuego sin tener que modificar continuamente nuestro ECS híbrido que ya funciona:

  • Si queremos añadir un nuevo tipo de enemigo, podemos hacerlo dentro del sistema de enemigos
  • Si necesitamos un nuevo tipo de proyectil, podemos ampliar el sistema de disparos
  • Si queremos crear un nuevo efecto visual, podemos añadir un nuevo tipo de partícula
  • Si necesitamos una nueva arma, podemos incorporarla al sistema de armas
  • Si necesitamos una nueva pantalla de configuración, podemos añadirla a pageClass y al flujo de gameClass.

Cada nueva funcionalidad puede incorporarse en el lugar donde corresponde.

Esto reduce las dependencias entre sistemas y evita que una única clase termine convirtiéndose en el centro de toda la lógica del videojuego.

Conclusiones ECS híbrido para la arquitectura de un videojuego

Después de recorrer toda la serie podemos observar que ECS no es realmente una arquitectura aislada que tengamos que colocar encima de nuestro videojuego. Es una forma de organizar determinadas partes del mismo.

En mi ECS híbrido para la arquitectura de un videojuego combina diferentes niveles de responsabilidad:

  • No es necesario que todas las partes utilicen exactamente el mismo patrón
  • Algunas necesitan encapsulación
  • Otras necesitan procesar grandes cantidades de datos
  • Otras simplemente necesitan controlar el flujo de la aplicación
  • La arquitectura debe adaptarse a estas necesidades

Después de construir todos los ejemplos de esta serie podemos ver que ECS no consiste simplemente en sustituir las clases por arrays de datos.

La verdadera ventaja aparece cuando somos capaces de identificar qué partes del videojuego se benefician de una organización basada en entidades, componentes y sistemas y cuáles necesitan otro tipo de solución.

En mi caso, ECS constituye la base para organizar y procesar las entidades del videojuego, mientras que las clases especializadas permiten encapsular responsabilidades concretas y coordinar aquellas partes que mantienen dependencias más estrechas:

  • Los controladores globales proporcionan servicios comunes como gráficos, sonido, teclado, ratón o creación de recursos
  • gameClass controla el flujo general de la aplicación
  • pageClass gestiona las diferentes páginas de información y configuración
  • levelClass ejecuta realmente la partida y coordina los diferentes sistemas
  • Los sistemas especializados procesan las entidades y componentes que forman el videojuego

El resultado es un ECS híbrido con una arquitectura donde cada pieza del videojuego tiene una responsabilidad concreta y puede evolucionar de forma independiente.

Para mí, esta es precisamente la principal ventaja de utilizar un ECS híbrido: no obliga a elegir entre POO y ECS, sino que permite utilizar ambos enfoques allí donde realmente aportan valor.

Después de todos los ejemplos que hemos visto, podemos entender ECS no como una metodología que deba aplicarse de forma estricta, sino como una herramienta más dentro de nuestra caja de herramientas de programación.

La arquitectura debe adaptarse al videojuego y no el videojuego a la arquitectura:

  • Si una clase simplifica una dependencia, utilizamos una clase
  • Si un sistema puede procesar miles de elementos de forma eficiente, utilizamos un sistema
  • Si unos datos pueden permanecer completamente independientes de la lógica, los mantenemos como datos

Y cuando todas estas técnicas trabajan juntas de forma coherente, podemos construir una arquitectura de videojuego clara, flexible, mantenible y con un buen rendimiento.

Este es, en definitiva, el objetivo de nuestro ECS híbrido para la arquitectura de un videojuego

Este artículo forma parte de la serie ECS: Entidades, Componentes y Sistemas, donde se explica su arquitectura y cómo aplicarla en la programación de videojuegos.

¡ Espero que este artículo sea de vuestro interés !

Logotipo de Safe CreativeLogotipo de Creative Commons Attribution 4.0

Deja un comentario