The concern that @philipp mentioned can be eliminated with a bit of fancy bit manipulation.
Rather than treat your Entity's ID as a single 32-bit value, split that value into two parts, one which represents the index slot in your array and another that represents an ever increasing, potentially rolling over version value.
struct EntityID
{
uint32_t index : 24;
uint32_t version : 8;
};
In this use case, you can have approximately 16.7 million different entities in your pool. Each entity will be assigned 255 different version values before they roll over, skipping 0 as a sentinel value.
So you end up with an array of EntityID
indexed based index
. So when you want to lookup an entity, you simply go to that index, compare the version values between the stored reference and the value in the array and if they match, the index is still valid. If they differ, the reference is stale and you can thus assume that entity has since been destroyed and react accordingly.
My personal choice is to use this approach mainly for cataloging entity ids and validating whether they're still valid at runtime and then store the components in their own separate pools per type.
For this to work, my component pool relies on two arrays to manage its memory. The first array is a simple sparsely populating array where the index from the entity ID holds the offset index in the second array that is maintained as a densely populated array of component types.
The benefit here is during processing of components, I can simply iterate the second array and avoid any lookup performance bottlenecks. Worse case scenario, I take several constant time lookup hits when I fetch a component by entity id which seems reasonable.
template<typename T> T** GetComponents() const {
return componentPool;
}
template<typename t> T* GetComponent(const EntityId& entityId)
{
if ( IsEntityIdValid( entityId ) )
{
uint32_t index = componentEntityArray[ entityId.index ];
return &componentPool[ index ];
}
else
return nullptr;
}
bool IsEntityIdValid(const EntityId& entityId) const
{
if ( entityId.index < 0 || entityId.index >= MAX_ENTITIES )
return false;
return entityIdArray[ entityId.index ].version == entityId.version;
}
void DestroyEntity(EntityId entityId) {
if ( entityId.index >= 0 && entityId.index < MAX_ENTITIES )
{
entityIdArray[ entityId.index ].version++;
entityIdFreeListQueue.add( entityId.index );
}
}
The other benefit is that now your entityIdArray
is also a sparse array but contiguous. The lookup is constant time either way, so that should not impact performance, if at all.
I would suggest however to make the queue use a first-in, first-out strategy. This avoids the concern with version numbers being incremented too high for a single slot, except in situations where you create and destroy a single entity repeatedly in a row.