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(capacity=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
# Add multiple components
entity.add(HealthComponent(current=100, maximum=100))
entity.add(PositionComponent(room_id=room.id))
entity.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
Lifecycle Events¶
The ECS emits events automatically when entities and components change:
| Operation | Event Emitted |
|---|---|
Entity.add(component) |
ComponentAddedEvent |
Entity.remove(component_type) |
ComponentRemovedEvent |
EntityManager.create() |
EntityCreatedEvent |
EntityManager.destroy(id) |
EntityDestroyedEvent |
Entity.add_tag(tag) |
TagAddedEvent |
Entity.remove_tag(tag) |
TagRemovedEvent |
These events enable reactive patterns — for example, the persistence system listens for ComponentAddedEvent to begin tracking new components.
Dirty Tracking¶
The Component base class includes a __setattr__ hook that marks the owning entity as dirty when any field is modified. The persistence layer uses this to efficiently save only changed entities each tick.
Component Registry¶
The ComponentRegistry maps type name strings to component classes, enabling serialization and deserialization of entities:
from maid_engine.core.ecs import ComponentRegistry
registry = ComponentRegistry()
registry.register("health", HealthComponent)
# Deserialize from stored data
component_cls = registry.get("health")
component = component_cls(**saved_data)
Content packs register their component types via the register_component_types(registry) method.
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 maid_engine.core.ecs import Component
class HealthComponent(Component):
"""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