Entity Component System

I have been putting this topic off for a while but my codebase - in particular the way a scene is structured - has gotten out of control. I have a Scene class that has a container of SceneObject, with automatic for loop support. You loop over the entire scene and do something with each SceneObject. The Scene class has a point light, directional light and camera as member variables. SceneObject has been a culmination of "what do I need entities in my scene to have?". It has member variables for a name, bounding box, an albedo texture, a normal map texture, Bullet triangle mesh. Oh and it also inherits from Mesh and Transformable.

ECS


Time to refactor. The idea is to implement an Entity Component System (ECS), a software architectural pattern mostly used in games. The pattern is relatively straight forward but implementations can differ a lot from each other. One problem that ECS tries to solve is the cost and trouble that comes with traditional OOP. C++'s virtual functions are certainly not part of its zero-cost abstractions, and codebases become a mess when dealing with multiple inheritance. The ECS pattern lends itself very well to data-oriented design, which is what this implementation focuses on.

Implementation 


Entities are register-size values that act as unique ID's for every entity in your game. In our case it is literally a 32 bit integer:
typedef uint32_t Entity;

Components are just data structs with additional member functions for simple operations, local to the component.

struct TransformComponent {
    glm::vec3 position      = { 0.0f0.0f0.0f };
    glm::vec3 scale         = { 1.0f1.0f1.0f };
    glm::vec3 rotation      = { 0.0f0.0f0.0f };


    glm::mat4 matrix        = glm::mat4(1.0f);


    glm::vec3 localPosition = { 0.0f0.0f0.0f };


    void recalculateMatrix() {
        matrix = glm::translate(glm::mat4(1.0f), position);
        auto rotationQuat = static_cast<glm::quat>(rotation);
        matrix = matrix * glm::toMat4(rotationQuat);
        matrix = glm::scale(matrix, scale);
    }
};

An entity can have any combination of components, it is just an integer associated with any component type. But how do we create the assocation? This is where most of the data-driven design comes in.


template<typename ComponentType>
class ComponentManager {
void create(Entity entity);


private:
    std::vector< ComponentType> components;
    std::vector<Entity> entities;
    std::unordered_map<Entity, size_t> lookup;
};

This handles all per-component data. Say we have a ComponentManager<TransformComponent> it holds all the transform components in a linear array, all the associated entities in a linear array and a map that holds the associations. For any entity we can do a look up in that map and get back the index into the components array. Linear arrays are very cache friendly and linear array indexing is O(1) time complexity, the only relatively slow thing here is the unordered map.


Systems

Systems actually perform operations on component data. State is only reflected in data, systems perform work on that data. You can think of a system as a simple function, consider the following pseudo code:

void moveAllEntities(ComponentManager<TransformComponent>& transforms) {
    for(transform in transforms) {
        transform.x += 1;
    }
}

 They don't have to be static functions but you get the idea.
At this point I am able to throw out my old Scene class. The new one holds a ComponentManager<> for every ComponentType, adding objects is now as simple as generating a new Entity and adding components by calling the create() function on the corresponding ComponentManager.
The only work left to do now is create component structs for everything I need e.g a LightComponent, MeshComponent, MaterialComponent etc.

Comments

Popular posts from this blog

Ray Tracing in One Weekend - OpenGL Compute

Shadows

Screen Space Ambient Occlusion