En este artículo vamos a explicar cómo implementar un sistema de partículas y efectos dentro de una arquitectura ECS híbrida. A diferencia de otros elementos que hemos visto a lo largo de esta serie, las partículas presentan una característica especialmente interesante: normalmente necesitamos gestionar una gran cantidad de elementos muy similares al mismo tiempo.
Explosiones, humo, chispas, fragmentos, destellos o pequeños efectos visuales pueden generar cientos de elementos simultáneamente. Crear una estructura compleja para cada uno de ellos no aportaría ninguna ventaja y podría complicar innecesariamente el código.
Por este motivo, las partículas y los efectos son un buen ejemplo de cómo un ECS puede aprovechar una estructura de datos sencilla y un sistema especializado capaz de procesarlos de forma masiva y con un alto rendimiento.
En mis videojuegos utilizo precisamente este enfoque: las partículas se almacenan como datos planos dentro de un sistema específico, mientras que toda la lógica necesaria para crearlas, actualizarlas y dibujarlas permanece centralizada en dicho sistema.
Partículas como datos
Una partícula no necesita conocer cómo funciona el resto del videojuego. Únicamente necesita almacenar los datos necesarios para representar su estado actual.
Por ejemplo, una partícula podría contener:
{
x: 100,
y: 200,
vx: 3,
vy: -2,
vida: 1,
type: FIRE,
degradacion: 0.02,
color: "rgb(255,120,0)"
}
Estos datos representan su posición, velocidad, vida restante, tipo de partícula, velocidad de degradación y color.
No necesitamos crear una clase Particle ni añadir métodos individuales para cada partícula. Todas las partículas utilizan la misma estructura de datos y serán procesadas posteriormente por el sistema de partículas.
Esto encaja especialmente bien con la filosofía ECS que estamos utilizando a lo largo de esta serie:
- los datos almacenan el estado;
- el sistema contiene la lógica;
- todas las partículas pueden procesarse de forma conjunta.
El sistema de partículas
Para administrar todas las partículas y efectos en nuestro ECS híbrido podemos crear un sistema especializado muy sencillo:
class systemParticleClass {
constructor() {
this.unit = [];
}
add(num, type, obj) {
// Crear partículas
}
update() {
// Actualizar partículas
}
draw() {
// Dibujar partículas
}
}
El constructor únicamente crea el array unit, que será el encargado de almacenar todas las partículas visualmente activas.
De esta forma, el sistema se convierte en el administrador de un conjunto homogéneo de datos. No necesitamos que cada partícula sea una clase independiente con sus propios métodos porque todas utilizan las mismas reglas de funcionamiento.
También es una diferencia importante respecto al sistema de armas que vimos anteriormente. En aquel caso existían dependencias muy directas entre el arma, su propietario y el sistema de proyectiles, por lo que era necesario encapsular esa coordinación dentro de la clase.
Las partículas, en cambio, son elementos mucho más independientes y homogéneos que normalmente no necesitan interactuar directamente con el resto del juego. Simplemente aparecen y desaparecen, por ello, un sistema que gestione directamente sus datos resulta la solución más sencilla.
Creando partículas y efectos
El sistema puede disponer de un método como add(num, type, obj) encargado de crear varias partículas a partir de los datos de un objeto del videojuego. Por ejemplo, cuando destruimos un enemigo podemos utilizar su posición y dimensiones como punto de origen:
add(num, type, obj) {
const centerX = obj.x + (obj.width >> 1);
const centerY = obj.y + (obj.height >> 1);
for (let i = 0; i < num; i++) {
let particle;
switch (type) {
case FIRE:
particle = {
x: centerX,
y: centerY,
vx: (Math.random() - 0.5) * 16,
vy: (Math.random() - 0.5) * 16,
vida: 1,
type: FIRE,
degradacion: Math.random() * 0.02 + 0.01,
color: `rgb(255, ${Math.random() * 150 + 50 | 0}, 0)`,
};
break;
case SMOKE:
particle = {
... datos ....
};
break;
}
this.unit.push(particle);
}
}
El parámetro num determina cuántas partículas queremos crear, type indica qué tipo de partícula queremos generar y obj proporciona la información necesaria para establecer su posición inicial. Para ello, asumimos que obj contiene los datos {x, y, width, height}.
En este ejemplo calculamos primero el centro del objeto destruido. De esta forma todas las partículas nacen desde un punto común y posteriormente se dispersan mediante sus velocidades (vx, vy).
La utilización de Math.random() permite variar ligeramente cada partícula. Aunque todas utilizan la misma estructura de datos y las mismas reglas, cada una recibe unos valores ligeramente diferentes que producen un resultado visual más natural.
Este es precisamente uno de los puntos fuertes de nuestro sistema ECS de partículas y efectos: podemos crear cientos de partículas utilizando exactamente la misma estructura.
Actualizando las partículas y efectos
Una vez creadas, las partículas deben actualizarse en cada ciclo del videojuego. Para ello, el método update() recorre todas las unidades almacenadas en nuestro Entity, Component, System:
update() {
for (let i = this.unit.length - 1; i >= 0; i--) {
let unit = this.unit[i];
unit.x += unit.vx;
unit.y += unit.vy;
unit.vida -= unit.degradacion;
if (unit.vida <= 0) {
this.unit.splice(i, 1);
}
}
}
La lógica es deliberadamente sencilla. En cada ciclo se modifica la posición utilizando la velocidad de la partícula y se reduce su vida mediante el valor de degradación. Si la vida es menor o igual a 0 eliminamos la partícula del array, por este motivo se recorre al revés y evitamos problemas con los índices.
Comportamientos y ciclo de vida
Todas las partículas son gestionadas por el mismo sistema y sus propios datos ya pueden determinar comportamientos diferentes. También podemos modificar un tipo de partículas para crear trayectorias curvas o comportamientos específicos:
switch (unit.type) {
case SPARK:
unit.vy += 0.1;
break;
}
En este caso, la velocidad vertical aumenta progresivamente, simulando un efecto de gravedad. Esto permite crear trayectorias curvas para partículas como chispas, fragmentos o pequeños restos que caen después de una explosión.
También podemos modificar durante la vida de una partícula otros valores como su tamaño, velocidad, opacidad o intensidad. Por ejemplo, vida puede utilizarse como un valor que disminuye progresivamente y que, además, podemos relacionar directamente con la opacidad de la partícula en el momento de dibujarla. De esta forma, la partícula se vuelve progresivamente más transparente hasta desaparecer.
El sistema puede decidir cuándo una partícula ha terminado su ciclo de vida. Sin embargo, no todas las partículas tienen por qué tener una duración limitada. Un efecto como el humo se puede implementar para permanecer activo mientras exista la entidad que lo genera, utilizando una lógica de ciclo de vida diferente. En estos casos podemos utilizar el tipo de partícula para establecer un comportamiento diferente, evitando que su ciclo de vida dependa únicamente de vida.
Lo importante es que las partículas continúan siendo únicamente datos. Es el sistema ECS quien interpreta esos datos y modifica el estado de las partículas y efectos durante cada ciclo de actualización.
Este mismo mecanismo permite controlar prácticamente cualquier propiedad temporal del efecto: duración, tamaño, transparencia, intensidad, velocidad, dirección o cualquier otro valor que necesitemos modificar durante la vida de la partícula.
Así podemos utilizar diferentes configuraciones para representar: explosiones, chispas, humo, fuego, fragmentos, polvo, destellos, efectos de impacto. Creando comportamientos visuales muy diferentes y utilizando una única estructura de datos y un único sistema de partículas, manteniendo intacta la filosofía de nuestro Entity, Component, System híbrido.
Dibujando las partículas
El último paso consiste en representar las partículas en pantalla. Para ello, nuestro método draw() vuelve a recorrer las unidades y utiliza sus datos para realizar el dibujo:
draw() {
for (let i=0; i<this.unit.length; i++) {
let unit = this.unit[i];
ctx.fillStyle = unit.color;
ctx.globalAlpha = unit.vida;
ctx.fillRect(unit.x, unit.y, 4, 4);
}
ctx.globalAlpha = 1;
}
La información necesaria para dibujar ya se encuentra almacenada en cada partícula. El sistema únicamente tiene que utilizar esos datos. En este caso utilizamos las instrucciones nativas de la API canvas para establecer el color y opacidad de cada partícula y la dibujamos como un rectángulo relleno de 4×4 píxeles.
Al utilizar su propiedad vida como nivel de transparencia, la partícula pierde vida y también desaparece progresivamente de la pantalla. Al finalizar el bucle restablecemos la opacidad global del juego.
Esto permite conseguir efectos visuales sencillos sin necesidad de crear sistemas independientes y complejos para cada tipo de partícula.
Partículas y ECS híbrido
Aquí es donde este diseño resulta especialmente interesante. Imaginemos una explosión que genera 100 partículas. Si tenemos varios enemigos destruyéndose simultáneamente, rápidamente podemos llegar a tener cientos o incluso miles de partículas activas. Sin embargo, el código necesario para procesarlas sigue siendo prácticamente el mismo.
Con un único sistema ECS especializado en partículas y efectos podemos procesar grandes cantidades de elementos de forma muy sencilla. Además, al tratarse de datos homogéneos, podemos mantener una estructura muy predecible y evitar crear una jerarquía de clases innecesaria.
Este sistema representa uno de los ejemplos más claros de ECS híbrido que hemos visto en esta serie. Las partículas se representan como datos:
{
x,
y,
vx,
vy,
vida,
type,
degradacion,
color
}
Mientras que el sistema contiene el comportamiento:
particleSystem
│
├── add()
├── update()
└── draw()
La entidad que provoca el efecto tampoco necesita conocer cómo funcionan internamente las partículas. Simplemente puede solicitar al sistema que cree un determinado número de ellas:
particleSystem.add(20, FIRE, enemy);
particleSystem.add(50, SMOKE, {x:100, y:100, width:50, height:100});
etc
El enemigo (o nosotros manualmente) proporciona la información necesaria para determinar dónde debe comenzar el efecto, mientras que el sistema de partículas se encarga del resto con los métodos update() y draw().
De esta forma podemos establecer una separación muy clara:
- el juego determina cuándo necesita producir un efecto
add()crea los elementos necesarios (datos)update()procesa su comportamientodraw()se encarga de su representación
Conclusiones ECS para partículas y efectos
Las partículas son un ejemplo perfecto de cómo una arquitectura ECS híbrida basada en datos y sistemas puede simplificar la gestión de grandes cantidades de elementos homogéneos.
- muchos elementos simultáneos
- estructuras de datos muy similares
- operaciones repetitivas
- poca necesidad de comportamiento individual
- actualización y representación masiva
Cada partícula únicamente almacena los datos necesarios para representar su estado, mientras que el sistema especializado se encarga de crearlas, actualizarlas y dibujarlas. Por este motivo no necesitamos una arquitectura compleja. Un array de datos y un sistema especializado pueden ser suficientes para gestionar una cantidad considerable de elementos.
A diferencia de otros elementos del videojuego que pueden necesitar relaciones más complejas, las partículas no necesitan convertirse en objetos independientes con su propia lógica. Un único sistema puede procesarlas de forma masiva utilizando estructuras de datos sencillas.
En el sistema de armas vimos cómo una clase POO podía simplificar unas dependencias muy estrechas entre propietario, armas y proyectiles. En cambio, para las partículas resulta mucho más natural utilizar directamente un sistema que administre un conjunto de datos homogéneos.
Ambas soluciones forman parte del mismo ECS híbrido.
En mis videojuegos busco precisamente ese equilibrio: utilizar componentes y sistemas cuando permiten organizar y procesar los datos de forma eficiente, y utilizar clases especializadas cuando existen relaciones o responsabilidades que se benefician de la encapsulación.
Al final, la arquitectura no debería convertirse en una limitación. Su objetivo es ayudarnos a construir videojuegos con un código claro, flexible, mantenible y con un buen rendimiento.
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 !
