Entity Management
Entities are game objects that implement behavior and live on Grids. While Grids handle rendering and mediate interactions, Entities encapsulate game logic like movement, combat, and AI.
Quick Reference
Parent System: Grid-System
Key Types:
mcrfpy.Entity- Game entities on gridsmcrfpy.Grid- Spatial container for entitiesmcrfpy.EntityCollection- Collection of entities on a grid
Key Files:
src/UIEntity.h/src/UIEntity.cppsrc/UIEntityCollection.h/.cppsrc/SpatialHash.h/src/SpatialHash.cpp- Spatial indexing
Related Issues:
- #115 - SpatialHash for fast queries - Implemented
- #117 - Memory Pool for entities (Deferred)
- #159 - EntityCollection iterator optimization - Fixed
Creating Entities
import mcrfpy
# Basic creation with keyword arguments
entity = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=0)
# With name for lookup
player = mcrfpy.Entity(grid_pos=(5, 5), sprite_index=0, name="player")
# Default (origin, no sprite)
e = mcrfpy.Entity() # grid_pos=(0, 0), sprite_index=0
Entity Properties
| Property | Type | Description |
|---|---|---|
grid_x, grid_y |
float | Grid cell position |
draw_x, draw_y |
float | Visual draw position (for animation) |
sprite_index |
int | Index in texture sprite sheet |
sprite_scale |
float | Scale of the entity sprite |
name |
str | Entity name for lookup |
visible |
bool | Whether entity is rendered |
grid |
Grid or None | Parent grid (read-only, set by collection) |
Entity-Grid Relationship
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(400, 400))
player = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=0, name="player")
# Before adding: entity has no grid
print(player.grid) # None
# Add to grid
grid.entities.append(player)
# After adding: bidirectional link established
print(player.grid is not None) # True
print(player in grid.entities) # True
# Removing breaks the link
grid.entities.remove(player)
print(player.grid) # None
Important: An entity can only be on 0 or 1 grids at a time. Adding to a new grid automatically removes from the old one.
Movement
# Direct position change (updates SpatialHash automatically)
player.grid_x = 15
player.grid_y = 20
# Animated movement (smooth visual transition)
player.animate("x", 15.0, 0.3, mcrfpy.Easing.EASE_OUT_QUAD)
player.animate("y", 20.0, 0.3, mcrfpy.Easing.EASE_OUT_QUAD)
# Move with callback on completion
def on_move_complete(target, prop, value):
target.grid.compute_fov((int(target.grid_x), int(target.grid_y)), radius=8)
player.animate("x", 15.0, 0.3, mcrfpy.Easing.EASE_OUT_QUAD, callback=on_move_complete)
Animatable Entity Properties
| Property | Type | Notes |
|---|---|---|
x, y |
float | Alias for draw position |
draw_x, draw_y |
float | Visual position in tile coords |
sprite_index |
int | Can animate through sprite frames |
sprite_scale |
float | Scale animation |
Spatial Queries with SpatialHash
Grids use SpatialHash for efficient spatial queries with O(k) time complexity:
entities_in_radius()
# Query entities within a radius
nearby = grid.entities_in_radius((10, 10), 5.0)
for entity in nearby:
print(f"{entity.name} at ({entity.grid_x}, {entity.grid_y})")
Note: The first argument is a (x, y) tuple, not separate x and y arguments.
Performance
| Entity Count | Linear Scan | SpatialHash | Speedup |
|---|---|---|---|
| 100 | 0.037ms | 0.008ms | 4.6x |
| 1,000 | 0.028ms | 0.004ms | 7.8x |
| 5,000 | 0.109ms | 0.003ms | 37x |
For N x N visibility checks (e.g., "what can everyone see?"):
| Entity Count | Linear | SpatialHash | Speedup |
|---|---|---|---|
| 1,000 | 21ms | 1ms | 35x |
| 5,000 | 431ms | 2ms | 217x |
EntityCollection
grid.entities is an EntityCollection with list-like operations:
# Add entities
grid.entities.append(entity)
grid.entities.extend([entity1, entity2, entity3])
# Remove entities
grid.entities.remove(entity)
# Query
count = len(grid.entities)
idx = grid.entities.index(entity)
# Iteration (O(n) - optimized in #159)
for entity in grid.entities:
print(f"{entity.name}: ({entity.grid_x}, {entity.grid_y})")
Entity Lifecycle
Creation and Placement
player = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=0, name="player")
grid.entities.append(player)
# player.grid is now set
# Entity is added to SpatialHash for fast queries
Removal
# Method 1: Remove from collection
grid.entities.remove(player)
# Method 2: Entity.die() - removes from parent grid and SpatialHash
player.die()
# After removal: player.grid is None
Transfer Between Grids
def transfer_entity(entity, to_grid, new_pos):
entity.die() # Remove from current grid
entity.grid_x = new_pos[0]
entity.grid_y = new_pos[1]
to_grid.entities.append(entity)
FOV and Visibility
Computing FOV
# Set up transparent cells
for x in range(50):
for y in range(50):
grid.at(x, y).transparent = True
# Mark walls
grid.at(5, 5).transparent = False
# Compute FOV from entity position
grid.compute_fov((int(player.grid_x), int(player.grid_y)), radius=10)
# Check if a cell is visible
if grid.is_in_fov((12, 14)):
print("Can see that cell!")
Fog of War with ColorLayer
# Create fog overlay
fog = mcrfpy.ColorLayer(name="fog", z_index=1)
grid.add_layer(fog)
# Initialize to fully dark
fog.fill(mcrfpy.Color(0, 0, 0, 255))
# Update fog based on FOV after each move
def update_fog(player, fog_layer, grid):
grid.compute_fov((int(player.grid_x), int(player.grid_y)), radius=8)
for x in range(grid.grid_w):
for y in range(grid.grid_h):
if grid.is_in_fov((x, y)):
fog_layer.set((x, y), mcrfpy.Color(0, 0, 0, 0)) # Clear
# Previously seen cells stay semi-transparent (don't re-darken)
Perspective System
# Set perspective entity (enables FOV-aware rendering)
grid.perspective = player
# Remove perspective (omniscient view)
grid.perspective = None
Common Patterns
Player Entity with Movement
class Player:
def __init__(self, grid, start_pos):
self.entity = mcrfpy.Entity(
grid_pos=start_pos, sprite_index=0, name="player"
)
grid.entities.append(self.entity)
def move(self, dx, dy):
new_x = int(self.entity.grid_x + dx)
new_y = int(self.entity.grid_y + dy)
point = self.entity.grid.at(new_x, new_y)
if point and point.walkable:
self.entity.animate("x", float(new_x), 0.15, mcrfpy.Easing.EASE_OUT_QUAD)
self.entity.animate("y", float(new_y), 0.15, mcrfpy.Easing.EASE_OUT_QUAD)
self.entity.grid_x = new_x
self.entity.grid_y = new_y
return True
return False
Enemy AI with SpatialHash
class Enemy:
def __init__(self, grid, pos, aggro_range=10):
self.entity = mcrfpy.Entity(
grid_pos=pos, sprite_index=1, name="enemy"
)
self.aggro_range = aggro_range
grid.entities.append(self.entity)
def update(self):
grid = self.entity.grid
# Use SpatialHash for efficient nearby entity detection
nearby = grid.entities_in_radius(
(self.entity.grid_x, self.entity.grid_y),
self.aggro_range
)
# Find player in nearby entities
player = None
for e in nearby:
if e.name == "player":
player = e
break
if player:
self.chase(player)
else:
self.wander()
def chase(self, target):
grid = self.entity.grid
path = grid.find_path(
(int(self.entity.grid_x), int(self.entity.grid_y)),
(int(target.grid_x), int(target.grid_y))
)
if path and len(path) > 0:
next_step = path.walk()
self.entity.grid_x = next_step.x
self.entity.grid_y = next_step.y
def wander(self):
import random
dx = random.choice([-1, 0, 1])
dy = random.choice([-1, 0, 1])
new_x = int(self.entity.grid_x + dx)
new_y = int(self.entity.grid_y + dy)
point = self.entity.grid.at(new_x, new_y)
if point and point.walkable:
self.entity.grid_x = new_x
self.entity.grid_y = new_y
Item Pickup
class Item:
def __init__(self, grid, pos, item_type):
self.entity = mcrfpy.Entity(
grid_pos=pos, sprite_index=10 + item_type, name=f"item_{item_type}"
)
self.item_type = item_type
grid.entities.append(self.entity)
def pickup(self, collector_inventory):
collector_inventory.append(self.item_type)
self.entity.die() # Remove from grid
Pathfinding
Entities use the grid's pathfinding capabilities:
# A* pathfinding
path = grid.find_path(
(int(entity.grid_x), int(entity.grid_y)),
(target_x, target_y)
)
if path and len(path) > 0:
next_step = path.walk() # Get next step as Vector
entity.grid_x = next_step.x
entity.grid_y = next_step.y
# Dijkstra for multi-target pathfinding
dm = grid.get_dijkstra_map((goal_x, goal_y))
distance = dm.distance((entity.grid_x, entity.grid_y))
next_step = dm.step_from((int(entity.grid_x), int(entity.grid_y)))
Pathfinding respects GridPoint.walkable properties.
Performance Considerations
| Operation | Performance | Notes |
|---|---|---|
| Entity Creation | ~90,000/sec | Sufficient for level generation |
| Iteration | ~9M reads/sec | Optimized iterators (#159) |
| Spatial Query | 0.003ms | SpatialHash O(k) (#115) |
| N x N Visibility (5000) | 2ms | 217x faster than O(n) |
Recommendations
- Use
entities_in_radius()for AI - O(k) queries instead of iterating all entities - Batch visibility updates - Compute FOV once after all moves, not per-move
- Use Timer for AI - Don't run expensive logic every frame
- Entity counts up to 5,000+ - SpatialHash makes large counts feasible
Related Systems
- Grid-System - Spatial container for entities
- Grid-Interaction-Patterns - Click handling, selection, context menus
- Animation-System - Smooth entity movement via
.animate() - AI-and-Pathfinding - FOV, pathfinding, AI patterns
- Input-and-Events - Callback signatures for mouse events
Last updated: 2026-02-07