Update "Entity-Management.-"
parent
0c220581c1
commit
2376e808b8
1 changed files with 538 additions and 487 deletions
|
|
@ -1,6 +1,3 @@
|
|||
# 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.
|
||||
|
|
@ -17,10 +14,12 @@ Entities are game objects that implement behavior and live on Grids. While Grids
|
|||
**Key Files:**
|
||||
- `src/UIEntity.h` / `src/UIEntity.cpp`
|
||||
- `src/UIEntityCollection.h` / `.cpp`
|
||||
- `src/SpatialHash.h` / `src/SpatialHash.cpp` - Spatial indexing
|
||||
|
||||
**Related Issues:**
|
||||
- [#115](../issues/115) - SpatialHash for fast queries (Open)
|
||||
- [#117](../issues/117) - Memory Pool for entities (Open)
|
||||
- [#115](../issues/115) - SpatialHash for fast queries ✅ Implemented
|
||||
- [#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
|
||||
|
||||
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
|
||||
|
||||
```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()
|
||||
|
||||
# With custom FOV settings
|
||||
|
|
@ -142,6 +186,8 @@ nearby = entity.visible_entities(radius=5)
|
|||
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
|
||||
|
||||
```python
|
||||
|
|
@ -217,37 +263,6 @@ if state.discovered:
|
|||
- 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
|
||||
|
||||
**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
|
||||
|
|
@ -271,11 +286,18 @@ idx = grid.entities.index(entity)
|
|||
n = grid.entities.count(entity)
|
||||
found = grid.entities.find("entity_name") # Find by name
|
||||
|
||||
# Iteration
|
||||
# Iteration (O(n) - optimized in #159)
|
||||
for entity in grid.entities:
|
||||
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
|
||||
|
|
@ -295,13 +317,13 @@ entity = mcrfpy.Entity(pos=(10, 10), sprite_index=0, name="player")
|
|||
```python
|
||||
grid.entities.append(entity)
|
||||
# entity.grid is now set to grid
|
||||
# Entity will be rendered with the grid
|
||||
# Entity is automatically added to SpatialHash for fast queries
|
||||
```
|
||||
|
||||
### Movement
|
||||
|
||||
```python
|
||||
# Direct position change
|
||||
# Direct position change (automatically updates SpatialHash)
|
||||
entity.pos = (new_x, new_y)
|
||||
|
||||
# Animated movement
|
||||
|
|
@ -318,7 +340,7 @@ entity.update_visibility()
|
|||
# Method 1: Remove from collection
|
||||
grid.entities.remove(entity)
|
||||
|
||||
# Method 2: Entity.die() - removes from parent grid
|
||||
# Method 2: Entity.die() - removes from parent grid and SpatialHash
|
||||
entity.die()
|
||||
|
||||
# After removal: entity.grid is None
|
||||
|
|
@ -374,7 +396,7 @@ class Player:
|
|||
if e.name and e.name.startswith("enemy")]
|
||||
```
|
||||
|
||||
### Enemy Entity
|
||||
### Enemy AI with SpatialHash
|
||||
|
||||
```python
|
||||
class Enemy:
|
||||
|
|
@ -382,21 +404,32 @@ class Enemy:
|
|||
self.entity = mcrfpy.Entity(pos=pos, sprite_index=1, name="enemy")
|
||||
self.aggro_range = aggro_range
|
||||
self.health = 100
|
||||
self.grid = grid
|
||||
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
|
||||
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
|
||||
)
|
||||
|
||||
if dist < self.aggro_range:
|
||||
self.chase(player_pos)
|
||||
# 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.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:
|
||||
next_cell = path[1] # path[0] is current position
|
||||
self.entity.pos = next_cell
|
||||
|
|
@ -412,6 +445,17 @@ class Enemy:
|
|||
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
|
||||
|
|
@ -436,13 +480,18 @@ For more interaction patterns (click handling, selection, context menus), see [[
|
|||
Entities have built-in pathfinding via libtcod:
|
||||
|
||||
```python
|
||||
# A* pathfinding to target
|
||||
path = entity.path_to((target_x, target_y))
|
||||
# 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.
|
||||
|
|
@ -451,27 +500,29 @@ Pathfinding respects `GridPoint.walkable` properties set on the grid.
|
|||
|
||||
## Performance Considerations
|
||||
|
||||
**Current:** Entity queries are O(n):
|
||||
```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]
|
||||
```
|
||||
### Current Performance (as of 2025-12-28)
|
||||
|
||||
**New in v1.0:** Use `GridPoint.entities` for cell-based queries:
|
||||
```python
|
||||
# O(n) but more convenient - filters grid.entities by position
|
||||
entities_here = grid.at(x, y).entities
|
||||
```
|
||||
| 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) |
|
||||
|
||||
**Workarounds:**
|
||||
- Keep entity counts reasonable (< 200 for best performance)
|
||||
- Use timer callbacks for AI updates, not per-frame
|
||||
- Cache query results when possible
|
||||
### Recommendations
|
||||
|
||||
**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*
|
||||
Loading…
Add table
Add a link
Reference in a new issue