Entity-Component Part 2: Entities

In the second part of this article series on entity-component systems, we are going to take a look at how entities can be managed. Don't forget to read part one which explained the very basics of the entity-component paradigm. Today we are finally going to start writing some actual code on entities management.

The manager

Some implementation define the manager (the big class reponsible for handling entities and their components) as a singleton. To allow for some flexibility, we are going to allow multiple instances of an entity manager to be created. However the entity handlers are going to store a reference to said manager. These references will be weak references, which is going to allow the entities to check whether the manager still exists or not. If you have already messed around with weak references in C++, you will know that they need to be instantiated from shared pointers. This means that every instance of a manager will have to have a shared pointer to itself. Thus, we need to prevent the manager from being instantiated on the stack. Also, don't try creating shared pointers to this in a class, it just won't work. To allow for this, we need to use the enable_shared_from_this feature. Let's write some code and discuss it afterwards.

class EntityManager : public std::enable_shared_from_this<EntityManager> {
public:
    EntityManager(const EntityManager &manager) = delete;
    EntityManager &operator=(const EntityManager &manager) = delete;

    static std::shared_ptr<EntityManager> makeInstance()
    {
        return std::shared_ptr<EntityManager>(new EntityManager());
    }

private:
    EntityManager() = default;
};

First, the manager extends enable_shared_from_this, which will allow us to call shared_from_this() later to get a shared pointer to the current instance of EntityManager. We have made the default constructor private so that it can only be instantiated dynamically via the makeInstance method. We've also deleted the copy methods because we shouldn't be able to copy a manager.

Entity definition

In our system, an entity is simply going to be an identifier. These identifiers will be reused after being discarded and we are thus going to need an invalidation method. To do so, we are going to store the entity version alongside its identifier. When an entity is discarded, its identitifier will be placed in a recycling list and its version will be incremented so that all future calls to the ECS done with an outdated entity handler will be discarded.

The entity identifiers and version numbers will be stored as 32-bits unsigned integers. This means that our system will support 4,294,967,296 concurrent entities, and a total of 18,446,744,073,709,551,616 entities throughout a runtime session, which should be enough, right? To allow for some flexibility, we are going to make the types configurable in case you need more entities.

typedef uint32_t EntityId;
typedef uint32_t EntityVersion;

Now that we have this, we can very simply define an entity:

class Entity {
public:
    Entity() = default;

    Entity(std::shared_ptr<EntityManager> manager, EntityId id, EntityVersion version) :
        _id(id), _version(version), _manager(manager)
    {}

    Entity(std::weak_ptr<EntityManager> manager, EntityId id, EntityVersion version) :
        _id(id), _version(version), _manager(manager)
    {}

    Entity(const Entity &entity) = default;
    Entity &operator=(const Entity &entity) = default;

private:
    friend class EntityManager;

    std::weak_ptr<EntityManager> _manager;
    EntityId _id;
    EntityVersion _version;
};

There, simple, easy. As you can see we are keeping a weak pointer to the entity manager, which allows us to check that the manager still exists without keeping it from being deallocated, which would have happened had we used a shared pointer. We can also define comparison operators between entities.

bool Entity::operator==(const Entity &entity) const
{
    shared_ptr<EntityManager> manager1 = _manager.lock();
    shared_ptr<EntityManager> manager2 = entity._manager.lock();

    if (!manager1 || !manager2) {
        return false;
    }

    return _version == entity._version && _id == entity._id && manager1 == manager2;
}

bool Entity::operator!=(const Entity &entity) const
{
    return !(*this == entity);
}

The first step to comparing entities is to check whether they share the same manager and whether those are still valid or not. the lock method on weak pointers allows us to transform it to a shared pointer used to dereference it. If the objects it points to is to longer valid, it will return a shared pointer to nullptr. The rest of the code is quite easy to understand.

Creating and deleting entities

To create entities, the manager will have to know which is the next entity identifier that can be used, as well as the version for each entity. Furthermore, is order to be able to reuse entity identifiers, we are going to store a list of identifiers that can be used.

class EntityManager {
    EntityId _nextEntity = 0;
    std::vector<EntityId> _freeList;
    std::vector<EntityVersion> _entityVersion;
};

This code should be quite explicit. Note that initially, no entity can be used because the list of free entities is empty. Thus, let's implement a method to grow the storage capacity of the manager.

void EntityManager::growCapacity()
{
    size_t newSize;

    if (_entityVersion.size() > 0) {
        newSize = _entityVersion.size();

        while (newSize <= _nextEntity) {
            newSize *= 2;
        }
    } else {
        newSize = 256;
    }

    _entityVersion.resize(newSize, 0);
}

This function is rather simple. If our arrays are empty at first, we initialize their size at 256. Otherwise, we double their capacity until we can store the next required entity identifier. Note that this function should be private. We now have everything we need to create entities.

Entity EntityManager::create()
{
    EntityId id;
    EntityVersion version;

    if (_freeList.empty()) {
        if (_nextEntity > numeric_limits<EntityId>::max()) {
            throw runtime_error("Entity id out of bounds");
        }

        growCapacity();
        id = _nextEntity++;
        version = _entityVersion[id] = 1;
    } else {
        id = _freeList.back();
        _freeList.pop_back();
        version = _entityVersion[id];
    }

    return Entity(shared_from_this(), id, version);
}

Firstly, when there is no identifier to recycle, the identifier will be the next available. In case we don't have enough space for it, we increase the capacity, and also check if we run out of identifiers. Note that the initial version of any entity is 1. The reason for this will be made clearer later. Otherwise, if the free list is not empty, we just get an available identifier and associated version number. Finally, we can create the entity using the shared_from_this function we discussed in the first part. This function is private and is only called from the entities. This EntityManager and Entity need to be friends. Now, on to deletion.

void EntityManager::destroy(Entity &entity)
{
    if (!entity || entity._manager.lock() != shared_from_this()) {
        return;
    }

    if (++_entityVersion[entity._id] != 0) {
        _freeList.push_back(entity._id);
    }

    entity._manager.reset();
}

The first thing to note here is the !entity in the first condition. This is something we are going to implement next. So firstly we test whether the current manager is the one associated with the entity to destroy. Then, we increment the entity version number to make it invalid for previous entity handlers that may still be used throughout the program. If this version number becomes 0, it means that we have exceeded the maximum number of versions, and the entity is thus not added to the recycling list. Finally, the pointer to the manager stored in the entity is reset. Finally, let's write the last method for today in the manager, which is really straightforward, and that I will thus not explain. Just note that we don't test that the manager is valid because we will only call this function from the entities. This function, just like destroy, is private.

bool EntityManager::isEntityValid(const Entity &entity) const
{
    return entity._id < _nextEntity && entity._version == _entityVersion[entity._id];
}

Back to entities

These last functions test the validity of an entity handler and destroy said entity. Given what has been said before, they should be fairly explicit, and I will thus not explain them.

Entity::operator bool() const
{
    return isValid();
}

bool Entity::isValid() const
{
    shared_ptr<EntityManager> manager = _manager.lock();

    if (!manager) {
        return false;
    }

    return manager->isEntityValid(*this);
}

void Entity::destroy()
{
    shared_ptr<EntityManager> manager = _manager.lock();

    if (manager) {
        manager->destroy(*this);
        _manager.reset();
    }
}

Final words

That's it for today. We can now create and destroy entities and check that existing entities are still valid. In the next part, we are going to mess with components, because right now the system is fairly useless. As always, all feedback is welcome.

Comments