4 Entity Management
John McCardle edited this page 2026-02-07 22:16:21 +00:00

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 grids
  • mcrfpy.Grid - Spatial container for entities
  • mcrfpy.EntityCollection - Collection of entities on a grid

Key Files:

  • src/UIEntity.h / src/UIEntity.cpp
  • src/UIEntityCollection.h / .cpp
  • src/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

  1. Use entities_in_radius() for AI - O(k) queries instead of iterating all entities
  2. Batch visibility updates - Compute FOV once after all moves, not per-move
  3. Use Timer for AI - Don't run expensive logic every frame
  4. Entity counts up to 5,000+ - SpatialHash makes large counts feasible


Last updated: 2026-02-07