ECS para colisiones

En este artículo vamos a explicar cómo implementar un sistema de colisiones dentro de una arquitectura ECS híbrida. Hasta ahora hemos visto cómo crear enemigos, máquinas de estados y sistemas de armas mediante componentes y sistemas especializados. Sin embargo, tarde o temprano cualquier videojuego necesita responder a una pregunta fundamental:

¿Qué ocurre cuando dos entidades entran en contacto?

En un videojuego moderno pueden existir cientos de jugadores, enemigos, proyectiles o elementos del escenario interactuando simultáneamente. Mantener un sistema de colisiones organizado resulta tan importante como gestionar correctamente la inteligencia artificial y las armas.

En mis videojuegos prefiero tratar las colisiones con un método especializado que simplemente recibe dos listados de elementos que pueden colisionar unos contra otros. Por ejemplo, jugadores y disparos enemigos. Pero la solución que propongo es totalmente compatible y flexible con un sistema ECS híbrido.

En estos listados, cada elemento gráfico susceptible de colisionar almacena la información necesaria para identificarse y representar su zona de impacto mediante los componentes {id, type, x, y, width, height}. De esta forma, un sistema ECS especializado se puede encargar de detectar las colisiones y gestionar el resultado según el tipo de entidad que colisiona.

Entidades y unidades gráficas

En artículos anteriores hemos utilizado entidades muy sencillas formadas principalmente por datos. Sin embargo, una entidad real suele estar compuesta por distintos elementos gráficos, formando una unidad gráfica cuyos componentes cumplen funciones diferentes. De modo que conviene diferenciar ambos conceptos:

  • Entidad: Objeto del videojuego que posee identidad propia y cuyos datos son procesados por los distintos sistemas de la arquitectura ECS.
  • Unidad gráfica: Conjunto de componentes gráficos y sistemas asociados que representan visualmente una entidad dentro del videojuego.

De este modo, un jugador podría representarse como una entidad y unidad gráfica así:

let player = {
    id: ID.PLAYER,
    type: TYPE.SHIP,
    icon: Factory.create(ICON.SHIP1),
    shield: Factory.create(ICON.SHIELD),
    orb: Factory.create(ICON.ORB),
    turbina: Factory.create(ICON.TURBINA),
    hitboxes: []
};

Aunque todos estos elementos pertenecen a la misma entidad (player), existe una unidad gráfica del jugador donde cada elemento gráfico representa una parte distinta de la entidad. El sprite principal (icon) dibuja la nave, el escudo (shield) puede activarse o desactivarse, la turbina (sprite) genera el efecto de propulsión y el orbe (orb) puede actuar como un elemento adicional del jugador. Todos los elementos gráficos están construidos alrededor de un icono principal (icon) y forman una unidad gráfica, dentro de la entidad jugador.

Cada componente mantiene sus propios datos gráficos, pero todos forman parte de la misma unidad gráfica, agrupada en la entidad del jugador.

NOTA: Factory.create() es una clase especializada en crear todos los gráficos del videojuego.

Introduciendo las hitboxes

Una vez creada la entidad, únicamente debemos indicar qué partes (o componentes ECS) podrán participar en las colisiones. Para ello utilizo una solución muy sencilla: un array hitboxes.

player.hitboxes = [
    player.icon,
    player.shield,
    player.orb
];

Cada elemento del array representa una zona susceptible de colisionar con otras entidades del videojuego. Esta solución ofrece una gran flexibilidad. Algunas entidades únicamente dispondrán de un único elemento en su array hitboxes, mientras que otras podrán incorporar varias zonas independientes. Por ejemplo, un enemigo sencillo podría utilizar únicamente:

enemy.hitboxes = [
    enemy.icon
];

Mientras que un jefe final podría disponer de numerosas zonas de impacto diferentes:

boss.hitboxes = [
    boss.body,
    boss.head,
    boss.leftArm,
    boss.rightArm,
    boss.core
];

No es necesario crear distintos sistemas para cada caso. El sistema ECS de colisiones simplemente recorrerá todas las hitboxes almacenadas en cada entidad. Para que el sistema de colisiones funcione de forma lógica, la unidad gráfica tiene un id que identifica a la entidad (jugador, enemigo, disparo, jefe final, etc). Al mismo tiempo, cada elemento gráfico de la unidad también tiene su propio identificador (nave, escudo, orbe, etc). De esta forma podemos identificar qué elementos están colisionando entre sí.

ECS aplicado a colisiones

Al igual que ocurría con los enemigos o los proyectiles, las colisiones también pueden implementarse mediante un sistema ECS especializado. Aunque yo prefiero implementarlo mediante un método dentro de cada nivel, puesto que puede variar entre niveles. De forma simplificada podríamos hacer:

for (const playerIcon of player.hitboxes) {
    for (const enemyIcon of enemy.hitboxes) {
        if (collision(playerIcon, enemyIcon)) {
            // Acciones comunes (explosiones, sonidos, etc)
            if (playerIcon.id == ICON.SHIELD) {
               // Enemigo destruido, obtener puntos, reducir vida escudo
            }
            if (playerIcon.id == ICON.SHIP) {
               // Destruir jugador, destruir enemigo, estado del juego a Crash
            }
            etc
        }
    }
}

Las entidades únicamente almacenan sus hitboxes. Toda la lógica relacionada con las detecciones permanece centralizada dentro del sistema ECS de colisiones. Este enfoque mantiene una clara separación de responsabilidades:

  • Las entidades almacenan datos
  • El sistema de colisiones procesa dichos datos
  • El resto del videojuego decide cómo reaccionar ante cada impacto

Procesando una colisión

Detectar una colisión únicamente indica que dos hitboxes han entrado en contacto. A partir de ese momento pueden ocurrir muchas cosas distintas dependiendo del videojuego:

  • Reducir la vida del enemigo
  • Destruir un proyectil
  • Activar un escudo
  • Reproducir una explosión
  • Sumar puntos
  • Iniciar una animación

Este es el verdadero motor del videojuego y el que establece la lógica del guión a seguir. Por este motivo las unidades gráficas deben estar identificadas mediante un identificador y su tipo (id, type) y los iconos mediante su id. Así, el sistema ECS de colisiones puede detectar los diferentes tipos de colisiones y establecer las acciones correspondientes, desde el nivel de entidades hasta el nivel de iconos gráficos.

De esta forma cada sistema continúa siendo responsable únicamente de su propia lógica y el sistema ECS de colisiones opera a través de componentes, continuando con nuestro ECS híbrido especializado en videojuegos.

Conclusiones ECS para colisiones

Las colisiones constituyen otro buen ejemplo de cómo ECS ayuda a organizar un videojuego mediante responsabilidades claramente diferenciadas. Las entidades únicamente describen qué partes pueden colisionar mediante el componente hitboxes, mientras que un sistema ECS independiente se encarga de procesar todas las detecciones.

En mis videojuegos cada entidad puede estar formada por numerosos componentes gráficos, pero todos ellos siguen organizándose como datos sencillos. Esto permite construir jugadores, enemigos o jefes finales con varias zonas de impacto sin aumentar innecesariamente la complejidad del código.

Como ocurre en el resto de esta serie, no busco una implementación ECS completamente purista, sino una arquitectura práctica y fácil de mantener. Si dividir las responsabilidades permite desarrollar videojuegos de forma más clara, ampliar el código con facilidad y mantener un buen rendimiento, considero que la arquitectura ha cumplido su objetivo.

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