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
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_vector
a 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.
fuente
Respuestas:
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
Render
componentes, 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.
fuente
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 =)
fuente
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.
fuente