ADR-001: Entity Component System Architecture¶
Status¶
Accepted
Date¶
2024-01-15
Context¶
MUD engines have traditionally modeled game entities (players, NPCs, items, rooms)
using deep class hierarchies. A typical approach looks like:
GameObject -> Character -> Player -> Warrior. This pattern leads to rigid
inheritance trees, diamond inheritance problems, and god-objects that accumulate
behavior over time. Adding a new capability (e.g., making an item also act as a
container) requires restructuring the class tree.
MAID needs to support a wide range of entity types that can be dynamically composed at runtime: an NPC might gain inventory, a room might gain weather effects, a player might gain faction membership. Content packs (third-party gameplay modules) need to attach arbitrary data to entities without modifying the core engine.
Decision¶
Use an Entity Component System (ECS) architecture where:
- Entities (
Entityinpackages/maid-engine/src/maid_engine/core/ecs/entity.py) are lightweight identifiers (UUIDs) that group components together. They carry no game logic themselves. - Components (
Componentinpackages/maid-engine/src/maid_engine/core/ecs/component.py) are pure data containers built on PydanticBaseModel. Examples includeHealthComponent,PositionComponent,InventoryComponent, andDialogueComponentinpackages/maid-stdlib/src/maid_stdlib/components/. They usevalidate_assignment=Trueandextra="forbid"for strict data integrity. - Systems (
Systeminpackages/maid-engine/src/maid_engine/core/ecs/system.py) contain all logic. They query entities by component type usingEntityManager.with_components()and process matching entities each tick. Systems are managed bySystemManager, which runs them in priority order.
The EntityManager maintains indexed lookups (_by_component, _by_tag) for
efficient queries. Components are keyed by their Python type, and entities support
both type-based queries and Protocol-based queries via Entity.find_by_protocol().
Consequences¶
Positive¶
- Composition over inheritance: Any entity can have any combination of components.
An item can become a container by adding
InventoryComponent. An NPC can become a quest-giver by addingDialogueComponent. - Content pack extensibility: Content packs like
maid-classic-rpgdefine their own components (e.g.,CharacterStatsComponent,CorpseComponent) without modifying the engine or stdlib. - Serialization: Since components are Pydantic models, serialization to/from
JSON and document stores is built-in via
model_dump()andEntity.to_dict(). - Efficient queries:
EntityManager.with_components()uses set intersection starting from the smallest index, making multi-component queries fast.
Negative¶
- Debugging complexity: An entity's behavior is spread across multiple systems.
To understand what happens when a player attacks, you must trace through
CombatSystem,DamageSystem, andHealthSystemrather than reading a singlePlayer.attack()method. - Component coupling: Systems often need to access multiple components together.
A system processing movement needs both
PositionComponentandMovementComponent, creating implicit coupling between component types. - Runtime type safety: Component queries return dynamic types. While
entity.get(HealthComponent)is type-hinted, the actual set of components on an entity is only known at runtime.
Alternatives Considered¶
Deep Class Hierarchy¶
The traditional MUD approach (e.g., Player(Character(LivingEntity(Entity))))
was rejected because it creates rigid structures that cannot be extended by
content packs without modifying the base classes.
Mixin-Based Composition¶
Python mixins (e.g., class Player(Attackable, Inventoried, Positioned)) were
considered. While more flexible than deep hierarchies, mixins still require
compile-time decisions about entity capabilities and lead to method name
collisions and MRO complexity. ECS allows runtime composition.
Data-Oriented Design (SoD)¶
A pure structure-of-arrays ECS (components stored in contiguous arrays per type) was considered for cache efficiency. This was rejected because Python's object model does not benefit from cache-line optimization the way C/C++ does, and Pydantic models provide more value through validation and serialization.