Add "Entity-Management"
parent
ea76c22954
commit
ef13428826
|
|
@ -0,0 +1,418 @@
|
||||||
|
# Entity Management
|
||||||
|
|
||||||
|
Creating, positioning, and managing game entities that live on grids.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
**System:** [[Grid-System]]
|
||||||
|
|
||||||
|
**Key Types:**
|
||||||
|
- `mcrfpy.Entity` - Game entities on grids
|
||||||
|
- `mcrfpy.Grid` - Spatial container for entities
|
||||||
|
- `grid.entities` - EntityCollection
|
||||||
|
|
||||||
|
**API Reference:** [mcrfpy.Entity](../../docs/api_reference_dynamic.html#Entity)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What are Entities?
|
||||||
|
|
||||||
|
Entities are game objects that:
|
||||||
|
- Live on a Grid (0 or 1 grid per entity)
|
||||||
|
- Have a sprite for rendering
|
||||||
|
- Have grid position (integer coordinates)
|
||||||
|
- Can have AI, pathfinding, field of view
|
||||||
|
- Represent players, enemies, items, etc.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/UIEntity.h` / `src/UIEntity.cpp`
|
||||||
|
- `src/UIEntityCollection.h` / `.cpp`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Creating Entities
|
||||||
|
|
||||||
|
### Basic Entity Creation
|
||||||
|
|
||||||
|
```python
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
# Create grid first
|
||||||
|
grid = mcrfpy.Grid(50, 50, 16, 16)
|
||||||
|
grid.texture = mcrfpy.createTexture("tiles.png")
|
||||||
|
|
||||||
|
# Create entity at position (10, 10) with sprite 0
|
||||||
|
player = mcrfpy.Entity(10, 10, 0)
|
||||||
|
player.sprite.texture = mcrfpy.createTexture("entities.png")
|
||||||
|
|
||||||
|
# Add entity to grid
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Grid is now set automatically
|
||||||
|
assert player.grid == grid
|
||||||
|
```
|
||||||
|
|
||||||
|
### Entity Properties
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Position (grid coordinates)
|
||||||
|
player.x = 15
|
||||||
|
player.y = 20
|
||||||
|
player.pos = (15, 20) # Tuple alternative
|
||||||
|
|
||||||
|
# Sprite (what to display)
|
||||||
|
player.sprite_index = 5 # Index in texture
|
||||||
|
player.sprite.scale_x = 1.5
|
||||||
|
player.sprite.scale_y = 1.5
|
||||||
|
|
||||||
|
# Visibility
|
||||||
|
player.visible = True
|
||||||
|
player.sprite.opacity = 0.8
|
||||||
|
|
||||||
|
# Grid association
|
||||||
|
current_grid = player.grid # Read-only
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Entity Lifecycle
|
||||||
|
|
||||||
|
### Adding to Grid
|
||||||
|
|
||||||
|
```python
|
||||||
|
grid.entities.append(entity)
|
||||||
|
|
||||||
|
# Automatically:
|
||||||
|
# - entity.grid = grid
|
||||||
|
# - Entity rendered with grid
|
||||||
|
# - Entity included in spatial queries
|
||||||
|
```
|
||||||
|
|
||||||
|
### Removing from Grid
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Method 1: Direct removal
|
||||||
|
grid.entities.remove(entity)
|
||||||
|
|
||||||
|
# Method 2: Entity.die() (planned #30)
|
||||||
|
# entity.die() # Will call grid.entities.remove(entity)
|
||||||
|
|
||||||
|
# Automatically:
|
||||||
|
# - entity.grid = None
|
||||||
|
# - Entity no longer rendered
|
||||||
|
# - Entity excluded from queries
|
||||||
|
```
|
||||||
|
|
||||||
|
### Moving Entities
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Method 1: Direct assignment
|
||||||
|
entity.x = new_x
|
||||||
|
entity.y = new_y
|
||||||
|
|
||||||
|
# Method 2: Tuple assignment
|
||||||
|
entity.pos = (new_x, new_y)
|
||||||
|
|
||||||
|
# Method 3: Animation (smooth movement)
|
||||||
|
mcrfpy.animate(entity, "x", target_x, 500, "ease_in_out_quad")
|
||||||
|
mcrfpy.animate(entity, "y", target_y, 500, "ease_in_out_quad")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Collision Detection:** Manual - check grid cells before moving
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Entity Collections
|
||||||
|
|
||||||
|
### Iteration
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Iterate all entities on grid
|
||||||
|
for entity in grid.entities:
|
||||||
|
print(f"Entity at ({entity.x}, {entity.y})")
|
||||||
|
|
||||||
|
# Count entities
|
||||||
|
num_entities = len(grid.entities)
|
||||||
|
|
||||||
|
# Index access
|
||||||
|
first_entity = grid.entities[0]
|
||||||
|
last_entity = grid.entities[-1]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Querying
|
||||||
|
|
||||||
|
**Current:** O(n) iteration - no spatial indexing
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Find entities at position
|
||||||
|
def entities_at(grid, x, y):
|
||||||
|
result = []
|
||||||
|
for entity in grid.entities:
|
||||||
|
if entity.x == x and entity.y == y:
|
||||||
|
result.append(entity)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Find entities in radius
|
||||||
|
def entities_in_radius(grid, center_x, center_y, radius):
|
||||||
|
result = []
|
||||||
|
for entity in grid.entities:
|
||||||
|
dx = entity.x - center_x
|
||||||
|
dy = entity.y - center_y
|
||||||
|
if (dx*dx + dy*dy) <= radius*radius:
|
||||||
|
result.append(entity)
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization:** [#115](../../issues/115) - SpatialHash for O(1) queries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Player Entity
|
||||||
|
|
||||||
|
```python
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
# Create player
|
||||||
|
player = mcrfpy.Entity(25, 25, 0)
|
||||||
|
player.sprite.texture = player_texture
|
||||||
|
player.sprite_index = 0
|
||||||
|
|
||||||
|
# Add to grid
|
||||||
|
game_grid.entities.append(player)
|
||||||
|
|
||||||
|
# Movement with collision
|
||||||
|
def move_player(dx, dy):
|
||||||
|
new_x = player.x + dx
|
||||||
|
new_y = player.y + dy
|
||||||
|
|
||||||
|
# Check walkable
|
||||||
|
if game_grid.walkable((new_x, new_y)):
|
||||||
|
# Check for other entities
|
||||||
|
if not entities_at(game_grid, new_x, new_y):
|
||||||
|
player.pos = (new_x, new_y)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Input handling
|
||||||
|
def on_keypress(key, pressed):
|
||||||
|
if pressed:
|
||||||
|
if key == mcrfpy.Key.Up:
|
||||||
|
move_player(0, -1)
|
||||||
|
elif key == mcrfpy.Key.Down:
|
||||||
|
move_player(0, 1)
|
||||||
|
elif key == mcrfpy.Key.Left:
|
||||||
|
move_player(-1, 0)
|
||||||
|
elif key == mcrfpy.Key.Right:
|
||||||
|
move_player(1, 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Enemy Entities
|
||||||
|
|
||||||
|
```python
|
||||||
|
import random
|
||||||
|
|
||||||
|
class Enemy:
|
||||||
|
def __init__(self, x, y):
|
||||||
|
self.entity = mcrfpy.Entity(x, y, 1)
|
||||||
|
self.entity.sprite.texture = enemy_texture
|
||||||
|
self.health = 100
|
||||||
|
self.aggro_range = 10
|
||||||
|
|
||||||
|
def update(self, player):
|
||||||
|
# Calculate distance to player
|
||||||
|
dx = player.x - self.entity.x
|
||||||
|
dy = player.y - self.entity.y
|
||||||
|
dist = (dx*dx + dy*dy) ** 0.5
|
||||||
|
|
||||||
|
if dist < self.aggro_range:
|
||||||
|
# Chase player
|
||||||
|
self.move_toward(player.x, player.y)
|
||||||
|
else:
|
||||||
|
# Wander randomly
|
||||||
|
self.wander()
|
||||||
|
|
||||||
|
def move_toward(self, target_x, target_y):
|
||||||
|
# Simple movement (pathfinding in [[AI-and-Pathfinding]])
|
||||||
|
if target_x > self.entity.x:
|
||||||
|
self.entity.x += 1
|
||||||
|
elif target_x < self.entity.x:
|
||||||
|
self.entity.x -= 1
|
||||||
|
elif target_y > self.entity.y:
|
||||||
|
self.entity.y += 1
|
||||||
|
elif target_y < self.entity.y:
|
||||||
|
self.entity.y -= 1
|
||||||
|
|
||||||
|
def wander(self):
|
||||||
|
# Random movement
|
||||||
|
dx = random.choice([-1, 0, 1])
|
||||||
|
dy = random.choice([-1, 0, 1])
|
||||||
|
|
||||||
|
new_x = self.entity.x + dx
|
||||||
|
new_y = self.entity.y + dy
|
||||||
|
|
||||||
|
if game_grid.walkable((new_x, new_y)):
|
||||||
|
self.entity.pos = (new_x, new_y)
|
||||||
|
|
||||||
|
# Spawn enemies
|
||||||
|
enemies = []
|
||||||
|
for i in range(10):
|
||||||
|
enemy = Enemy(random.randint(0, 49), random.randint(0, 49))
|
||||||
|
game_grid.entities.append(enemy.entity)
|
||||||
|
enemies.append(enemy)
|
||||||
|
|
||||||
|
# Update all enemies
|
||||||
|
def update_enemies():
|
||||||
|
for enemy in enemies:
|
||||||
|
enemy.update(player)
|
||||||
|
|
||||||
|
mcrfpy.setTimer("enemy_ai", lambda ms: update_enemies(), 500) # Every 0.5s
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Item Entities
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Item:
|
||||||
|
def __init__(self, x, y, item_type):
|
||||||
|
self.entity = mcrfpy.Entity(x, y, 10 + item_type)
|
||||||
|
self.entity.sprite.texture = item_texture
|
||||||
|
self.type = item_type # 0=potion, 1=key, 2=coin, etc
|
||||||
|
self.pickupable = True
|
||||||
|
|
||||||
|
def on_pickup(self, player):
|
||||||
|
# Add to inventory
|
||||||
|
player.inventory.append(self.type)
|
||||||
|
|
||||||
|
# Remove from grid
|
||||||
|
if self.entity.grid:
|
||||||
|
self.entity.grid.entities.remove(self.entity)
|
||||||
|
|
||||||
|
# Check for item pickup
|
||||||
|
def check_pickup(player):
|
||||||
|
items_here = entities_at(game_grid, player.x, player.y)
|
||||||
|
for entity in items_here:
|
||||||
|
# Find corresponding Item object
|
||||||
|
for item in all_items:
|
||||||
|
if item.entity == entity and item.pickupable:
|
||||||
|
item.on_pickup(player)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Entity-Grid Relationship
|
||||||
|
|
||||||
|
### Lifecycle
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Entity created (not on grid)
|
||||||
|
entity.grid == None
|
||||||
|
|
||||||
|
2. Entity added to grid
|
||||||
|
grid.entities.append(entity)
|
||||||
|
→ entity.grid = grid
|
||||||
|
|
||||||
|
3. Entity removed from grid
|
||||||
|
grid.entities.remove(entity)
|
||||||
|
→ entity.grid = None
|
||||||
|
|
||||||
|
4. Entity destroyed
|
||||||
|
Python GC handles cleanup
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** Entities can only be on 0 or 1 grids at a time.
|
||||||
|
|
||||||
|
### Multiple Grids
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Move entity between grids
|
||||||
|
def transfer_entity(entity, from_grid, to_grid, new_x, new_y):
|
||||||
|
# Remove from old grid
|
||||||
|
if entity.grid:
|
||||||
|
entity.grid.entities.remove(entity)
|
||||||
|
|
||||||
|
# Update position
|
||||||
|
entity.pos = (new_x, new_y)
|
||||||
|
|
||||||
|
# Add to new grid
|
||||||
|
to_grid.entities.append(entity)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Current Limitations
|
||||||
|
|
||||||
|
**Entity Iteration:** O(n) for spatial queries
|
||||||
|
- Finding entities at position: Must check all entities
|
||||||
|
- Finding entities in radius: Must check all entities
|
||||||
|
- Bottleneck: 500+ entities
|
||||||
|
|
||||||
|
**Workarounds:**
|
||||||
|
- Limit entity count (< 200 for best performance)
|
||||||
|
- Update entities in batches
|
||||||
|
- Use timer callbacks instead of per-frame updates
|
||||||
|
|
||||||
|
### Planned Optimizations
|
||||||
|
|
||||||
|
**[#115](../../issues/115): SpatialHash**
|
||||||
|
- O(1) entity lookup by position
|
||||||
|
- O(k) radius queries (k = nearby entities)
|
||||||
|
- Expected: 100x+ improvement
|
||||||
|
|
||||||
|
**[#117](../../issues/117): Memory Pool**
|
||||||
|
- Reuse entity objects instead of allocate/destroy
|
||||||
|
- Reduced memory fragmentation
|
||||||
|
- Faster spawning
|
||||||
|
|
||||||
|
**[#116](../../issues/116): Dirty Flags**
|
||||||
|
- Only re-render grid when entities move
|
||||||
|
- Significant improvement for static entities
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration with Other Systems
|
||||||
|
|
||||||
|
### With Pathfinding
|
||||||
|
|
||||||
|
See [[AI-and-Pathfinding]] for:
|
||||||
|
- `entity.path_to(x, y)` - A* pathfinding
|
||||||
|
- Dijkstra maps for AI
|
||||||
|
- Path following
|
||||||
|
|
||||||
|
### With FOV
|
||||||
|
|
||||||
|
See [[AI-and-Pathfinding]] for:
|
||||||
|
- Per-entity field of view
|
||||||
|
- `grid.perspective` property
|
||||||
|
- Fog of war
|
||||||
|
|
||||||
|
### With Animation
|
||||||
|
|
||||||
|
See [[Animation-System]] for:
|
||||||
|
- Smooth entity movement
|
||||||
|
- Sprite animation
|
||||||
|
- Visual effects
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Animate entity movement
|
||||||
|
mcrfpy.animate(entity, "x", target_x, 300, "ease_out_quad")
|
||||||
|
mcrfpy.animate(entity, "y", target_y, 300, "ease_out_quad")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [[Grid-System]] - Entity spatial container
|
||||||
|
- [[AI-and-Pathfinding]] - Entity AI and pathfinding
|
||||||
|
- [[Animation-System]] - Animating entity movement
|
||||||
|
- [[UI-Component-Hierarchy]] - Entity as UIDrawable
|
||||||
|
|
||||||
|
**Tutorial:** [McRogueFace Does The Entire Roguelike Tutorial](../../roguelike_tutorial/) - Parts 2-6 cover entity management
|
||||||
|
|
||||||
|
**Open Issues:**
|
||||||
|
- [#30](../../issues/30) - Entity.die() method
|
||||||
|
- [#115](../../issues/115) - SpatialHash for fast queries
|
||||||
|
- [#117](../../issues/117) - Memory pool for entities
|
||||||
Loading…
Reference in New Issue