Implementing a Component System
Create the C++ framework for adding, retrieving, and removing components from game entities.
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:
src/Component.h
#pragma once
#include <SDL3/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:
src/Entity.h
#pragma once
#include <memory>
#include <vector>
#include <SDL3/SDL.h>
#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:
src/Entity.h
#pragma once
#include <memory>
#include <vector>
#include <SDL3/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:
src/Scene.h
#pragma once
#include <SDL3/SDL.h>
#include <vector>
#include "Entity.h"
using EntityPtr = std::unique_ptr<Entity>;
using EntityPtrs = std::vector<EntityPtr>;
class Scene {
public:
void HandleEvent(const 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:
Files
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:
src/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 full copy of this type is available below:
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:
src/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:
src/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:
src/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:
src/Entity.h
// ...
#include <iostream>
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:
- Determine the index of the value we want to erase
- Create an iterator to that index by adding it to the value returned by the
begin()method - Pass that iterator to the
erase()method.
It looks like this:
src/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
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:
src/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 .
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:
src/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.
src/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:
src/Entity.h
// ...
class Entity {
public:
// ...
using ImageComponents = std::vector<ImageComponent*>;
ImageComponents GetImageComponents() const {
ImageComponents Result;
for (const 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:
src/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:
src/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: 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 newAddPhysicsComponent()method to ourEntityclass. - 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
Entityclass to support the constructor's parameter list. - We'd soon find ourselves with dozens of similar methods in our
Entityclass 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.
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:
src/Entity.h
// ...
class Entity {
// ...
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 Owner, and 1, 2, and 3 are additional arguments for that type's constructor:
src/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.
Using Templates
Our ideal design would include all 3 benefits:
- the flexibility of allowing any component type with any argument list to be constructed without needing code updates
- the
Entityshould manage the full lifecycle of its components - the API should be easy to use
That API might look something like this:
src/Scene.h
// ...
class Scene {
public:
Scene() {
EntityPtr& Player{Entities.emplace_back(
std::make_unique<Entity>()
)};
ComponentType* NewComponent{
Player->AddComponent<ComponentType>(1, 2, 3)
};
}
// ...
};However, implementing this API within the Entity class involves using much more complex C++ features than we've covered so far:
src/Scene.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:
- 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
ComponentTypeto represent that type. External code then provides that parameter as an argument using<and>syntax when they use our template:AddComponent<SomeType>() - The
requiressyntax is an example of a C++20 feature called . In this case, we're using it to ensure theComponentTypetemplate argument that the external code supplied is either theComponenttype, or a type that derives fromComponent. - The
...syntax is used to define a function or template with an unknown number of parameters, sometimes called a (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, ourAddComponent()function is collecting arguments to forward to a constructor on theComponentTypeclass. - The double-ampersand
&&next to theArgstype, and the use of thestd::forward()function template, relates to a technique called . This ensures each argument get forwarded from one function to the next without performance loss through unnecessary copying, and without losing characteristics such asconst.
Replacing our GetTransformComponent() with a template GetComponent() function would look like this:
src/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:
src/Entity.h
// 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;
}
// ...
};We'll stick with the basic approach of separate functions in future examples, but if you're following along and feel more comfortable, feel free to use one of the more advanced techniques.
Complete Code
Our complete code, which we'll build upon in the next lesson, is available below:
Files
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 the foundations of ECS. std::unique_ptrwithin astd::vectorprovides 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()andRemoveComponent()style methods.
Entity and Component Interaction
This lesson provides an in-depth exploration of using inherited methods and variables in C++, covering constructor calls, variable modification, and function shadowing