Table of Contents
- Grid-Entity Lifecycle
- Overview
- Entity Lifecycle States
- State 1: Entity Creation
- State 2: Adding to Grid
- State 3: Active Entity
- What Happens When Entity is on Grid
- Entity Position
- Entity Velocity
- Collision with Grid
- Collision with Other Entities
- State 4: Removing from Grid
- State 5: Python GC Cleanup
- Common Patterns
- Performance Considerations
- Troubleshooting
- Issue: Entity Doesn't Render
- Issue: Entity Collision Not Working
- Issue: Entity Memory Leak
- Issue: Entities Flicker When Moving
- API Reference
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 definitionsrc/UIEntity.cpp- Entity implementationsrc/UIGrid.h- Grid's entity collection
Related Issues:
- #30 - Entity.die() cleanup (completed)
- #115 - SpatialHash for efficient entity queries
- #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:
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
# 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: 0size: 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:
# 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
# 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
# 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:
- Renders during grid render pass (Stage 3 of Grid-Rendering-Pipeline)
- Updates position and velocity every frame
- Collides with grid cells and other entities (if collision enabled)
- Can be queried via grid spatial queries
Entity Position
Entities have floating-point positions allowing smooth sub-cell movement:
# 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:
# 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:
# 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:
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
# 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
# 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:
# 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:
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++:
# 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
# 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
# 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:
# 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
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
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
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:
# 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>>:
# 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))
# 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
# 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
# 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
# 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:
- Entity not added to grid
- Entity position out of viewport bounds
- Entity opacity = 0
- Grid not added to scene UI
Debug:
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:
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:
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:
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 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 positionentity.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 entitiesgrid.entities.append(entity)- Add entity to gridgrid.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