Table of Contents
- Entity Management
- Entity Management
- Quick Reference
- What Are Entities?
- Entity-Grid Relationship
- Entity Properties
- Field of View & Visibility
- FOV Configuration
- Updating Visibility
- Querying Visible Entities
- Fog of War with ColorLayers
- One-Time FOV Draw
- Gridstate Access
- GridPointState.point - Accessing Cell Data (#16)
- EntityCollection
- Entity Lifecycle
- Common Patterns
- Pathfinding
- Performance Considerations
- Related Systems
Entity Management
Last modified: 2025-12-01
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/.cpp
Related Issues:
What Are Entities?
Entities are game objects that:
- Live on a Grid (0 or 1 grid at a time)
- Have a sprite for visual rendering
- Have grid position (integer cell coordinates)
- Implement behavior (movement, AI, combat, inventory)
- Track visibility (which cells they can see / have seen)
Key distinction: Entities implement behavior. Grids mediate interaction between entities and render them to screen.
Entity-Grid Relationship
The Entity-Grid relationship mirrors the UIDrawable parent-child pattern:
| Relationship | Property | Automatic Behavior |
|---|---|---|
| Entity → Grid | entity.grid |
Set when added to grid.entities |
| Grid → Entities | grid.entities |
Collection of all entities on grid |
import mcrfpy
# Create grid and entity
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(400, 400))
player = mcrfpy.Entity(pos=(10, 10), sprite_index=0)
# 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 == grid) # 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.
Entity Properties
Position
# Grid coordinates (integer cells)
entity.x = 15
entity.y = 20
entity.pos = (15, 20) # Tuple form
# Draw position (float, for animation interpolation)
print(entity.draw_pos) # Actual render position
Sprite
entity.sprite_index = 5 # Index in texture sprite sheet
Visibility
entity.visible = True
entity.opacity = 0.8 # 0.0 to 1.0
Grid Reference
current_grid = entity.grid # Read-only, set by collection operations
Field of View & Visibility
Entities track what they can see via gridstate - a per-cell record of visible and discovered states.
FOV Configuration
# Grid-level FOV settings
grid.fov = mcrfpy.FOV.SHADOW # Algorithm (BASIC, DIAMOND, SHADOW, etc.)
grid.fov_radius = 10 # Default view radius
# Module-level default
mcrfpy.default_fov = mcrfpy.FOV.PERMISSIVE_2
Updating Visibility
# Compute FOV from entity's position and update gridstate
entity.update_visibility()
# This also updates any ColorLayers bound via apply_perspective()
Querying Visible Entities
# Get list of other entities this entity can see
visible_enemies = entity.visible_entities()
# With custom FOV settings
nearby = entity.visible_entities(radius=5)
visible = entity.visible_entities(fov=mcrfpy.FOV.BASIC, radius=8)
Fog of War with ColorLayers
# Create a ColorLayer for fog of war
fov_layer = grid.add_layer('color', z_index=-1)
fov_layer.fill((0, 0, 0, 255)) # Start black (unknown)
# Bind to entity - layer auto-updates when entity.update_visibility() is called
fov_layer.apply_perspective(
entity=player,
visible=(0, 0, 0, 0), # Transparent when visible
discovered=(40, 40, 60, 180), # Dark overlay when discovered
unknown=(0, 0, 0, 255) # Black when never seen
)
# Now whenever player moves:
player.x = new_x
player.y = new_y
player.update_visibility() # Automatically updates the fog layer
One-Time FOV Draw
# Draw FOV without binding (useful for previews, spell ranges, etc.)
fov_layer.draw_fov(
source=(player.x, player.y),
radius=10,
fov=mcrfpy.FOV.SHADOW,
visible=(255, 255, 200, 64),
discovered=(100, 100, 100, 128),
unknown=(0, 0, 0, 255)
)
Gridstate Access
# Entity's per-cell visibility memory
for state in entity.gridstate:
print(f"visible={state.visible}, discovered={state.discovered}")
# Access specific cell state
state = entity.at((x, y))
if state.visible:
print("Entity can currently see this cell")
elif state.discovered:
print("Entity has seen this cell before")
GridPointState.point - Accessing Cell Data (#16)
The GridPointState.point property provides access to the underlying GridPoint from an entity's perspective:
state = entity.at((x, y))
# If entity has NOT discovered this cell, point is None
if not state.discovered:
print(state.point) # None - entity doesn't know what's here
# If entity HAS discovered the cell, point gives access to GridPoint
if state.discovered:
point = state.point # Live reference to GridPoint
print(f"walkable: {point.walkable}")
print(f"transparent: {point.transparent}")
print(f"entities here: {point.entities}") # List of entities at cell
Key behaviors:
- Returns
Noneifdiscovered=False(entity has never seen this cell) - Returns live
GridPointreference ifdiscovered=True - Changes to the
GridPointare immediately visible throughstate.point - This is intentionally not a cached copy - for historical memory, implement your own system in Python
Use case - Entity perspective queries:
def can_entity_see_walkable_path(entity, x, y):
"""Check if entity knows this cell is walkable."""
state = entity.at((x, y))
if state.point is None:
return None # Unknown - entity hasn't discovered it
return state.point.walkable
def get_known_entities_at(entity, x, y):
"""Get entities at cell if entity has discovered it."""
state = entity.at((x, y))
if state.point is None:
return [] # Entity doesn't know this cell
return state.point.entities
Ground truth access:
If you need the actual cell data regardless of entity perspective, access it through the grid directly:
# Entity perspective (respects discovered state)
state = entity.at((x, y))
point_or_none = state.point
# Ground truth (always returns GridPoint)
point = entity.grid.at(x, y)
EntityCollection
grid.entities is an EntityCollection with list-like operations:
# Add entities
grid.entities.append(entity)
grid.entities.extend([entity1, entity2, entity3])
grid.entities.insert(0, entity) # Insert at index
# Remove entities
grid.entities.remove(entity)
entity = grid.entities.pop() # Remove and return last
entity = grid.entities.pop(0) # Remove and return at index
# Query
count = len(grid.entities)
idx = grid.entities.index(entity)
n = grid.entities.count(entity)
found = grid.entities.find("entity_name") # Find by name
# Iteration
for entity in grid.entities:
print(entity.pos)
Entity Lifecycle
Creation
# Basic creation
entity = mcrfpy.Entity(pos=(10, 10), sprite_index=0)
# With name for later lookup
entity = mcrfpy.Entity(pos=(10, 10), sprite_index=0, name="player")
Adding to Grid
grid.entities.append(entity)
# entity.grid is now set to grid
# Entity will be rendered with the grid
Movement
# Direct position change
entity.pos = (new_x, new_y)
# Animated movement
mcrfpy.Animation("x", target_x, 0.3, "easeOutQuad").start(entity)
mcrfpy.Animation("y", target_y, 0.3, "easeOutQuad").start(entity)
# Update visibility after movement
entity.update_visibility()
Removal
# Method 1: Remove from collection
grid.entities.remove(entity)
# Method 2: Entity.die() - removes from parent grid
entity.die()
# After removal: entity.grid is None
Transfer Between Grids
def transfer_entity(entity, to_grid, new_pos):
"""Move entity to a different grid."""
entity.die() # Remove from current grid
entity.pos = new_pos
to_grid.entities.append(entity)
Common Patterns
Player Entity with FOV
class Player:
def __init__(self, grid, start_pos):
self.entity = mcrfpy.Entity(pos=start_pos, sprite_index=0, name="player")
grid.entities.append(self.entity)
# Set up fog of war
self.fov_layer = grid.add_layer('color', z_index=-1)
self.fov_layer.fill((0, 0, 0, 255))
self.fov_layer.apply_perspective(
entity=self.entity,
visible=(0, 0, 0, 0),
discovered=(30, 30, 50, 180),
unknown=(0, 0, 0, 255)
)
self.entity.update_visibility()
def move(self, dx, dy):
new_x = self.entity.x + dx
new_y = self.entity.y + dy
point = self.entity.grid.at(new_x, new_y)
if point and point.walkable:
self.entity.pos = (new_x, new_y)
self.entity.update_visibility() # Update FOV after move
return True
return False
def get_visible_enemies(self):
"""Get enemies this player can currently see."""
return [e for e in self.entity.visible_entities()
if e.name and e.name.startswith("enemy")]
Enemy Entity
class Enemy:
def __init__(self, grid, pos, aggro_range=10):
self.entity = mcrfpy.Entity(pos=pos, sprite_index=1, name="enemy")
self.aggro_range = aggro_range
self.health = 100
grid.entities.append(self.entity)
def update(self, player_pos):
dx = player_pos[0] - self.entity.x
dy = player_pos[1] - self.entity.y
dist = (dx*dx + dy*dy) ** 0.5
if dist < self.aggro_range:
self.chase(player_pos)
else:
self.wander()
def chase(self, target):
# Use pathfinding
path = self.entity.path_to(target)
if path and len(path) > 1:
next_cell = path[1] # path[0] is current position
self.entity.pos = next_cell
def wander(self):
import random
dx = random.choice([-1, 0, 1])
dy = random.choice([-1, 0, 1])
new_pos = (self.entity.x + dx, self.entity.y + dy)
point = self.entity.grid.at(*new_pos)
if point and point.walkable:
self.entity.pos = new_pos
Item Entity
class Item:
def __init__(self, grid, pos, item_type):
self.entity = mcrfpy.Entity(pos=pos, sprite_index=10 + item_type)
self.item_type = item_type
grid.entities.append(self.entity)
def pickup(self, collector):
"""Called when another entity picks up this item."""
collector.inventory.append(self.item_type)
self.entity.die() # Remove from grid
For more interaction patterns (click handling, selection, context menus), see Grid-Interaction-Patterns.
Pathfinding
Entities have built-in pathfinding via libtcod:
# A* pathfinding to target
path = entity.path_to((target_x, target_y))
# Returns list of (x, y) tuples, or empty if no path
if path:
next_step = path[1] # path[0] is current position
entity.pos = next_step
Pathfinding respects GridPoint.walkable properties set on the grid.
Performance Considerations
Current: Entity queries are O(n):
# Finding entities at position requires iteration
def entities_at(grid, x, y):
return [e for e in grid.entities if e.x == x and e.y == y]
New in v1.0: Use GridPoint.entities for cell-based queries:
# O(n) but more convenient - filters grid.entities by position
entities_here = grid.at(x, y).entities
Workarounds:
- Keep entity counts reasonable (< 200 for best performance)
- Use timer callbacks for AI updates, not per-frame
- Cache query results when possible
Future: #115 SpatialHash will provide O(1) position queries.
See Performance-and-Profiling for optimization guidance.
Related Systems
- Grid-System - Spatial container for entities
- Grid-Interaction-Patterns - Click handling, selection, context menus
- Animation-System - Smooth entity movement
- Performance-and-Profiling - Entity performance metrics
Last updated: 2025-12-01