Component architecture
Deep inheritance breaks down when entities need multiple capabilities. Composition — attaching small components to a plain entity object — scales cleanly to complex games.
- Contrast inheritance-based and composition-based entity design
- Implement a minimal component system in Python
- Identify when the overhead of a component system is and is not worth it
Every game developer hits the inheritance wall eventually. You start with
Enemy, add FlyingEnemy, then ShootingEnemy, then need a FlyingShootingEnemy.
Python technically allows multiple inheritance, but the resulting method
resolution order becomes opaque, and any change to one base class risks
breaking both subclasses. The deeper the hierarchy, the more fragile it becomes.
The root problem is that inheritance models "is-a" relationships (a FlyingEnemy is-a Enemy), but what games actually need is "has-a" relationships (an entity has-a position, has-a health, has-a AI controller). Composition models this directly.
The component idea
An entity is a minimal container — just an ID and a dictionary of components. A component is a plain data/logic object that does one thing. Systems query entities for the components they need.
class Entity:
_next_id = 0
def __init__(self):
self.id = Entity._next_id
Entity._next_id += 1
self._components = {}
def add(self, component):
self._components[type(component)] = component
return self # allow chaining
def get(self, component_type):
return self._components.get(component_type)
def has(self, *component_types):
return all(t in self._components for t in component_types)Components are plain classes or dataclasses:
from dataclasses import dataclass, field
import pygame
Vector2 = pygame.math.Vector2
@dataclass
class Position:
pos: Vector2 = field(default_factory=lambda: Vector2(0, 0))
@dataclass
class Velocity:
vel: Vector2 = field(default_factory=lambda: Vector2(0, 0))
@dataclass
class Health:
hp: int = 100
max_hp: int = 100
@dataclass
class AIController:
state: str = "PATROL"Building a flying shooting enemy:
enemy = (Entity()
.add(Position(Vector2(200, 150)))
.add(Velocity())
.add(Health(hp=60))
.add(AIController(state="PATROL")))There is no FlyingShootingEnemy class. The entity just happens to have the
right components.
Systems process component combinations
Systems are functions (or classes) that iterate entities, filtering for those that have the required components:
def movement_system(entities, dt):
for entity in entities:
pos = entity.get(Position)
vel = entity.get(Velocity)
if pos and vel:
pos.pos += vel.vel * dt
def health_system(entities, bus):
for entity in entities:
hp = entity.get(Health)
if hp and hp.hp <= 0:
bus.publish("ENTITY_DIED", entity_id=entity.id)Adding a new system (a rendering system, a collision system, a projectile system) never touches existing systems or entities.
Minimal manager
class World:
def __init__(self):
self.entities = []
def create(self):
e = Entity()
self.entities.append(e)
return e
def destroy(self, entity):
self.entities.remove(entity)
def query(self, *component_types):
return [e for e in self.entities if e.has(*component_types)]world.query(Position, Health) returns all entities that have both a
Position and a Health — the pattern systems use to find the objects they
care about.
When is the overhead worth it?
A component system adds indirection and requires more upfront design. For a
small game with 3–4 entity types, a flat inheritance or even direct pygame
Sprite subclasses is faster to write and easier to debug.
The component approach pays off when:
- You have many entity types that share subsets of capabilities.
- You want to add or remove capabilities at runtime (a power-up that grants flight temporarily).
- Multiple programmers are working on different systems in parallel — the interface between systems is just the component data, which rarely changes.
Full entity-component-system frameworks (ECS) separate data entirely from logic and store components in contiguous arrays for cache efficiency. That level of rigour is mainly relevant for games with thousands of entities. The pattern shown here is "ECS-inspired" and appropriate for typical 2D games.
Where to go next
Next: spatial hashing — replacing O(n²) all-pairs collision with an O(1) grid lookup structure.
The observer pattern
Publisher/subscriber decouples systems so an achievement engine, a sound manager, and a UI can all react to game events without the player or enemy knowing they exist.
Spatial hashing
Checking all pairs of entities for collision is O(n²). A grid-based spatial hash reduces lookup to O(1) by assigning entities to cells and only testing neighbours.