Skip to content

Entity Component System (ECS)

MAID uses an Entity Component System architecture for managing game objects.

What is ECS?

ECS is a data-oriented design pattern that separates:

  • Entities - Unique identifiers for game objects (players, NPCs, items, rooms)
  • Components - Pure data containers attached to entities
  • Systems - Logic that operates on entities with specific components

Benefits

  1. Composition over Inheritance - Build complex objects by combining simple components
  2. Cache-Friendly - Components of the same type are stored together
  3. Decoupled Logic - Systems are independent and can be tested in isolation
  4. Hot-Swappable - Add/remove components at runtime
  5. Flexible - The same component can be used by different entity types

When to Use Components vs Inheritance

Use Components when:

  • Multiple entity types share the same data/behavior (e.g., both players and NPCs have health)
  • You need to add/remove capabilities at runtime (e.g., add flying to a player with a spell)
  • The feature is optional for an entity type (e.g., not all items are containers)

Use Inheritance when:

  • There's truly shared implementation code (but consider mixins instead)
  • You need Python's type system for static analysis
  • The relationship is genuinely "is-a" not "has-a"
# Prefer composition (components)
player.add(HealthComponent(100, 100))
player.add(InventoryComponent(slots=20))
player.add(PositionComponent(room_id))

# Over inheritance
class Player(HasHealth, HasInventory, HasPosition):  # Avoid this
    pass

Entity Lifecycle

Creating Entities

# Create through the EntityManager for proper tracking
entity = world.entities.create()

# Or create with a specific ID
from uuid import UUID
entity = world.entities.create(entity_id=UUID("..."))

Adding Components

from maid_stdlib.components import HealthComponent, PositionComponent

# Fluent API for adding multiple components
entity.add(HealthComponent(current=100, maximum=100))
      .add(PositionComponent(room_id=room.id))
      .add_tag("player")

# Check for components
if entity.has(HealthComponent):
    health = entity.get(HealthComponent)
    health.current -= damage

Querying Entities

# Get all entities with specific components
for entity in world.entities.with_components(HealthComponent, PositionComponent):
    health = entity.get(HealthComponent)
    pos = entity.get(PositionComponent)
    # Process entity...

# Query by tag
for entity in world.entities.with_tag("npc"):
    # Process NPCs...

# Combined queries
for entity in world.entities.with_tags("enemy", "boss"):
    # Process boss enemies...

Removing Components and Entities

# Remove a component
entity.remove(HealthComponent)

# Destroy an entity (removes from EntityManager)
world.entities.destroy(entity.id)

Systems

Systems contain the game logic that operates on entities:

from maid_engine.core.ecs import System

class RegenerationSystem(System):
    # Lower priority = runs earlier in the tick
    priority = 50

    async def update(self, delta: float) -> None:
        """Called every tick. delta is time since last tick."""
        for entity in self.entities.with_components(HealthComponent):
            health = entity.get(HealthComponent)
            if health.current < health.maximum:
                # Regenerate 1 HP per second
                health.current = min(
                    health.current + delta,
                    health.maximum
                )

    async def startup(self) -> None:
        """Called when the system is registered."""
        # Subscribe to events, load resources, etc.
        pass

    async def shutdown(self) -> None:
        """Called when the system is unregistered."""
        # Clean up resources
        pass

System Priority

Systems run in priority order each tick:

class InputSystem(System):
    priority = 10  # Runs first - process player input

class MovementSystem(System):
    priority = 20  # Runs second - apply movement

class CombatSystem(System):
    priority = 30  # Runs third - resolve combat

class RenderSystem(System):
    priority = 100  # Runs last - send output to players

Component Design Guidelines

  1. Components should be pure data - No methods that modify other entities
  2. Keep components small and focused - One responsibility per component
  3. Use Pydantic models for validation - Automatic type checking and serialization
from pydantic import BaseModel

class HealthComponent(BaseModel):
    """Health data for entities that can take damage."""
    current: int
    maximum: int
    regeneration_rate: float = 0.0

    @property
    def is_alive(self) -> bool:
        return self.current > 0

    @property
    def percentage(self) -> float:
        return (self.current / self.maximum) * 100 if self.maximum > 0 else 0

Further Reading