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.
owning_entityyweak_entity?shared_ptryweak_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 queweak_ptrse restablezca cada vezweak_entityque no lo haría.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 elWorld. Cada unoEntityHandletiene un puntero alWorld(en mi caso, solo lo llamoEntityManager), y los métodos de manipulación / recuperación de datos enEntityHandlerealidad son llamadas aWorld: por ejemplo, para agregarComponenta una entidad, puede llamarEntityHandle.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.fuente
Worldejemplo, podría arrojar una excepción al intentar manipular / recuperar datos asociados con una entidad "muerta".