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 isEntityValid
o 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_id
para referencias a largo plazo. Utilícelo getEntity
para recuperar un objeto de acceso más rápido solo en código local. También puede usar una std::deque
o 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 world
todos 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 entity
clase (y a cualquier método que cree en ella) de forma bastante natural. El _world
miembro podría ser un singleton o global, también, si lo prefiere.
Su código solo usa un entity_ptr
en 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_ptr
todas partes y no pensar más sobre referencias y propiedad. O bien, y esto es lo que prefiero, haga una separación owning_entity
y weak_entity
tipos 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 entity
poco 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^16
entonces, 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_entity
yweak_entity
?shared_ptr
yweak_ptr
tenga 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_ptr
en particular puede no hacer lo que quieres; evita que una entidad se desasigne / reutilice por completo hasta queweak_ptr
se restablezca cada vezweak_entity
que 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
EntityHandle
instancias devueltas desde elWorld
. Cada unoEntityHandle
tiene un puntero alWorld
(en mi caso, solo lo llamoEntityManager
), y los métodos de manipulación / recuperación de datos enEntityHandle
realidad son llamadas aWorld
: por ejemplo, para agregarComponent
a una entidad, puede llamarEntityHandle.addComponent(component)
, lo que a su vez llamaráWorld.addComponent(this, component)
.De esta manera, las
Entity
clases 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
World
ejemplo, podría arrojar una excepción al intentar manipular / recuperar datos asociados con una entidad "muerta".