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¶
- Composition over Inheritance - Build complex objects by combining simple components
- Cache-Friendly - Components of the same type are stored together
- Decoupled Logic - Systems are independent and can be tested in isolation
- Hot-Swappable - Add/remove components at runtime
- 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¶
- Components should be pure data - No methods that modify other entities
- Keep components small and focused - One responsibility per component
- 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¶
- Architecture Overview - How ECS fits into MAID
- Content Packs - Registering systems and components
- Events - How systems communicate