1 Entity-Management
John McCardle edited this page 2025-10-25 22:39:01 +00:00

Entity Management

Creating, positioning, and managing game entities that live on grids.

Quick Reference

System: Grid-System

Key Types:

  • mcrfpy.Entity - Game entities on grids
  • mcrfpy.Grid - Spatial container for entities
  • grid.entities - EntityCollection

API Reference: mcrfpy.Entity


What are Entities?

Entities are game objects that:

  • Live on a Grid (0 or 1 grid per entity)
  • Have a sprite for rendering
  • Have grid position (integer coordinates)
  • Can have AI, pathfinding, field of view
  • Represent players, enemies, items, etc.

Files:

  • src/UIEntity.h / src/UIEntity.cpp
  • src/UIEntityCollection.h / .cpp

Creating Entities

Basic Entity Creation

import mcrfpy

# Create grid first
grid = mcrfpy.Grid(50, 50, 16, 16)
grid.texture = mcrfpy.createTexture("tiles.png")

# Create entity at position (10, 10) with sprite 0
player = mcrfpy.Entity(10, 10, 0)
player.sprite.texture = mcrfpy.createTexture("entities.png")

# Add entity to grid
grid.entities.append(player)

# Grid is now set automatically
assert player.grid == grid

Entity Properties

# Position (grid coordinates)
player.x = 15
player.y = 20
player.pos = (15, 20)  # Tuple alternative

# Sprite (what to display)
player.sprite_index = 5  # Index in texture
player.sprite.scale_x = 1.5
player.sprite.scale_y = 1.5

# Visibility
player.visible = True
player.sprite.opacity = 0.8

# Grid association
current_grid = player.grid  # Read-only

Entity Lifecycle

Adding to Grid

grid.entities.append(entity)

# Automatically:
# - entity.grid = grid
# - Entity rendered with grid
# - Entity included in spatial queries

Removing from Grid

# Method 1: Direct removal
grid.entities.remove(entity)

# Method 2: Entity.die() (planned #30)
# entity.die()  # Will call grid.entities.remove(entity)

# Automatically:
# - entity.grid = None
# - Entity no longer rendered
# - Entity excluded from queries

Moving Entities

# Method 1: Direct assignment
entity.x = new_x
entity.y = new_y

# Method 2: Tuple assignment
entity.pos = (new_x, new_y)

# Method 3: Animation (smooth movement)
mcrfpy.animate(entity, "x", target_x, 500, "ease_in_out_quad")
mcrfpy.animate(entity, "y", target_y, 500, "ease_in_out_quad")

Collision Detection: Manual - check grid cells before moving


Entity Collections

Iteration

# Iterate all entities on grid
for entity in grid.entities:
    print(f"Entity at ({entity.x}, {entity.y})")

# Count entities
num_entities = len(grid.entities)

# Index access
first_entity = grid.entities[0]
last_entity = grid.entities[-1]

Querying

Current: O(n) iteration - no spatial indexing

# Find entities at position
def entities_at(grid, x, y):
    result = []
    for entity in grid.entities:
        if entity.x == x and entity.y == y:
            result.append(entity)
    return result

# Find entities in radius
def entities_in_radius(grid, center_x, center_y, radius):
    result = []
    for entity in grid.entities:
        dx = entity.x - center_x
        dy = entity.y - center_y
        if (dx*dx + dy*dy) <= radius*radius:
            result.append(entity)
    return result

Optimization: #115 - SpatialHash for O(1) queries


Common Patterns

Pattern 1: Player Entity

import mcrfpy

# Create player
player = mcrfpy.Entity(25, 25, 0)
player.sprite.texture = player_texture
player.sprite_index = 0

# Add to grid
game_grid.entities.append(player)

# Movement with collision
def move_player(dx, dy):
    new_x = player.x + dx
    new_y = player.y + dy
    
    # Check walkable
    if game_grid.walkable((new_x, new_y)):
        # Check for other entities
        if not entities_at(game_grid, new_x, new_y):
            player.pos = (new_x, new_y)
            return True
    return False

# Input handling
def on_keypress(key, pressed):
    if pressed:
        if key == mcrfpy.Key.Up:
            move_player(0, -1)
        elif key == mcrfpy.Key.Down:
            move_player(0, 1)
        elif key == mcrfpy.Key.Left:
            move_player(-1, 0)
        elif key == mcrfpy.Key.Right:
            move_player(1, 0)

Pattern 2: Enemy Entities

import random

class Enemy:
    def __init__(self, x, y):
        self.entity = mcrfpy.Entity(x, y, 1)
        self.entity.sprite.texture = enemy_texture
        self.health = 100
        self.aggro_range = 10
    
    def update(self, player):
        # Calculate distance to player
        dx = player.x - self.entity.x
        dy = player.y - self.entity.y
        dist = (dx*dx + dy*dy) ** 0.5
        
        if dist < self.aggro_range:
            # Chase player
            self.move_toward(player.x, player.y)
        else:
            # Wander randomly
            self.wander()
    
    def move_toward(self, target_x, target_y):
        # Simple movement (pathfinding in [[AI-and-Pathfinding]])
        if target_x > self.entity.x:
            self.entity.x += 1
        elif target_x < self.entity.x:
            self.entity.x -= 1
        elif target_y > self.entity.y:
            self.entity.y += 1
        elif target_y < self.entity.y:
            self.entity.y -= 1
    
    def wander(self):
        # Random movement
        dx = random.choice([-1, 0, 1])
        dy = random.choice([-1, 0, 1])
        
        new_x = self.entity.x + dx
        new_y = self.entity.y + dy
        
        if game_grid.walkable((new_x, new_y)):
            self.entity.pos = (new_x, new_y)

# Spawn enemies
enemies = []
for i in range(10):
    enemy = Enemy(random.randint(0, 49), random.randint(0, 49))
    game_grid.entities.append(enemy.entity)
    enemies.append(enemy)

# Update all enemies
def update_enemies():
    for enemy in enemies:
        enemy.update(player)

mcrfpy.setTimer("enemy_ai", lambda ms: update_enemies(), 500)  # Every 0.5s

Pattern 3: Item Entities

class Item:
    def __init__(self, x, y, item_type):
        self.entity = mcrfpy.Entity(x, y, 10 + item_type)
        self.entity.sprite.texture = item_texture
        self.type = item_type  # 0=potion, 1=key, 2=coin, etc
        self.pickupable = True
    
    def on_pickup(self, player):
        # Add to inventory
        player.inventory.append(self.type)
        
        # Remove from grid
        if self.entity.grid:
            self.entity.grid.entities.remove(self.entity)

# Check for item pickup
def check_pickup(player):
    items_here = entities_at(game_grid, player.x, player.y)
    for entity in items_here:
        # Find corresponding Item object
        for item in all_items:
            if item.entity == entity and item.pickupable:
                item.on_pickup(player)

Entity-Grid Relationship

Lifecycle

1. Entity created (not on grid)
   entity.grid == None

2. Entity added to grid
   grid.entities.append(entity)
   → entity.grid = grid

3. Entity removed from grid
   grid.entities.remove(entity)
   → entity.grid = None

4. Entity destroyed
   Python GC handles cleanup

Important: Entities can only be on 0 or 1 grids at a time.

Multiple Grids

# Move entity between grids
def transfer_entity(entity, from_grid, to_grid, new_x, new_y):
    # Remove from old grid
    if entity.grid:
        entity.grid.entities.remove(entity)
    
    # Update position
    entity.pos = (new_x, new_y)
    
    # Add to new grid
    to_grid.entities.append(entity)

Performance Considerations

Current Limitations

Entity Iteration: O(n) for spatial queries

  • Finding entities at position: Must check all entities
  • Finding entities in radius: Must check all entities
  • Bottleneck: 500+ entities

Workarounds:

  • Limit entity count (< 200 for best performance)
  • Update entities in batches
  • Use timer callbacks instead of per-frame updates

Planned Optimizations

#115: SpatialHash

  • O(1) entity lookup by position
  • O(k) radius queries (k = nearby entities)
  • Expected: 100x+ improvement

#117: Memory Pool

  • Reuse entity objects instead of allocate/destroy
  • Reduced memory fragmentation
  • Faster spawning

#116: Dirty Flags

  • Only re-render grid when entities move
  • Significant improvement for static entities

Integration with Other Systems

With Pathfinding

See AI-and-Pathfinding for:

  • entity.path_to(x, y) - A* pathfinding
  • Dijkstra maps for AI
  • Path following

With FOV

See AI-and-Pathfinding for:

  • Per-entity field of view
  • grid.perspective property
  • Fog of war

With Animation

See Animation-System for:

  • Smooth entity movement
  • Sprite animation
  • Visual effects
# Animate entity movement
mcrfpy.animate(entity, "x", target_x, 300, "ease_out_quad")
mcrfpy.animate(entity, "y", target_y, 300, "ease_out_quad")

Tutorial: McRogueFace Does The Entire Roguelike Tutorial - Parts 2-6 cover entity management

Open Issues:

  • #30 - Entity.die() method
  • #115 - SpatialHash for fast queries
  • #117 - Memory pool for entities