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.