Chapter 26

The Component Pattern

Every game eventually produces a god-object: a Player class, or a GameObject class, or an Entity class that keeps growing until it handles input, physics, rendering, animation, AI, audio, networking, and three other things. The Component pattern is the standard antidote. This chapter covers why it's needed, how it works, and how it relates to the Entity Component System (ECS) architecture used in modern engines.

The God-Object Problem

Start with a simple game character:

class Character
{
    // Input
    void handleInput(const SDL_Event&);

    // Physics
    float x, y, vx, vy;
    void applyGravity(float dt);

    // Rendering
    SDL_Texture* texture;
    void draw(SDL_Renderer*);
};

That's fine. Add enemies, NPCs, animated props, a turret that can't move but can aim, a moving platform that doesn't respond to input, a pickup that renders but has no physics. Each new entity type shares some of these concerns but not others. You have three choices:

  1. Duplicate code. Copy the physics into every class that needs it. Code doubles; bugs multiply.
  2. Use inheritance. Pull shared behaviour into a base class. Every tutorial does this. But what's the base of a turret and a moving platform? PhysicsEntity? RenderableEntity? The hierarchy becomes a taxonomy argument, and multiple inheritance brings its own mess.
  3. Use composition. Give each object the behaviours it actually needs as separate, attached components. This is the Component pattern.

Why Inheritance Doesn't Fix It

Inheritance is great for "is-a" relationships that are genuinely stable. A Dog is an Animal; that's not going to change. But in a game, a character is a bundle of interchangeable behaviours: it has input handling, it has physics, it has rendering. Those aren't types — they're capabilities. And capabilities compose better than they inherit.

The classic demonstration is the "flying AI swimming player" problem. You have a Player class, an AICharacter class, and you decide to make a boss that swims. If swimming is a method on a base class, every entity that needs swimming has to inherit it — including ones that don't swim. You end up with a deep, wide inheritance tree where every leaf carries a lot of functionality it doesn't use, and adding a new behaviour means touching the hierarchy.

The C++ slogan is: prefer composition over inheritance. The Component pattern is composition in practice.

Extracting Components

We break a character's behaviour into three components:

class InputComponent
{
public:
    virtual ~InputComponent() = default;
    virtual void update(class GameObject& owner, const SDL_Event& ev) = 0;
};

class PhysicsComponent
{
public:
    virtual ~PhysicsComponent() = default;
    virtual void update(class GameObject& owner, float dt) = 0;
};

class GraphicsComponent
{
public:
    virtual ~GraphicsComponent() = default;
    virtual void render(const class GameObject& owner, SDL_Renderer*) = 0;
};

Each is a pure-virtual base (an interface, in the Chapter 20 sense). Then the GameObject itself just holds components and delegates to them:

class GameObject
{
public:
    // World state — shared by all components
    float x = 0, y = 0;
    float vx = 0, vy = 0;
    float angle = 0;

    // Components — owned via unique_ptr
    std::unique_ptr<InputComponent>    input;
    std::unique_ptr<PhysicsComponent>  physics;
    std::unique_ptr<GraphicsComponent> graphics;

    void handleEvent(const SDL_Event& ev)
    {
        if (input) input->update(*this, ev);
    }

    void update(float dt)
    {
        if (physics) physics->update(*this, dt);
    }

    void render(SDL_Renderer* r)
    {
        if (graphics) graphics->render(*this, r);
    }
};

The GameObject class is now a pure container. It has no behaviour of its own — just world state and component slots. The behaviour lives entirely in the components.

Swappable Implementations

The power of the pattern appears when you create multiple implementations of the same component interface. For input:

// Human player — reads keyboard
class PlayerInputComponent : public InputComponent
{
public:
    void update(GameObject& owner, const SDL_Event& ev) override
    {
        if (ev.type != SDL_EVENT_KEY_DOWN) return;
        switch (ev.key.scancode)
        {
            case SDL_SCANCODE_A: case SDL_SCANCODE_LEFT:  owner.vx -= 1.0f; break;
            case SDL_SCANCODE_D: case SDL_SCANCODE_RIGHT: owner.vx += 1.0f; break;
            default: break;
        }
    }
};

// AI-controlled — seeks the player
class AIInputComponent : public InputComponent
{
public:
    explicit AIInputComponent(const GameObject& target) : target_(target) {}

    void update(GameObject& owner, const SDL_Event&) override
    {
        // Move toward target
        float dx = target_.x - owner.x;
        float dy = target_.y - owner.y;
        float len = std::sqrt(dx*dx + dy*dy);
        if (len > 0.001f)
        {
            owner.vx = (dx / len) * AI_SPEED;
            owner.vy = (dy / len) * AI_SPEED;
        }
    }

private:
    const GameObject& target_;
    static constexpr float AI_SPEED = 80.0f;
};

Now creating a player-controlled character and an AI-controlled enemy looks like:

// Player
auto player = std::make_unique<GameObject>();
player->input    = std::make_unique<PlayerInputComponent>();
player->physics  = std::make_unique<SimplePhysicsComponent>();
player->graphics = std::make_unique<SpriteGraphicsComponent>(playerSprite);

// Enemy (AI chases the player)
auto enemy = std::make_unique<GameObject>();
enemy->input    = std::make_unique<AIInputComponent>(*player);
enemy->physics  = std::make_unique<SimplePhysicsComponent>();
enemy->graphics = std::make_unique<SpriteGraphicsComponent>(enemySprite);

To make the player AI-controlled (for a demo mode, say), you swap one pointer:

player->input = std::make_unique<AIInputComponent>(*someTarget);

The GameObject doesn't know or care. The rest of the game doesn't know or care. Only the input slot changed.

Component Communication

Components need to communicate with each other. There are three approaches, each with a different trade-off:

Shared State on the GameObject

The simplest approach: components read and write shared fields on the owning GameObject. The physics component writes owner.x and owner.y; the graphics component reads them. This works well when the shared state is simple and stable.

The downside: the GameObject's public fields become an implicit protocol that all components depend on. If you add a new field that only two components care about, it still pollutes the whole object.

Direct References

A component can hold a direct pointer or reference to another component. The AI input component above holds a reference to its target's GameObject. This is fine for well-defined, stable relationships.

The downside: components are no longer independent — they depend on each other existing. Be careful about lifetimes.

Message Passing

Components communicate by posting messages to a queue that the GameObject dispatches. This decouples components completely but adds complexity. For small games it's overkill; for large ones with many component types it pays off.

In practice, most game code uses shared state for the common case and direct references for specific relationships.

Optional Components and Null Checks

Notice that the GameObject::update and render methods check if (input) before calling through the component pointer. This means a component slot can be empty — a nullptr unique_ptr.

This is deliberate. A static prop has a graphics component but no input component and no physics component. Rather than creating stub implementations that do nothing, you just leave those slots empty. The null checks are cheap and the design is cleaner.

You could also use std::optional<T> here to be more explicit, but unique_ptr acting as an optional pointer is idiomatic C++.

Preview: Entity Component Systems

The Component pattern as described here is a clean architecture solution to the god-object problem. But it has a performance cost: each component is a separate heap allocation (unique_ptr), and calling through virtual dispatch on every update means many small scattered memory accesses.

At game-object counts in the hundreds to thousands — which covers almost every game in this book — this performance cost doesn't matter. But at counts of tens of thousands (particle simulations, RTS units, bullets in bullet hells), it starts to matter a great deal.

This is the motivation for the Entity Component System (ECS) architecture used in modern engines like Unity (since 2019) and in specialised libraries like EnTT. In an ECS:

The performance benefit comes from the data layout: the chapter on Data Locality (Chapter 27) explains exactly why contiguous arrays of plain data are much faster to process than the scattered virtual-dispatch approach.

ECS is the architecture the Component pattern points toward. For the final project in this book, we use a simple variant of the Component pattern — not a full ECS — because the scale doesn't demand it. But understanding the Component pattern is the prerequisite for understanding ECS when you meet it.

When to Use the Component Pattern

Use it when:

Don't use it when:

Summary