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

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

 

I have spent a fair time with game engines and entity component system. And quite honestly it also became sort of a hobby. So this article is all about entity component system as I've understood them so far.

This Is A SDL Example

I'll use SDL (Simple Direct Media Layer) in this example. This will help us to create a window on the screen, render images to it and get keyboard inputs. For dependency management we'll use conan. You can find the build commands in the README on GitHub.

In this example we'll start with a simple game class. For the beginning this looks like this:

class game
{
    public: 
        game() {/* SDL details here to create a window */}
        ~game() {/* SDL details to cleanup allocated memory */}

        // this we'll use in our mainloop to check if our game is running
        bool is_running() { return m_is_running; }
        // read keyboard / mouse input
        void read_input() 
        {
            // read sdl events
            SDL_Event sdl_event;
            SDL_PollEvent(&sdl_event); 
            // get all keys here
            const Uint8* keystates = SDL_GetKeyboardState(NULL);
            
            // quit / close our window by pressing escape
            if (keystates[SDL_SCANCODE_ESCAPE]) {
                m_is_running = false;
            }
        }
        void update() {/* here we'll update all components*/}
        void render(){/* here we'll render*/}

    private:
        // our members for now ... 
        std::size_t m_width;
        std::size_t m_height;
        SDL_Window* m_window; 
        SDL_Renderer* m_renderer;
        bool m_is_running = true;
};


// .... 

// which will create a window by running this main
// and closes the window by pressing escape

int main(int argc, char* argv[]) 
{
    // create a game on a 800x600 window
    cwt::game game(800, 600);
    while(game.is_running()) 
    {
        game.read_input();
        game.update();
        game.render();
    }
    return 0;
}

It’s quite simple and nice, but I’m not gonna lie it is not the most exciting window …

ECS - Entity Component System

So let's get started with the actual ECS, but what is it exactly:

  • Entity: Represents just an ID, not more, not less.
  • Component: Represents data which is linked to an Entity
  • System: Represents logic, which processes our data, in this case the components

To make things easier, consider the following image of the older super mario from the super nintendo. For instance, think of Mario and what kind of data he needs in the game:

Mario would consist of these data:

  • Position and velocity
  • Sprite, which is rendered to the screen
  • Controls/Keyinput, because you can move him
  • Collisions/Physics, he walks on the ground, can collide with enemies, can collect items, etc.
  • and many more...

On the other hand, the item box would consist of:

  • Sprite
  • Colliisions

If you'd now design this into components, you could have these entities:

These entites could describe Mario (1), the item box (2) and HUD items (2)

Systems

Now there are Systems missing. When we render our game, we'll iterate for each frame over all entities and process specific components there. For example:

A Movementsystem would process the keyinputs and moves our character.

Put ECS Into Code

We'll create a naiv approach now, so we can understand how things work and later we'll use a framework which does most of the work for us. This approach here is far from perfect and is only for demonstration.

First, we'll start with creating an entity:

// an alias to make it more expressive
using entity = std::size_t;
// we need the maximum count, this isn't the best solution, but it does the job for now
entity max_entity = 0;
// and here we create some unique ID's
std::size_t create_entity()
{
    static std::size_t entities = 0;
    ++entities;
    max_entity = entities;
    return entities;
}

Second, we add some structs, which are our components:

// a sdl rect holds width, height, x and y
// a sprite takes a source  rectangle 
// and places it to the given destination on the screen 
struct sprite_component {
    SDL_Rect src;
    SDL_Rect dst;
    SDL_Texture* texture;
};

// a position and a velocity represented as transform
struct transform_component {
    float pos_x;
    float pos_y;
    float vel_x;
    float vel_y;
};

// we'll only create a type for keyinputs, we get the pressed keys later 
struct keyinputs_component {};

Additionally, we'd need something to store them. Here we'll use a registry where all components are stored in a unordered_map. I'm used to the term registry and I think this is pretty common:

struct registry {
    std::unordered_map<entity, sprite_component> sprites;
    std::unordered_map<entity, transform_component> transforms;
    std::unordered_map<entity, keyinputs_component> keys;
};

And lastly, we make two systems. Each system will have two steps which will be called in our mainloop:

  • update: here we update data
  • render: we actually render something

Sprite System

struct sprite_system 
{
    // first we'll update all sprites
    void update(registry& reg)
    {
        // we iterate over all entities
        for (int e = 1 ; e <= max_entity ; e++) {
            // all entities with a sprite AND a transform 
            // will update their position
            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 finally all sprites are rendered in this render method
        // to their appropriate position on the screen
        for (int e = 1 ; e <= max_entity ; e++) {
            if (reg.sprites.contains(e)){
                SDL_RenderCopy(
                    renderer, 
                    reg.sprites[e].texture, 
                    &reg.sprites[e].src, 
                    &reg.sprites[e].dst
                );
            }
        }
    }
};

Transform System

struct transform_system 
{
    // we'll use a constant time for this demonstration
    float dt = 0.1f;
    
    void update(registry& reg)
    {
        for (int e = 1 ; e <= max_entity ; e++) {
            // and each entity with a transform compnent will update
            // their position with respect to their velocity
            if (reg.transforms.contains(e)){
                reg.transforms[e].pos_x += reg.transforms[e].vel_x*dt;
                reg.transforms[e].pos_y += reg.transforms[e].vel_y*dt;
            }
        }
    }
};

Movement System

struct movement_system 
{  
    void update(registry& reg)
    {
        // here we get an array of all keys from sdl
        const Uint8* keys = SDL_GetKeyboardState(NULL);

        for (int e = 1 ; e <= max_entity ; e++) {
        
            // and all entities with a movement component get their velocity 
            // set if A, S, D or W is pressed on the keyboard
            if (reg.transforms.contains(e) && reg.keys.contains(e)){
                
                if (keys[SDL_SCANCODE_A]) { reg.transforms[e].vel_x = -1.0f; } 
                if (keys[SDL_SCANCODE_S]) { reg.transforms[e].vel_y = 1.0f; }
                if (keys[SDL_SCANCODE_W]) { reg.transforms[e].vel_y = -1.0f; }
                if (keys[SDL_SCANCODE_D]) { reg.transforms[e].vel_x = 1.0f; }
                
                if (!keys[SDL_SCANCODE_A] && !keys[SDL_SCANCODE_D]) { reg.transforms[e].vel_x = 0.0f; }
                if (!keys[SDL_SCANCODE_S] && !keys[SDL_SCANCODE_W]) { reg.transforms[e].vel_y = 0.0f; }
            } 
        }
    }
};

And that's it, we're almost done. We only need to:

  • Add the regeistry and the systems to our game class,
  • Call systems update and render functions and
  • Create entities for our game
class game
{
public: 
    // ... 
    
    // here we call all update functions
    void update()
    {
        m_transform_system.update(m_registry);
        m_movement_system.update(m_registry);
        m_sprite_system.update(m_registry);
    }
    // here we call the render functions
    void render()
    {
        SDL_RenderClear(m_renderer);
        m_sprite_system.render(m_registry, m_renderer);
        SDL_RenderPresent(m_renderer);
    }
private:
    // ...
    // and here the members
    registry m_registry;

    sprite_system m_sprite_system;
    transform_system m_transform_system;
    movement_system m_movement_system;
};

// ...

int main(int argc, char* argv[]) 
{
    // we create a game in a 800x600 window
    cwt::game game(800, 600);
    
    // here we create the entity, remember this is just an alias for std::size_t
    cwt::entity bird = cwt::create_entity();
    
    // here we add a sprite component with the [] operator from the unordered map
    game.get_registry().sprites[bird] = cwt::sprite_component{
        SDL_Rect{0, 0, 300, 230}, 
        SDL_Rect{10, 10, 100, 73}, 
        // this sdl function creates a texture by it's path
        // bird_path is the path to the bird.png generated by cmake
        // so the path is not relative or depends on my system
        IMG_LoadTexture(game.get_renderer(), bird_path)
    };
    
    // a transform component
    game.get_registry().transforms[bird] = cwt::transform_component{10, 10, 0, 0};
    // and we add key inputs
    game.get_registry().keys[bird] = cwt::keyinputs_component { };
    
    while(game.is_running()) {/*...*/}
}

And that’s the bird, which you can move with A, S, D and W

I added two more birds to this example, one which is moving and a constant one, so you can see the difference with different component.

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

Conclusion

Well, this is a naive draft of what an Entity-Component-System is. Of course, in real game engines you can imagine what is going on and there is way more of what I demonstrated here. But, I really like the approach and the separation from data and logic. I often use this in my projects (non game applications) and it gives me a lot of advantages.

I'll continue on that soon, because there is a framework EnTT (pronounced En Tee Tee -> entity) which does the actual job for us. There you have a registry type, you can create entities and add components. We'll refactor this code here and integrate EnTT.

So I hope that helped.

Best Thomas

 
 
 
Previous
Previous

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

Next
Next

[C++] CRTP For Mixin Classes