Asignación de entidades dentro de un sistema de entidades


9

No estoy seguro de cómo debo asignar / asemejar mis entidades dentro de mi sistema de entidades. Tengo varias opciones, pero la mayoría de ellas parecen tener desventajas asociadas con ellas. En todos los casos, las entidades se asemejan a un ID (entero), y posiblemente tenga una clase de contenedor asociada. Esta clase de contenedor tiene métodos para agregar / eliminar componentes a / de la entidad.

Antes de mencionar las opciones, aquí está la estructura básica de mi sistema de entidades:

  • Entidad
    • Un objeto que describe un objeto dentro del juego.
  • Componente
    • Se usa para almacenar datos para la entidad
  • Sistema
    • Contiene entidades con componentes específicos.
    • Se utiliza para actualizar entidades con componentes específicos.
  • Mundo
    • Contiene entidades y sistemas para el sistema de entidades.
    • Puede crear / destruir entidades y tener sistemas agregados / eliminados de / a ella

Aquí están mis opciones, que he pensado:

Opción 1:

No almacene las clases de contenedor de Entity, y solo almacene la siguiente ID / ID eliminada. En otras palabras, las entidades serán devueltas por valor, así:

Entity entity = world.createEntity();

Esto es muy parecido a entityx, excepto que veo algunos defectos en este diseño.

Contras

  • Puede haber clases de contenedor de entidades duplicadas (ya que el copiador debe implementarse y los sistemas deben contener entidades)
  • Si se destruye una entidad, las clases de contenedor de entidad duplicadas no tendrán un valor actualizado

Opcion 2:

Almacene las clases de contenedor de entidades dentro de un grupo de objetos. es decir, las entidades se devolverán por puntero / referencia, así:

Entity& e = world.createEntity();

Contras

  • Si hay entidades duplicadas, cuando una entidad se destruye, el mismo objeto de entidad se puede reutilizar para asignar otra entidad.

Opcion 3:

Use ID sin procesar y olvídese de las clases de entidad de contenedor. La desventaja de esto, creo, es la sintaxis que se requerirá para ello. Estoy pensando en hacer esto, ya que parece ser el más simple y fácil de implementar. No estoy muy seguro al respecto, debido a la sintaxis.

es decir, para agregar un componente con este diseño, se vería así:

Entity e = world.createEntity();
world.addComponent<Position>(e, 0, 3);

Según lo dispuesto a esto:

Entity e = world.createEntity();
e.addComponent<Position>(0, 3);

Contras

  • Sintaxis
  • ID duplicados

Respuestas:


12

Sus ID deben ser una mezcla de índice y versión . Esto le permitirá reutilizar las ID de manera eficiente, usar la ID para encontrar componentes rápidamente y hacer que su "opción 2" sea mucho más fácil de implementar (aunque la opción 3 se puede hacer mucho más aceptable con algún trabajo).

struct entity {
  uint16 version;
  /* and other crap that doesn't belong in components */
};

std::vector<entity> pool;
std::vector<uint16> freelist;
typedef uint32 entity_id; /* this shoudl be a wrapper class */

entity_id createEntity()
{
  uint16 index;
  if (!freelist.empty())
  {
    pool.push_back(entity());
    freelist.push_back(pool.size() - 1);
  }
  index = freelist.pop_back();

  return (pool[id].version << 16) | index;
}

void deleteEntity(entity_id id)
{
   uint16 index = id & 0xFFFF;
   ++pool[index].version;
   freelist.push_back(index);
}

entity* getEntity(entity_id id)
{
  uint16 index = id & 0xFFFF;
  uint16 version = id >> 16;
  if (index < pool.size() && pool[index].version == version)
    return &pool[index];
  else
    return NULL;
 }

Eso asignará un nuevo entero de 32 bits que es una combinación de un índice único (que es único entre todos los objetos vivos) y una etiqueta de versión (que será única para todos los objetos que alguna vez ocuparon ese índice).

Al eliminar una entidad, incrementa la versión. Ahora, si tiene alguna referencia a esa identificación flotando, ya no tendrá la misma etiqueta de versión que la entidad que ocupa ese lugar en el grupo. Cualquier intento de llamar getEntity(o uno isEntityValido lo que prefiera) fallará. Si asigna un nuevo objeto en esa posición, las ID antiguas seguirán fallando.

Puede usar algo como esto para su "opción 2" para asegurarse de que simplemente funcione sin preocuparse por las referencias de entidades antiguas. Tenga en cuenta que nunca debe almacenar un archivo entity*ya que podrían moverse (¡ pool.push_back()podrían reasignar y mover todo el grupo!) Y solo usarlo entity_idpara referencias a largo plazo. Utilícelo getEntitypara recuperar un objeto de acceso más rápido solo en código local. También puede usar una std::dequeo similar para evitar la invalidación del puntero si lo desea.

Su "opción 3" es una opción perfectamente válida. No hay nada intrínsecamente malo en el uso en world.foo(e)lugar de e.foo(), especialmente porque probablemente quiera la referencia de worldtodos modos y no es necesariamente mejor (aunque no necesariamente peor) almacenar esa referencia en la entidad misma.

Si realmente desea que la e.foo()sintaxis se mantenga, considere un "puntero inteligente" que maneje esto por usted. A partir del código de ejemplo que di anteriormente, podría tener algo como:

class entity_ptr {
  world* _world;
  entity_id _id;

public:
  entity_ptr() : _id(0) { }
  entity_ptr(world& world, entity_id id) : _world(&world), _id(id) { }

  bool empty() const { return _world != NULL && _world->getEntity(_id) != NULL; }
  void clear() { _world = NULL; _id = 0; }
  entity* get() { assert(!empty()); return _world->getEntity(_id); }
  entity* operator->() { return get(); }
  entity& operator*() { return *get(); }
  // add const method where appropriate
};

Ahora tiene una manera de almacenar una referencia a una entidad que usa una ID única y que puede usar el ->operador para acceder a la entityclase (y a cualquier método que cree en ella) de forma bastante natural. El _worldmiembro podría ser un singleton o global, también, si lo prefiere.

Su código solo usa un entity_ptren lugar de cualquier otra referencia de entidad y se va. Incluso podría agregar un conteo automático de referencias a la clase si lo desea (algo más confiable si actualiza todo ese código a C ++ 11 y usa semántica de movimiento y referencias de valor) para que pueda usar en entity_ptrtodas partes y no pensar más sobre referencias y propiedad. O bien, y esto es lo que prefiero, haga una separación owning_entityy weak_entitytipos con solo los recuentos de referencia de gestión anteriores para que pueda usar el sistema de tipos para diferenciar entre los identificadores que mantienen viva una entidad y los que solo la referencian hasta que se destruye.

Tenga en cuenta que la sobrecarga es muy baja. La manipulación de bits es barata. La búsqueda adicional en el grupo no es un costo real si accede a otros campos entitypoco después de todos modos. Si sus entidades son realmente solo identificadores y nada más, entonces podría haber un poco de sobrecarga adicional. Personalmente, la idea de un ECS donde las entidades son solo identificaciones y nada más me parece un poco ... académica. Hay al menos algunas banderas que querrás almacenar en la entidad general, y los juegos más grandes probablemente querrán una colección de los componentes de la entidad de algún tipo (lista enlazada en línea, si no otra cosa) para herramientas y soporte de serialización.

Como una nota bastante final, intencionalmente no inicialicé entity::version. No importa. No importa cuál sea la versión inicial, siempre que la incrementemos cada vez que estemos bien. Si termina cerca de 2^16entonces, simplemente se envolverá. Si termina terminando de forma tal que las ID antiguas permanecen válidas, cambie a versiones más grandes (e ID de 64 bits si es necesario). Para estar seguro, probablemente deberías borrar entity_ptr cada vez que lo verifiques y esté vacío. Puede hacer empty()esto por usted con un mutable _world_y _id, solo tenga cuidado con el enhebrado.


¿Por qué no contener la ID dentro de la estructura de la entidad? Estoy bastante confundido ¿También podría usar std :: shared_ptr / weak_ptr para owning_entityy weak_entity?
miguel.martin 01 de

Puede contener la ID en su lugar si lo desea. El único punto es que el valor de la ID cambia cuando se destruye una entidad en la ranura, mientras que la ID también contiene el índice de la ranura para una búsqueda eficiente. Puede usar shared_ptry weak_ptrtenga en cuenta que están destinados a objetos asignados individualmente (aunque pueden tener eliminadores personalizados para modificar eso) y, por lo tanto, no son los tipos más eficientes para usar. weak_ptren particular puede no hacer lo que quieres; evita que una entidad se desasigne / reutilice por completo hasta que weak_ptrse restablezca cada vez weak_entityque no lo haría.
Sean Middleditch

Sería mucho más fácil explicar este enfoque si tuviera una pizarra o no fuera demasiado vago para dibujar esto en Paint o algo así. :) Creo que visualizar la estructura lo deja extremadamente claro.
Sean Middleditch

gamesfromwithin.com/managing-data-relationships Este artículo parece presentar algo de lo mismo que dijiste en tu respuesta, ¿es esto lo que quieres decir?
miguel.martin 01 de

1
Soy el autor de EntityX , y la reutilización de índices me ha molestado por un tiempo. Según su comentario, he actualizado EntityX para que también incluya una versión. Gracias @SeanMiddleditch!
Alec Thomas

0

De hecho, estoy trabajando en algo similar en este momento, y he estado usando una solución más cercana a su número 1.

Tengo EntityHandleinstancias devueltas desde el World. Cada uno EntityHandletiene un puntero al World(en mi caso, solo lo llamo EntityManager), y los métodos de manipulación / recuperación de datos en EntityHandlerealidad son llamadas a World: por ejemplo, para agregar Componenta una entidad, puede llamar EntityHandle.addComponent(component), lo que a su vez llamará World.addComponent(this, component).

De esta manera, las Entityclases de contenedor no se almacenan y evita la sobrecarga adicional en la sintaxis que obtendría con su opción 3. También evita el problema de "Si se destruye una entidad, las clases de contenedor de entidad duplicadas no tendrán un valor actualizado ", porque todos apuntan a los mismos datos.


¿Qué sucede si hace que otro EntityHandle se parezca a la misma entidad y luego intenta eliminar uno de los identificadores? El otro identificador seguirá teniendo la misma ID, lo que significa que "controla" una entidad muerta.
miguel.martin 01 de

Es cierto, los otros identificadores restantes apuntarán a la ID que ya no "retiene" una entidad. Por supuesto, se deben evitar situaciones en las que elimine una entidad y luego intente acceder a ella desde otro lugar. Por Worldejemplo, podría arrojar una excepción al intentar manipular / recuperar datos asociados con una entidad "muerta".
vijoc 01 de

Si bien es mejor evitarlo, en el mundo real esto sucederá. Las secuencias de comandos se aferrarán a las referencias, los objetos de juego "inteligentes" (como buscar misiles) se aferrarán a las referencias, etc. Realmente necesita un sistema que sea capaz de hacer frente adecuadamente a las referencias obsoletas o que rastrea y pone a cero los débiles referencias
Sean Middleditch

El mundo podría, por ejemplo, lanzar una excepción al tratar de manipular / recuperar datos asociados con una entidad "muerta". No si el ID anterior ahora está asignado a una nueva entidad.
miguel.martin 01 de
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.