Skip to content
Maiko Steeman edited this page Jan 28, 2020 · 5 revisions

Design rationale

Our ECS is designed to be simple and fast. For our game we have a use case where we have a lot of entities, but those entities are relatively the same.

Usage

The API of the Ecs is relatively simple.

// Creation
EntityId Ecs::CreateEntity();
// Deletion
void Ecs::DestroyEntity(EntityId id);
// Validate EntityId
bool IsValid(EntityId id);
// Add component
template<T>
void Ecs::Assign(EntityId id, T&& cmp);
// Remove component
template<T>
void Ecs::Remove(EntityId id);
// Validate component(s)
template<Ts..>
bool Ecs::Has(EntityId id); //<-- Public API not exposed yet
// Get component
template<T>
T& Ecs::Get(EntityId id);
// Get a list of entities with the all specified components
template<Ts...>
vector<EntityId> Ecs::QueryEntities();

Example Code:

using namespace eyos;

using EyosEcs = Ecs<Transform, Model3D, InstancedModel, ecs_builtins::EcsTrackable>;

void Test() {
    EyosEcs ecs{};
    Material testMaterial{};
    {
        EntityId model = ecs.CreateEntity();
        ecs.Assign(model, Transform{ glm::vec3{0, 0, 0}, glm::quat{ glm::vec3{} } });
        ecs.Assign(model, Model3D{ bunnyMesh, &testMaterial });
    }
    {
        EntityId model = ecs.CreateEntity();
        ecs.Assign(model, Transform{ glm::vec3{25, 0, 0}, glm::quat{ glm::vec3{0, 0, 0} }, glm::vec3{ 0.25f } });
        ecs.Assign(model, Model3D{ bunnyMesh, &testMaterial });
    }

    std::vector<EntityId> entityModels = ecs.QueryEntities<Transform, Model3D>();
    for (EntityId id : entityModels) {
        auto& transform = ecs.Get<Transform>(id);
        auto& model = ecs.Get<Model3D>(id);

        RenderModel(transform, model);
    }
}

EntityId lifetime and destroying

EntityId's are not guaranteed to stay valid.

The reason for this is: In the ECS we optimize DestroyEntity(id) by swapping to the end to avoid copying all other entities 1 place back. This has as a consequence that any entity at the end of the entityArray can be invalidated, care must be used when storing a EntityId for longer then 1 Query.

EcsTrackable

In the case that the ability to reference an entity is required, a solution exists: ecs_builtins::EcsTrackable. By Assigning this component to an entity. A sparseToDense entry will be kept in memory so in the case that the Entity will be swapped away from it's original index, the new index can be used. This is often necessary for gameplay.

For instance, when selecting a unit, it's required that we keep track of the same entity. So, at runtime when the unit is selected, we can Assign<ecs_builtins::EcsTrackable>(entityId) and be safe that the EntityId will be valid. When we deselect the unit, we can Remove<ecs_builtins::EcsTrackable>(entityId) to remove the sparseToDense entry internally.

Storage

The Ecs only uses 1 dense entity vector. Which means that every entity will take up the space of all the components that can possibly fit on the entity. 0 sized structs will only be kept in the componentBitsets. And are thus a good fit for tagging entities.

Other types

EntityId

The idea behind the EntityId is that it is just a number and a version packed into 1 struct. The id is (usually) just the index into the entity array. And the version is to check if the id is still valid. It looks approximately like this:

struct EntityId {
    uint32_t index : 24;
    uint8_t version;
};

Implementation

The Ecs only uses 1 dense entity vector. 0 sized structs will only be kept in the componentBitsets. We can track an entity by bookkeeping a sparseToDense entry in a map.

Limitations

Maximum entities

At this time, the EntityId::index consists of 24 bits. This means that we can store a maximum of 2^24=16.777.216 entities. The reason for this is that we use the last 8 bits of the EntityId for keeping a version, thus leaving 32-8=24 bits for the index.

Maximum components

At this time, the componentBitsets are hard coded as uint16_t. This means that we cannot store more than 16 component types. Although this can be easily changed once we reach the limit.

Duplicate component types

At this time, we have no use for multiple of the same components per entity, so no thought has gone into the implementation to return multiple components of the same type (It would most likely give you the same component back twice).

Systems

Design

It should be a multithreaded system

Usage

template<typename... Ts>
struct ComponentQuery {
    std::tuple<std::vector<Ts>...> componentArrays;
    
    auto begin();
    auto end();
};

class SystemScheduler{
    //TODO: Write out the public API
};

Example code

namespace eyos{
    template<>
    struct SystemThings<class MovementSystem> {
        // We want to get a mutable ref to position, a readonly ref to velocity, and a copy of the health component.
        using Query = Types<Position3D, const Velocity&, Health>;
        using Dependencies = Types<>;
    }

    class MovementSystem : eyos::System<MovementSystem> {
        using Super = eyos::System<MovementSystem>;

    public:
        void Init(World& world) override;
        void Update(Super::SystemComponentQuery& query);
        void Shutdown() override;

    private:
        World* world;
    };

    void MovementSystem::Update(Super::SystemComponentQuery& query) {
        for(auto entityProxy : query) {
             auto&&[pos, vel, health] = entityProxy;
             
             if(health.health > 0)
                 pos += vel * world->time.GetDeltaTime();
        }
    }
}

int main() {
    eyos::World world{};
    eyos::SystemScheduler scheduler{};
    scheduler.AddSystem(eyos::MovementSystem {});

    while(true) {
        scheduler.Update(world);
    }
}