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:
- Duplicate code. Copy the physics into every class that needs it. Code doubles; bugs multiply.
- 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. - 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:
- Entities are just integer IDs — no data, no behaviour, just a number.
- Components are plain data structs stored in contiguous arrays, one array per component type. All
Positioncomponents together, allVelocitycomponents together. - Systems are free functions that iterate over entities that have specific component combinations and process them in batch.
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:
- You have many entity types that share subsets of behaviours
- You want to swap behaviours at runtime (AI vs player control, different physics modes)
- Your inheritance hierarchy is getting deep and hard to reason about
- You want to add new behaviours without modifying existing classes
Don't use it when:
- You have a small fixed set of entity types with stable behaviour (the Asteroids project's separate vectors per type is simpler)
- The behaviour genuinely is "is-a" rather than "has-a" (a Dog truly is an Animal — inheritance is fine)
- The overhead of component coordination would complicate simpler code
Summary
- The god-object problem: classes accumulate responsibilities until they're too large and too coupled to maintain.
- Composition over inheritance: prefer giving objects the behaviours they need as separate components over creating deep hierarchies.
- Define components as pure-virtual base classes (interfaces). Give
GameObjectcomponent slots asunique_ptrmembers. - Multiple implementations of the same interface (
PlayerInputComponent,AIInputComponent) make swapping behaviours trivial. - Components communicate via shared state on the owner, direct references, or message passing — shared state is the simplest starting point.
- The Component pattern is the conceptual step toward ECS — which brings performance benefits by storing components in contiguous memory.