IGNA.LOG
post

ecs_(es)

este es un análisis sobre la transición de una arquitectura de ecs basada en sparse sets/maps a una arquitectura basada en archetypes, detallando las implicaciones en el diseño de memoria y el rendimiento del procesador. está basada en la experiencia que tive aplicándolo a un juego/motor que estoy programando.

arquitectura ecs: de la flexibilidad dinámica a la densidad de arquetipos

el mayor desafío de construir un motor de videojuegos eficiente radica en cómo organizamos los datos para maximizar el throughput del procesador. la evolución de un ecs suele pasar por tres fases críticas: la semántica, la distribución de memoria y la orquestación.

1. el ecs "lento": orientado a la búsqueda (sparse sets)

en una implementación inicial, el "mundo" actúa como un despachador de mapas. cada tipo de componente tiene su propio almacenamiento independiente.

estructura de memoria en mapas:

entidades: [ id: 1 ] [ id: 2 ] [ id: 3 ]

componente posición (map): 
   { 1: [x,y], 2: [x,y], 3: [x,y] } -> disperso en el heap

componente velocidad (map): 
   { 2: [vx,vy] } -> disperso en el heap

problema técnico: cache misses cuando un sistema de física itera sobre estas entidades, debe saltar entre diferentes regiones de la memoria ram para obtener la posición y luego la velocidad. en cpus modernas, un cache miss puede costar cientos de ciclos de reloj mientras se espera que los datos lleguen desde la memoria principal.

2. el ecs "rápido": orientado a arquetipos

un arquetipo es una firma única que representa una combinación específica de componentes. si las entidades a y b tienen exactamente posición y velocidad, pertenecen al mismo arquetipo y se almacenan juntas en una tabla.

diseño de almacenamiento por arquetipos:

arquetipo a: [posición]
+-------+-------------------+
| entity| data (x, y)       |
+-------+-------------------+
| id: 1 | [10.0, 20.0]      |
| id: 3 | [15.0, 25.0]      |
+-------+-------------------+

arquetipo b: [posición, velocidad]
+-------+-------------------+-------------------+
| entity| data (x, y)       | data (vx, vy)     |
+-------+-------------------+-------------------+
| id: 2 | [0.0, 0.0]        | [1.0, 1.0]        |
+-------+-------------------+-------------------+

ventaja: acceso secuencial al iterar sobre el arquetipo b, la cpu carga una cache line (típicamente 64 bytes) que contiene múltiples componentes contiguos. esto permite que el hardware prefetcher anticipe los siguientes datos, eliminando prácticamente la latencia de memoria.

3. orquestación: el grafo de arquetipos

para consultar qué tablas deben procesarse, el ecs utiliza un sistema de indexación. cada vez que se solicita una consulta (query) como has(posicion, velocidad), el motor no busca entidad por entidad, sino que busca en el registro de arquetipos.

diagrama de flujo de consulta:

query: <posicion, velocidad>
          |
          v
+-----------------------------+
|    buscador de arquetipos   |-----> [tabla: pos+vel] --(procesar)--> cpu
| (intersección de bitsets)    |-----> [tabla: pos+vel+salud] --(procesar)--> cpu
+-----------------------------+

la paradoja del ecs lento como metadato

gestionar estas tablas de arquetipos (añadir columnas, rastrear qué id está en qué fila) es una tarea compleja de gestión de datos.

  • data plane (rápido): arrays densos de componentes donde los sistemas operan a todo gas.
  • control plane (lento): una capa superior (puede ser el ecs basado en mapas original) que gestiona los objetos "tabla", "columna" y "firma".

4. el costo de la mutación (edge cases)

el mayor inconveniente técnico de los arquetipos es el cambio de estructura en tiempo de ejecución.

si llamamos a entity.addcomponent<velocidad>():

  1. el ecs identifica el nuevo arquetipo objetivo.
  2. se reserva espacio en la nueva tabla.
  3. se realiza un memcpy de los componentes existentes (posición).
  4. se inicializa el nuevo componente (velocidad).
  5. se elimina la fila de la tabla anterior (usando swap-and-pop para mantener la densidad).

diagrama de swap-and-pop:

antes de borrar id: 1          después de borrar id: 1
[ id: 1 ] [ id: 2 ] [ id: 3 ]  [ id: 3 ] [ id: 2 ] 
    ^                   |          ^
    |___________________|          | (el último ocupa el lugar del borrado)

conclusión

la transición a arquetipos sacrifica la velocidad de modificación estructural (añadir/quitar componentes) a cambio de una velocidad de iteración casi imbatible. es la elección ideal para sistemas con miles de entidades activas donde el 90% del tiempo de cpu se dedica a transformar datos existentes. en el motor que estoy haciendo utilizaba la implementación sencilla y no notaba problemas con ella, pero al empezar a diseñar y probar escenarios reales me dí cuenta que algo olía chistoso. cuando spawneaba muchos enemigos de distintas capacidades, se iba todo al garete. en MI caso de uso, la nueva implementación es mejor en sobremanera.

mi motor no tiene repositorio ṕúblico aún. cuando lo tenga, actualizaré este post agregando bloques de código e imágenes. hasta entonces.

navigation

back to posts home contact