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)¶
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¶
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¶
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:
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