Entity and Component Interaction

Explore component communication, dependency management, and specialized entity types within your ECS

Ryan McCombe
Updated

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 an Owner 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 from Entity to provide default component setups.
Next Lesson
Lesson 114 of 129

Creating an Input Component

Implement player controls and AI actions cleanly using the Command pattern

Have a question about this lesson?
Purchase the course to ask your own questions