University of Wales Trinity Saint David
ACCB5017 - Games Architecture and Mechanics

Basic ECS

Overview

This lab builds a small entity component system that moves a single entity across an SFML window. You will create an Entity class that is nothing but an id, a ComponentManager that stores components of any type keyed by entity id, and a MovementSystem that reads a PositionComponent and a VelocityComponent for every entity that has both and integrates position over time. Each piece is built and proven on its own, a component is attached and read back before any system exists, before the two are wired together into a window's per-frame loop so the entity's position visibly changes on screen.

Learning Objectives

By the end of this lab you will be able to:

Concept

A game object represented as a single class quickly runs into a wall once objects need different combinations of behaviour. A Pickup that also moves and a Pickup that also damages the player each want a different mix of capability, and a single inheritance tree can only express those combinations by growing new, increasingly specific subclasses, or by cramming unrelated behaviour into a shared base class that every object pays for whether it needs it or not.

An Entity Component System avoids this by separating identity, data and logic into three pieces. An Entity is nothing but an id, an integer used only to tie a set of components together. A Component is a plain struct holding only data, a PositionComponent stores nothing but x and y. A ComponentManager stores every component of a given type in its own map keyed by entity id, so any entity can be given any combination of components without inheriting anything. A System then reads and writes whichever components it cares about, a MovementSystem only ever touches entities that have both a PositionComponent and a VelocityComponent.

addComponent<T>(entityId, component) attaches a component of type T to an entity, getComponent<T>(entityId) returns a pointer to one entity's component of type T or nullptr, and getComponents<T>() returns every component of type T across every entity so a system can loop over all of them at once. Each type gets its own storage because getComponentMap<T>() returns a function-local static map, one instantiated per type T the compiler ever sees used, so ComponentManager needs no upfront list of the component types it will store.

Tasks

Task 1 - Create the project

Create the project and confirm SFML is linked correctly, before any custom code exists.

  1. Create a new Console App (C++) project the same way as the SFML project in the Environment Setup lab, named ECSDemo, with SFML linked and the platform set to x64.
  2. Replace the contents of the generated .cpp file with the code below, a bare window with nothing drawn in it yet, and run it with Ctrl+F5.

Success condition: a window titled ECS Demo opens, filled with a solid colour, and closes cleanly when you click its close button.

ECSDemo.cpp
#include <SFML/Graphics.hpp>
#include <optional>

int main()
{
    sf::RenderWindow window(sf::VideoMode({ 800, 600 }), "ECS Demo");
    window.setFramerateLimit(60);

    while (window.isOpen())
    {
        while (const std::optional event = window.pollEvent())
        {
            if (event->is<sf::Event::Closed>())
            {
                window.close();
            }
        }

        window.clear(sf::Color(30, 30, 40));
        window.display();
    }

    return 0;
}

Task 2 - Create the Entity class

An entity in this lab carries no data and no behaviour of its own, it is nothing but an id that ties a set of components together. In Solution Explorer, use Add > New Item... to create a header file named Entity.h. Replace its generated content with the code below.

Success condition: the project still builds with Entity.h in place. Nothing constructs an Entity yet, so there is nothing to run until the next task.

Entity.h
#pragma once

class Entity
{
public:
    explicit Entity(int id) : id(id) {}

    int getId() const
    {
        return id;
    }

private:
    int id;
};

Task 3 - Create the ComponentManager class

Every component, whatever its type, needs somewhere to live. Use Add > Class... to create ComponentManager, generating ComponentManager.h and ComponentManager.cpp. Replace the contents of ComponentManager.h with the code below, then complete the two marked functions.

  1. In addComponent(), get this type's component map with getComponentMap<T>() and store component at componentMap[entityId].
  2. In getComponent(), get this type's component map, then use find(entityId) to look up the entry. Return a pointer to its value if one was found (&it->second), or nullptr if it was not.

Replace the contents of ComponentManager.cpp with the single line below. Every function on ComponentManager is a template defined directly inside the class, so nothing here needs a separate definition, the source file exists only because Visual Studio's wizard always creates one alongside the header.

Success condition: the project still builds with both marked functions completed. There is still nothing to run yet, since nothing has added a component until the next task.

ComponentManager.h
#pragma once

#include <unordered_map>

class ComponentManager
{
public:
    template<typename T>
    void addComponent(int entityId, T component)
    {
        // TODO: get this type's component map and store component at entityId
    }

    template<typename T>
    T* getComponent(int entityId)
    {
        // TODO: get this type's component map, find entityId in it, and
        // return a pointer to its value if found, or nullptr if it is not
    }

    template<typename T>
    const std::unordered_map<int, T>& getComponents()
    {
        return getComponentMap<T>();
    }

private:
    template<typename T>
    std::unordered_map<int, T>& getComponentMap()
    {
        static std::unordered_map<int, T> componentMap;
        return componentMap;
    }
};
ComponentManager.cpp
#include "ComponentManager.h"

Task 4 - Define components and store one on an entity

A component is a plain struct that holds only data, no logic of its own. Use Add > New Item... to create a header file named Components.h, holding the two components this lab uses. Replace its generated content with the code below.

Open ECSDemo.cpp and replace its contents with the code below, this constructs a ComponentManager and an Entity, attaches a PositionComponent and a VelocityComponent to it through addComponent(), then reads the position back through getComponent() and prints it, before opening the same window as the previous task.

Success condition: run the program. Before the window opens, the console prints Position: 100, 100, proving addComponent() and getComponent() both work for a real component type.

Components.h
#pragma once

struct PositionComponent
{
    float x, y;
};

struct VelocityComponent
{
    float vx, vy;
};
ECSDemo.cpp
#include <SFML/Graphics.hpp>
#include <iostream>
#include <optional>

#include "ComponentManager.h"
#include "Components.h"
#include "Entity.h"

int main()
{
    ComponentManager componentManager;
    Entity player(1);

    componentManager.addComponent(player.getId(), PositionComponent{ 100.f, 100.f });
    componentManager.addComponent(player.getId(), VelocityComponent{ 50.f, 50.f });

    auto* position = componentManager.getComponent<PositionComponent>(player.getId());
    if (position)
    {
        std::cout << "Position: " << position->x << ", " << position->y << std::endl;
    }

    sf::RenderWindow window(sf::VideoMode({ 800, 600 }), "ECS Demo");
    window.setFramerateLimit(60);

    while (window.isOpen())
    {
        while (const std::optional event = window.pollEvent())
        {
            if (event->is<sf::Event::Closed>())
            {
                window.close();
            }
        }

        window.clear(sf::Color(30, 30, 40));
        window.display();
    }

    return 0;
}

Task 5 - Create the MovementSystem class

A system reads and writes components for whichever entities have the right combination attached, without ever touching Entity itself. Use Add > Class... to create MovementSystem, generating MovementSystem.h and MovementSystem.cpp. Replace the contents of MovementSystem.h with the code below. The diagram shows what update() needs to do for every entity that has a PositionComponent.

getComponents<PositionComponent>() returns a const map, since handing out a non-const reference would let a system resize or erase entries it has no business touching, so each entry's position is cast back to a mutable reference with const_cast before it can be changed, that part is given complete below. Complete the two marked lines that use deltaTime to move mutablePosition along velocity.

Replace the contents of MovementSystem.cpp with the single line below, the same as ComponentManager.cpp, since MovementSystem's only function is defined directly inside the class.

Success condition: the project still builds with both marked lines completed. Nothing calls update() yet, so there is still nothing to run until the next task.

MovementSystem.h
#pragma once

#include "ComponentManager.h"
#include "Components.h"

class MovementSystem
{
public:
    explicit MovementSystem(ComponentManager& componentManager) : cm(componentManager) {}

    void update(float deltaTime)
    {
        const auto& positions = cm.getComponents<PositionComponent>();

        for (const auto& entry : positions)
        {
            int entityId = entry.first;
            PositionComponent& mutablePosition = const_cast<PositionComponent&>(entry.second);

            auto* velocity = cm.getComponent<VelocityComponent>(entityId);
            if (velocity)
            {
                // TODO: add velocity->vx * deltaTime to mutablePosition.x
                // TODO: add velocity->vy * deltaTime to mutablePosition.y
            }
        }
    }

private:
    ComponentManager& cm;
};
MovementSystem.cpp
#include "MovementSystem.h"

Task 6 - Wire MovementSystem into the frame loop

Open ECSDemo.cpp and replace its contents with the code below, this constructs a MovementSystem, measures elapsed time each frame with an sf::Clock, and draws the entity's current position as a circle. Complete the one marked line inside the loop.

  1. Call movementSystem.update(deltaTime) so the entity's PositionComponent integrates this frame's VelocityComponent before anything is drawn.

Success condition: build and run. A circle starts near the top-left of the window and moves diagonally across it at a steady speed, continuing off the edge of the window since nothing stops it there.

ECSDemo.cpp
#include <SFML/Graphics.hpp>
#include <optional>

#include "ComponentManager.h"
#include "Components.h"
#include "Entity.h"
#include "MovementSystem.h"

int main()
{
    ComponentManager componentManager;
    Entity player(1);

    componentManager.addComponent(player.getId(), PositionComponent{ 100.f, 100.f });
    componentManager.addComponent(player.getId(), VelocityComponent{ 50.f, 50.f });

    MovementSystem movementSystem(componentManager);

    sf::RenderWindow window(sf::VideoMode({ 800, 600 }), "ECS Demo");
    window.setFramerateLimit(60);

    sf::Clock clock;
    while (window.isOpen())
    {
        while (const std::optional event = window.pollEvent())
        {
            if (event->is<sf::Event::Closed>())
            {
                window.close();
            }
        }

        float deltaTime = clock.restart().asSeconds();

        // TODO: call movementSystem.update(deltaTime)

        window.clear(sf::Color(30, 30, 40));

        auto* position = componentManager.getComponent<PositionComponent>(player.getId());
        if (position)
        {
            sf::CircleShape shape(10.f);
            shape.setPosition({ position->x, position->y });
            window.draw(shape);
        }

        window.display();
    }

    return 0;
}

Solution

Try the tasks above before revealing the solution.