[C++] Use EnTT When You Need An ECS

You can find this project here on GitHub. This link refers to the tag entt.

 

I showed the basic idea of an entity component system in my last article. Now we continue and we refactor our the code by introducing EnTT.

EnTT (pronounced "en tee tee" like entity) is a open-source, header-only library which gives us all we need to use a entity component system.

Getting Started With EnTT

There is really not much to talk about in the first place, so let's take a look on my comments in the basic example from EnTT:

// we'll only need this header
#include <entt/entt.hpp>

// we have two example components here
struct position {
    float x;
    float y;
};

struct velocity {
    float dx;
    float dy;
};

int main() {
    // we can create a entt::registry to store our entities
    entt::registry registry;
    
    // we'll create 10 entities 
    for(std::size_t i = 0; i < 10; ++i) {
        // to create an entity, use the create function
        const auto entity = registry.create();
        // and by using emplace with a specific component
        // we add a component to an entity and forward all their arguments
        registry.emplace<position>(entity, i * 1.f, i * 1.f);
        // if we have a even number, we'll also add a velocity component to it
        if(i % 2 == 0) { 
            registry.emplace<velocity>(entity, i * .1f, i * .1f); 
        }
    }
    
    // now we can create a view, which is a partial registry
    // pass in all the components we need and in the view we'll have 
    // all entities with the given components
    auto view = registry.view<const position, velocity>();

    // now there are three different options to loop through the entities from our view
    // 1.: a basic lambda which will be called with the given components
    // note: the lambda arguments need to match the components with which we create this view
    view.each([](const auto &pos, auto &vel) { /* ... */ });

    // 2.: an extended lambda which also gives us the entity if we need it
    view.each([](const auto entity, const auto &pos, auto &vel) { /* ... */ });

    // 3. a for loop by using structured bindings
    for(auto [entity, pos, vel]: view.each()) {
        // ...
    }

    // 4. a for loop with a forward iterator
    for(auto entity: view) {
        auto &vel = view.get<velocity>(entity);
        // ...
    }
}

It's pretty simple, isn't it.

Let's Refactor

We begin with removing the registry and replace it with entt::registry:

class game
{
// ...  
    // we return entt::registry now
    // if we used auto as returntype, we wouldn't need to change this here ... 
    entt::registry& get_registry() 
    { 
        return m_registry; 
    }

private:
    // ...
    // here we'll store our entities
    entt::registry m_registry;
};

We continue with the systems, where we used this style here:

struct sprite_system 
{
    void update(registry& reg)
    {
        for (int e = 1 ; e <= max_entity ; e++) {
            if (reg.sprites.contains(e) && reg.transforms.contains(e)){
                reg.sprites[e].dst.x = reg.transforms[e].pos_x;
                reg.sprites[e].dst.y = reg.transforms[e].pos_y;
            }
        }
    }
    void render(registry& reg, SDL_Renderer* renderer)
    {
        // ... 
    }
};

And the systems now implemented with a entt::registry (same applies for all other systems):

struct sprite_system 
{
    // we pass in entt::registry now
    void update(entt::registry& reg)
    {
        // we create a view for sprite and transform
        auto view = reg.view<sprite_component, transform_component>();
        // we iterate over all entities with a sprite and transform component
        // the logic inside the lambda is the same as in the previous for loop
        view.each([](auto &s, auto &t){
                s.dst.x = t.pos_x;
                s.dst.y = t.pos_y;
        });
    }
    void render(entt::registry& reg, SDL_Renderer* renderer)
    {
        //...
    }
};

And finally we change the main function. Where we now create the bird entities from entt::registry and emplace the components accordingly:

int main(int argc, char* argv[]) 
{
    cwt::game game(800, 600);
    
    // a bird entity
    auto bird_1 = game.get_registry().create();
    // we emplace the sprite component
    game.get_registry().emplace<cwt::sprite_component>(bird_1, 
        SDL_Rect{0, 0, 300, 230}, 
        SDL_Rect{10, 10, 100, 73}, 
        IMG_LoadTexture(game.get_renderer(), bird_path)
    );
    // we emplace the transform component
    game.get_registry().emplace<cwt::transform_component>(bird_1, 10.0f, 10.0f, 0.0f, 0.0f);
    // and we emplace the keyinputs
    game.get_registry().emplace<cwt::keyinputs_component>(bird_1);
    
    // ... and two more birds

    // our game loop remains the same
    while(game.is_running()) 
    {
        game.read_input();
        game.update();
        game.render();
    }
    
    return 0;
}

There we are, we have now integrated EnTT

You can find this project here on GitHub. This link refers to the tag entt.

Conclusion

There is no reason to reinvent the wheel. And most things we need as programmers already exist. So use it, take advantage of it and you'll get fast progress.

I've used EnTT also in non-gaming applications and I can only recommend it. Code gets simpler, you get fast progress and you can spend your time more on the details you need to spend.

I hope that helped.

Best Thomas

Previous
Previous

[C++] User-Defined Literals To Handle Units

Next
Next

[C++] An Entity-Component-System From Scratch