Build a Game Engine

ECS and the Game World

Prerequisites

Article

As you think about objects in the game world, there are a near infinite number of ways they can be programmed. One of the most common is an object-oriented or inheritance based approach. For example, you may have an Animal class, which has properties like height and weight and functions like Sleep and Eat, a Pet subclass for domesticated animals that can do things like Eat from a bowl, get pet and go to the bathroom where they are supposed to. You may have Cat and Dog subclasses that do things like meow and sleep all day or bark and play fetch. This relationship defines types and subtypes and works well when each subclass "is a" more specific version of its parent class (a Cat "is a" Pet which "is an" Animal).

While object oriented approaches are used throughout game engine programming, it is rarely the right approach for game play programming. Consider implementing a war game... you will have buildings, vehicles, soldiers. You may start with a Unit class that all objects share, then create a Vehicle class, subclassing to Submarines, Tanks, and Airplanes. Now consider whether those vehicles are allies or opponents, which will impact their behavior towards you as the player. An object-oriented approach might create AllyTank and OpponentTank classes or tag each vehicle with an is_opponent boolean, but is that really an attribute of the vehicle? Will you add that to buildings, land / borders, and other objects as attributes?

Addressing the flexibility and complexity of creating game objects is where the concept of object composition comes in. You start with an empty object, and you "compose" it by adding the components that make up the whole. Put another way, instead of creating "is-a" relationships, you create "has-a" relationships that define an object's properties and behaviors.

Returning to our war game example, a game play programmer may still use object oriented programming to create a Vehicle class and Tank, Submarine and Airplane subclasses, since those follow a clear "is-a" relationship. But now, they would also create Ally and Opponent classes or scripts that would define the attributes and behavior of allies and enemies. When starting from an empty game object, you can now compose it of both Tank and Opponent classes.

Some programmers will take this to an extreme and make up very tiny pieces: instead of a Tank, you now simply "compose" a tank of a base Vehicle, plus Treads, a Cannon, and an Enemy class or script. While this example likely calls for a mix of object-oriented and composition approaches, you can imagine a procedurally generated world that allows the computer to pick permutations of components to create new and wonderful things.

Returning to the concepts in an entity-component-system or ECS design, we have now covered the C in the middle with components. Entities are simply what we will call our game objects that will be filled with and defined by their components.

Let's take a look at a very basic Entity class:

    class UEntity : public UObject {
    public:
        // Entity ID
        uint32_t id;

        // Entity name
        std::string name;

        // Components
        std::vector<UComponent*> components;
    }

As you can see, our Entity (a subclass of our UObject base class) has three parts: an integer ID (useful later for serializing or syncing between client and server), a string name (useful for humans to identify it, usually in an editor), and a list of components.

To cover the S in ECS, we need to discuss a little bit more about components. Above, we said that when you compose objects with components, "you create "has-a" relationships that define an object's properties and behaviors". In an ECS design, components are actually just the data or properties that are needed to process its behavior. The actual behaviors or processing of the components are done by Systems.

Imagine in our war game that a Tank fires a Projectile. A new game object is spawned that is composed of (at least) the Projectile. It likely contains a reference to the Entity that shot it, as well as a type, a direction, and more. In an ECS model, you may create a ProjectileSystem, whose job it is to monitor and process all Projectiles. This system would move projectiles around the screen each frame, process any collisions (checking whether the originating Entity "has an" Ally or Opponent component), and inflict any necessary damage.

Let's take a look at an example Component base class and a more useful Projectile component example:

    class UEntity; // Forward declaration

    class UComponent : public UObject {
    public:
        UComponent() : entity(0) { }
        virtual ~UComponent() = default;

        // Entity we are attached to
        UEntity* entity;
    }

    class Projectile : public UComponent {
    public:
        // Originating entity that shot the projectile
        UEntity* originalEntity;

        // Type
        std::string type;

        // Direction and speed
        glm::vec3 direction;
        float speed;
    }

Component is simply a base class from which to create other classes so that we can add all of them to our Entity objects. The only property is to go one level up to the entity it is currently attached to.

Projectile is our first real component. Following the ECS design, it contains only data, no functions or behaviors. The first property is a reference to the original Entity that shot it. In an ECS design, every object in your game is a separate entity. That means that every time our Tank fires a Projectile, that Projectile component is attached to a new Entity (instead of still being attached to the Tank). That separation makes sense, since visually each projectile appears as its own object as well. It likely has a Sprite or Mesh component for rendering, maybe a 2D box collider component for collision detection, and more.

Projectile also has a type (in this case a string for ease of reading code, but I'm sure you would use an Enum or Integer to track the type in your game), as well as a direction and speed. We'll use these in our ProjectileSystem to help process all of our projectiles currently in play.

Before we look at our System classes, let's re-visit the Entity class and give ourselves a couple of ways to add, find, and remove components:

class UEntity : public UObject {
    public:
        UEntity() : m_id(0) { }
        ~UEntity() = default;


        // Create a component and return it
        template<class T> T* CreateComponent() {
            T* component = new T();
            m_components.push_back(component);
        }

        // Add an existing component to our entity
        void AddComponent(UComponent* component) {
            m_components.push_back(components);
        }

        // Search for a component of a specific type
        template<class T> T* FindComponent() {
            for(auto it = m_components.begin(); it != m_components.end(); it++) {
                T* obj = dynamic_cast<T*>(*it);
                if(obj) return(obj);
            }

            // Not found, return zero
            return(0);
        }

        // Remove a component
        void RemoveComponent(UComponent* component) {
            auto it = std::find(m_components.begin(), m_components.end(), component);
            if(it != m_components.end()) {
                m_components.erase(it);
            }
        }

    public:
        // Entity ID
        uint32_t id;

        // Entity name
        std::string name;

        // Entity transform
    UTransform transform;

    protected:
        // Components
        std::vector<UComponent*> m_components;
    }

First, we have two ways to add a component: by creating one that now belongs to our Entity, the most common use case in code, or by adding an existing component, allowing us to do things like deserialize a set of components from a file, database, or the network and add it to our Entity.

Next, we have a FindComponent method, which searches for a component of a specific type already attached to our Entity, or returns zero if not found. If you've ever used Unity's scripting language, this will look familiar to the GameObject.GetComponent function. Here we use a dynamic_cast to check whether the class type is in the hierarchy of what we're looking for - this casting allows us to go things like return all Vehicle components, which will return all Airplane, Tank, and Submarine components. Finally, we have a removal method that takes a reference to a Component - this will be most common in a game editor, so we'll most likely already have a reference to the component we want to remove.

With our Entity and Components now looking good, let's move on to our System class:

    class USystem : public UObject {
    public:
        USystem() = default;
        virtual ~USystem() = default;

        virtual void Initialize() { }
        virtual void Update(float delta) { }
        virtual void Shutdown() { }
    }

Like our Component class, our System base class is also very simple. A system, by definition, contains no data or permanent storage - it is simply a processor of components. We have three virtual functions that can be overridden: Initialize for when a system is created or started, Update(float) to be called each frame, and Shutdown for system cleanup.

Let's take a look at a more practical system:

    class ProjectileSystem : public USystem {
    public:
        void Update(float delta) {
            std::vector<Projectile*> projectiles = World::Instance()->GetComponents<Projectile>();
            for(auto it = projectiles.begin(); it != projectiles.end(); it++) {
                // Easier to read
                Projectile* projectile = (*it);

                // Update the projectile's position on the screen
                projectile->entity->Transform()->Translate(projectile->direction * projectile->speed * delta);
            }
        }
    }

This system is simply an example, but it illustrates a common or real use case. Ideally, systems process only Entities with their own components. In this example, we could also do collision checking, but that is generally best left to a physics or collision detection system that will search for overlaps and fire events.

One final class you'll see referenced as part of our ECS implementation is the World class. The World is a singleton container and owner for all of the Entity objects, and gives us a few handy functions for our Systems. Let's take a look:

    class UWorld : public UObject {
    public:
        // Create an entity
        UEntity* CreateEntity(uint32_t id = 0, std::string name = "");

        // Add an existing entity (eg. a deserialized entity)
        void AddEntity(UEntity* entity);

        // Find entity
        UEntity* FindEntity(uint32_t id);
        UEntity* FindEntity(std::string name);

        // Remove entity
        void RemoveEntity(UEntity* entity);

        // Singleton
        static UWorld* Instance();

        // Find all components inside of entities of a specific type
        template<class T> std::vector<T*> GetComponents() {
            std::vector<T*> components;
            for(auto it = m_entities.begin(); it != m_entities.end(); it++) {
                T* component = it->second->FindComponent<T>();
                if(component != 0) components.push_back(component);
            }
            return(components);
        }

    protected:
        // Next assignable entity ID
        uint32_t m_nextEntityID;

        // List of entities
        std::map<uint32_t, UEntity*> m_entities;

        // Singleton instance
        static UWorld* m_instance;
    }

This class does a couple of interesting things: first, it keeps track of and assigns entity IDs. When entities are created or added, the next entity ID will be adjusted to be higher than the max ID added so far. Entities are also stored by entity ID for quick lookup by the FindEntity function. Our most common call to the FindEntity function will be from the networking system, which will serialize and deserialize entities streamed over the network. On the other hand, most scripts will find an entity by name, but it's generally recommended they do that in a Start() (if you're used to Unity) or other initialization function - that's because that function will iterate over every Entity and do a string by string comparison. You could store the map either way, or store two separate maps with both the integer ID and string name as keys if you wanted to optimize for those use cases.

Finally, we have the GetComponents function, which will iterate over all entities and find a component type. This is particularly useful for calling from our systems is a polymorphic way... we might have a VehicleSystem that processes all Vehicles and its subclasses and a ProjectileSystem that only looks for Projectile components.

Finding Systems - The Service Locator Pattern

Now that we are starting to build systems, we'll need a way to find them. We only want one of each type of system and we need a way to find it. This could lend itself to a singleton, but a singleton requires awareness of the final implementation. For example, in our rendering module, we will implement a base RenderSystem class, along with platform specific GLRenderSystem and DXRenderSystem classes. In most of our engine, we don't really care which RenderSystem is doing the work, we just want to get a pointer to whichever one is in use. Since a singleton is static, it is bound to the type of class, eg. a RenderSystem::Instance function cannot be overridden to return different types.

Enter the Service Locator Pattern - a way for us to create and find objects without knowing their implementation details. In our case, we're going to start building out a Application class that will help us create, register, and find Systems:

class UApplication : public UObject {
public: 
    UApplication();
    ~UApplication() = default;

    template<class T> T* CreateSystem(float tickRate = 0) {
        T* sys = new T();
        sys->Initialize();

        RegisteredSystem* rs = new RegisteredSystem();
        rs->system = sys;
        rs->tickRate = (tickRate == 0) ? 0 : (1.0f / tickRate);
        m_systems.push_back(rs);
        return(sys);
    }

    template<class T> T* GetSystem() {
        auto it = m_systems.begin();
        for (; it != m_systems.end(); it++) {
            T* sys = dynamic_cast<T*>((*it)->system);
            if (sys) return(sys);
        }

        return(0);
    }

protected:
    struct RegisteredSystem {
        USystem* system;
        float tickRate;
        float ticker;
    };

public:
    static UApplication* Instance();

protected:
    // Singleton instance
    static UApplication* m_instance;

    // Registered systems
    std::vector<RegisteredSystem*> m_systems;
};

Our UApplication class is actually a singleton itself, because we always know there will only be one UApplication and this is its implementation. Inside of that are two useful functions: CreateSystem, which takes a tick rate (which we'll discuss in our Dates and Times article) and adds it to a list, and GetSystem, which searches the list. Note that these are templated functions, and GetSystem uses a dynamic_cast, which allows us to use inheritance to find objects by their base class - dynamic_cast will return an object if it can be up or down-casted into the necessary type.

Finally, we'll create a helper function that will abbreviate our usage of this pattern:

template<class T> T* GetUSystem() {
    UApplication* app = UApplication::Instance();
    return(app->GetSystem<T>());
}

This function means we can now find any implementation of our RenderSystem like so:

URenderSystem* renderSystem = GetUSystem<RenderSystem>();

This pattern will help us significantly throughout our engine.

Next Steps

As we design our game engine through the rest of this series, we will also follow an Entity-Component-System design approach, creating discrete components that the player can add to Entity objects and Systems that will process them. For example, we'll create a RenderSystem that will search our world for Sprite, Mesh, Light, and Camera components to create our scene graph, an AudioSystem that will play and track SoundEmitter components, and a ScriptingSystem that will process any scripts attached to game objects.

While we will follow the pattern as closely as possible, there is no such thing as a perfect system, and we're not trying to enforce compliance. If you've ever used Unity, you'll see a similar but flexible model... the user created GameObjects and attaches components. To any GameObject, you can attach one or more scripts. Each one of those looks more like a System, with Update and Start functions, but also contains properties you can add to your GameObject and expose in the editor for game designers to modify. You'll also find callback functions like OnCollisionEnter, which allow you to process collisions with your GameObject.

Should collisions be handled by a central system? Should none of the scripts use the Update function and defer that to systems? The right answer depends on the complexity of the game being created (and the budget, timeline, experience of the team, engine choice, and many other factors). The key is that there is no single design that works in all circumstances.

We first and foremost want to ensure that game designers and developers can use what we create without getting burdened or overwhelmed by forcing them into a specific pattern all of the time. Creation is king, and the ECS design provides a flexible pattern for engine developers and creators to follow.