ECS para programadores POO

En este artículo vamos a explicar ECS para programadores tradicionales POO y cómo cambiar la forma de pensar entre ambos sistemas. Después de comprender qué son las entidades, los componentes y los sistemas, llega el momento de responder una pregunta muy habitual:

¿Por qué ECS puede resultar extraño para los programadores tradicionales acostumbrados a la POO ?

La respuesta es sencilla: ambos enfoques organizan el código de forma completamente distinta.

Mientras que la Programación Orientada a Objetos agrupa datos y comportamiento dentro de una misma entidad, ECS separa ambas responsabilidades para conseguir sistemas más flexibles y fáciles de reutilizar.

En este artículo veremos cómo piensan los programadores tradicionales POO, cómo piensan los programadores ECS y qué cambio de mentalidad se debe realizar para adaptarnos de POO a la arquitectura ECS.

Cómo se piensa en POO

La mayoría de los programadores POO comienzan utilizando objetos lógicos a similitud de la vida cotidiana, los cuales contienen tanto los datos como el comportamiento encapsulados dentro de su clase. Por ejemplo:

class Enemy {
    constructor() {
        this.x = 100;
        this.y = 200;
        this.hp = 50;
    }
    move() {
        this.x += 2;
    }
    attack() {
        console.log("Ataque");
    }
    render() {
        console.log("Dibujar enemigo");
    }
}

Todo pertenece al mismo objeto o clase Enemy y estos datos y comportamientos se pueden heredar y modificar para crear nuevos enemigos con datos y comportamientos diferentes pero reutilizando el código común. En este tipo de programación POO los comportamientos o acciones son los métodos definidos dentro de la clase y se utilizan de forma ligada a ella:

enemy.move();
enemy.attack();
enemy.render();

Este modelo está perfectamente encapsulado y resulta muy intuitivo porque representa cada elemento o entidad del juego como un objeto lógico e independiente. Tenemos todo el código unificado, localizado y es muy fácil de modificar y depurar errores.

En este punto el modelo POO parece casi una solución perfecta para un programador tradicional. Al principio todo funciona perfectamente, sin embargo, conforme aparecen nuevos tipos de enemigos, armas y comportamientos, las clases comienzan a crecer. De manera que cada nueva característica en un nuevo enemigo suele terminar añadiendo más variables, más métodos, más herencia y en definitiva más dependencias.

Poco a poco aparecen más clases heredadas en una jerarquía donde cada vez resulta más difícil mantener el código unificado y localizado, de modo que también se hace más difícil modificar y depurar errores. La solución perfecta empieza a ser la pesadilla de todo programador: tocar una cosa puede modificar otras.

Enemy
├─ FlyingEnemy
├─ TankEnemy
├─ BossEnemy
├─ MageEnemy
└─ etc

Cómo se piensa en ECS

Los programadores ECS abordan el problema desde una perspectiva completamente diferente a POO. En lugar de crear objetos lógicos con comportamientos (métodos) encapsulados dentro de la propia entidad, en ECS se crea entidades simples a las que únicamente se le añaden componentes (datos). Por ejemplo:

let enemy = {
    position: {
        x: 100,
        y: 200
    },
    velocity: {
        vx: 2,
        vy: 0
    },
    health: {
        hp: 50
    }
};

La entidad Enemy únicamente almacena datos, no contiene comportamiento (acciones o métodos) y por tanto no tiene ningún tipo de vínculo lógico. La lógica vive dentro de los sistemas, quienes son funciones independientes que sólo operan con los datos (componentes) que les proporcionan las entidades. Por ejemplo:

function movementSystem(entities) {
    for (const entity of entities) {
        if (!entity.position || !entity.velocity) continue;
        entity.position.x += entity.velocity.vx;
        entity.position.y += entity.velocity.vy;
    }
}

En ECS el movimiento no pertenece al objeto o entidad Enemy, sinó que pertenece al sistema, y al sistema únicamente le interesa saber los datos (componentes) con los que debe trabajar. Esta es la diferencia más importante entre ambos enfoques y en la forma de pensar. Para los programadores ECS existe una libertad total para operar con los datos de cualquier entidad sin tener los métodos asociados de una clase POO. Veámoslo de forma esquemática:

Programadores POO

A diferencia de ECS, para los programadores tradicionales POO cada objeto sabe cómo funciona y sus métodos están asociados a él.

Objeto

Datos
+
Comportamiento

enemy.move();
enemy.attack();
enemy.render();

Programadores ECS

A diferencia de POO, para los programadores ECS la entidad no sabe hacer nada, en cambio los sistemas trabajan sobre sus componentes (datos).

Entidad
    ↓
Componentes (datos)

Sistemas
    ↓
Comportamiento

moveLinearSystem(enemy);
moveFollowSystem(enemy);
moveAngleSystem(enemy);

Al trabajar con ECS suele parecer que simplemente hemos movido los métodos fuera de los objetos. Sin embargo, la ventaja real aparece cuando una misma entidad puede ser procesada por distintos sistemas sin necesidad de modificar su estructura. La entidad no necesita cambiar y los componentes tampoco, lo único que cambia es el sistema que procesa esos datos.

Esta es la verdadera ventaja y cambio en la forma de pensar y procesar los datos que tiene ECS.

ECS comparado con POO

En la Programación Orientada a Objetos, cuando aparecen nuevos tipos de movimiento solemos terminar creando nuevas clases únicamente para saber que tiene un nuevo método move() y diferenciarlos mediante un nuevo nombre de clase:

class Enemy {
    move() {
        // Movimiento lineal
    }
}

class FlyingLinearEnemy extends Enemy
class FlyingFollowEnemy extends Enemy
class FlyingAngleEnemy extends Enemy

Otra solución puede ser añadiendo más condiciones dentro del método move(), de forma que a medida que aumenta el número de comportamientos, la complejidad también aumenta.

move() {
    switch(this.type) {
        case "linear":
            ...
            break;

        case "follow":
            ...
            break;

        case "angle":
            ...
            break;
    }
}

En cambio ECS permite reutilizar esos mismos datos con sistemas diferentes sin tener que rediseñar las entidades continuamente.

moveLinearSystem(enemy);
moveFollowSystem(enemy);
moveAngleSystem(enemy);

Ideas clave entre ECS y POO

  • POO tiene una encapsulación lógica donde al principio todo se ve limpio, lógico y reutilizable
  • POO empieza a crear clases heredadas a las que queremos cambiar datos y comportamientos
  • POO crece y resulta difícil de mantener debido a su encapsulamiento e incluso puede empezar a decaer su rendimiento debido al recorrido que se hace en el árbol de clases heredadas
  • ECS utiliza clases o entidades vacías a las que sólo se añaden componentes (datos)
  • ECS utiliza sistemas para dar comportamientos (acciones) a cada uno de los componentes (datos)
  • ECS desvincula la lógica y el encapsulamiento de los métodos en POO, logrando libertad a la hora de decidir qué sistema opera sobre cada componente (dato)
  • ECS mantiene su tasa de rendimiento intacta aunque crezca el número de sistemas o componentes
  • Cuando descubrimos la Programación Orientada a Objetos parece que hemos encontrado la solución perfecta para modelar cualquier sistema. Cada objeto posee sus propios datos y comportamientos, de forma muy parecida a cómo entendemos los objetos del mundo real.
  • Más adelante aprendemos a reutilizar código mediante herencia. Un vehículo puede convertirse en coche, moto, camión o autobús compartiendo características comunes. Esto permite organizar mejor el código y evitar duplicaciones innecesarias.
  • Sin embargo, los videojuegos no siempre siguen las reglas del mundo real. Un enemigo puede volar, disparar, perseguir al jugador, teletransportarse, transformarse en otro enemigo o combinar comportamientos completamente distintos entre sí. A medida que intentamos representar estas situaciones mediante jerarquías de clases, el diseño comienza a complicarse.
  • Es precisamente en este punto cuando ECS empieza a cobrar sentido. En lugar de preguntarnos qué es una entidad, comenzamos a preguntarnos qué datos posee y qué sistemas pueden procesarla. Este cambio de mentalidad permite construir comportamientos complejos sin depender de jerarquías cada vez más difíciles de mantener.

¿Es necesario abandonar la POO?

No, de ninguna manera. De hecho, muchos proyectos utilizan arquitecturas híbridas y es perfectamente posible combinar:

  • Objetos.
  • Datos planos.
  • Componentes.
  • Sistemas.

La clave consiste en utilizar cada herramienta donde aporte más ventajas. Por este motivo muchos programadores incorporan ideas de ECS de forma gradual sin abandonar completamente la POO o Programación Orientada a Objetos.

Esta arquitectura híbrida es un paradigma de programación que poco a poco todo programador va buscando de forma inconsciente y que yo personalmente he ido implementando en mis videojuegos. Sin ir más lejos, la forma de trabajar de mi librería gráfica utiliza un esquema ECS para dibujar sus elementos gráficos con el mayor rendimiento posible.

ECS para programadores POO

Para los programadores, la principal diferencia entre POO y ECS no está en la sintaxis ni en el lenguaje utilizado, sino en la forma de organizar el código. Mientras que la Programación Orientada a Objetos agrupa datos y comportamiento dentro de un mismo objeto, ECS separa ambas responsabilidades mediante entidades, componentes y sistemas.

Comprender este cambio de mentalidad es probablemente el paso más importante para empezar a trabajar con ECS. Una vez superada esa barrera inicial, resulta mucho más sencillo entender cómo construir sistemas reutilizables, flexibles y fáciles de mantener dentro 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