Implementing a Component System

Create the C++ framework for adding, retrieving, and removing components from game entities.
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

Let's put the composition pattern into practice by constructing a foundational Entity-Component System (ECS).

We will establish the necessary base classes (Entity, Component), use smart pointers (std::unique_ptr) for memory management, and implement the mechanisms for adding components to entities, querying entities for specific components, and removing components when they're no longer needed.

Base Classes

Let’s start with our base classes. These will all follow a pattern we’re familiar with by now, using our typical set of HandleEvent(), Tick() , and Render() functions.

The Component Class

Our base Component class looks like the following. Because we’ll be using runtime polymorphism, we’ll set our three functions to be virtual. We’ll also add a virtual destructor to reduce the possibility of polymorphism-related memory leaks:

// Component.h
#pragma once
#include <SDL.h>

class Component {
public:
  virtual void HandleEvent(const SDL_Event& E) {}
  virtual void Tick(float DeltaTime) {}
  virtual void Render(SDL_Surface* Surface) {}
  virtual ~Component() = default;
};

The Entity Class

Next, let’s create the Entity class to manage these components. To support polymorphism, entities will store their components as an array of pointers.

We also want to automate memory management as much as possible, so we’ll establish an ownership model where Entity objects "own" their components. As such, we’ll store them as unique pointers:

// Entity.h
#pragma once
#include <memory>
#include <vector>

#include "Component.h"

using ComponentPtr = std::unique_ptr<Component>;
using ComponentPtrs = std::vector<ComponentPtr>;

class Entity {
  ComponentPtrs Components;
};

Our Entity objects will hook up to our game loop using the usual three functions, and it will forward all these calls to each of its components. We’ll also mark these as virtual to allow entities to be polymorphic, too:

// Entity.h
#pragma once
#include <memory>
#include <vector>
#include <SDL.h>
#include "Component.h"

using ComponentPtr = std::unique_ptr<Component>;
using ComponentPtrs = std::vector<ComponentPtr>;

class Entity {
 public:
  virtual void HandleEvent(const SDL_Event& E) {
    for (ComponentPtr& C : Components) {
      C->HandleEvent(E);
    }
  }

  virtual void Tick(float DeltaTime) {
    for (ComponentPtr& C : Components) {
      C->Tick(DeltaTime);
    }
  }

  virtual void Render(SDL_Surface* Surface) {
    for (ComponentPtr& C : Components) {
      C->Render(Surface);
    }
  }

  virtual ~Entity() = default;

private:
  ComponentPtrs Components;
};

The Scene Class

Moving one level up the chain, our Scene class will look very similar, except it manages entities instead of components. We won’t need the virtual functions here as we’ll only have a single scene in our project:

// Scene.h
#pragma once
#include <SDL.h>
#include <vector>
#include "Entity.h"

using EntityPtr = std::unique_ptr<Entity>;
using EntityPtrs = std::vector<EntityPtr>;

class Scene {
public:
  void HandleEvent(SDL_Event& E) {
    for (EntityPtr& Entity : Entities) {
      Entity->HandleEvent(E);
    }
  }

  void Tick(float DeltaTime) {
    for (EntityPtr& Entity : Entities) {
      Entity->Tick(DeltaTime);
    }
  }

  void Render(SDL_Surface* Surface) {
    for (EntityPtr& Entity : Entities) {
      Entity->Render(Surface);
    }
  }

private:
  EntityPtrs Entities;
};

The Game Loop

Finally, we’ll hook everything up to a game loop and render it in an SDL_Window. The main.cpp and Window.h files are provided below, and they have not changed from what we have been using in previous sections:

#include <SDL.h>
#include "Window.h"
#include "Scene.h"

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;
  Scene GameScene;

  Uint64 LastTick{SDL_GetPerformanceCounter()};
  SDL_Event Event;
  while (true) {
    while (SDL_PollEvent(&Event)) {
      GameScene.HandleEvent(Event);
      if (Event.type == SDL_QUIT) {
        SDL_Quit();
        return 0;
      }
    }

    Uint64 CurrentTick{SDL_GetPerformanceCounter()};
    float DeltaTime{static_cast<float>(
      CurrentTick - LastTick) /
      SDL_GetPerformanceFrequency()
    };
    LastTick = CurrentTick;

    // Tick
    GameScene.Tick(DeltaTime);

    // Render
    GameWindow.Render();
    GameScene.Render(GameWindow.GetSurface());

    // Swap
    GameWindow.Update();
  }

  return 0;
}
#pragma once
#include <iostream>
#include <SDL.h>

class Window {
public:
  Window() {
    SDLWindow = SDL_CreateWindow(
      "Scene",
      SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED,
      700, 300, 0
    );
  }

  ~Window() {
    if (SDLWindow) {
      SDL_DestroyWindow(SDLWindow);
    }
  }

  Window(const Window&) = delete;
  Window& operator=(const Window&) = delete;

  void Render() {
    SDL_FillRect(
      GetSurface(), nullptr,
      SDL_MapRGB(GetSurface()->format,
        220, 220, 220));
  }

  void Update() {
    SDL_UpdateWindowSurface(SDLWindow);
  }

  SDL_Surface* GetSurface() {
    return SDL_GetWindowSurface(SDLWindow);
  }

private:
  SDL_Window* SDLWindow;
};

Creating Components

Most game objects need a position, rotation, or scale - something to define where they are or how they’re oriented. Let’s create a TransformComponent for that:

// TransformComponent.h
#pragma once
#include <iostream>
#include "Vec2.h"
#include "Component.h"

class TransformComponent : public Component {
public:
  TransformComponent() {
    std::cout << "TransformComponent created\n";
  }

  void Tick(float DeltaTime) override {
    std::cout << "TransformComponent ticking\n";
  }

  Vec2 GetPosition() const {
    return Position;
  }

private:
  Vec2 Position{0, 0};
};

Note that this TransformComponent is using the Vec2 type we created earlier in the course to represent two-dimensional vectors. A fully copy of this type is available below:

#pragma once
#include <iostream>

struct Vec2 {
  float x;
  float y;

  float GetLength() const {
    return std::sqrt(x * x + y * y);
  }

  float GetDistance(const Vec2& Other) const {
    return (*this - Other).GetLength();
  }

  Vec2 Normalize() const {
    return *this / GetLength();
  }

  Vec2 operator*(float Multiplier) const {
    return Vec2{x * Multiplier, y * Multiplier};
  }

  Vec2 operator/(float Divisor) const {
    if (Divisor == 0.0f) {
      return Vec2{0, 0};
    }

    return Vec2{x / Divisor, y / Divisor};
  }

  Vec2& operator*=(float Multiplier) {
    x *= Multiplier;
    y *= Multiplier;
    return *this;
  }

  Vec2 operator/=(float Divisor) {
    if (Divisor == 0.0f) {
      return *this;
    }

    x /= Divisor;
    y /= Divisor;
    return *this;
  }

  Vec2 operator+(const Vec2& Other) const {
    return Vec2{x + Other.x, y + Other.y};
  }

  Vec2 operator-(const Vec2& Other) const {
    return *this + (-Other);
  }

  Vec2& operator+=(const Vec2& Other) {
    x += Other.x;
    y += Other.y;
    return *this;
  }

  Vec2& operator-=(const Vec2& Other) {
    return *this += (-Other);
  }

  Vec2 operator-() const {
    return Vec2{-x, -y};
  }

  float Dot(const Vec2 Other) const {
    return (x * Other.x) + (y * Other.y);
  }
};

inline Vec2 operator*(float M, const Vec2& V) {
  return V * M;
}

inline std::ostream& operator<<(
  std::ostream& Stream, const Vec2& V
) {
  Stream << "{ x = " << V.x
    << ", y = " << V.y << " }";
  return Stream;
}

Let’s update our Entity to give them the ability to create a TransformComponent for themselves. This time, however, we won’t just construct the component within our Entity.

Remember, our goal here is to allow each Entity object to only have the capabilities it needs. An Entity that is designed to play background music, for example, may not need to have a position, so does not need a TransformComponent.

So instead of having every Entity construct a TransformComponent for itself, we’ll let whatever code is constructing our actor decide if it needs one. We’ll provide them with a AddTransformComponent() function to call if it does.

In general, when a function adds a component to our actor, we’ll also return a pointer to that component, so the calling code can interact with it:

// Entity.h
// ...
#include "TransformComponent.h"

class Entity {
public:
  // ...
  TransformComponent* AddTransformComponent() {
    // Add a new TransformComponent, emplace_back()
    // constructs it in place and returns a
    // reference to the added element
    ComponentPtr& NewComponent{
      Components.emplace_back(
        std::make_unique<
          TransformComponent>())};

    // Convert the ComponentPtr ie 
    // std::unique_ptr<Component> to the
    // the return type: a TransformComponent* 
    return static_cast<TransformComponent*>(
      NewComponent.get());
  }
  // ...
};

Let’s update our Scene to add an Entity, and then add a component to that Entity:

// Scene.h
// ...

class Scene {
public:
  Scene() {
    EntityPtr& NewEntity{Entities.emplace_back(
      std::make_unique<Entity>()
    )};
    NewEntity->AddTransformComponent();
  }
  // ...
};
TransformComponent created
TransformComponent ticking
TransformComponent ticking
TransformComponent ticking
...

Retrieving Components

Often, external code or other components attached to the same Entity will need to access the data or functionality provided by a specific component. For example, a rendering component will need to know the entity's position, which is stored in its TransformComponent.

To facilitate this, we need a way to retrieve a specific component from an Entity. Since our Components vector stores base Component pointers, we'll iterate through the vector and use dynamic_cast to check if a component is of the requested type (TransformComponent, in this case).

The dynamic_cast operator safely converts pointers within an inheritance hierarchy at runtime. If the cast is successful, it returns a valid pointer to the derived type; otherwise, it returns nullptr. Our function will return the pointer to the found component, or nullptr if the Entity doesn't have one of that type:

// Entity.h
// ...

class Entity {
public:
  // ...
  TransformComponent* GetTransformComponent() const {
    for (const ComponentPtr& C : Components) {
      // Try to cast the base Component pointer
      // to a TransformComponent pointer
      if (auto Ptr{dynamic_cast<
        TransformComponent*>(C.get())}) {
        // Cast successful, we found it!
        return Ptr;
      }
    }

    // Went through all components, didn't
    // find a transform component
    return nullptr;
  }
  // ...
};

Our Entity objects should have, at most, a single TransformComponent, so let’s update our AddTransformComponent() function to enforce this with the help of this new GetTransformComponent() function:

// Entity.h
// ...

class Entity {
public:
  TransformComponent* AddTransformComponent() {
    if (GetTransformComponent()) {
      std::cout << "Error: Cannot have "
        "multiple transform components";
      return nullptr;
    }
    
    ComponentPtr& NewComponent{
      Components.emplace_back(
        std::make_unique<TransformComponent>()
      )
    };

    return static_cast<TransformComponent*>(
      NewComponent.get());
  }
};

Removing Components

Just as we need to add components to entities, we also need the ability to remove them. This might happen when an effect wears off, a weapon is dropped, or an entity changes state.

We'll add a RemoveComponent() function to our Entity class. It will take a raw pointer (Component*) to the component instance that needs to be removed.

This function then finds the std::unique_ptr managing the component associated with the raw pointer and removes it from the Components vector. There are a few ways to erase items from a std::vector. The approach that is likely to be most familiar is:

  1. Determine the index of the value we want to erase
  2. Create an iterator to that index by adding it to the value returned by the begin() method
  3. Pass that iterator to the erase() method.

It looks like this:

// Entity.h
// ...

class Entity {
public:
  // ...
  void RemoveComponent(Component* PtrToRemove) {
    // Iterate through the vector to find
    // the component to remove
    for (size_t i{0}; i < Components.size(); ++i) {
      // Check if the raw pointer managed by
      // the unique_ptr matches
      if (Components[i].get() == PtrToRemove) {
        // Found it! Erase the element at this index.
        // Components.begin() + i gives an iterator
        // to the element.
        Components.erase(Components.begin() + i);
        // Assuming only one component matches,
        // so we can stop searching
        return;
      }
    }
    // If the loop finishes, the component
    // wasn't found
    std::cout << "Warning: Attempted to remove "
      "a component not found on this entity.\n";
  }
  // ...
};

We covered std::vector() and the erase() method in more detail in our introductory course:

Advanced: Lambdas and std::erase_if()

A slightly more elegent (but also more advanced) way of conditionally erasing elements from an a array is through std::erase_if(), introduced in C++20.

The std::erase_if() function receives the array as the first argument, and a function as the second. The function we provide will be called for every element in the array, receiving that element as an argument. It should return true if that element should be erased, or false if it should be kept.

Rewriting our previous example using std::erase_if() and a lambda would look like this:

// Entity.h
// ...

class Entity {
public:
  // ..
  void RemoveComponent(Component* PtrToRemove) {
    std::erase_if(Components,
      [PtrToRemove](const ComponentPtr& P) {
        return P.get() == PtrToRemove;
      }
    );
  }
  // ...
};

We cover lambdas in our advanced course:

Multiple Components of the Same Type

One of the powerful advantages of composition over inheritance is the ability for an entity to possess multiple components of the same type. With inheritance, an object is a specific type (e.g., a Player is a Character). With composition, an entity has components.

There's no inherent restriction preventing an entity from having, for instance, two separate ImageComponent instances if we wanted it to render two different images simultaneously (perhaps one for the body and one for a held item).

We’ll build a complete ImageComponent later in the chapter, but let’s create a basic scaffolding now:

// ImageComponent.h
#pragma once
#include <iostream>
#include "Component.h"

class ImageComponent : public Component {
 public:
  ImageComponent() {
    std::cout << "ImageComponent created\n";
  }
};

To add these ImageComponent instances to our entities, we'll need an AddImageComponent() function in our Entity class, similar to the AddTransformComponent() we created earlier.

It will construct a new ImageComponent, add its unique_ptr to the Components vector, and return a raw pointer to the newly created component.

// Entity.h
// ...
#include "ImageComponent.h" 

class Entity {
public:
  // ...
  ImageComponent* AddImageComponent() {
    ComponentPtr& NewComponent{
      Components.emplace_back(
        std::make_unique<ImageComponent>())};

    return static_cast<ImageComponent*>(
      NewComponent.get());
  }
  // ...
};

Note: Our AddTransformComponent() and AddImageComponent() have a lot of very similar logic. We’ll endure this duplication for now, but will discuss better designs in the next section.

Since an entity can have multiple ImageComponent instances, a function like GetTransformComponent() (which returns only one) isn't sufficient. We need a way to retrieve all ImageComponent instances associated with an entity.

Let's add a GetImageComponents() function. This function will iterate through the Components vector, perform a dynamic_cast() for each one, and collect all successful ImageComponent pointers into a std::vector. It then returns this vector, giving the caller access to all relevant components:

// Entity.h
// ...

class Entity {
public:
  // ...
  
  using ImageComponents =
    std::vector<ImageComponent*>;
  ImageComponents GetImageComponents() const {
    ImageComponents Result;
    for (ComponentPtr& C : Components) {
      // Try to cast to ImageComponent*
      if (auto Ptr{dynamic_cast<
        ImageComponent*>(C.get())}
      ) {
        // If successful, add it to our
        // result vector
        Result.push_back(Ptr);
      }
    }
    return Result;
  }

  // ...
};

Advanced: Views

The GetImageComponents() function above works, but it has a minor inefficiency: it creates and returns a brand new std::vector every time it's called. This involves memory allocation and copying pointers. For performance-critical code or frequent calls, this could be undesirable.

C++20 introduced Ranges and Views, which provide a more modern and often more efficient way to work with sequences of data. A view is a lightweight object that represents a sequence of elements (often by referring to an existing container) but doesn't own the elements itself.

Views allow us to compose algorithms (like filtering and transforming) lazily, meaning the work is only done when the elements are actually accessed, and often without intermediate allocations.

We can rewrite GetImageComponents() to return a view instead of a vector. This view would represent the sequence of ImageComponent pointers without needing to create a separate container:

// Entity.h
// ...
#include <ranges> // Required for views

class Entity {
public:
  // ...
  auto GetImageComponents() {
    // Define the transformation:
    //   ComponentPtr -> ImageComponent*
    auto ToImagePtr{[](const ComponentPtr& C) {
      return dynamic_cast<ImageComponent*>(C.get());
    }};

    // Define the filter:
    //    Keep only non-nullptr pointers
    auto IsNotNull{[](ImageComponent* Ptr){
      return Ptr != nullptr;
    }};

    // Create the view:
    // 1. View the Components vector.
    // 2. Transform each element using ToImagePtr.
    // 3. Filter the results using IsNotNull.
    return Components
      | std::views::transform(ToImagePtr)
      | std::views::filter(IsNotNull);
  }
  // ...
};

Views support most of the same capabilities as their underlying container. For example, we can use the view to count how many ImageComponents an Entity has, and to iterate over them:

// Scene.h
// ...

class Scene {
 public:
  Scene() {
    EntityPtr& NewEntity{Entities.emplace_back(
      std::make_unique<Entity>()
    )};
    NewEntity->AddImageComponent();
    NewEntity->AddImageComponent();
    NewEntity->AddImageComponent();

    std::cout << "Image Component Count: "
      << std::ranges::distance(
           NewEntity->GetImageComponents());

    for (ImageComponent* C :
         NewEntity->GetImageComponents()) {
      std::cout << "\nDoing something with"
                   " an ImageComponent...";
      // ...
    }
  }
};
ImageComponent created
ImageComponent created
ImageComponent created
Image Component Count: 3
Doing something with an ImageComponent...
Doing something with an ImageComponent...
Doing something with an ImageComponent...

We cover views in much more detail in our advanced course:

Advanced: Alternative Designs

This section reimplements our previous functionality using more advanced C++ techniques. We’ll continue to use the simple approach created above for the rest of the course, so feel free to skip this section if preferred.

This approach where we define methods like AddTransformComponent() and AddImageComponent() on our Entity base class has a bit of a design problem in more complex programs:

  • Every time we add a new component type, such as a PhysicsComponent, we need to add a new AddPhysicsComponent() method to our Entity class.
  • Every time we add a new constructor to a component type, or update an existing constructor’s parameter list, we need to add or update a method on the Entity class to support the constructor’s parameter list.
  • We’d soon find ourselves with dozens of similar methods in our Entity class and, if we need to change something about how components get added in general, we’d probably need to update all of those methods.

We’ll cover two alternative designs in this section. The first option solves the problem but creates a different issue, and the second option solves the problem but requires more advanced C++ techniques than what we’ve covered so far.

1. Using std::move()

An immediate solution to this problem would be to have the code outside of our Entity object be responsible for creating the components, and then std::move() them into the entity:

// Entity.h
// ...

class Entity {
  // ...
  Component* AddComponent(
    ComponentPtr&& NewComponent
  ) {
    Component* AddComponent(ComponentPtr&& NewComponent) {
    Components.push_back(std::move(NewComponent));
    return NewComponent.get();
  }
  // ...
}

Using this function to add a component to an entity might look something like below, where ComponentType is a Component subclass, Player.get() is the component’s future Owner, and 1, 2, and 3 are additional arguments for that type’s constructor:

// Scene.h
// ...

class Scene {
public:
  Scene() {
    EntityPtr& Player{Entities.emplace_back(
      std::make_unique<Entity>())};

    // Create the component
    std::unique_ptr NewComponent{
      std::make_unique<ComponentType>(
        Player.get(), 1, 2, 3)};

    // Move it to the Entity
    Component* Ptr{Player->AddComponent(
      std::move(NewComponent))};

    // Cast the return value to the derived type
    // if needed
    ComponentType* Casted{static_cast<
      ComponentType*>(Ptr)};
    Casted->SomeDerivedFunction();
  // ...
};

This is more flexible, but also has design problems. The obvious problem shown above is that the AddComponent() function is difficult to use - adding a component to an entity requires an unreasonable amount of code and complexity.

In a larger program, our AddComponent() function might be used in hundreds of locations. We really want the complexity to be in the one place where AddComponent() is defined (the Entity class) rather than the hundred places where it is used.

We’d also prefer that our Entity class manage the full lifecycle of its components to keep memory management simple and reduce the liklihood of memory-related bugs.

2. Using Templates

A better design that combines flexibility whilst allowing the Entity to manage the full lifecycle of its components might look something like this:

// Scene.h
// ...

class Scene {
public:
  Scene() {
    EntityPtr& Player{Entities.emplace_back(
      std::make_unique<Entity>()
    )};
    
    ComponentType* NewComponent{
      Player->AddComponent<ComponentType>(A, B, C)
    };
  }
  // ...
};

However, implementing this API within the Entity class involves using much more complex C++ features than we’ve covered so far:

// Entity.h
// ...

class Entity {
public:
  // ...
  
ImageComponent* AddImageComponent() {} template <typename CType, typename... CArgs> requires std::derived_from<CType, Component> CType* AddComponent(CArgs&&... ConstructorArgs) { // Construct the component in our vector ComponentPtr& NewComponent{ Components.emplace_back( std::make_unique<CType>( // Pass constructor args here std::forward<CArgs>(ConstructorArgs)... ) ) }; // Return component in the appropriate type return static_cast<CType*>( NewComponent.get()); } // ... };

We cover these techniques in full detail in the advanced course, but to summarise the key points as they’re applied here:

  • Function templates allow us to define function behaviours without necessarily knowing all the types we will be dealing with. In this example, we don’t know the exact type of component we’re adding. So, we create a template parameter called ComponentType to represent that type. External code then provides that parameter as an argument using < and > syntax when they use our template: AddComponent<SomeType>()
  • The requires syntax is an example of a C++20 feature called concepts. In this case, we’re using it to ensure the ComponentType template argument that the external code supplied is either the Component type, or a type that derives from Component.
  • The syntax is used to define a function or template with an unknown number of parameters, sometimes called a variadic function or variadic template. This is primarily used for functions and templates that collect arguments to forward to some other function or template. In this case, our AddComponent() function is collecting arguments to forward to a constructor on the ComponentType class.
  • The double-ampersand && next to the Args type, and the use of the std::forward() function template, relates to a technique called perfect forwarding. This ensures each argument get forwarded from one function to the next without performance loss through unnecessary copying, and without losing characteristics such as const.

Replacing our GetTransformComponent() with a template GetComponent() function would look like this:

// Entity.h
// ...

class Entity {
public:
  // ...
TransformComponent* GetTransformComponent() {} template <typename CType> requires std::derived_from<CType, Component> CType* GetComponent() { for (const ComponentPtr& C : Components) { // Try to cast the base Component pointer // to a CType pointer if (auto Ptr{dynamic_cast<CType*>(C.get())}) { // Cast successful, we found it! return Ptr; } } // Went through all components, didn't // find a CType component return nullptr; } // ... };

And we can replace GetImageComponents() with a GetComponents() function in a similar way:

// Entity.h
// ...

class Entity {
public:
  // ...
  
ImageComponents GetImageComponents() {} template <typename CType> requires std::derived_from<CType, Component> std::vector<CType*> GetComponents() { std::vector<CType*> Results; for (const ComponentPtr& C : Components) { // Try to cast to ImageComponent* if (auto Ptr{dynamic_cast<CType*>(C.get())}) { // If successful, add it to our result vector Results.push_back(Ptr); } } return Results; } // ... };

Complete Code

Our complete code, which we’ll build upon throughout this chapter, is available below:

#include <SDL.h>
#include "Window.h"
#include "Scene.h"

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;
  Scene GameScene;

  Uint64 LastTick{SDL_GetPerformanceCounter()};
  SDL_Event Event;
  while (true) {
    while (SDL_PollEvent(&Event)) {
      GameScene.HandleEvent(Event);
      if (Event.type == SDL_QUIT) {
        SDL_Quit();
        return 0;
      }
    }

    Uint64 CurrentTick{SDL_GetPerformanceCounter()};
    float DeltaTime{static_cast<float>(
      CurrentTick - LastTick) /
      SDL_GetPerformanceFrequency()
    };
    LastTick = CurrentTick;

    // Tick
    GameScene.Tick(DeltaTime);

    // Render
    GameWindow.Render();
    GameScene.Render(GameWindow.GetSurface());

    // Swap
    GameWindow.Update();
  }

  return 0;
}
#pragma once
#include <iostream>
#include <SDL.h>

class Window {
public:
  Window() {
    SDLWindow = SDL_CreateWindow(
      "Scene",
      SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED,
      700, 300, 0
    );
  }

  ~Window() {
    if (SDLWindow) {
      SDL_DestroyWindow(SDLWindow);
    }
  }

  Window(const Window&) = delete;
  Window& operator=(const Window&) = delete;

  void Render() {
    SDL_FillRect(
      GetSurface(), nullptr,
      SDL_MapRGB(GetSurface()->format,
        220, 220, 220));
  }

  void Update() {
    SDL_UpdateWindowSurface(SDLWindow);
  }

  SDL_Surface* GetSurface() {
    return SDL_GetWindowSurface(SDLWindow);
  }

private:
  SDL_Window* SDLWindow;
};
#pragma once
#include <SDL.h>
#include <vector>
#include "Entity.h"

using EntityPtr = std::unique_ptr<Entity>;
using EntityPtrs = std::vector<EntityPtr>;

class Scene {
 public:
  Scene() {
    EntityPtr& Player{Entities.emplace_back(
      std::make_unique<Entity>())};

    Player->AddTransformComponent();
    Player->AddImageComponent();
  }

  void HandleEvent(SDL_Event& E) {
    for (EntityPtr& Entity : Entities) {
      Entity->HandleEvent(E);
    }
  }

  void Tick(float DeltaTime) {
    for (EntityPtr& Entity : Entities) {
      Entity->Tick(DeltaTime);
    }
  }

  void Render(SDL_Surface* Surface) {
    for (EntityPtr& Entity : Entities) {
      Entity->Render(Surface);
    }
  }

 private:
  EntityPtrs Entities;
};
#pragma once
#include <memory>
#include <vector>
#include <SDL.h>
#include "Component.h"
#include "TransformComponent.h"
#include "ImageComponent.h"

using ComponentPtr = std::unique_ptr<Component>;
using ComponentPtrs = std::vector<ComponentPtr>;

class Entity {
public:
  virtual void HandleEvent(const SDL_Event& E) {
    for (ComponentPtr& C : Components) {
      C->HandleEvent(E);
    }
  }

  virtual void Tick(float DeltaTime) {
    for (ComponentPtr& C : Components) {
      C->Tick(DeltaTime);
    }
  }

  virtual void Render(SDL_Surface* Surface) {
    for (ComponentPtr& C : Components) {
      C->Render(Surface);
    }
  }

  virtual ~Entity() = default;

  TransformComponent* AddTransformComponent() {
    if (GetTransformComponent()) {
      std::cout << "Error: Cannot have "
        "multiple transform components";
      return nullptr;
    }

    ComponentPtr& NewComponent{
      Components.emplace_back(std::make_unique<
        TransformComponent>())};

    return static_cast<TransformComponent*>(
      NewComponent.get());
  }

  TransformComponent* GetTransformComponent() const {
    for (const ComponentPtr& C : Components) {
      if (auto Ptr{
        dynamic_cast<TransformComponent*>(C.get())
      }) {
        return Ptr;
      }
    }
    return nullptr;
  }

  ImageComponent* AddImageComponent() {
    ComponentPtr& NewComponent{
      Components.emplace_back(
        std::make_unique<ImageComponent>())};

    return static_cast<ImageComponent*>(
      NewComponent.get());
  }

  using ImageComponents =
    std::vector<ImageComponent*>;
  ImageComponents GetImageComponents() const {
    ImageComponents Result;
    for (const ComponentPtr& C : Components) {
      if (auto Ptr{dynamic_cast<
        ImageComponent*>(C.get())}
      ) {
        Result.push_back(Ptr);
      }
    }
    return Result;
  }

  void RemoveComponent(Component* PtrToRemove) {
    for (size_t i{0}; i < Components.size(); ++i) {
      if (Components[i].get() == PtrToRemove) {
        Components.erase(Components.begin() + i);
        return;
      }
    }

    std::cout << "Warning: Attempted to remove "
      "a component not found on this entity.\n";
  }

private:
  ComponentPtrs Components;
};
#pragma once
#include <SDL.h>

class Entity;

class Component {
 public:
  virtual void HandleEvent(const SDL_Event& E) {}
  virtual void Tick(float DeltaTime) {}
  virtual void Render(SDL_Surface* Surface) {}
  virtual ~Component() = default;
};
#pragma once
#include "Vec2.h"
#include "Component.h"

class TransformComponent : public Component {
public:
  TransformComponent() {
    std::cout << "TransformComponent created\n";
  }

  Vec2 GetPosition() const {
    return Position;
  }

private:
  Vec2 Position{0, 0};
};
#pragma once
#include <iostream>

#include "Component.h"

class ImageComponent : public Component {
 public:
  ImageComponent() {
    std::cout << "ImageComponent created\n";
  }
};

Summary

This lesson laid the groundwork for our Entity-Component System. We defined the Component and Entity base classes, making them polymorphic with virtual functions and destructors.

Entities now manage their components through a vector of unique pointers, ensuring proper ownership. We implemented key methods: AddComponent-style functions to attach capabilities, GetComponent-style functions (using dynamic_cast) to access them, and RemoveComponent() to detach them.

We also differentiated between components expected once per entity versus those allowed multiple times. Key takeaways:

  • Polymorphic base classes (Component, Entity) are crucial for ECS.
  • std::unique_ptr within a std::vector provides robust component ownership for entities.
  • Adding components involves creating derived types and storing base pointers.
  • Retrieving specific components requires runtime type checking (dynamic_cast).
  • The system supports both single-instance (e.g., Transform) and multi-instance (e.g., Image) component types per entity.
  • Entities can dynamically gain and lose components via AddComponent() and RemoveComponent() -style methods.
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Ryan McCombe
Ryan McCombe
Updated
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Components and Composition
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

This course includes:

  • 105 Lessons
  • 92% Positive Reviews
  • Regularly Updated
  • Help and FAQs
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved