[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;
}
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