Update Entity Management wiki with Entity(grid_pos=), .animate(), SpatialHash, current callback signatures, Timer objects

John McCardle 2026-02-07 22:16:21 +00:00
commit b0b18f7a0f

@ -1,538 +1,405 @@
# Entity Management # 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. 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 ## Quick Reference
**Parent System:** [[Grid-System]] **Parent System:** [[Grid-System]]
**Key Types:** **Key Types:**
- `mcrfpy.Entity` - Game entities on grids - `mcrfpy.Entity` - Game entities on grids
- `mcrfpy.Grid` - Spatial container for entities - `mcrfpy.Grid` - Spatial container for entities
- `mcrfpy.EntityCollection` - Collection of entities on a grid - `mcrfpy.EntityCollection` - Collection of entities on a grid
**Key Files:** **Key Files:**
- `src/UIEntity.h` / `src/UIEntity.cpp` - `src/UIEntity.h` / `src/UIEntity.cpp`
- `src/UIEntityCollection.h` / `.cpp` - `src/UIEntityCollection.h` / `.cpp`
- `src/SpatialHash.h` / `src/SpatialHash.cpp` - Spatial indexing - `src/SpatialHash.h` / `src/SpatialHash.cpp` - Spatial indexing
**Related Issues:** **Related Issues:**
- [#115](../issues/115) - SpatialHash for fast queries ✅ Implemented - [#115](../issues/115) - SpatialHash for fast queries - Implemented
- [#117](../issues/117) - Memory Pool for entities (Deferred) - [#117](../issues/117) - Memory Pool for entities (Deferred)
- [#159](../issues/159) - EntityCollection iterator optimization ✅ Fixed - [#159](../issues/159) - EntityCollection iterator optimization - Fixed
--- ---
## What Are Entities? ## Creating Entities
Entities are game objects that: ```python
- **Live on a Grid** (0 or 1 grid at a time) import mcrfpy
- **Have a sprite** for visual rendering
- **Have grid position** (integer cell coordinates) # Basic creation with keyword arguments
- **Implement behavior** (movement, AI, combat, inventory) entity = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=0)
- **Track visibility** (which cells they can see / have seen)
# With name for lookup
**Key distinction:** Entities implement behavior. Grids mediate interaction between entities and render them to screen. 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-Grid Relationship ```
The Entity-Grid relationship mirrors the UIDrawable parent-child pattern: ### Entity Properties
| Relationship | Property | Automatic Behavior | | Property | Type | Description |
|--------------|----------|-------------------| |----------|------|-------------|
| Entity → Grid | `entity.grid` | Set when added to `grid.entities` | | `grid_x`, `grid_y` | float | Grid cell position |
| Grid → Entities | `grid.entities` | Collection of all entities on grid | | `draw_x`, `draw_y` | float | Visual draw position (for animation) |
| `sprite_index` | int | Index in texture sprite sheet |
```python | `sprite_scale` | float | Scale of the entity sprite |
import mcrfpy | `name` | str | Entity name for lookup |
| `visible` | bool | Whether entity is rendered |
# Create grid and entity | `grid` | Grid or None | Parent grid (read-only, set by collection) |
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 ## Entity-Grid Relationship
print(player.grid) # None
```python
# Add to grid grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(400, 400))
grid.entities.append(player) player = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=0, name="player")
# After adding: bidirectional link established # Before adding: entity has no grid
print(player.grid == grid) # True print(player.grid) # None
print(player in grid.entities) # True
# Add to grid
# Removing breaks the link grid.entities.append(player)
grid.entities.remove(player)
print(player.grid) # None # After adding: bidirectional link established
``` print(player.grid is not None) # True
print(player in grid.entities) # True
**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.
# Removing breaks the link
--- grid.entities.remove(player)
print(player.grid) # None
## Entity Properties ```
### Position **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.
```python ---
# Grid coordinates (integer cells)
entity.x = 15 ## Movement
entity.y = 20
entity.pos = (15, 20) # Tuple form ```python
# Direct position change (updates SpatialHash automatically)
# Draw position (float, for animation interpolation) player.grid_x = 15
print(entity.draw_pos) # Actual render position player.grid_y = 20
```
# Animated movement (smooth visual transition)
### Sprite player.animate("x", 15.0, 0.3, mcrfpy.Easing.EASE_OUT_QUAD)
player.animate("y", 20.0, 0.3, mcrfpy.Easing.EASE_OUT_QUAD)
```python
entity.sprite_index = 5 # Index in texture sprite sheet # 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)
### Visibility
player.animate("x", 15.0, 0.3, mcrfpy.Easing.EASE_OUT_QUAD, callback=on_move_complete)
```python ```
entity.visible = True
entity.opacity = 0.8 # 0.0 to 1.0 ### Animatable Entity Properties
```
| Property | Type | Notes |
### Grid Reference |----------|------|-------|
| `x`, `y` | float | Alias for draw position |
```python | `draw_x`, `draw_y` | float | Visual position in tile coords |
current_grid = entity.grid # Read-only, set by collection operations | `sprite_index` | int | Can animate through sprite frames |
``` | `sprite_scale` | float | Scale animation |
--- ---
## Spatial Queries with SpatialHash ## Spatial Queries with SpatialHash
As of commit 7d57ce2, grids use **SpatialHash** for efficient spatial queries. This provides O(k) query time where k is the number of nearby entities, instead of O(n) scanning all entities. Grids use SpatialHash for efficient spatial queries with O(k) time complexity:
### entities_in_radius() ### entities_in_radius()
```python ```python
# Query entities within a radius (uses SpatialHash internally) # Query entities within a radius
nearby = grid.entities_in_radius(x, y, radius) nearby = grid.entities_in_radius((10, 10), 5.0)
# Example: Find all entities within 10 cells of position (50, 50) for entity in nearby:
threats = grid.entities_in_radius(50, 50, 10) print(f"{entity.name} at ({entity.grid_x}, {entity.grid_y})")
for entity in threats: ```
print(f"Entity at ({entity.x}, {entity.y})")
``` **Note:** The first argument is a `(x, y)` tuple, not separate x and y arguments.
### Performance Comparison ### Performance
| Entity Count | O(n) Query | SpatialHash | Speedup | | Entity Count | Linear Scan | SpatialHash | Speedup |
|--------------|------------|-------------|---------| |--------------|-------------|-------------|---------|
| 100 | 0.037ms | 0.008ms | 4.6× | | 100 | 0.037ms | 0.008ms | 4.6x |
| 500 | 0.061ms | 0.009ms | 7.2× | | 1,000 | 0.028ms | 0.004ms | 7.8x |
| 1,000 | 0.028ms | 0.004ms | 7.8× | | 5,000 | 0.109ms | 0.003ms | **37x** |
| 2,000 | 0.043ms | 0.003ms | 13× |
| 5,000 | 0.109ms | 0.003ms | **37×** | For N x N visibility checks (e.g., "what can everyone see?"):
### N×N Visibility (AI "What can everyone see?") | Entity Count | Linear | SpatialHash | Speedup |
|--------------|--------|-------------|---------|
| Entity Count | O(n) approach | SpatialHash | Speedup | | 1,000 | 21ms | 1ms | 35x |
|--------------|---------------|-------------|---------| | 5,000 | 431ms | 2ms | **217x** |
| 1,000 | 21ms | 1ms | 35× |
| 2,000 | 85ms | 1ms | 87× | ---
| 5,000 | 431ms | 2ms | **217×** |
## EntityCollection
### When to Use Which Method
`grid.entities` is an `EntityCollection` with list-like operations:
| Use Case | Method | Complexity |
|----------|--------|------------| ```python
| Nearby entities (AI, combat) | `grid.entities_in_radius(x, y, r)` | O(k) | # Add entities
| FOV-based visibility | `entity.visible_entities()` | O(n) + FOV | grid.entities.append(entity)
| All entities iteration | `for e in grid.entities` | O(n) | grid.entities.extend([entity1, entity2, entity3])
| Single cell lookup | `grid.at(x, y).entities` | O(n) filter |
# Remove entities
--- grid.entities.remove(entity)
## Field of View & Visibility # Query
count = len(grid.entities)
Entities track what they can see via `gridstate` - a per-cell record of visible and discovered states. idx = grid.entities.index(entity)
### FOV Configuration # Iteration (O(n) - optimized in #159)
for entity in grid.entities:
```python print(f"{entity.name}: ({entity.grid_x}, {entity.grid_y})")
# Grid-level FOV settings ```
grid.fov = mcrfpy.FOV.SHADOW # Algorithm (BASIC, DIAMOND, SHADOW, etc.)
grid.fov_radius = 10 # Default view radius ---
# Module-level default ## Entity Lifecycle
mcrfpy.default_fov = mcrfpy.FOV.PERMISSIVE_2
``` ### Creation and Placement
### Updating Visibility ```python
player = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=0, name="player")
```python grid.entities.append(player)
# Compute FOV from entity's position and update gridstate # player.grid is now set
entity.update_visibility() # Entity is added to SpatialHash for fast queries
```
# This also updates any ColorLayers bound via apply_perspective()
``` ### Removal
### Querying Visible Entities ```python
# Method 1: Remove from collection
```python grid.entities.remove(player)
# Get list of other entities this entity can see (uses FOV + line-of-sight)
visible_enemies = entity.visible_entities() # Method 2: Entity.die() - removes from parent grid and SpatialHash
player.die()
# With custom FOV settings
nearby = entity.visible_entities(radius=5) # After removal: player.grid is None
visible = entity.visible_entities(fov=mcrfpy.FOV.BASIC, radius=8) ```
```
### Transfer Between Grids
**Note:** `visible_entities()` checks FOV and line-of-sight. For pure distance queries without FOV, use `grid.entities_in_radius()`.
```python
### Fog of War with ColorLayers def transfer_entity(entity, to_grid, new_pos):
entity.die() # Remove from current grid
```python entity.grid_x = new_pos[0]
# Create a ColorLayer for fog of war entity.grid_y = new_pos[1]
fov_layer = grid.add_layer('color', z_index=-1) to_grid.entities.append(entity)
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, ## FOV and Visibility
visible=(0, 0, 0, 0), # Transparent when visible
discovered=(40, 40, 60, 180), # Dark overlay when discovered ### Computing FOV
unknown=(0, 0, 0, 255) # Black when never seen
) ```python
# Set up transparent cells
# Now whenever player moves: for x in range(50):
player.x = new_x for y in range(50):
player.y = new_y grid.at(x, y).transparent = True
player.update_visibility() # Automatically updates the fog layer
``` # Mark walls
grid.at(5, 5).transparent = False
### One-Time FOV Draw
# Compute FOV from entity position
```python grid.compute_fov((int(player.grid_x), int(player.grid_y)), radius=10)
# Draw FOV without binding (useful for previews, spell ranges, etc.)
fov_layer.draw_fov( # Check if a cell is visible
source=(player.x, player.y), if grid.is_in_fov((12, 14)):
radius=10, print("Can see that cell!")
fov=mcrfpy.FOV.SHADOW, ```
visible=(255, 255, 200, 64),
discovered=(100, 100, 100, 128), ### Fog of War with ColorLayer
unknown=(0, 0, 0, 255)
) ```python
``` # Create fog overlay
fog = mcrfpy.ColorLayer(name="fog", z_index=1)
### Gridstate Access grid.add_layer(fog)
```python # Initialize to fully dark
# Entity's per-cell visibility memory fog.fill(mcrfpy.Color(0, 0, 0, 255))
for state in entity.gridstate:
print(f"visible={state.visible}, discovered={state.discovered}") # Update fog based on FOV after each move
def update_fog(player, fog_layer, grid):
# Access specific cell state grid.compute_fov((int(player.grid_x), int(player.grid_y)), radius=8)
state = entity.at((x, y)) for x in range(grid.grid_w):
if state.visible: for y in range(grid.grid_h):
print("Entity can currently see this cell") if grid.is_in_fov((x, y)):
elif state.discovered: fog_layer.set((x, y), mcrfpy.Color(0, 0, 0, 0)) # Clear
print("Entity has seen this cell before") # Previously seen cells stay semi-transparent (don't re-darken)
``` ```
### GridPointState.point - Accessing Cell Data (#16) ### Perspective System
The `GridPointState.point` property provides access to the underlying `GridPoint` from an entity's perspective: ```python
# Set perspective entity (enables FOV-aware rendering)
```python grid.perspective = player
state = entity.at((x, y))
# Remove perspective (omniscient view)
# If entity has NOT discovered this cell, point is None grid.perspective = 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: ## Common Patterns
point = state.point # Live reference to GridPoint
print(f"walkable: {point.walkable}") ### Player Entity with Movement
print(f"transparent: {point.transparent}")
print(f"entities here: {point.entities}") # List of entities at cell ```python
``` class Player:
def __init__(self, grid, start_pos):
**Key behaviors:** self.entity = mcrfpy.Entity(
- Returns `None` if `discovered=False` (entity has never seen this cell) grid_pos=start_pos, sprite_index=0, name="player"
- Returns live `GridPoint` reference if `discovered=True` )
- Changes to the `GridPoint` are immediately visible through `state.point` grid.entities.append(self.entity)
- This is intentionally **not** a cached copy - for historical memory, implement your own system in Python
def move(self, dx, dy):
--- new_x = int(self.entity.grid_x + dx)
new_y = int(self.entity.grid_y + dy)
## EntityCollection
point = self.entity.grid.at(new_x, new_y)
`grid.entities` is an `EntityCollection` with list-like operations: if point and point.walkable:
self.entity.animate("x", float(new_x), 0.15, mcrfpy.Easing.EASE_OUT_QUAD)
```python self.entity.animate("y", float(new_y), 0.15, mcrfpy.Easing.EASE_OUT_QUAD)
# Add entities self.entity.grid_x = new_x
grid.entities.append(entity) self.entity.grid_y = new_y
grid.entities.extend([entity1, entity2, entity3]) return True
grid.entities.insert(0, entity) # Insert at index return False
```
# Remove entities
grid.entities.remove(entity) ### Enemy AI with SpatialHash
entity = grid.entities.pop() # Remove and return last
entity = grid.entities.pop(0) # Remove and return at index ```python
class Enemy:
# Query def __init__(self, grid, pos, aggro_range=10):
count = len(grid.entities) self.entity = mcrfpy.Entity(
idx = grid.entities.index(entity) grid_pos=pos, sprite_index=1, name="enemy"
n = grid.entities.count(entity) )
found = grid.entities.find("entity_name") # Find by name self.aggro_range = aggro_range
grid.entities.append(self.entity)
# Iteration (O(n) - optimized in #159)
for entity in grid.entities: def update(self):
print(entity.pos) grid = self.entity.grid
``` # Use SpatialHash for efficient nearby entity detection
nearby = grid.entities_in_radius(
### Iterator Performance (#159) (self.entity.grid_x, self.entity.grid_y),
self.aggro_range
EntityCollection iteration was optimized in commit 8f2407b: )
- **Before:** O(n²) due to index-based list traversal
- **After:** O(n) using proper list iterators # Find player in nearby entities
- **Speedup:** 103× at 2,000 entities player = None
for e in nearby:
--- if e.name == "player":
player = e
## Entity Lifecycle break
### Creation if player:
self.chase(player)
```python else:
# Basic creation self.wander()
entity = mcrfpy.Entity(pos=(10, 10), sprite_index=0)
def chase(self, target):
# With name for later lookup grid = self.entity.grid
entity = mcrfpy.Entity(pos=(10, 10), sprite_index=0, name="player") path = grid.find_path(
``` (int(self.entity.grid_x), int(self.entity.grid_y)),
(int(target.grid_x), int(target.grid_y))
### Adding to Grid )
if path and len(path) > 0:
```python next_step = path.walk()
grid.entities.append(entity) self.entity.grid_x = next_step.x
# entity.grid is now set to grid self.entity.grid_y = next_step.y
# Entity is automatically added to SpatialHash for fast queries
``` def wander(self):
import random
### Movement dx = random.choice([-1, 0, 1])
dy = random.choice([-1, 0, 1])
```python new_x = int(self.entity.grid_x + dx)
# Direct position change (automatically updates SpatialHash) new_y = int(self.entity.grid_y + dy)
entity.pos = (new_x, new_y) point = self.entity.grid.at(new_x, new_y)
if point and point.walkable:
# Animated movement self.entity.grid_x = new_x
mcrfpy.Animation("x", target_x, 0.3, "easeOutQuad").start(entity) self.entity.grid_y = new_y
mcrfpy.Animation("y", target_y, 0.3, "easeOutQuad").start(entity) ```
# Update visibility after movement ### Item Pickup
entity.update_visibility()
``` ```python
class Item:
### Removal def __init__(self, grid, pos, item_type):
self.entity = mcrfpy.Entity(
```python grid_pos=pos, sprite_index=10 + item_type, name=f"item_{item_type}"
# Method 1: Remove from collection )
grid.entities.remove(entity) self.item_type = item_type
grid.entities.append(self.entity)
# Method 2: Entity.die() - removes from parent grid and SpatialHash
entity.die() def pickup(self, collector_inventory):
collector_inventory.append(self.item_type)
# After removal: entity.grid is None self.entity.die() # Remove from grid
``` ```
### Transfer Between Grids ---
```python ## Pathfinding
def transfer_entity(entity, to_grid, new_pos):
"""Move entity to a different grid.""" Entities use the grid's pathfinding capabilities:
entity.die() # Remove from current grid
entity.pos = new_pos ```python
to_grid.entities.append(entity) # A* pathfinding
``` path = grid.find_path(
(int(entity.grid_x), int(entity.grid_y)),
--- (target_x, target_y)
)
## Common Patterns
if path and len(path) > 0:
### Player Entity with FOV next_step = path.walk() # Get next step as Vector
entity.grid_x = next_step.x
```python entity.grid_y = next_step.y
class Player:
def __init__(self, grid, start_pos): # Dijkstra for multi-target pathfinding
self.entity = mcrfpy.Entity(pos=start_pos, sprite_index=0, name="player") dm = grid.get_dijkstra_map((goal_x, goal_y))
grid.entities.append(self.entity) distance = dm.distance((entity.grid_x, entity.grid_y))
next_step = dm.step_from((int(entity.grid_x), int(entity.grid_y)))
# Set up fog of war ```
self.fov_layer = grid.add_layer('color', z_index=-1)
self.fov_layer.fill((0, 0, 0, 255)) Pathfinding respects `GridPoint.walkable` properties.
self.fov_layer.apply_perspective(
entity=self.entity, ---
visible=(0, 0, 0, 0),
discovered=(30, 30, 50, 180), ## Performance Considerations
unknown=(0, 0, 0, 255)
) | Operation | Performance | Notes |
self.entity.update_visibility() |-----------|-------------|-------|
| Entity Creation | ~90,000/sec | Sufficient for level generation |
def move(self, dx, dy): | Iteration | ~9M reads/sec | Optimized iterators (#159) |
new_x = self.entity.x + dx | Spatial Query | 0.003ms | SpatialHash O(k) (#115) |
new_y = self.entity.y + dy | N x N Visibility (5000) | 2ms | 217x faster than O(n) |
point = self.entity.grid.at(new_x, new_y) ### Recommendations
if point and point.walkable:
self.entity.pos = (new_x, new_y) 1. **Use `entities_in_radius()` for AI** - O(k) queries instead of iterating all entities
self.entity.update_visibility() # Update FOV after move 2. **Batch visibility updates** - Compute FOV once after all moves, not per-move
return True 3. **Use Timer for AI** - Don't run expensive logic every frame
return False 4. **Entity counts up to 5,000+** - SpatialHash makes large counts feasible
def get_visible_enemies(self): ---
"""Get enemies this player can currently see."""
return [e for e in self.entity.visible_entities() ## Related Systems
if e.name and e.name.startswith("enemy")]
``` - [[Grid-System]] - Spatial container for entities
- [[Grid-Interaction-Patterns]] - Click handling, selection, context menus
### Enemy AI with SpatialHash - [[Animation-System]] - Smooth entity movement via `.animate()`
- [[AI-and-Pathfinding]] - FOV, pathfinding, AI patterns
```python - [[Input-and-Events]] - Callback signatures for mouse events
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 *Last updated: 2026-02-07*
self.health = 100
self.grid = grid
grid.entities.append(self.entity)
def update(self):
# Use SpatialHash for efficient nearby entity detection
nearby = self.grid.entities_in_radius(
self.entity.x, self.entity.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.x, player.y))
else:
self.wander()
def chase(self, target):
# Use pathfinding
path = self.entity.grid.find_path(
self.entity.x, self.entity.y, target[0], target[1]
)
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
```
### Efficient Multi-Entity AI Loop
```python
def update_all_enemies(grid, enemies):
"""Update all enemies efficiently using SpatialHash."""
for enemy in enemies:
# Each query is O(k) not O(n)
nearby = grid.entities_in_radius(enemy.x, enemy.y, enemy.aggro_range)
enemy.react_to_nearby(nearby)
```
### Item Entity
```python
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:
```python
# A* pathfinding to target (via Grid)
path = grid.find_path(entity.x, entity.y, 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
# Dijkstra for multi-target pathfinding
grid.compute_dijkstra(goal_x, goal_y)
distance = grid.get_dijkstra_distance(entity.x, entity.y)
path = grid.get_dijkstra_path(entity.x, entity.y)
```
Pathfinding respects `GridPoint.walkable` properties set on the grid.
---
## Performance Considerations
### Current Performance (as of 2025-12-28)
| 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×N Visibility (5000) | 2ms | 217× faster than O(n) |
### Recommendations
1. **Use `entities_in_radius()` for AI** - O(k) queries instead of iterating all entities
2. **Batch visibility updates** - Call `update_visibility()` once after all moves
3. **Use timer callbacks for AI** - Don't run expensive logic every frame
4. **Entity counts up to 5,000+** - SpatialHash makes large counts feasible
### Internal Architecture
- **SpatialHash:** Bucket-based spatial indexing (32-cell buckets)
- **Automatic updates:** Hash updates on entity add/remove/move
- **Weak references:** Hash doesn't prevent entity garbage collection
See [[Performance-and-Profiling]] for detailed 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-28*