Add "Grid-Entity-Lifecycle"

John McCardle 2025-10-25 22:51:08 +00:00
parent 36e2cb9ce1
commit a0eb64142c
1 changed files with 632 additions and 0 deletions

632
Grid-Entity-Lifecycle.-.md Normal file

@ -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<std::shared_ptr<UIEntity>>`:
```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