From ef1342882639a08a0460079effb9a48d1b92612d Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sat, 25 Oct 2025 22:39:01 +0000 Subject: [PATCH] Add "Entity-Management" --- Entity-Management.-.md | 418 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 Entity-Management.-.md diff --git a/Entity-Management.-.md b/Entity-Management.-.md new file mode 100644 index 0000000..531481f --- /dev/null +++ b/Entity-Management.-.md @@ -0,0 +1,418 @@ +# Entity Management + +Creating, positioning, and managing game entities that live on grids. + +## Quick Reference + +**System:** [[Grid-System]] + +**Key Types:** +- `mcrfpy.Entity` - Game entities on grids +- `mcrfpy.Grid` - Spatial container for entities +- `grid.entities` - EntityCollection + +**API Reference:** [mcrfpy.Entity](../../docs/api_reference_dynamic.html#Entity) + +--- + +## What are Entities? + +Entities are game objects that: +- Live on a Grid (0 or 1 grid per entity) +- Have a sprite for rendering +- Have grid position (integer coordinates) +- Can have AI, pathfinding, field of view +- Represent players, enemies, items, etc. + +**Files:** +- `src/UIEntity.h` / `src/UIEntity.cpp` +- `src/UIEntityCollection.h` / `.cpp` + +--- + +## Creating Entities + +### Basic Entity Creation + +```python +import mcrfpy + +# Create grid first +grid = mcrfpy.Grid(50, 50, 16, 16) +grid.texture = mcrfpy.createTexture("tiles.png") + +# Create entity at position (10, 10) with sprite 0 +player = mcrfpy.Entity(10, 10, 0) +player.sprite.texture = mcrfpy.createTexture("entities.png") + +# Add entity to grid +grid.entities.append(player) + +# Grid is now set automatically +assert player.grid == grid +``` + +### Entity Properties + +```python +# Position (grid coordinates) +player.x = 15 +player.y = 20 +player.pos = (15, 20) # Tuple alternative + +# Sprite (what to display) +player.sprite_index = 5 # Index in texture +player.sprite.scale_x = 1.5 +player.sprite.scale_y = 1.5 + +# Visibility +player.visible = True +player.sprite.opacity = 0.8 + +# Grid association +current_grid = player.grid # Read-only +``` + +--- + +## Entity Lifecycle + +### Adding to Grid + +```python +grid.entities.append(entity) + +# Automatically: +# - entity.grid = grid +# - Entity rendered with grid +# - Entity included in spatial queries +``` + +### Removing from Grid + +```python +# Method 1: Direct removal +grid.entities.remove(entity) + +# Method 2: Entity.die() (planned #30) +# entity.die() # Will call grid.entities.remove(entity) + +# Automatically: +# - entity.grid = None +# - Entity no longer rendered +# - Entity excluded from queries +``` + +### Moving Entities + +```python +# Method 1: Direct assignment +entity.x = new_x +entity.y = new_y + +# Method 2: Tuple assignment +entity.pos = (new_x, new_y) + +# Method 3: Animation (smooth movement) +mcrfpy.animate(entity, "x", target_x, 500, "ease_in_out_quad") +mcrfpy.animate(entity, "y", target_y, 500, "ease_in_out_quad") +``` + +**Collision Detection:** Manual - check grid cells before moving + +--- + +## Entity Collections + +### Iteration + +```python +# Iterate all entities on grid +for entity in grid.entities: + print(f"Entity at ({entity.x}, {entity.y})") + +# Count entities +num_entities = len(grid.entities) + +# Index access +first_entity = grid.entities[0] +last_entity = grid.entities[-1] +``` + +### Querying + +**Current:** O(n) iteration - no spatial indexing + +```python +# Find entities at position +def entities_at(grid, x, y): + result = [] + for entity in grid.entities: + if entity.x == x and entity.y == y: + result.append(entity) + return result + +# Find entities in radius +def entities_in_radius(grid, center_x, center_y, radius): + result = [] + for entity in grid.entities: + dx = entity.x - center_x + dy = entity.y - center_y + if (dx*dx + dy*dy) <= radius*radius: + result.append(entity) + return result +``` + +**Optimization:** [#115](../../issues/115) - SpatialHash for O(1) queries + +--- + +## Common Patterns + +### Pattern 1: Player Entity + +```python +import mcrfpy + +# Create player +player = mcrfpy.Entity(25, 25, 0) +player.sprite.texture = player_texture +player.sprite_index = 0 + +# Add to grid +game_grid.entities.append(player) + +# Movement with collision +def move_player(dx, dy): + new_x = player.x + dx + new_y = player.y + dy + + # Check walkable + if game_grid.walkable((new_x, new_y)): + # Check for other entities + if not entities_at(game_grid, new_x, new_y): + player.pos = (new_x, new_y) + return True + return False + +# Input handling +def on_keypress(key, pressed): + if pressed: + if key == mcrfpy.Key.Up: + move_player(0, -1) + elif key == mcrfpy.Key.Down: + move_player(0, 1) + elif key == mcrfpy.Key.Left: + move_player(-1, 0) + elif key == mcrfpy.Key.Right: + move_player(1, 0) +``` + +### Pattern 2: Enemy Entities + +```python +import random + +class Enemy: + def __init__(self, x, y): + self.entity = mcrfpy.Entity(x, y, 1) + self.entity.sprite.texture = enemy_texture + self.health = 100 + self.aggro_range = 10 + + def update(self, player): + # Calculate distance to player + dx = player.x - self.entity.x + dy = player.y - self.entity.y + dist = (dx*dx + dy*dy) ** 0.5 + + if dist < self.aggro_range: + # Chase player + self.move_toward(player.x, player.y) + else: + # Wander randomly + self.wander() + + def move_toward(self, target_x, target_y): + # Simple movement (pathfinding in [[AI-and-Pathfinding]]) + if target_x > self.entity.x: + self.entity.x += 1 + elif target_x < self.entity.x: + self.entity.x -= 1 + elif target_y > self.entity.y: + self.entity.y += 1 + elif target_y < self.entity.y: + self.entity.y -= 1 + + def wander(self): + # Random movement + dx = random.choice([-1, 0, 1]) + dy = random.choice([-1, 0, 1]) + + new_x = self.entity.x + dx + new_y = self.entity.y + dy + + if game_grid.walkable((new_x, new_y)): + self.entity.pos = (new_x, new_y) + +# Spawn enemies +enemies = [] +for i in range(10): + enemy = Enemy(random.randint(0, 49), random.randint(0, 49)) + game_grid.entities.append(enemy.entity) + enemies.append(enemy) + +# Update all enemies +def update_enemies(): + for enemy in enemies: + enemy.update(player) + +mcrfpy.setTimer("enemy_ai", lambda ms: update_enemies(), 500) # Every 0.5s +``` + +### Pattern 3: Item Entities + +```python +class Item: + def __init__(self, x, y, item_type): + self.entity = mcrfpy.Entity(x, y, 10 + item_type) + self.entity.sprite.texture = item_texture + self.type = item_type # 0=potion, 1=key, 2=coin, etc + self.pickupable = True + + def on_pickup(self, player): + # Add to inventory + player.inventory.append(self.type) + + # Remove from grid + if self.entity.grid: + self.entity.grid.entities.remove(self.entity) + +# Check for item pickup +def check_pickup(player): + items_here = entities_at(game_grid, player.x, player.y) + for entity in items_here: + # Find corresponding Item object + for item in all_items: + if item.entity == entity and item.pickupable: + item.on_pickup(player) +``` + +--- + +## Entity-Grid Relationship + +### Lifecycle + +``` +1. Entity created (not on grid) + entity.grid == None + +2. Entity added to grid + grid.entities.append(entity) + → entity.grid = grid + +3. Entity removed from grid + grid.entities.remove(entity) + → entity.grid = None + +4. Entity destroyed + Python GC handles cleanup +``` + +**Important:** Entities can only be on 0 or 1 grids at a time. + +### Multiple Grids + +```python +# Move entity between grids +def transfer_entity(entity, from_grid, to_grid, new_x, new_y): + # Remove from old grid + if entity.grid: + entity.grid.entities.remove(entity) + + # Update position + entity.pos = (new_x, new_y) + + # Add to new grid + to_grid.entities.append(entity) +``` + +--- + +## Performance Considerations + +### Current Limitations + +**Entity Iteration:** O(n) for spatial queries +- Finding entities at position: Must check all entities +- Finding entities in radius: Must check all entities +- Bottleneck: 500+ entities + +**Workarounds:** +- Limit entity count (< 200 for best performance) +- Update entities in batches +- Use timer callbacks instead of per-frame updates + +### Planned Optimizations + +**[#115](../../issues/115): SpatialHash** +- O(1) entity lookup by position +- O(k) radius queries (k = nearby entities) +- Expected: 100x+ improvement + +**[#117](../../issues/117): Memory Pool** +- Reuse entity objects instead of allocate/destroy +- Reduced memory fragmentation +- Faster spawning + +**[#116](../../issues/116): Dirty Flags** +- Only re-render grid when entities move +- Significant improvement for static entities + +--- + +## Integration with Other Systems + +### With Pathfinding + +See [[AI-and-Pathfinding]] for: +- `entity.path_to(x, y)` - A* pathfinding +- Dijkstra maps for AI +- Path following + +### With FOV + +See [[AI-and-Pathfinding]] for: +- Per-entity field of view +- `grid.perspective` property +- Fog of war + +### With Animation + +See [[Animation-System]] for: +- Smooth entity movement +- Sprite animation +- Visual effects + +```python +# Animate entity movement +mcrfpy.animate(entity, "x", target_x, 300, "ease_out_quad") +mcrfpy.animate(entity, "y", target_y, 300, "ease_out_quad") +``` + +--- + +## Related Documentation + +- [[Grid-System]] - Entity spatial container +- [[AI-and-Pathfinding]] - Entity AI and pathfinding +- [[Animation-System]] - Animating entity movement +- [[UI-Component-Hierarchy]] - Entity as UIDrawable + +**Tutorial:** [McRogueFace Does The Entire Roguelike Tutorial](../../roguelike_tutorial/) - Parts 2-6 cover entity management + +**Open Issues:** +- [#30](../../issues/30) - Entity.die() method +- [#115](../../issues/115) - SpatialHash for fast queries +- [#117](../../issues/117) - Memory pool for entities \ No newline at end of file