diff --git a/Grid-Entity-Lifecycle.-.md b/Grid-Entity-Lifecycle.-.md new file mode 100644 index 0000000..15e5bec --- /dev/null +++ b/Grid-Entity-Lifecycle.-.md @@ -0,0 +1,632 @@ +# Grid-Entity Lifecycle + +## Overview + +Entities in McRogueFace have a lifecycle tied to the grids they inhabit. Understanding this lifecycle is critical for proper memory management, collision detection, and rendering. + +**Parent Page:** [[Grid-System]] + +**Related Pages:** +- [[Entity-Management]] - Entity creation and properties +- [[Grid-Rendering-Pipeline]] - How entities are rendered within grids +- [[UI-Component-Hierarchy]] - Entity as UIDrawable + +**Key Files:** +- `src/UIEntity.h` - Entity class definition +- `src/UIEntity.cpp` - Entity implementation +- `src/UIGrid.h` - Grid's entity collection + +**Related Issues:** +- [#30](../../issues/30) - Entity.die() cleanup (completed) +- [#115](../../issues/115) - SpatialHash for efficient entity queries +- [#117](../../issues/117) - Memory pool for entities + +--- + +## Entity Lifecycle States + +An entity progresses through these states: + +``` +1. Created (entity.grid == None) + ↓ +2. Added to Grid (entity.grid = grid) + ↓ +3. Active (updates, renders, collides) + ↓ +4. Removed from Grid (entity.grid = None) + ↓ +5. Python GC Cleanup (shared_ptr refcount → 0) +``` + +--- + +## State 1: Entity Creation + +### Creating an Entity + +Entities are created without a grid: + +```python +import mcrfpy + +# Create entity with position and sprite +player = mcrfpy.Entity( + grid_pos=(10, 15), # Initial position + sprite_index=1 # Sprite from texture +) + +# At this point: +# - player.grid is None +# - Entity exists in Python but is not rendered +# - Entity has no collision detection +``` + +### Entity Constructor Parameters + +```python +# Full constructor +entity = mcrfpy.Entity( + grid_pos=(x, y), # Grid coordinates (int, int) + sprite_index=5, # Sprite index from texture sheet + size=(16, 16) # Optional: sprite size override +) +``` + +**Default values:** +- `sprite_index`: 0 +- `size`: Inherited from texture or (16, 16) + +--- + +## State 2: Adding to Grid + +### The Grid Property + +Entities track which grid they belong to via the `entity.grid` property: + +```python +# Check if entity is on a grid +if player.grid is None: + print("Player is not on any grid") +else: + print(f"Player is on grid: {player.grid}") +``` + +### Adding Entity to Grid + +```python +# Create grid and entity +grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600)) +player = mcrfpy.Entity(grid_pos=(25, 25), sprite_index=1) + +# Add entity to grid +grid.entities.append(player) + +# Now: +# - player.grid == grid (automatically set) +# - Entity will render with grid +# - Entity participates in collision detection +``` + +**Key Insight:** `grid.entities.append()` automatically sets `entity.grid` to point back to the grid. + +### Multiple Entities + +```python +# Add many entities +enemies = [] +for i in range(10): + enemy = mcrfpy.Entity( + grid_pos=(random.randint(0, 49), random.randint(0, 49)), + sprite_index=random.randint(10, 15) + ) + grid.entities.append(enemy) + enemies.append(enemy) + +# All enemies now have enemy.grid == grid +``` + +--- + +## State 3: Active Entity + +### What Happens When Entity is on Grid + +Once added to a grid, the entity: + +1. **Renders** during grid render pass (Stage 3 of [[Grid-Rendering-Pipeline]]) +2. **Updates** position and velocity every frame +3. **Collides** with grid cells and other entities (if collision enabled) +4. **Can be queried** via grid spatial queries + +### Entity Position + +Entities have **floating-point positions** allowing smooth sub-cell movement: + +```python +# Fractional positions +player.x = 10.5 # Halfway between cells 10 and 11 +player.y = 15.75 + +# Position is independent of grid coordinates +# grid_pos property rounds to nearest cell +print(player.grid_pos) # (11, 16) - rounded for convenience +``` + +### Entity Velocity + +Entities have built-in velocity: + +```python +# Set velocity +player.velocity_x = 0.1 # Move right 0.1 cells per frame +player.velocity_y = 0.0 + +# Velocity is applied automatically during render +# (position updated by engine, not Python) +``` + +### Collision with Grid + +Entities can detect grid cell properties: + +```python +# Check if entity is on walkable cell +cell = grid.at((int(player.x), int(player.y))) +if not cell.walkable: + print("Player is on non-walkable terrain!") + # Push player back or handle collision +``` + +### Collision with Other Entities + +Check for entity collisions: + +```python +def check_collision(entity1, entity2, radius=0.5): + """Check if two entities are within radius of each other""" + dx = entity1.x - entity2.x + dy = entity1.y - entity2.y + distance = (dx*dx + dy*dy) ** 0.5 + return distance < radius + +# Check player collision with all enemies +for enemy in enemies: + if check_collision(player, enemy): + print("Player hit enemy!") + handle_combat(player, enemy) +``` + +**Performance Note:** O(n²) collision detection. See **#115** for spatial hash optimization. + +--- + +## State 4: Removing from Grid + +### Method 1: Remove via Collection + +```python +# Remove entity from grid +grid.entities.remove(player) + +# Now: +# - player.grid is None (automatically cleared) +# - Entity no longer renders +# - Entity no longer participates in collision +# - Python object still exists (can re-add later) +``` + +### Method 2: Clear Grid Property + +```python +# Alternative: clear grid property directly +player.grid = None + +# Entity is removed from grid's entity collection +# (This happens automatically when grid property is cleared) +``` + +### Entity.die() Method + +**Issue #30** added `Entity.die()` for safe cleanup: + +```python +# Proper entity cleanup +player.die() + +# This: +# 1. Removes entity from grid (if on one) +# 2. Clears entity.grid property +# 3. Marks entity as "dead" (dead=True) +# 4. Stops all animations on entity +``` + +**Best Practice:** Always use `entity.die()` instead of manual removal. + +### Dead Entities + +After `die()` is called: + +```python +if entity.dead: + print("Entity is dead, don't update") + +# Dead entities should not be updated or interacted with +# Remove dead entities from your tracking lists +enemies = [e for e in enemies if not e.dead] +``` + +--- + +## State 5: Python GC Cleanup + +### Shared Pointer Lifecycle + +Entities use `shared_ptr` in C++: + +```python +# Create entity +entity = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=1) +# C++ shared_ptr refcount: 1 (Python owns it) + +grid.entities.append(entity) +# C++ shared_ptr refcount: 2 (Python + grid's list) + +entity = None # Drop Python reference +# C++ shared_ptr refcount: 1 (grid still owns it) + +grid.entities.remove(entity) +# C++ shared_ptr refcount: 0 → C++ object deleted +``` + +### Memory Leaks to Avoid + +**Leak Pattern 1: Circular References** + +```python +# BAD: Entity stores reference to itself +entity.self_ref = entity + +# Even after removing from grid, refcount never reaches 0 +grid.entities.remove(entity) +# Entity leaks! + +# FIX: Don't store circular references +``` + +**Leak Pattern 2: Forgotten Entity Lists** + +```python +# BAD: Keep entity in tracking list after death +enemies = [enemy1, enemy2, enemy3] +enemy1.die() + +# enemy1 removed from grid, but enemies list still holds reference +# Entity leaks! + +# FIX: Clean up tracking lists +enemies = [e for e in enemies if not e.dead] +``` + +### Weak References + +For observer patterns, use weak references: + +```python +# Can't use weakref directly on mcrfpy.Entity currently +# Workaround: Use entity IDs + +entity_id = id(entity) +entity_map = {entity_id: entity} + +# Later, check if entity still exists +if entity_id in entity_map and not entity_map[entity_id].dead: + entity_map[entity_id].update() +``` + +--- + +## Common Patterns + +### Spawning Enemies + +```python +def spawn_enemy(grid, x, y, sprite_index): + """Spawn enemy at grid position""" + enemy = mcrfpy.Entity(grid_pos=(x, y), sprite_index=sprite_index) + grid.entities.append(enemy) + + # Set AI behavior + enemy.velocity_x = random.uniform(-0.1, 0.1) + enemy.velocity_y = random.uniform(-0.1, 0.1) + + # Track enemy for updates + enemies.append(enemy) + + return enemy + +# Spawn 10 enemies +for i in range(10): + spawn_enemy(grid, random.randint(0, 49), random.randint(0, 49), + sprite_index=random.randint(10, 15)) +``` + +### Entity Death and Cleanup + +```python +def kill_entity(entity): + """Kill entity and play death animation""" + if entity.dead: + return # Already dead + + # Play death animation + mcrfpy.animate(entity, "opacity", 0, duration=500, + easing="ease_out_cubic") + + # Mark as dead + entity.die() + + # Schedule cleanup after animation + def cleanup(ms): + # Remove from tracking + if entity in enemies: + enemies.remove(entity) + + mcrfpy.setTimer(f"cleanup_{id(entity)}", cleanup, 500) + +# Kill all enemies +for enemy in enemies[:]: # Copy list to avoid modification during iteration + kill_entity(enemy) +``` + +### Moving Between Grids + +```python +def move_to_grid(entity, from_grid, to_grid, new_x, new_y): + """Move entity from one grid to another""" + # Remove from old grid + if entity.grid == from_grid: + from_grid.entities.remove(entity) + + # Update position + entity.x = new_x + entity.y = new_y + + # Add to new grid + to_grid.entities.append(entity) + + # entity.grid now points to to_grid + +# Example: player enters new room +move_to_grid(player, current_room.grid, next_room.grid, 5, 5) +``` + +### Entity Pooling (Future Optimization) + +**Issue #117** proposes entity memory pooling: + +```python +# Proposed pattern (not yet implemented) +class EntityPool: + def __init__(self, grid, sprite_index, pool_size=100): + self.pool = [] + self.grid = grid + self.sprite_index = sprite_index + + # Pre-allocate entities + for _ in range(pool_size): + e = mcrfpy.Entity(grid_pos=(0, 0), sprite_index=sprite_index) + e.active = False + self.pool.append(e) + + def spawn(self, x, y): + """Get entity from pool""" + for entity in self.pool: + if not entity.active: + entity.x = x + entity.y = y + entity.active = True + self.grid.entities.append(entity) + return entity + return None # Pool exhausted + + def despawn(self, entity): + """Return entity to pool""" + entity.active = False + self.grid.entities.remove(entity) + +# Use pool +enemy_pool = EntityPool(grid, sprite_index=10, pool_size=50) +enemy = enemy_pool.spawn(20, 20) +# ... later ... +enemy_pool.despawn(enemy) +``` + +--- + +## Performance Considerations + +### Entity Collection Iteration + +Grid entity collection is a `std::list>`: + +```python +# Iteration is O(n) +for entity in grid.entities: + entity.update() + +# Removal during iteration is safe (list allows it) +for entity in grid.entities: + if entity.health <= 0: + entity.die() # Safe to remove during iteration +``` + +**Performance:** +- Iteration: O(n) +- Append: O(1) +- Remove: O(n) - must search list +- Count: O(n) - must traverse list + +### Spatial Queries (Current: O(n)) + +```python +# Find entities near position - O(n) +def find_entities_near(grid, x, y, radius): + nearby = [] + for entity in grid.entities: + dx = entity.x - x + dy = entity.y - y + if dx*dx + dy*dy <= radius*radius: + nearby.append(entity) + return nearby + +# Very slow for 1000+ entities! +``` + +**Future Optimization:** Issue #115 proposes SpatialHash for O(1) spatial queries. + +### Entity Update Patterns + +**Pattern 1: Update All Entities** + +```python +# Update every entity every frame - expensive! +def update_all_entities(ms): + for entity in grid.entities: + entity.ai_update() + entity.check_collision() + +mcrfpy.setTimer("entity_update", update_all_entities, 16) # 60 FPS +``` + +**Pattern 2: Update Only Active Entities** + +```python +# Track only active entities +active_entities = [] + +def update_active_entities(ms): + for entity in active_entities: + entity.ai_update() + + # Remove dead entities + active_entities[:] = [e for e in active_entities if not e.dead] + +mcrfpy.setTimer("entity_update", update_active_entities, 16) +``` + +**Pattern 3: Update Based on Distance** + +```python +# Only update entities near player +def update_nearby_entities(ms): + for entity in grid.entities: + distance = abs(entity.x - player.x) + abs(entity.y - player.y) + if distance < 20: # Within 20 cells + entity.ai_update() + +mcrfpy.setTimer("entity_update", update_nearby_entities, 16) +``` + +--- + +## Troubleshooting + +### Issue: Entity Doesn't Render + +**Causes:** +1. Entity not added to grid +2. Entity position out of viewport bounds +3. Entity opacity = 0 +4. Grid not added to scene UI + +**Debug:** +```python +if entity.grid is None: + print("Entity not on grid!") +else: + print(f"Entity on grid at ({entity.x}, {entity.y})") + print(f"Entity opacity: {entity.opacity}") + print(f"Grid in scene UI: {grid in mcrfpy.sceneUI('game')}") +``` + +### Issue: Entity Collision Not Working + +**Cause:** Forgot to check `entity.grid` before collision test. + +**Fix:** +```python +def check_collision(e1, e2): + # Ensure both entities are on same grid + if e1.grid != e2.grid: + return False + + # ... collision logic ... +``` + +### Issue: Entity Memory Leak + +**Cause:** Entity still referenced in Python list after death. + +**Debug:** +```python +import sys + +# Check refcount +print(f"Entity refcount: {sys.getrefcount(entity)}") +# Should be 2 (Python + grid) if only on grid + +entity.die() +print(f"After die: {sys.getrefcount(entity)}") +# Should be 1 (just Python variable) + +# Find references +import gc +print(gc.get_referrers(entity)) +``` + +### Issue: Entities Flicker When Moving + +**Cause:** Position updated during render, causing frame-to-frame inconsistency. + +**Fix:** Update positions in timer, not during render: +```python +def update_positions(ms): + for entity in entities: + entity.x += entity.velocity_x + entity.y += entity.velocity_y + +mcrfpy.setTimer("position_update", update_positions, 16) +``` + +--- + +## API Reference + +See [`docs/api_reference_dynamic.html`](../../src/branch/master/docs/api_reference_dynamic.html) for complete Entity API. + +**Entity Constructor:** +- `mcrfpy.Entity(grid_pos=(x, y), sprite_index=i, size=(w, h))` → Entity + +**Entity Properties:** +- `entity.grid` - Grid entity belongs to (None if not on grid) +- `entity.x`, `entity.y` - Floating-point position +- `entity.velocity_x`, `entity.velocity_y` - Velocity (cells per frame) +- `entity.dead` - Boolean, True after die() called + +**Entity Methods:** +- `entity.die()` - Remove from grid and mark as dead + +**Grid Entity Collection:** +- `grid.entities` - List-like collection of entities +- `grid.entities.append(entity)` - Add entity to grid +- `grid.entities.remove(entity)` - Remove entity from grid + +--- + +**Navigation:** +- [[Grid-System]] - Parent page +- [[Entity-Management]] - Entity creation and properties +- [[Grid-Rendering-Pipeline]] - Entity rendering within grids +- [[AI-and-Pathfinding]] - Entity AI and movement