Now that we have our basic Entity-Component System structure, let's focus on making the pieces work together. This lesson explores how components can access their owning entity and interact with sibling components.
We'll implement mechanisms for handling dependencies between components and see how traditional inheritance can still be combined with composition by creating specialized entity subtypes.
Components rarely exist in isolation. They often need to know about the Entity
they belong to (e.g., to access its other components or trigger entity-level actions) or interact with sibling components (like our ImageComponent
needing position data from a TransformComponent
).
To enable this communication, we'll give every Component
a way to access its owning Entity
. We can achieve this by passing a reference or pointer to the Entity
into the Component
's constructor and storing it as a member variable.
We'll use a raw pointer here, as the Entity
owns the Component
, not the other way around:
// Component.h
#pragma once
#include <SDL.h>
// Forward declaration to avoid circular includes
class Entity;
class Component {
public:
// Constructor now takes a pointer to the
// owning Entity
Component(Entity* Owner) : Owner(Owner) {}
// Getter for the owning entity
Entity* GetOwner() const {
return Owner;
}
// ...
private:
// Store a pointer to the owner
Entity* Owner{nullptr};
};
We’ll update our TransformComponent
and ImageComponent
to inherit this constructor:
// TransformComponent.h
#pragma once
#include "Vec2.h"
#include "Component.h"
class TransformComponent : public Component {
public:
// Inherit the constructor from the base class
using Component::Component;
TransformComponent() { // <d>
std::cout << "TransformComponent created\n"; // <d>
} // <d>
// ...
};
// ImageComponent.h
#pragma once
#include "Component.h"
class ImageComponent : public Component {
public:
// Inherit the constructor from the base class
using Component::Component;
ImageComponent() {
std::cout << "ImageComponent created\\n";
}
// ...
};
Let’s update our AddTransformComponent()
and AddImageComponent()
to pass a pointer to the Entity
that is constructing the component:
// 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<
// Pass 'this' (the Entity*)
TransformComponent>(this))};
return static_cast<TransformComponent*>(
NewComponent.get());
}
ImageComponent* AddImageComponent() {
ComponentPtr& NewComponent{
Components.emplace_back(
// Pass 'this' (the Entity*)
std::make_unique<ImageComponent>(this))};
return static_cast<ImageComponent*>(
NewComponent.get());
}
// ...
};
We can see our inter-component communication in action by having our ImageComponent
retrieve the position of the Entity
they’re attached to by accessing its TransformComponent
.
Note that the following code assumes that the entity the ImageComponent
is attached to always has a TransformComponent
. Later in the lesson, we’ll add logic to ensure that is the case:
// ImageComponent.h
#pragma once
#include "Component.h"
class ImageComponent : public Component {
public:
using Component::Component;
// ...
void Render(SDL_Surface* Surface) override;
};
// ImageComponent.cpp
#include <iostream>
#include "ImageComponent.h"
#include "Entity.h"
void ImageComponent::Render(
SDL_Surface* Surface
) {
// Assume we have an owner, and the owner
// has a TransformComponent
TransformComponent* Transform{
GetOwner()->GetTransformComponent()
};
std::cout << "ImageComponent rendering at: "
<< Transform->GetPosition() << '\n';
}
TransformComponent ticking
ImageComponent rendering at: { x = 0, y = 0 }
TransformComponent ticking
ImageComponent rendering at: { x = 0, y = 0 }
TransformComponent ticking
ImageComponent rendering at: { x = 0, y = 0 }
...
Since our ImageComponent
needs a TransformComponent
to know where to render, it's a good idea to take steps to enforce this dependency. We can add a check within the ImageComponent
itself.
A reasonable place is right after it's constructed and added to the entity, perhaps via an Initialize()
method, or even directly within the constructor (though modifying the owner's component list during construction can be tricky).
Most systems handle this by having a separate Initialize()
function which is called after the object is constructed. Let’s add that as a virtual
function to our Component
base class:
// Component.h
// ...
class Component {
public:
// ...
virtual void Initialize() {}
// ...
};
We’ll call it at the appropriate times within our AddTransformComponent()
and AddImageComponent()
functions:
// 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>(this))};
NewComponent->Initialize(); // <h>
return static_cast<TransformComponent*>(
NewComponent.get());
}
ImageComponent* AddImageComponent() {
ComponentPtr& NewComponent{
Components.emplace_back(
std::make_unique<ImageComponent>(this))};
NewComponent->Initialize();
return static_cast<ImageComponent*>(
NewComponent.get());
}
// ...
};
Our ImageComponent
can now override
this to make sure it’s being added to an entity that has a TransformComponent
. If not, the ImageComponent
will log an error message and request its own removal:
// ImageComponent.h
// ...
class ImageComponent : public Component {
public:
using Component::Component;
// Override Initialize
void Initialize() override;
// ...
};
// ImageComponent.cpp
// ...
void ImageComponent::Initialize() {
Entity* Owner{GetOwner()};
if (!Owner->GetTransformComponent()) {
std::cout << "Error: ImageComponent "
"requires TransformComponent on its Owner\n";
// Request removal
Owner->RemoveComponent(this);
}
}
Our ImageComponent
is checking if its owner has a TransformComponent
at the point it’s constructed, but it is possible that the TransformComponent
be removed afterwards.
This would leave our ImageComponent
in a broken state. We could check for the TransformComponent
every time we Render()
, but that has a performance cost that may not be worth paying.
If it’s a realistic scenario that a component depends on some other component that might be removed from the entity, we should spend some time expanding our system to support this. For example, any time a component is removed from an entity, that entity could push an SDL_Event
onto the event queue.
Alternatively, we could handle this within our Entity
and Component
infrastructure. An example solution would be to add an OnComponentRemoved()
method to our Component
base class:
// Component.h
// ...
class Component {
public:
// ...
virtual void OnComponentRemoved(Component* C) {}
// ...
};
Before an entity removes one of it’s components, it can iterate through all the other components to notify them of what is happening:
// Entity.h
// ...
class Entity {
public:
void RemoveComponent(Component* PtrToRemove) {
for (size_t i{0}; i < Components.size(); ++i) {
if (Components[i].get() == PtrToRemove) {
for (ComponentPtr& C : Components) {
C->OnComponentRemoved(PtrToRemove);
}
Components.erase(Components.begin() + i);
return;
}
}
std::cout << "Warning: Attempted to remove "
"a component not found on this entity.\n";
}
// ...
};
Then, any component that cares when some other component gets removed can override the OnComponentRemoved()
method and react accordingly:
// ImageComponent.h
#pragma once
#include "Component.h"
class ImageComponent : public Component {
public:
// ...
void OnComponentRemoved(Component*) override;
// ...
};
// ImageComponent.cpp
// ...
void ImageComponent::OnComponentRemoved(
Component* Ptr
) {
// Check if the component being removed is a
// TransformComponent
if (dynamic_cast<TransformComponent*>(Ptr)) {
std::cout << "ImageComponent: Owner's "
"TransformComponent was removed!\n";
// React to removal...
}
}
Note that we should be careful about calling Owner->RemoveComponent()
here - at this point, there is already an invocation of RemoveComponent()
in progress - that’s why OnComponentRemoved()
has been called. If we call RemoveComponent()
again, that call can reduce the size of our array and change the indices of elements, disrupting the iteration that the previous invocation was in the middle of.
Note that the code in our components also assume that the component has an owner - that is, Owner
is not a nullptr
or, worse, a dangling pointer.
Our component’s will always have an owner in our examples, but it’s technically possible for someone to write code that violates this. An obvious example is just directly creating and using a component with no Entity
involved:
// Unintended usage
ImageComponent BrokenComponent{nullptr};
// Likely crash
BrokenComponent.Render(Surface);
We cover techniques that can reduce this risk, such as private constructors and the friend
keyword, in our advanced course.
However, we’ll keep things simple in this course. Our components will always have the correct owners, and we won’t remove components that other components depend upon. As such, we won’t need this OnComponentRemoved()
notification system but, in more complex projects, it’s worth considering what capabilities you need to have, and then build robust systems to support them.
While the component system provides great flexibility through composition, it doesn't mean we have to abandon inheritance entirely. We can still create specialized Entity subclasses if it makes sense for our game structure, and get all the same benefits of inheritence.
Entity subtypes can customise how they manage their components, too. For instance, all characters in our game might need both a position (TransformComponent
) and a visual representation (ImageComponent
). We can create a Character
class that inherits from Entity and automatically adds these essential components in its constructor.
// Character.h
#pragma once
#include "Entity.h"
#include "ImageComponent.h"
#include "TransformComponent.h"
class Character : public Entity {
public:
Character() {
// Automatically add required components
// upon construction. The AddComponent
// methods already handle passing 'this'
Transform = AddTransformComponent();
Image = AddImageComponent();
}
// Optionally add Character-specific methods here
void SayHello() const {
std::cout << "Character says hello!\n";
}
private:
// Optional: store direct pointers for
// convenience if frequently accessed.
TransformComponent* Transform{nullptr};
ImageComponent* Image{nullptr};
};
Let’s update our Scene
to include a Character
.
// Scene.h
// ...
#include "Character.h"
class Scene {
public:
Scene() {
// Create a Character instead of a generic Entity
// The Character constructor handles adding
// its default components
EntityPtr& NewCharacter{Entities.emplace_back(
std::make_unique<Character>()
)};
}
// ...
};
The Scene
doesn’t need to worry about the character’s components - the Character
makes sure it has what it needs:
TransformComponent ticking
ImageComponent rendering at: { x = 0, y = 0 }
TransformComponent ticking
ImageComponent rendering at: { x = 0, y = 0 }
TransformComponent ticking
ImageComponent rendering at: { x = 0, y = 0 }
...
Additionally, just because our Character
objects comes with some components as standard, that doesn’t mean our Scene
(or any other code) can’t add more. We have the flexibility to do whatever we need:
// Scene.h
// ...
class Scene {
public:
Scene() {
EntityPtr& Player{
Entities.emplace_back(
std::make_unique<Character>())};
// The player character needs more components
Player->AddImageComponent();
Player->AddImageComponent();
}
// ...
};
TransformComponent ticking
ImageComponent rendering at: { x = 0, y = 0 }
ImageComponent rendering at: { x = 0, y = 0 }
ImageComponent rendering at: { x = 0, y = 0 }
// ...
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>(this))};
NewComponent->Initialize();
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>(this))};
NewComponent->Initialize();
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:
Component(Entity* Owner) : Owner(Owner) {}
virtual void Initialize() {}
virtual void HandleEvent(const SDL_Event& E) {}
virtual void Tick(float DeltaTime) {}
virtual void Render(SDL_Surface* Surface) {}
virtual ~Component() = default;
Entity* GetOwner() const { return Owner; }
private:
Entity* Owner{nullptr};
};
#pragma once
#include "Vec2.h"
#include "Component.h"
class TransformComponent : public Component {
public:
using Component::Component;
Vec2 GetPosition() const {
return Position;
}
private:
Vec2 Position{0, 0};
};
#pragma once
#include "Component.h"
class ImageComponent : public Component {
public:
using Component::Component;
void Initialize() override;
void Render(SDL_Surface* Surface) override;
};
#include <iostream>
#include "ImageComponent.h"
#include "Entity.h"
void ImageComponent::Render(
SDL_Surface* Surface
) {
TransformComponent* Transform{
GetOwner()->GetTransformComponent()
};
std::cout << "ImageComponent rendering at: "
<< Transform->GetPosition() << '\n';
}
void ImageComponent::Initialize() {
Entity* Owner{GetOwner()};
if (!Owner->GetTransformComponent()) {
std::cout << "Error: ImageComponent requires"
" TransformComponent on its Owner\n";
// React to removal...
}
}
This lesson enhanced our ECS by enabling communication and dependency management. We gave components access to their owning entity via an Owner
pointer, allowing sibling component interaction.
We introduced an Initialize()
step to enforce dependencies (like ImageComponent
requiring TransformComponent
) and demonstrated how an OnComponentRemoved()
notification system could be added for robust dependency handling.
Finally, we saw how entity subtypes (Character
) can combine inheritance with composition for convenient setup of standard entities.
Key takeaways:
Entity
via an Owner
pointer passed during construction.GetOwner()->GetComponent()
-style functions.Initialize()
method allows components to verify dependencies after being added.OnComponentRemoved()
notification helps components react if a depended-upon component is removed later.Character
) can inherit from Entity
to provide default component setups.Explore component communication, dependency management, and specialized entity types within your ECS
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games