Entity and Component Interaction
Explore component communication, dependency management, and specialized entity types within your ECS
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.
Communication Between Entities and Components
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 }
...
Component Dependencies
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);
}
}
Entity Subtypes
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 }
// ...
Complete Code
Our complete code, which we'll build upon throughout this chapter, is available below:
Summary
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:
- Components can access their
Entity
via anOwner
pointer passed during construction. - Sibling components can be found using
GetOwner()->GetComponent()
-style functions. - An
Initialize()
method allows components to verify dependencies after being added. - Components can request their own removal if dependencies aren't met.
- An
OnComponentRemoved()
notification helps components react if a depended-upon component is removed later. - Entity subclasses (like
Character
) can inherit fromEntity
to provide default component setups.
Creating an Input Component
Implement player controls and AI actions cleanly using the Command pattern