Update Entity Management wiki with Entity(grid_pos=), .animate(), SpatialHash, current callback signatures, Timer objects
parent
cbf263d0f8
commit
b0b18f7a0f
1 changed files with 405 additions and 538 deletions
|
|
@ -17,40 +17,46 @@ Entities are game objects that implement behavior and live on Grids. While Grids
|
||||||
- `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)
|
|
||||||
- **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.
|
# 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
|
## 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 |
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
# Create grid and entity
|
|
||||||
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(400, 400))
|
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(400, 400))
|
||||||
player = mcrfpy.Entity(pos=(10, 10), sprite_index=0)
|
player = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=0, name="player")
|
||||||
|
|
||||||
# Before adding: entity has no grid
|
# Before adding: entity has no grid
|
||||||
print(player.grid) # None
|
print(player.grid) # None
|
||||||
|
|
@ -59,7 +65,7 @@ print(player.grid) # None
|
||||||
grid.entities.append(player)
|
grid.entities.append(player)
|
||||||
|
|
||||||
# After adding: bidirectional link established
|
# After adding: bidirectional link established
|
||||||
print(player.grid == grid) # True
|
print(player.grid is not None) # True
|
||||||
print(player in grid.entities) # True
|
print(player in grid.entities) # True
|
||||||
|
|
||||||
# Removing breaks the link
|
# Removing breaks the link
|
||||||
|
|
@ -71,197 +77,65 @@ print(player.grid) # None
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Entity Properties
|
## Movement
|
||||||
|
|
||||||
### Position
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Grid coordinates (integer cells)
|
# Direct position change (updates SpatialHash automatically)
|
||||||
entity.x = 15
|
player.grid_x = 15
|
||||||
entity.y = 20
|
player.grid_y = 20
|
||||||
entity.pos = (15, 20) # Tuple form
|
|
||||||
|
|
||||||
# Draw position (float, for animation interpolation)
|
# Animated movement (smooth visual transition)
|
||||||
print(entity.draw_pos) # Actual render position
|
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)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Sprite
|
### Animatable Entity Properties
|
||||||
|
|
||||||
```python
|
| Property | Type | Notes |
|
||||||
entity.sprite_index = 5 # Index in texture sprite sheet
|
|----------|------|-------|
|
||||||
```
|
| `x`, `y` | float | Alias for draw position |
|
||||||
|
| `draw_x`, `draw_y` | float | Visual position in tile coords |
|
||||||
### Visibility
|
| `sprite_index` | int | Can animate through sprite frames |
|
||||||
|
| `sprite_scale` | float | Scale animation |
|
||||||
```python
|
|
||||||
entity.visible = True
|
|
||||||
entity.opacity = 0.8 # 0.0 to 1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Grid Reference
|
|
||||||
|
|
||||||
```python
|
|
||||||
current_grid = entity.grid # Read-only, set by collection operations
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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})")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Performance Comparison
|
**Note:** The first argument is a `(x, y)` tuple, not separate x and y arguments.
|
||||||
|
|
||||||
| Entity Count | O(n) Query | SpatialHash | Speedup |
|
### Performance
|
||||||
|--------------|------------|-------------|---------|
|
|
||||||
| 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 | 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** |
|
||||||
|
|
||||||
| Entity Count | O(n) approach | SpatialHash | Speedup |
|
For N x N visibility checks (e.g., "what can everyone see?"):
|
||||||
|--------------|---------------|-------------|---------|
|
|
||||||
| 1,000 | 21ms | 1ms | 35× |
|
|
||||||
| 2,000 | 85ms | 1ms | 87× |
|
|
||||||
| 5,000 | 431ms | 2ms | **217×** |
|
|
||||||
|
|
||||||
### When to Use Which Method
|
| Entity Count | Linear | SpatialHash | Speedup |
|
||||||
|
|--------------|--------|-------------|---------|
|
||||||
| Use Case | Method | Complexity |
|
| 1,000 | 21ms | 1ms | 35x |
|
||||||
|----------|--------|------------|
|
| 5,000 | 431ms | 2ms | **217x** |
|
||||||
| 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.
|
|
||||||
|
|
||||||
### FOV Configuration
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 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
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Compute FOV from entity's position and update gridstate
|
|
||||||
entity.update_visibility()
|
|
||||||
|
|
||||||
# This also updates any ColorLayers bound via apply_perspective()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Querying Visible Entities
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Get list of other entities this entity can see (uses FOV + line-of-sight)
|
|
||||||
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)
|
|
||||||
```
|
|
||||||
|
|
||||||
**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
|
|
||||||
# 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
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 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
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 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:
|
|
||||||
|
|
||||||
```python
|
|
||||||
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 `None` if `discovered=False` (entity has never seen this cell)
|
|
||||||
- Returns live `GridPoint` reference if `discovered=True`
|
|
||||||
- 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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -273,127 +147,133 @@ if state.discovered:
|
||||||
# Add entities
|
# Add entities
|
||||||
grid.entities.append(entity)
|
grid.entities.append(entity)
|
||||||
grid.entities.extend([entity1, entity2, entity3])
|
grid.entities.extend([entity1, entity2, entity3])
|
||||||
grid.entities.insert(0, entity) # Insert at index
|
|
||||||
|
|
||||||
# Remove entities
|
# Remove entities
|
||||||
grid.entities.remove(entity)
|
grid.entities.remove(entity)
|
||||||
entity = grid.entities.pop() # Remove and return last
|
|
||||||
entity = grid.entities.pop(0) # Remove and return at index
|
|
||||||
|
|
||||||
# Query
|
# Query
|
||||||
count = len(grid.entities)
|
count = len(grid.entities)
|
||||||
idx = grid.entities.index(entity)
|
idx = grid.entities.index(entity)
|
||||||
n = grid.entities.count(entity)
|
|
||||||
found = grid.entities.find("entity_name") # Find by name
|
|
||||||
|
|
||||||
# Iteration (O(n) - optimized in #159)
|
# Iteration (O(n) - optimized in #159)
|
||||||
for entity in grid.entities:
|
for entity in grid.entities:
|
||||||
print(entity.pos)
|
print(f"{entity.name}: ({entity.grid_x}, {entity.grid_y})")
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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
|
||||||
|
|
||||||
### Creation
|
### Creation and Placement
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Basic creation
|
player = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=0, name="player")
|
||||||
entity = mcrfpy.Entity(pos=(10, 10), sprite_index=0)
|
grid.entities.append(player)
|
||||||
|
# player.grid is now set
|
||||||
# With name for later lookup
|
# Entity is added to SpatialHash for fast queries
|
||||||
entity = mcrfpy.Entity(pos=(10, 10), sprite_index=0, name="player")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Adding to Grid
|
|
||||||
|
|
||||||
```python
|
|
||||||
grid.entities.append(entity)
|
|
||||||
# entity.grid is now set to grid
|
|
||||||
# Entity is automatically added to SpatialHash for fast queries
|
|
||||||
```
|
|
||||||
|
|
||||||
### Movement
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Direct position change (automatically updates SpatialHash)
|
|
||||||
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
|
### Removal
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Method 1: Remove from collection
|
# Method 1: Remove from collection
|
||||||
grid.entities.remove(entity)
|
grid.entities.remove(player)
|
||||||
|
|
||||||
# Method 2: Entity.die() - removes from parent grid and SpatialHash
|
# Method 2: Entity.die() - removes from parent grid and SpatialHash
|
||||||
entity.die()
|
player.die()
|
||||||
|
|
||||||
# After removal: entity.grid is None
|
# After removal: player.grid is None
|
||||||
```
|
```
|
||||||
|
|
||||||
### Transfer Between Grids
|
### Transfer Between Grids
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def transfer_entity(entity, to_grid, new_pos):
|
def transfer_entity(entity, to_grid, new_pos):
|
||||||
"""Move entity to a different grid."""
|
|
||||||
entity.die() # Remove from current grid
|
entity.die() # Remove from current grid
|
||||||
entity.pos = new_pos
|
entity.grid_x = new_pos[0]
|
||||||
|
entity.grid_y = new_pos[1]
|
||||||
to_grid.entities.append(entity)
|
to_grid.entities.append(entity)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## FOV and Visibility
|
||||||
|
|
||||||
|
### Computing FOV
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 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
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 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
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Set perspective entity (enables FOV-aware rendering)
|
||||||
|
grid.perspective = player
|
||||||
|
|
||||||
|
# Remove perspective (omniscient view)
|
||||||
|
grid.perspective = None
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Common Patterns
|
## Common Patterns
|
||||||
|
|
||||||
### Player Entity with FOV
|
### Player Entity with Movement
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class Player:
|
class Player:
|
||||||
def __init__(self, grid, start_pos):
|
def __init__(self, grid, start_pos):
|
||||||
self.entity = mcrfpy.Entity(pos=start_pos, sprite_index=0, name="player")
|
self.entity = mcrfpy.Entity(
|
||||||
|
grid_pos=start_pos, sprite_index=0, name="player"
|
||||||
|
)
|
||||||
grid.entities.append(self.entity)
|
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):
|
def move(self, dx, dy):
|
||||||
new_x = self.entity.x + dx
|
new_x = int(self.entity.grid_x + dx)
|
||||||
new_y = self.entity.y + dy
|
new_y = int(self.entity.grid_y + dy)
|
||||||
|
|
||||||
point = self.entity.grid.at(new_x, new_y)
|
point = self.entity.grid.at(new_x, new_y)
|
||||||
if point and point.walkable:
|
if point and point.walkable:
|
||||||
self.entity.pos = (new_x, new_y)
|
self.entity.animate("x", float(new_x), 0.15, mcrfpy.Easing.EASE_OUT_QUAD)
|
||||||
self.entity.update_visibility() # Update FOV after move
|
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 True
|
||||||
return False
|
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 AI with SpatialHash
|
### Enemy AI with SpatialHash
|
||||||
|
|
@ -401,16 +281,18 @@ class Player:
|
||||||
```python
|
```python
|
||||||
class Enemy:
|
class Enemy:
|
||||||
def __init__(self, grid, pos, aggro_range=10):
|
def __init__(self, grid, pos, aggro_range=10):
|
||||||
self.entity = mcrfpy.Entity(pos=pos, sprite_index=1, name="enemy")
|
self.entity = mcrfpy.Entity(
|
||||||
|
grid_pos=pos, sprite_index=1, name="enemy"
|
||||||
|
)
|
||||||
self.aggro_range = aggro_range
|
self.aggro_range = aggro_range
|
||||||
self.health = 100
|
|
||||||
self.grid = grid
|
|
||||||
grid.entities.append(self.entity)
|
grid.entities.append(self.entity)
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
|
grid = self.entity.grid
|
||||||
# Use SpatialHash for efficient nearby entity detection
|
# Use SpatialHash for efficient nearby entity detection
|
||||||
nearby = self.grid.entities_in_radius(
|
nearby = grid.entities_in_radius(
|
||||||
self.entity.x, self.entity.y, self.aggro_range
|
(self.entity.grid_x, self.entity.grid_y),
|
||||||
|
self.aggro_range
|
||||||
)
|
)
|
||||||
|
|
||||||
# Find player in nearby entities
|
# Find player in nearby entities
|
||||||
|
|
@ -421,118 +303,103 @@ class Enemy:
|
||||||
break
|
break
|
||||||
|
|
||||||
if player:
|
if player:
|
||||||
self.chase((player.x, player.y))
|
self.chase(player)
|
||||||
else:
|
else:
|
||||||
self.wander()
|
self.wander()
|
||||||
|
|
||||||
def chase(self, target):
|
def chase(self, target):
|
||||||
# Use pathfinding
|
grid = self.entity.grid
|
||||||
path = self.entity.grid.find_path(
|
path = grid.find_path(
|
||||||
self.entity.x, self.entity.y, target[0], target[1]
|
(int(self.entity.grid_x), int(self.entity.grid_y)),
|
||||||
|
(int(target.grid_x), int(target.grid_y))
|
||||||
)
|
)
|
||||||
if path and len(path) > 1:
|
if path and len(path) > 0:
|
||||||
next_cell = path[1] # path[0] is current position
|
next_step = path.walk()
|
||||||
self.entity.pos = next_cell
|
self.entity.grid_x = next_step.x
|
||||||
|
self.entity.grid_y = next_step.y
|
||||||
|
|
||||||
def wander(self):
|
def wander(self):
|
||||||
import random
|
import random
|
||||||
dx = random.choice([-1, 0, 1])
|
dx = random.choice([-1, 0, 1])
|
||||||
dy = random.choice([-1, 0, 1])
|
dy = random.choice([-1, 0, 1])
|
||||||
|
new_x = int(self.entity.grid_x + dx)
|
||||||
new_pos = (self.entity.x + dx, self.entity.y + dy)
|
new_y = int(self.entity.grid_y + dy)
|
||||||
point = self.entity.grid.at(*new_pos)
|
point = self.entity.grid.at(new_x, new_y)
|
||||||
if point and point.walkable:
|
if point and point.walkable:
|
||||||
self.entity.pos = new_pos
|
self.entity.grid_x = new_x
|
||||||
|
self.entity.grid_y = new_y
|
||||||
```
|
```
|
||||||
|
|
||||||
### Efficient Multi-Entity AI Loop
|
### Item Pickup
|
||||||
|
|
||||||
```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
|
```python
|
||||||
class Item:
|
class Item:
|
||||||
def __init__(self, grid, pos, item_type):
|
def __init__(self, grid, pos, item_type):
|
||||||
self.entity = mcrfpy.Entity(pos=pos, sprite_index=10 + item_type)
|
self.entity = mcrfpy.Entity(
|
||||||
|
grid_pos=pos, sprite_index=10 + item_type, name=f"item_{item_type}"
|
||||||
|
)
|
||||||
self.item_type = item_type
|
self.item_type = item_type
|
||||||
grid.entities.append(self.entity)
|
grid.entities.append(self.entity)
|
||||||
|
|
||||||
def pickup(self, collector):
|
def pickup(self, collector_inventory):
|
||||||
"""Called when another entity picks up this item."""
|
collector_inventory.append(self.item_type)
|
||||||
collector.inventory.append(self.item_type)
|
|
||||||
self.entity.die() # Remove from grid
|
self.entity.die() # Remove from grid
|
||||||
```
|
```
|
||||||
|
|
||||||
For more interaction patterns (click handling, selection, context menus), see [[Grid-Interaction-Patterns]].
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Pathfinding
|
## Pathfinding
|
||||||
|
|
||||||
Entities have built-in pathfinding via libtcod:
|
Entities use the grid's pathfinding capabilities:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# A* pathfinding to target (via Grid)
|
# A* pathfinding
|
||||||
path = grid.find_path(entity.x, entity.y, target_x, target_y)
|
path = grid.find_path(
|
||||||
# Returns list of (x, y) tuples, or empty if no path
|
(int(entity.grid_x), int(entity.grid_y)),
|
||||||
|
(target_x, target_y)
|
||||||
|
)
|
||||||
|
|
||||||
if path:
|
if path and len(path) > 0:
|
||||||
next_step = path[1] # path[0] is current position
|
next_step = path.walk() # Get next step as Vector
|
||||||
entity.pos = next_step
|
entity.grid_x = next_step.x
|
||||||
|
entity.grid_y = next_step.y
|
||||||
|
|
||||||
# Dijkstra for multi-target pathfinding
|
# Dijkstra for multi-target pathfinding
|
||||||
grid.compute_dijkstra(goal_x, goal_y)
|
dm = grid.get_dijkstra_map((goal_x, goal_y))
|
||||||
distance = grid.get_dijkstra_distance(entity.x, entity.y)
|
distance = dm.distance((entity.grid_x, entity.grid_y))
|
||||||
path = grid.get_dijkstra_path(entity.x, entity.y)
|
next_step = dm.step_from((int(entity.grid_x), int(entity.grid_y)))
|
||||||
```
|
```
|
||||||
|
|
||||||
Pathfinding respects `GridPoint.walkable` properties set on the grid.
|
Pathfinding respects `GridPoint.walkable` properties.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Performance Considerations
|
## Performance Considerations
|
||||||
|
|
||||||
### Current Performance (as of 2025-12-28)
|
|
||||||
|
|
||||||
| Operation | Performance | Notes |
|
| Operation | Performance | Notes |
|
||||||
|-----------|-------------|-------|
|
|-----------|-------------|-------|
|
||||||
| Entity Creation | ~90,000/sec | Sufficient for level generation |
|
| Entity Creation | ~90,000/sec | Sufficient for level generation |
|
||||||
| Iteration | ~9M reads/sec | Optimized iterators (#159) |
|
| Iteration | ~9M reads/sec | Optimized iterators (#159) |
|
||||||
| Spatial Query | 0.003ms | SpatialHash O(k) (#115) |
|
| Spatial Query | 0.003ms | SpatialHash O(k) (#115) |
|
||||||
| N×N Visibility (5000) | 2ms | 217× faster than O(n) |
|
| N x N Visibility (5000) | 2ms | 217x faster than O(n) |
|
||||||
|
|
||||||
### Recommendations
|
### Recommendations
|
||||||
|
|
||||||
1. **Use `entities_in_radius()` for AI** - O(k) queries instead of iterating all entities
|
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
|
2. **Batch visibility updates** - Compute FOV once after all moves, not per-move
|
||||||
3. **Use timer callbacks for AI** - Don't run expensive logic every frame
|
3. **Use Timer for AI** - Don't run expensive logic every frame
|
||||||
4. **Entity counts up to 5,000+** - SpatialHash makes large counts feasible
|
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
|
## Related Systems
|
||||||
|
|
||||||
- [[Grid-System]] - Spatial container for entities
|
- [[Grid-System]] - Spatial container for entities
|
||||||
- [[Grid-Interaction-Patterns]] - Click handling, selection, context menus
|
- [[Grid-Interaction-Patterns]] - Click handling, selection, context menus
|
||||||
- [[Animation-System]] - Smooth entity movement
|
- [[Animation-System]] - Smooth entity movement via `.animate()`
|
||||||
- [[Performance-and-Profiling]] - Entity performance metrics
|
- [[AI-and-Pathfinding]] - FOV, pathfinding, AI patterns
|
||||||
|
- [[Input-and-Events]] - Callback signatures for mouse events
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Last updated: 2025-12-28*
|
*Last updated: 2026-02-07*
|
||||||
Loading…
Add table
Add a link
Reference in a new issue