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:
// 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:
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:
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:
- 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:
// 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:
Dynamic Arrays using std::vector
Explore the fundamentals of dynamic arrays with an introduction to std::vector
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: 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 ourEntity
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.
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.
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 theComponentType
template argument that the external code supplied is either theComponent
type, 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 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, ourAddComponent()
function is collecting arguments to forward to a constructor on theComponentType
class. - The double-ampersand
&&
next to theArgs
type, and the use of thestd::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 asconst
.
Function Templates
Understand the fundamentals of C++ function templates and harness generics for more modular, adaptable code.
Concepts in C++20
Learn how to use C++20 concepts to constrain template parameters, improve error messages, and enhance code readability.
Variadic Functions
An introduction to variadic functions, which allow us to pass a variable quantity of arguments
Perfect Forwarding and std::forward
An introduction to problems that can arise when our functions forward their parameters to other functions, and how we can solve those problems with std::forward
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:
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 astd::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()
andRemoveComponent()
-style methods.
Entity and Component Interaction
Explore component communication, dependency management, and specialized entity types within your ECS