From 466148a5806e01c48b53ba065e5d4b585b06b639 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sat, 29 Nov 2025 23:32:35 +0000 Subject: [PATCH] Update "Entity-Management" --- Entity-Management.md | 315 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 Entity-Management.md diff --git a/Entity-Management.md b/Entity-Management.md new file mode 100644 index 0000000..dd1c740 --- /dev/null +++ b/Entity-Management.md @@ -0,0 +1,315 @@ +# Entity Management + +Entities are game objects that implement behavior and live on Grids. While Grids handle rendering and mediate interactions, Entities encapsulate game logic like movement, combat, and AI. + +## Quick Reference + +**Parent System:** [[Grid-System]] + +**Key Types:** +- `mcrfpy.Entity` - Game entities on grids +- `mcrfpy.Grid` - Spatial container for entities +- `mcrfpy.EntityCollection` - Collection of entities on a grid + +**Key Files:** +- `src/UIEntity.h` / `src/UIEntity.cpp` +- `src/UIEntityCollection.h` / `.cpp` + +**Related Issues:** +- [#115](../../issues/115) - SpatialHash for fast queries (Open) +- [#117](../../issues/117) - Memory Pool for entities (Open) + +--- + +## What Are Entities? + +Entities are game objects that: +- **Live on a Grid** (0 or 1 grid at a time) +- **Have a sprite** for visual rendering +- **Have grid position** (integer cell coordinates) +- **Implement behavior** (movement, AI, combat, inventory) + +**Key distinction:** Entities implement behavior. Grids mediate interaction between entities and render them to screen. + +--- + +## Entity-Grid Relationship + +The Entity-Grid relationship mirrors the UIDrawable parent-child pattern: + +| Relationship | Property | Automatic Behavior | +|--------------|----------|-------------------| +| Entity → Grid | `entity.grid` | Set when added to `grid.entities` | +| Grid → Entities | `grid.entities` | Collection of all entities on grid | + +```python +import mcrfpy + +# Create grid and entity +grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(400, 400)) +player = mcrfpy.Entity(pos=(10, 10), sprite_index=0) + +# Before adding: entity has no grid +print(player.grid) # None + +# Add to grid +grid.entities.append(player) + +# After adding: bidirectional link established +print(player.grid == grid) # True +print(player in grid.entities) # True + +# Removing breaks the link +grid.entities.remove(player) +print(player.grid) # None +``` + +**Important:** An entity can only be on 0 or 1 grids at a time. Adding to a new grid automatically removes from the old one. + +--- + +## Entity Properties + +### Position + +```python +# Grid coordinates (integer cells) +entity.x = 15 +entity.y = 20 +entity.pos = (15, 20) # Tuple form + +# Draw position (float, for animation interpolation) +print(entity.draw_pos) # Actual render position +``` + +### Sprite + +```python +entity.sprite_index = 5 # Index in texture sprite sheet +``` + +### Visibility + +```python +entity.visible = True +entity.opacity = 0.8 # 0.0 to 1.0 +``` + +### Grid Reference + +```python +current_grid = entity.grid # Read-only, set by collection operations +``` + +--- + +## EntityCollection + +`grid.entities` is an `EntityCollection` with list-like operations: + +```python +# Add entities +grid.entities.append(entity) +grid.entities.extend([entity1, entity2, entity3]) +grid.entities.insert(0, entity) # Insert at index + +# Remove entities +grid.entities.remove(entity) +entity = grid.entities.pop() # Remove and return last +entity = grid.entities.pop(0) # Remove and return at index + +# Query +count = len(grid.entities) +idx = grid.entities.index(entity) +n = grid.entities.count(entity) +found = grid.entities.find("entity_name") # Find by name + +# Iteration +for entity in grid.entities: + print(entity.pos) +``` + +--- + +## Entity Lifecycle + +### Creation + +```python +# Basic creation +entity = mcrfpy.Entity(pos=(10, 10), sprite_index=0) + +# With name for later lookup +entity = mcrfpy.Entity(pos=(10, 10), sprite_index=0, name="player") +``` + +### Adding to Grid + +```python +grid.entities.append(entity) +# entity.grid is now set to grid +# Entity will be rendered with the grid +``` + +### Movement + +```python +# Direct position change +entity.pos = (new_x, new_y) + +# Animated movement +mcrfpy.Animation("x", target_x, 0.3, "easeOutQuad").start(entity) +mcrfpy.Animation("y", target_y, 0.3, "easeOutQuad").start(entity) +``` + +### Removal + +```python +# Method 1: Remove from collection +grid.entities.remove(entity) + +# Method 2: Entity.die() - removes from parent grid +entity.die() + +# After removal: entity.grid is None +``` + +### Transfer Between Grids + +```python +def transfer_entity(entity, to_grid, new_pos): + """Move entity to a different grid.""" + entity.die() # Remove from current grid + entity.pos = new_pos + to_grid.entities.append(entity) +``` + +--- + +## Common Patterns + +### Player Entity + +```python +class Player: + def __init__(self, grid, start_pos): + self.entity = mcrfpy.Entity(pos=start_pos, sprite_index=0, name="player") + grid.entities.append(self.entity) + + def move(self, dx, dy): + new_x = self.entity.x + dx + new_y = self.entity.y + dy + + # Check walkability via grid + point = self.entity.grid.at(new_x, new_y) + if point and point.walkable: + self.entity.pos = (new_x, new_y) + return True + return False +``` + +### Enemy Entity + +```python +class Enemy: + def __init__(self, grid, pos, aggro_range=10): + self.entity = mcrfpy.Entity(pos=pos, sprite_index=1) + self.aggro_range = aggro_range + self.health = 100 + grid.entities.append(self.entity) + + def update(self, player_pos): + dx = player_pos[0] - self.entity.x + dy = player_pos[1] - self.entity.y + dist = (dx*dx + dy*dy) ** 0.5 + + if dist < self.aggro_range: + self.chase(player_pos) + else: + self.wander() + + def chase(self, target): + # Use pathfinding + path = self.entity.path_to(target) + if path and len(path) > 1: + next_cell = path[1] # path[0] is current position + self.entity.pos = next_cell + + def wander(self): + import random + dx = random.choice([-1, 0, 1]) + dy = random.choice([-1, 0, 1]) + + new_pos = (self.entity.x + dx, self.entity.y + dy) + point = self.entity.grid.at(*new_pos) + if point and point.walkable: + self.entity.pos = new_pos +``` + +### Item Entity + +```python +class Item: + def __init__(self, grid, pos, item_type): + self.entity = mcrfpy.Entity(pos=pos, sprite_index=10 + item_type) + self.item_type = item_type + grid.entities.append(self.entity) + + def pickup(self, collector): + """Called when another entity picks up this item.""" + collector.inventory.append(self.item_type) + self.entity.die() # Remove from grid +``` + +For more interaction patterns (click handling, selection, context menus), see [[Grid-Interaction-Patterns]]. + +--- + +## Pathfinding + +Entities have built-in pathfinding via libtcod: + +```python +# A* pathfinding to target +path = entity.path_to((target_x, target_y)) +# Returns list of (x, y) tuples, or empty if no path + +if path: + next_step = path[1] # path[0] is current position + entity.pos = next_step +``` + +Pathfinding respects `GridPoint.walkable` properties set on the grid. + +--- + +## Performance Considerations + +**Current:** Entity queries are O(n): +```python +# Finding entities at position requires iteration +def entities_at(grid, x, y): + return [e for e in grid.entities if e.x == x and e.y == y] +``` + +**Workarounds:** +- Keep entity counts reasonable (< 200 for best performance) +- Use timer callbacks for AI updates, not per-frame +- Cache query results when possible + +**Future:** [#115](../../issues/115) SpatialHash will provide O(1) position queries. + +See [[Performance-and-Profiling]] for optimization guidance. + +--- + +## Related Systems + +- [[Grid-System]] - Spatial container for entities +- [[Grid-Interaction-Patterns]] - Click handling, selection, context menus +- [[Animation-System]] - Smooth entity movement +- [[Performance-and-Profiling]] - Entity performance metrics + +--- + +*Last updated: 2025-11-29* \ No newline at end of file