Update "Entity-Management.-"

John McCardle 2025-12-28 13:26:16 +00:00
commit 2376e808b8

@ -1,6 +1,3 @@
# Entity Management
*Last modified: 2025-12-01*
# 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.
@ -17,10 +14,12 @@ Entities are game objects that implement behavior and live on Grids. While Grids
**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
**Related Issues:** **Related Issues:**
- [#115](../issues/115) - SpatialHash for fast queries (Open) - [#115](../issues/115) - SpatialHash for fast queries ✅ Implemented
- [#117](../issues/117) - Memory Pool for entities (Open) - [#117](../issues/117) - Memory Pool for entities (Deferred)
- [#159](../issues/159) - EntityCollection iterator optimization ✅ Fixed
--- ---
@ -107,6 +106,51 @@ current_grid = entity.grid # Read-only, set by collection operations
--- ---
## 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.
### entities_in_radius()
```python
# Query entities within a radius (uses SpatialHash internally)
nearby = grid.entities_in_radius(x, y, radius)
# Example: Find all entities within 10 cells of position (50, 50)
threats = grid.entities_in_radius(50, 50, 10)
for entity in threats:
print(f"Entity at ({entity.x}, {entity.y})")
```
### Performance Comparison
| Entity Count | O(n) Query | SpatialHash | Speedup |
|--------------|------------|-------------|---------|
| 100 | 0.037ms | 0.008ms | 4.6× |
| 500 | 0.061ms | 0.009ms | 7.2× |
| 1,000 | 0.028ms | 0.004ms | 7.8× |
| 2,000 | 0.043ms | 0.003ms | 13× |
| 5,000 | 0.109ms | 0.003ms | **37×** |
### N×N Visibility (AI "What can everyone see?")
| Entity Count | O(n) approach | SpatialHash | Speedup |
|--------------|---------------|-------------|---------|
| 1,000 | 21ms | 1ms | 35× |
| 2,000 | 85ms | 1ms | 87× |
| 5,000 | 431ms | 2ms | **217×** |
### When to Use Which Method
| Use Case | Method | Complexity |
|----------|--------|------------|
| Nearby entities (AI, combat) | `grid.entities_in_radius(x, y, r)` | O(k) |
| FOV-based visibility | `entity.visible_entities()` | O(n) + FOV |
| All entities iteration | `for e in grid.entities` | O(n) |
| Single cell lookup | `grid.at(x, y).entities` | O(n) filter |
---
## Field of View & Visibility ## Field of View & Visibility
Entities track what they can see via `gridstate` - a per-cell record of visible and discovered states. Entities track what they can see via `gridstate` - a per-cell record of visible and discovered states.
@ -134,7 +178,7 @@ entity.update_visibility()
### Querying Visible Entities ### Querying Visible Entities
```python ```python
# Get list of other entities this entity can see # Get list of other entities this entity can see (uses FOV + line-of-sight)
visible_enemies = entity.visible_entities() visible_enemies = entity.visible_entities()
# With custom FOV settings # With custom FOV settings
@ -142,6 +186,8 @@ nearby = entity.visible_entities(radius=5)
visible = entity.visible_entities(fov=mcrfpy.FOV.BASIC, radius=8) visible = entity.visible_entities(fov=mcrfpy.FOV.BASIC, radius=8)
``` ```
**Note:** `visible_entities()` checks FOV and line-of-sight. For pure distance queries without FOV, use `grid.entities_in_radius()`.
### Fog of War with ColorLayers ### Fog of War with ColorLayers
```python ```python
@ -217,37 +263,6 @@ if state.discovered:
- Changes to the `GridPoint` are immediately visible through `state.point` - Changes to the `GridPoint` are immediately visible through `state.point`
- This is intentionally **not** a cached copy - for historical memory, implement your own system in Python - This is intentionally **not** a cached copy - for historical memory, implement your own system in Python
**Use case - Entity perspective queries:**
```python
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:
```python
# 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 ## EntityCollection
@ -271,11 +286,18 @@ idx = grid.entities.index(entity)
n = grid.entities.count(entity) n = grid.entities.count(entity)
found = grid.entities.find("entity_name") # Find by name found = grid.entities.find("entity_name") # Find by name
# Iteration # Iteration (O(n) - optimized in #159)
for entity in grid.entities: for entity in grid.entities:
print(entity.pos) print(entity.pos)
``` ```
### Iterator Performance (#159)
EntityCollection iteration was optimized in commit 8f2407b:
- **Before:** O(n²) due to index-based list traversal
- **After:** O(n) using proper list iterators
- **Speedup:** 103× at 2,000 entities
--- ---
## Entity Lifecycle ## Entity Lifecycle
@ -295,13 +317,13 @@ entity = mcrfpy.Entity(pos=(10, 10), sprite_index=0, name="player")
```python ```python
grid.entities.append(entity) grid.entities.append(entity)
# entity.grid is now set to grid # entity.grid is now set to grid
# Entity will be rendered with the grid # Entity is automatically added to SpatialHash for fast queries
``` ```
### Movement ### Movement
```python ```python
# Direct position change # Direct position change (automatically updates SpatialHash)
entity.pos = (new_x, new_y) entity.pos = (new_x, new_y)
# Animated movement # Animated movement
@ -318,7 +340,7 @@ entity.update_visibility()
# Method 1: Remove from collection # Method 1: Remove from collection
grid.entities.remove(entity) grid.entities.remove(entity)
# Method 2: Entity.die() - removes from parent grid # Method 2: Entity.die() - removes from parent grid and SpatialHash
entity.die() entity.die()
# After removal: entity.grid is None # After removal: entity.grid is None
@ -374,7 +396,7 @@ class Player:
if e.name and e.name.startswith("enemy")] if e.name and e.name.startswith("enemy")]
``` ```
### Enemy Entity ### Enemy AI with SpatialHash
```python ```python
class Enemy: class Enemy:
@ -382,21 +404,32 @@ class Enemy:
self.entity = mcrfpy.Entity(pos=pos, sprite_index=1, name="enemy") self.entity = mcrfpy.Entity(pos=pos, sprite_index=1, name="enemy")
self.aggro_range = aggro_range self.aggro_range = aggro_range
self.health = 100 self.health = 100
self.grid = grid
grid.entities.append(self.entity) grid.entities.append(self.entity)
def update(self, player_pos): def update(self):
dx = player_pos[0] - self.entity.x # Use SpatialHash for efficient nearby entity detection
dy = player_pos[1] - self.entity.y nearby = self.grid.entities_in_radius(
dist = (dx*dx + dy*dy) ** 0.5 self.entity.x, self.entity.y, self.aggro_range
)
if dist < self.aggro_range: # Find player in nearby entities
self.chase(player_pos) player = None
for e in nearby:
if e.name == "player":
player = e
break
if player:
self.chase((player.x, player.y))
else: else:
self.wander() self.wander()
def chase(self, target): def chase(self, target):
# Use pathfinding # Use pathfinding
path = self.entity.path_to(target) path = self.entity.grid.find_path(
self.entity.x, self.entity.y, target[0], target[1]
)
if path and len(path) > 1: if path and len(path) > 1:
next_cell = path[1] # path[0] is current position next_cell = path[1] # path[0] is current position
self.entity.pos = next_cell self.entity.pos = next_cell
@ -412,6 +445,17 @@ class Enemy:
self.entity.pos = new_pos 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 ### Item Entity
```python ```python
@ -436,13 +480,18 @@ For more interaction patterns (click handling, selection, context menus), see [[
Entities have built-in pathfinding via libtcod: Entities have built-in pathfinding via libtcod:
```python ```python
# A* pathfinding to target # A* pathfinding to target (via Grid)
path = entity.path_to((target_x, target_y)) path = grid.find_path(entity.x, entity.y, target_x, target_y)
# Returns list of (x, y) tuples, or empty if no path # Returns list of (x, y) tuples, or empty if no path
if path: if path:
next_step = path[1] # path[0] is current position next_step = path[1] # path[0] is current position
entity.pos = next_step 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. Pathfinding respects `GridPoint.walkable` properties set on the grid.
@ -451,27 +500,29 @@ Pathfinding respects `GridPoint.walkable` properties set on the grid.
## Performance Considerations ## Performance Considerations
**Current:** Entity queries are O(n): ### Current Performance (as of 2025-12-28)
```python
# 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: | Operation | Performance | Notes |
```python |-----------|-------------|-------|
# O(n) but more convenient - filters grid.entities by position | Entity Creation | ~90,000/sec | Sufficient for level generation |
entities_here = grid.at(x, y).entities | 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) |
**Workarounds:** ### Recommendations
- Keep entity counts reasonable (< 200 for best performance)
- Use timer callbacks for AI updates, not per-frame
- Cache query results when possible
**Future:** [#115](../issues/115) SpatialHash will provide O(1) position queries. 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
See [[Performance-and-Profiling]] for optimization guidance. ### 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.
--- ---
@ -484,4 +535,4 @@ See [[Performance-and-Profiling]] for optimization guidance.
--- ---
*Last updated: 2025-12-01* *Last updated: 2025-12-28*