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.
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.
Component
ClassOur 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;
};
Entity
ClassNext, 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;
};
Scene
ClassMoving 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;
};
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;
};
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
...
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());
}
};
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:
begin()
methoderase()
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:
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:
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;
}
// ...
};
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 ImageComponent
s 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:
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:
PhysicsComponent
, we need to add a new AddPhysicsComponent()
method to our Entity
class.Entity
class to support the constructor’s parameter list.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.
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.
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:
ComponentType
to represent that type. External code then provides that parameter as an argument using <
and >
syntax when they use our template: AddComponent<SomeType>()
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
.…
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.&&
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;
}
// ...
};
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";
}
};
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:
Component
, Entity
) are crucial for ECS.std::unique_ptr
within a std::vector
provides robust component ownership for entities.dynamic_cast
).Transform
) and multi-instance (e.g., Image
) component types per entity.AddComponent()
and RemoveComponent()
-style methods.Create the C++ framework for adding, retrieving, and removing components from game entities.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games