1 Grid-Entity-Lifecycle
John McCardle edited this page 2025-10-25 22:51:08 +00:00

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:

Key Files:

  • src/UIEntity.h - Entity class definition
  • src/UIEntity.cpp - Entity implementation
  • src/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: 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:

# 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:

  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:

# 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:

  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:

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 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: