Add "Grid-Entity-Lifecycle"
parent
36e2cb9ce1
commit
a0eb64142c
|
|
@ -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
|
||||
Loading…
Reference in New Issue