Systems in the Entity-Component-System Design Pattern are responsible for applying logic to a set of components. Examples of systems are a Rendering System or a Physics System.

  • The Rendering System sends data to the GPU and calls OpenGL functions to display the scene on the screen.
  • The Physics System updates each object according to a set of rules. A valuable property of the systems is that they only need to know a subset of the components of an entity to do its job (e.g., the Physics System doesn’t require any vertex data). This way, the complexity can be kept low, which could be seen as an application of the Law of Demeter.

Another advantage is that it allows for optimisations much more easily (e.g., grouping of rendering calls) because a single system is aware of everything that needs to be rendered.

Rendering System

Unfortunately, the advantages of Systems discussed in the previous section, won’t be discussed further in this post. The focus of the initial implementation is to reduce the amount of responsibilities in the Entity class. The current implementation is very far from a good application of the Single-Responsibility Principle.

#pragma once
 
#include "engine/components/collider.hpp"
#include "engine/components/component.hpp"
#include "engine/components/model.hpp"
#include "engine/components/transform.hpp"
#include "engine/tree.hpp"
 
#include <glm/glm.hpp>
#include <memory>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <utility>
 
class ShaderRegistry;
 
namespace Engine {
    class Entity final : public TreeNode<Entity> {
      public:
        explicit Entity(const Entity* parent, std::string name);
        explicit Entity();
        ~Entity() override = default;
 
        auto update(int dt) -> void;
 
        void render(ShaderRegistry& shaderContainer) const;
 
        auto getName() const -> const std::string&;
        auto getComponents() const
            -> const std::unordered_map<std::size_t, std::unique_ptr<Components::Component>>&;
 
        template <typename TComponentType>
        auto registerComponent(std::unique_ptr<TComponentType> component) -> void;
        template <typename TComponentType, typename... TArgs>
        auto createAndRegisterComponent(TArgs&&... args) -> void;
 
        template <typename TComponentType>
        auto get() const -> TComponentType*;
        template <typename TComponentType>
        auto getRequired() const -> TComponentType&;
 
      protected:
        auto visitImpl(std::function<void(const Entity&)> callback) const -> void override;
        auto visitImpl(std::function<void(Entity&)> callback) -> void override;
 
        std::unordered_map<std::size_t, std::unique_ptr<Components::Component>> m_components;
 
        std::string m_name = "Invalid Entity Name";
    };
}

An entity is a grouping of components, but the render and update functions seem out of place in this context.

With the Rendering System in place, the render function in Entity is not required anymore. Preferably, the System only works with components, but that is not possible in this first implementation, because there are dependencies in rendering order. These are the result of the glow effect that disables rendering to the depth buffer.

void Glow::render(ShaderRegistry& shaderRegistry) const {
    glDisable(GL_DEPTH_TEST);
    shaderRegistry.getOrCreate<GlowShader>().use();
    m_entity.getRequired<Model>().glDraw();
    glEnable(GL_DEPTH_TEST);
}

Because of the way the effect is implemented, it always has to be rendered before the object itself, creating a strict dependency in rendering.

The implementation of the rendering system is as follows:

#include "engine/system/rendering.hpp"
 
#include "engine/components/effect.hpp"
#include "engine/components/gui_component.hpp"
#include "engine/components/model.hpp"
 
namespace Engine::System {
    Rendering::Rendering(ShaderRegistry& shaderRegistry)
          : m_shaderRegistry{shaderRegistry} {
    }
 
    auto Rendering::render() const -> void {
        for (const auto& entity : m_entities) {
            if (const auto& effect = entity->get<Components::Effect>()) {
                entity->getRequired<Components::Transform>().passModelMatrixToShader(
                    m_shaderRegistry);
                effect->renderEffects(m_shaderRegistry);
            }
            if (const auto& model = entity->get<Components::Model>()) {
                entity->getRequired<Components::Transform>().passModelMatrixToShader(
                    m_shaderRegistry);
                model->render();
            }
            if (const auto& guiComponent = entity->get<Components::GuiComponent>()) {
                guiComponent->render();
            }
        }
    }
 
    auto Rendering::registerEntity(Entity* entity) -> void {
        m_entities.insert(entity);
    }
 
    auto Rendering::unregisterEntity(Entity* entity) -> void {
        m_entities.extract(entity);
    }
}

Entities explicitly have to be unregistered from the Rendering System, because the Rendering System uses pointers to Entities and can’t know when they have been destructed. This leads to additional code in the Scene implementation:

#pragma once
 
(...)
 
namespace Engine {
    class Scene {
        (...)
 
      private:
        (...)
 
        std::vector<std::unique_ptr<Entity>> m_entities;
        System::Rendering m_renderingSystem;
    };
}
#include "engine/scene.hpp"
 
(...)
 
namespace Engine {
    (...)
  
    auto Scene::render() -> void {
        m_renderingSystem.render();
 
        renderGui();
    }
 
    auto Scene::addEntity(std::unique_ptr<Entity> entity) -> void {
        m_renderingSystem.registerEntity(entity.get());
        m_entities.push_back(std::move(entity));
    }
 
    auto Scene::getEntities() -> const std::vector<std::unique_ptr<Entity>>& {
        return m_entities;
    }
 
    auto Scene::clearEntities() -> void {
        for (auto& entity : m_entities) {
            m_renderingSystem.unregisterEntity(entity.get());
        }
 
        m_entities.clear();
    }
}

Looking at the remaining code in Entity, only the update function should still be replaced by a system. Besides that, all complexity has been removed.

Other Updates

Finally, let’s also discuss some of the other code improvements that have been done in the meantime.

getOrCreate in Shader Registry

The ShaderRegistry had to be aware of every type of shader that was used in the application because they had to be instantiated in the constructor. A cleaner way to avoid this dependency is to allow users to lazy instantiate the shaders. This way, shaders are only compiled when they are actually used and the Shader Registry doesn’t require the dependency anymore.

#pragma once
 
(...)
 
namespace Engine {
    class ShaderRegistry final {
      public:
        (...)
 
        template <typename TShaderType>
        auto getOrCreate() -> TShaderType&;
 
      private:
        template <typename TShaderType>
        auto registerShader(std::unique_ptr<TShaderType> shader);
        
        (...)
 
        std::unordered_map<std::size_t, std::unique_ptr<Shader>> m_shaders;
    };
 
    template <typename TShaderType>
    auto ShaderRegistry::getOrCreate() -> TShaderType& {
        auto iterator = m_shaders.find(typeid(TShaderType).hash_code());
        if (iterator == m_shaders.end()) {
            iterator = registerShader(std::make_unique<TShaderType>());
        }
        return static_cast<TShaderType&>(*iterator->second);
    }
 
    template <typename TShaderType>
    auto ShaderRegistry::registerShader(std::unique_ptr<TShaderType> shader) {
        if (m_matrixBlockIndex == GL_INVALID_INDEX) {
            // Bind Uniform Block to Uniform Buffer Object
            m_matrixBlockIndex = shader->getUniformBlockIndex("ModelViewProjection");
            // Bind buffer to index
            glBindBufferRange(GL_UNIFORM_BUFFER,
                              Shader::BINDING_INDEX,
                              m_matrixUBO,
                              0,
                              sizeof(glm::mat4) * 3);
        }
        shader->setMatrixBlockIndex(m_matrixBlockIndex);
 
        SDL_LogInfo(SDL_LOG_CATEGORY_SYSTEM,
                    "Registering Shader with ID %s",
                    typeid(TShaderType).name());
 
        auto result = m_shaders.insert({typeid(TShaderType).hash_code(), std::move(shader)});
        assert(result.second);
        return result.first;
    }
}

Removing the Generic Shader Component

When shaders were implemented as components, there were two types of components created: ShaderComponent and GenericShaderComponent. ShaderComponent was the interface for a component, while the GenericShaderComponent was the implementation of that interface that contained a reference to the shader itself.

This was an unnecessary level of indirection. The original idea was to have specialised components for each shader that contained the GUI for the options in the shader. At this point, there are no specialisations, so this complexity can be removed. I prefer to only introduce the complexity when it is needed.

#pragma once
 
(...)
 
namespace Engine::Components {
    /**
     * Component implementation of a Shader. Specialized Shaders with specific configuration can
     * inherit from this implementation to override the renderConfiguration function.
     *
     * The distinction between Shader as a Component and the Shader itself is required to avoid
     * compiling the same Shader multiple times because they are attached as components to different
     * entities.
     */
    class Shader : public Component {
      public:
        Shader(Entity& entity, Engine::Shader& shader);
        ~Shader() override = default;
 
        auto use() -> void;
 
        // --- Component ---
        auto renderConfiguration() -> void override;
 
      private:
        Engine::Shader& m_shader;
    };
}
#include "engine/components/shader.hpp"
 
(...)
 
namespace Engine::Components {
    Shader::Shader(Entity& entity, Engine::Shader& shader)
          : Component{entity, "Generic Shader"}
          , m_shader{shader} {};
 
    auto Shader::use() -> void {
        m_shader.use();
    }
 
    auto Shader::renderConfiguration() -> void {
        ImGui::Text("Shader Component");
        if (ImGui::Button("Recompile")) {
            m_shader.recompile();
        }
    }
}

Copying components

I always try to avoid putting too many restrictions on what users can do with an instance. There are cases though, where you know up front that some behaviour is simply a bug. An example of this is the copyability of components. Normally, a component is created once for an entity and never copied afterwards. In the original implementation of the render function in Entity, the components were accidentally copied:

void Entity::render(ShaderRegistry& shaderRegistry) const {
    auto effect = get<Components::Effect>();
    if (effect) {
        getRequired<Components::Transform>().passModelMatrixToShader(shaderRegistry);
        effect->renderEffects(shaderRegistry);
    }
    auto model = get<Components::Model>();
    if (model) {
        getRequired<Components::Transform>().passModelMatrixToShader(shaderRegistry);
        model->render();
    }
    for (const auto& child : m_children) {
        child->render(shaderRegistry);
    }
 
    auto guiComponent = get<Components::GuiComponent>();
    if (guiComponent) {
        guiComponent->render();
    }
}

It is a very subtle difference, only a single &-sign, but it becomes a costly mistake once you start copying over vertex data on each render call. The simple solution to prevent the bug from happening again is to remove the copy constructor (and copy assignment operator) of the component class.

#pragma once
 
#include <string>
 
namespace Engine {
    class Entity;
}
 
namespace Engine::Components {
    class Component {
      public:
        Component(Entity& entity, const std::string& m_name);
        virtual ~Component() = default;
 
        Component(const Component&) = delete;
        auto operator=(const Component&) -> Component& = delete;
 
        virtual auto renderConfiguration() -> void;
 
      protected:
        Entity& m_entity;
        std::string m_name;
    };
}