Agrupando entidades del mismo conjunto de componentes en memoria lineal


11

Partimos del enfoque básico de sistemas-componentes-entidades .

Creemos ensamblajes (término derivado de este artículo) simplemente a partir de información sobre tipos de componentes . Se realiza dinámicamente en tiempo de ejecución, al igual que agregaríamos / eliminaríamos componentes a una entidad uno por uno, pero vamos a nombrarlo con mayor precisión ya que solo se trata de información de tipo.

Luego construimos entidades especificando ensamblaje para cada una de ellas. Una vez que creamos la entidad, su ensamblaje es inmutable, lo que significa que no podemos modificarla directamente en su lugar, pero aún así podemos obtener la firma de la entidad existente en una copia local (junto con el contenido), realizar los cambios adecuados y crear una nueva entidad. de eso.

Ahora para el concepto clave: cada vez que se crea una entidad, se asigna a un objeto llamado conjunto de ensamblaje , lo que significa que todas las entidades de la misma firma estarán en el mismo contenedor (por ejemplo, en std :: vector).

Ahora los sistemas simplemente recorren cada segmento de su interés y hacen su trabajo.

Este enfoque tiene algunas ventajas:

  • los componentes se almacenan en algunos fragmentos de memoria contiguos (precisamente: cantidad de cubos): esto mejora la facilidad de uso de la memoria y es más fácil volcar el estado del juego completo
  • los sistemas procesan los componentes de forma lineal, lo que significa una mejor coherencia de la memoria caché: adiós diccionarios y saltos aleatorios de memoria
  • crear una nueva entidad es tan fácil como mapear un ensamblaje en un cubo y devolver los componentes necesarios a su vector
  • eliminar una entidad es tan fácil como llamar a std :: move para intercambiar el último elemento con el eliminado, porque el orden no importa en este momento

ingrese la descripción de la imagen aquí

Si tenemos muchas entidades con firmas completamente diferentes, los beneficios de la coherencia de caché disminuyen, pero no creo que suceda en la mayoría de las aplicaciones.

También hay un problema con la invalidación del puntero una vez que los vectores se reasignan; esto podría resolverse introduciendo una estructura como:

struct assemblage_bucket {
    struct entity_watcher {
        assemblage_bucket* owner;
        entity_id real_index_in_vector;
    };

    std::unordered_map<entity_id, std::vector<entity_watcher*>> subscribers;

    //...
};

Entonces, por alguna razón en nuestra lógica de juego, queremos hacer un seguimiento de una entidad recién creada, dentro del cubo registramos un entity_watcher , y una vez que la entidad debe ser std :: move'd durante la eliminación, buscamos sus observadores y actualizamos sus real_index_in_vectora nuevos valores. La mayoría de las veces esto impone una sola búsqueda de diccionario para cada eliminación de entidad.

¿Hay más desventajas en este enfoque?

¿Por qué no se menciona la solución en ninguna parte, a pesar de ser bastante obvia?

EDITAR : estoy editando la pregunta para "responder las respuestas", ya que los comentarios son insuficientes.

pierde la naturaleza dinámica de los componentes conectables, que se creó específicamente para alejarse de la construcción de clase estática.

Yo no. Tal vez no lo expliqué con suficiente claridad:

auto signature = world.get_signature(entity_id); // this would just return entity_id.bucket_owner->bucket_signature or so
signature.add(foo_component);
signature.remove(bar_component);
world.delete_entity(entity_id); // entity_id would hold information about its bucket owner
world.create_entity(signature); // automatically assigns new entity to an existing or a new bucket

Es tan simple como tomar la firma de la entidad existente, modificarla y cargarla nuevamente como una nueva entidad. ¿ Naturaleza dinámica y conectable ? Por supuesto. Aquí me gustaría enfatizar que solo hay una clase de "ensamblaje" y una de "cubeta". Los depósitos se basan en datos y se crean en tiempo de ejecución en una cantidad óptima.

deberías revisar todos los cubos que pueden contener un objetivo válido. Sin una estructura de datos externa, la detección de colisiones podría ser igualmente difícil.

Bueno, esta es la razón por la que tenemos las estructuras de datos externos antes mencionadas . La solución alternativa es tan simple como introducir un iterador en la clase del sistema que detecta cuándo saltar al siguiente grupo. El salto sería puramente transparente a la lógica.


También leí el artículo de Randy Gaul sobre el almacenamiento de todos los componentes en vectores y dejé que sus sistemas los procesen. Veo dos grandes problemas allí: qué pasa si quiero actualizar solo un subconjunto de entidades (piense en la eliminación por ejemplo). Debido a eso, los componentes se unirán nuevamente con las entidades. Para cada paso de iteración de componentes, necesitaría verificar si la entidad a la que pertenece ha sido seleccionada para una actualización. El otro problema es que algunos sistemas necesitan procesar múltiples tipos de componentes diferentes, eliminando nuevamente la ventaja de coherencia de caché. ¿Alguna idea de cómo lidiar con estos problemas?
Tiguchi

Respuestas:


7

Básicamente, ha diseñado un sistema de objetos estáticos con un asignador de agrupación y con clases dinámicas.

Escribí un sistema de objetos que funciona casi de manera idéntica a su sistema de "ensamblajes" en mis días de escuela, aunque siempre tiendo a llamar a "ensamblajes" ya sea "planos" o "arquetipos" en mis propios diseños. La arquitectura era más dolorosa que los sistemas de objetos ingenuos y no tenía ventajas de rendimiento medibles sobre algunos de los diseños más flexibles con los que lo comparé. La capacidad de modificar dinámicamente un objeto sin necesidad de reificarlo o reasignarlo es sumamente importante cuando trabaja en un editor de juegos. Los diseñadores querrán arrastrar y soltar componentes en las definiciones de sus objetos. El código de tiempo de ejecución incluso podría necesitar modificar componentes de manera eficiente en algunos diseños, aunque personalmente no me gusta eso. Dependiendo de cómo enlace las referencias de objetos en su editor,

Obtendrá una peor coherencia de caché de lo que cree en la mayoría de los casos no triviales. Su sistema de IA, por ejemplo, no se preocupa por los Rendercomponentes, pero termina atascado iterando sobre ellos como parte de cada entidad. Los objetos que se repiten son más grandes, y las solicitudes de línea de caché terminan obteniendo datos innecesarios, y cada solicitud devuelve menos objetos completos. Todavía será mejor que el método ingenuo, y la composición de objetos del método ingenuo se usa incluso en grandes motores AAA, por lo que probablemente no necesite algo mejor, pero al menos no piense que no puede mejorarlo más.

Su enfoque tiene más sentido para algunoscomponentes, pero no todos. No me gusta mucho ECS porque aboga por colocar siempre cada componente en un contenedor separado, lo que tiene sentido para la física o los gráficos o cualquier otra cosa, pero no tiene ningún sentido si permite múltiples componentes de script o IA componible. Si deja que el sistema de componentes se use para algo más que solo objetos integrados, sino también como una forma para que los diseñadores y programadores de juegos compongan el comportamiento de los objetos, entonces puede tener sentido agrupar todos los componentes de AI (que a menudo interactuarán) o todos los guiones componentes (ya que desea actualizarlos todos en un lote). Si desea el sistema con mejor rendimiento, necesitará una combinación de esquemas de asignación y almacenamiento de componentes y se dará el tiempo para determinar de manera concluyente cuál es el mejor para cada tipo particular de componente.


Dije: no podemos cambiar la firma de la entidad, y quise decir que no podemos modificarla directamente en el lugar, pero aún así podemos obtener el ensamblaje existente en una copia local, realizar cambios y cargarlo nuevamente como una nueva entidad, y estos Las operaciones son bastante baratas, como he demostrado en la pregunta. Una vez más, solo hay UNA clase de "cubeta". "Ensamblajes" / "Firmas" / "vamos a nombrarlo como queramos" se puede crear dinámicamente en tiempo de ejecución como en un enfoque estándar, incluso iría tan lejos como pensar en una entidad como una "firma".
Patryk Czachurski

Y dije que no necesariamente quieres lidiar con la reificación. "Crear una nueva entidad" podría significar romper todos los identificadores existentes en la entidad, dependiendo de cómo funcione su sistema de identificadores. Tu llamada si son lo suficientemente baratas o no. Me pareció que era un fastidio tener que lidiar con eso.
Sean Middleditch

Bien, ahora tengo tu punto sobre este. De todos modos, creo que incluso si agregar / eliminar fuera un poco más costoso, sucede de vez en cuando que todavía vale la pena simplificar enormemente el proceso de acceso a los componentes, que sucede en tiempo real. Por lo tanto, la sobrecarga de "cambio" es insignificante. Sobre su ejemplo de IA, ¿no vale la pena estos pocos sistemas que de todos modos necesitan datos de múltiples componentes?
Patryk Czachurski

Mi punto fue que la IA era un lugar donde su enfoque es mejor, pero para otros componentes no es necesariamente así.
Sean Middleditch

4

Lo que has hecho es rediseñar objetos C ++. La razón por la que esto se siente obvio es que si reemplaza la palabra "entidad" con "clase" y "componente" con "miembro", este es un diseño OOP estándar que utiliza mixins.

1) pierde la naturaleza dinámica de los componentes conectables, que se creó específicamente para alejarse de la construcción de clase estática.

2) la coherencia de la memoria es más importante dentro de un tipo de datos, no dentro de un objeto que unifica múltiples tipos de datos en un solo lugar. Esta es una de las razones por las que se crearon sistemas de componentes +, para alejarse del fragmento de memoria de clase + objeto.

3) este diseño también vuelve al estilo de clase C ++ porque está pensando en la entidad como un objeto coherente cuando, en un diseño de sistema + componente, la entidad es simplemente una etiqueta / ID para que el funcionamiento interno sea comprensible para los humanos.

4) es tan fácil que un componente se serialice a sí mismo como un objeto complejo para serializar múltiples componentes dentro de sí mismo, si no es más fácil de seguir como programador.

5) el siguiente paso lógico en este camino es eliminar los Sistemas y poner ese código directamente en la entidad, donde tiene todos los datos que necesita para funcionar. Todos podemos ver lo que eso implica =)


2) tal vez no entiendo el almacenamiento en caché por completo, pero digamos que hay un sistema que funciona con digamos 10 componentes. En un enfoque estándar, procesar cada entidad significa acceder a la RAM 10 veces, porque los componentes están dispersos en lugares aleatorios en la memoria, incluso si se usan grupos, porque los diferentes componentes pertenecen a grupos diferentes. ¿No sería "importante" almacenar en caché toda la entidad a la vez y procesar todos los componentes sin una sola falta de caché, sin siquiera tener que hacer búsquedas en el diccionario? Además, hice una edición para cubrir el 1) punto
Patryk Czachurski el

@Sean Middleditch tiene una buena descripción de este desglose de almacenamiento en caché en su respuesta.
Patrick Hughes

3) No son objetos coherentes de ninguna manera. Como el componente A está justo al lado del componente B en la memoria, es solo "coherencia de memoria", no "coherencia lógica", como ha señalado John. Los cubos, en su creación, incluso podrían barajar componentes en la firma a cualquier orden y principios deseados aún se mantendrían. 4) puede ser igualmente fácil "realizar un seguimiento" si tenemos suficiente abstracción: de lo que estamos hablando es simplemente de un esquema de almacenamiento provisto de iteradores y quizás un mapa de desplazamiento de bytes puede hacer que el procesamiento sea tan fácil como en un enfoque estándar.
Patryk Czachurski

5) Y no creo que nada en esta idea apunte a esta dirección. No es que no quiera estar de acuerdo con usted, solo tengo curiosidad por saber a dónde puede llevar esta discusión, aunque de todos modos probablemente conducirá a una especie de "medición" o la conocida "optimización prematura". :)
Patryk Czachurski

@PatrykCzachurski pero sus sistemas no funcionan con 10 componentes.
user253751

3

Mantener como entidades juntas no es tan importante como podría pensar, por eso es difícil pensar en una razón válida que no sea "porque es una unidad". Pero dado que realmente está haciendo esto para la coherencia de caché en lugar de la coherencia lógica, podría tener sentido.

Una dificultad que podría tener es la interacción entre componentes en diferentes categorías. No es terriblemente sencillo encontrar algo a lo que tu IA pueda disparar, por ejemplo, tendrías que pasar por todos los cubos que podrían contener un objetivo válido. Sin una estructura de datos externa, la detección de colisiones podría ser igualmente difícil.

Para continuar organizando entidades juntas para lograr coherencia lógica, la única razón por la que podría tener que mantener entidades juntas es para propósitos de identificación en mis misiones. Necesito saber si acabas de crear la entidad tipo A o tipo B, y lo soluciono ... lo adivinaste: agregando un nuevo componente que identifica el ensamblaje que une a esta entidad. Incluso entonces, no estoy reuniendo todos los componentes para una gran tarea, solo necesito saber de qué se trata. Así que no creo que esta parte sea terriblemente útil.


Tengo que admitir que no entiendo bien tu respuesta. ¿Qué quiere decir con "coherencia lógica"? Sobre las dificultades en las interacciones, hice una edición.
Patryk Czachurski

"Coherencia lógica", como en: tiene "sentido lógico" mantener juntos todos los componentes que forman una entidad de árbol.
John McDonald
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.