Skip to content

Entities in MAID

Entities are the foundation of MAID's Entity Component System (ECS). Every game object - players, NPCs, items, rooms - is represented as an entity.

What is an Entity?

An entity is essentially an identifier (UUID) that groups components together. Entities themselves hold no data or logic - they are containers for components that describe what the entity is and systems that define what it does.

from maid_engine.core.ecs import Entity

# An entity is just an ID with attached components
entity = Entity()
print(entity.id)  # UUID like 'a1b2c3d4-e5f6-...'

Creating Entities

Via EntityManager

The recommended way to create entities is through the EntityManager:

from maid_engine.core.ecs import EntityManager

manager = EntityManager()

# Create a new entity
entity = manager.create()

# Create with a specific ID (useful for persistence)
from uuid import UUID
entity = manager.create(entity_id=UUID("12345678-1234-1234-1234-123456789abc"))

Via World

In practice, you'll usually create entities through the World:

# In a system or command handler
entity = self.world.create_entity()

# Or with a specific ID
entity = self.world.create_entity(entity_id=some_uuid)

Entity Properties

ID

Every entity has a unique UUID:

entity = manager.create()
print(entity.id)  # UUID object
print(str(entity.id))  # String representation

Timestamps

Entities track creation and update times:

entity = manager.create()
print(entity.created_at)  # datetime when created
print(entity.updated_at)  # datetime of last component/tag change

Tags

Tags are string labels for categorizing entities:

entity = manager.create()

# Add tags
entity.add_tag("player")
entity.add_tag("human")

# Check tags
if entity.has_tag("player"):
    print("This is a player!")

# Get all tags
print(entity.tags)  # {'player', 'human'}

# Remove tags
entity.remove_tag("human")

Adding Components

Components attach data to entities:

from maid_engine.core.ecs import Component

class PositionComponent(Component):
    room_id: UUID
    x: int = 0
    y: int = 0

class HealthComponent(Component):
    current: int
    maximum: int

# Add components to entity
entity = manager.create()
entity.add(PositionComponent(room_id=room.id, x=5, y=10))
entity.add(HealthComponent(current=100, maximum=100))

Method Chaining

add() returns the entity for chaining:

entity = (
    manager.create()
    .add(PositionComponent(room_id=room.id))
    .add(HealthComponent(current=100, maximum=100))
    .add_tag("player")
    .add_tag("human")
)

Retrieving Components

Get (with error)

# Raises KeyError if component not found
health = entity.get(HealthComponent)
print(health.current)

Try Get (returns None)

# Returns None if component not found
health = entity.try_get(HealthComponent)
if health:
    print(health.current)

Check If Has Component

# Check for single component
if entity.has(HealthComponent):
    print("Entity has health!")

# Check for multiple components (AND)
if entity.has(HealthComponent, PositionComponent):
    print("Entity has both health and position!")

# Check for any of multiple components (OR)
if entity.has_any(HealthComponent, ManaComponent):
    print("Entity has health or mana (or both)!")

Iterate All Components

for component in entity.components:
    print(f"{type(component).__name__}: {component}")

Removing Components

# Remove and get the component (returns None if not found)
removed_health = entity.remove(HealthComponent)
if removed_health:
    print(f"Removed health: {removed_health.current}/{removed_health.maximum}")

Entity Manager Queries

The EntityManager provides efficient queries for finding entities.

Find by Component

# Find all entities with specific components
for entity in manager.with_components(HealthComponent):
    health = entity.get(HealthComponent)
    print(f"{entity.id}: {health.current}/{health.maximum}")

# Find entities with multiple components (AND query)
for entity in manager.with_components(HealthComponent, PositionComponent):
    # Entity has both components
    health = entity.get(HealthComponent)
    pos = entity.get(PositionComponent)

Find by Tag

# Find all players
for entity in manager.with_tag("player"):
    print(f"Player: {entity.id}")

# Find entities with multiple tags (AND query)
for entity in manager.with_tags("player", "online"):
    print(f"Online player: {entity.id}")

Get Entity by ID

# Returns None if not found
entity = manager.get(some_uuid)

# Raises KeyError if not found
entity = manager.get_or_raise(some_uuid)

Iterate All Entities

# Count entities
print(f"Total entities: {manager.count()}")
print(f"Total entities: {len(manager)}")

# Iterate all
for entity in manager.all():
    print(entity)

Destroying Entities

# Destroy by ID
success = manager.destroy(entity.id)

# Clear all entities
manager.clear()

Serialization

Entities can be serialized to dictionaries for persistence:

# Serialize
data = entity.to_dict()
# {
#     "id": "12345678-...",
#     "tags": ["player", "human"],
#     "components": {
#         "HealthComponent": {"current": 100, "maximum": 100},
#         "PositionComponent": {"room_id": "...", "x": 5, "y": 10}
#     },
#     "created_at": "2024-01-15T10:30:00Z",
#     "updated_at": "2024-01-15T11:45:00Z"
# }

# Deserialize (you need to reconstruct manually)
from uuid import UUID

new_entity = manager.create(entity_id=UUID(data["id"]))
for tag in data["tags"]:
    new_entity.add_tag(tag)

# Reconstruct components based on type names
component_map = {
    "HealthComponent": HealthComponent,
    "PositionComponent": PositionComponent,
}
for type_name, comp_data in data["components"].items():
    if type_name in component_map:
        component = component_map[type_name](**comp_data)
        new_entity.add(component)

Best Practices

1. Use the EntityManager

Always create entities through the EntityManager (or World) so they're properly indexed:

# Good
entity = manager.create()

# Bad - entity won't be tracked
entity = Entity()

2. Use Tags for Categories

Use tags for entity categories, not components:

# Good - tags for categorization
entity.add_tag("npc")
entity.add_tag("merchant")

# Bad - empty component just for categorization
class NPCMarker(Component):
    pass
entity.add(NPCMarker())

3. Keep Components Focused

Each component should represent one aspect:

# Good - focused components
entity.add(HealthComponent(current=100, maximum=100))
entity.add(ManaComponent(current=50, maximum=50))

# Bad - god component
entity.add(CharacterStats(
    health=100, max_health=100,
    mana=50, max_mana=50,
    strength=10, dexterity=12,
    # ... many more fields
))

4. Check Before Acting

Always check for components before using them:

# Good - safe access
health = entity.try_get(HealthComponent)
if health and health.current > 0:
    process_living_entity(entity)

# Bad - might raise KeyError
health = entity.get(HealthComponent)  # Crash if no health!

5. Use Component Queries in Systems

Let the EntityManager do the filtering:

class HealthRegenSystem(System):
    async def update(self, delta: float) -> None:
        # Good - efficient query
        for entity in self.entities.with_components(HealthComponent):
            health = entity.get(HealthComponent)
            if health.current < health.maximum:
                health.current = min(health.current + delta, health.maximum)

Example: Creating a Player Entity

from uuid import UUID
from maid_engine.core.ecs import Component

class NameComponent(Component):
    name: str
    title: str = ""

class PositionComponent(Component):
    room_id: UUID

class HealthComponent(Component):
    current: int
    maximum: int

class InventoryComponent(Component):
    items: list[UUID] = []
    max_slots: int = 20

def create_player(world, name: str, starting_room: UUID) -> Entity:
    """Create a new player entity with default components."""
    player = world.create_entity()

    player.add(NameComponent(name=name))
    player.add(PositionComponent(room_id=starting_room))
    player.add(HealthComponent(current=100, maximum=100))
    player.add(InventoryComponent())

    player.add_tag("player")
    player.add_tag("human")

    return player

# Usage
player = create_player(world, "Hero", town_square.id)

Next Steps

  • Components - Learn about designing components
  • Systems - Learn about implementing systems