Update Grid System wiki with standalone layer API, current constructor, SpatialHash, callback signatures, Tiled/LDtk refs

John McCardle 2026-02-07 22:16:05 +00:00
commit cbf263d0f8

@ -1,245 +1,433 @@
# Grid System # Grid System
The Grid System is McRogueFace's core spatial container for roguelike game maps. It provides tilemap rendering, entity management, FOV (field of view), and pathfinding integration with libtcod. The Grid System is McRogueFace's core spatial container for roguelike game maps. It provides tilemap rendering, entity management, FOV (field of view), and pathfinding integration with libtcod.
## Quick Reference ## Quick Reference
**Related Issues:** **Related Issues:**
- [#113](../issues/113) - Batch Operations for Grid (Open - Tier 1) - [#113](../issues/113) - Batch Operations for Grid (Open - Tier 1)
- [#124](../issues/124) - Grid Point Animation (Open - Tier 1) - [#124](../issues/124) - Grid Point Animation (Open - Tier 1)
- [#150](../issues/150) - User-driven Layer Rendering (Closed - Implemented) - [#150](../issues/150) - User-driven Layer Rendering (Closed - Implemented)
- [#148](../issues/148) - Dirty Flag RenderTexture Caching (Closed - Implemented) - [#148](../issues/148) - Dirty Flag RenderTexture Caching (Closed - Implemented)
- [#147](../issues/147) - Dynamic Layer System (Closed - Implemented) - [#147](../issues/147) - Dynamic Layer System (Closed - Implemented)
- [#123](../issues/123) - Chunk-based Grid Rendering (Closed - Implemented) - [#123](../issues/123) - Chunk-based Grid Rendering (Closed - Implemented)
- [#115](../issues/115) - SpatialHash for Entity Queries (Closed - Implemented)
**Key Files:**
- `src/UIGrid.h` / `src/UIGrid.cpp` - Main grid implementation **Key Files:**
- `src/GridLayers.h` / `src/GridLayers.cpp` - ColorLayer and TileLayer - `src/UIGrid.h` / `src/UIGrid.cpp` - Main grid implementation
- `src/UIGridPoint.h` - Individual grid cell (walkability, transparency) - `src/GridLayers.h` / `src/GridLayers.cpp` - ColorLayer and TileLayer
- `src/UIGridPointState.h` - Per-entity perspective/knowledge - `src/UIGridPoint.h` - Individual grid cell (walkability, transparency)
- `src/UIEntity.h` / `src/UIEntity.cpp` - Entity system (lives on grid) - `src/UIGridPointState.h` - Per-entity perspective/knowledge
- `src/SpatialHash.h` / `src/SpatialHash.cpp` - Spatial indexing for entities
**API Reference:**
- See [mcrfpy.Grid](../docs/api_reference_dynamic.html#Grid) in generated API docs ---
- See [mcrfpy.Entity](../docs/api_reference_dynamic.html#Entity) in generated API docs
## Architecture Overview
## Architecture Overview
### Three-Layer Design
### Three-Layer Design
The Grid System uses a three-layer architecture for sophisticated roguelike features:
The Grid System uses a three-layer architecture for sophisticated roguelike features:
1. **Visual Layer** (Rendering Layers)
1. **Visual Layer** (Rendering Layers) - What's displayed: tile sprites, colors, overlays
- What's displayed: tile sprites, colors, overlays - Implemented via `ColorLayer` and `TileLayer` objects
- Implemented via `ColorLayer` and `TileLayer` objects - Multiple layers per grid with z_index ordering
- Multiple layers per grid with z_index ordering - Files: `src/GridLayers.h`, `src/GridLayers.cpp`
- Files: `src/GridLayers.h`, `src/GridLayers.cpp`
2. **World State Layer** (`TCODMap`)
2. **World State Layer** (`TCODMap`) - Physical properties: walkable, transparent
- Physical properties: walkable, transparent, cost - Used for pathfinding and FOV calculations
- Used for pathfinding and FOV calculations - Integration: libtcod via `src/UIGrid.cpp`
- Integration: libtcod via `src/UIGrid.cpp`
3. **Perspective Layer** (`UIGridPointState`)
3. **Perspective Layer** (`UIGridPointState`) - Per-entity knowledge: what each entity has seen/explored
- Per-entity knowledge: what each entity has seen/explored - Enables fog of war, asymmetric information
- Enables fog of war, asymmetric information - File: `src/UIGridPointState.h`
- File: `src/UIGridPointState.h`
- **Note:** Python access currently limited - see Known Issues ---
This architecture follows proven patterns from Caves of Qud, Cogmind, and DCSS. ## Creating a Grid
### Dynamic Layer System ```python
import mcrfpy
Grids support multiple rendering layers that can be added/removed at runtime:
# Basic grid (gets a default TileLayer at z_index=-1)
| Layer Type | Purpose | Methods | grid = mcrfpy.Grid(
|------------|---------|---------| grid_size=(50, 50), # 50x50 cells
| `ColorLayer` | Solid colors per cell (fog, highlights) | `set(x, y, color)`, `at(x, y)`, `fill(color)` | pos=(100, 100), # Screen position
| `TileLayer` | Texture sprites per cell (terrain, items) | `set(x, y, sprite_index)`, `at(x, y)`, `fill(index)` | size=(400, 400) # Viewport size in pixels
)
**z_index semantics:**
- Negative z_index: Renders **below** entities # Grid with specific layers passed at creation
- Zero or positive z_index: Renders **above** entities terrain = mcrfpy.TileLayer(name="terrain", z_index=-1)
- Lower z_index renders first (behind higher z_index) fog = mcrfpy.ColorLayer(name="fog", z_index=1)
grid = mcrfpy.Grid(
```python grid_size=(50, 50),
# Create grid with no default layers pos=(100, 100),
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(100, 100), size=(400, 400), layers={}) size=(400, 400),
layers=[terrain, fog]
# Add layers explicitly )
background = grid.add_layer("color", z_index=-2) # Furthest back
tiles = grid.add_layer("tile", z_index=-1) # Above background, below entities # Grid with no layers (add them later)
fog_overlay = grid.add_layer("color", z_index=1) # Above entities (fog of war) grid = mcrfpy.Grid(grid_size=(50, 50), pos=(100, 100), size=(400, 400), layers=[])
# Access existing layers # Add to scene
for layer in grid.layers: scene = mcrfpy.Scene("game")
print(f"{type(layer).__name__} at z={layer.z_index}") scene.children.append(grid)
mcrfpy.current_scene = scene
# Remove a layer ```
grid.remove_layer(fog_overlay)
``` ---
### Grid → Entity Relationship ## Layer System
**Entity Lifecycle:** Layers are standalone objects created independently, then added to grids:
- Entities live on exactly 0 or 1 grids
- `grid.entities.append(entity)` sets `entity.grid = grid` ### TileLayer
- `grid.entities.remove(entity)` sets `entity.grid = None`
- Entity removal handled automatically on grid destruction Renders per-cell sprite indices from a texture atlas:
**Spatial Queries:** ```python
- Currently: Linear iteration through entity list texture = mcrfpy.Texture("assets/kenney_tinydungeon.png")
- Planned: SpatialHash for O(1) lookups (see [#115](../issues/115))
# Create standalone layer
## Sub-Pages terrain = mcrfpy.TileLayer(name="terrain", z_index=-1, texture=texture)
- [[Grid-Rendering-Pipeline]] - How grid renders each frame (chunks, dirty flags, caching) # Add to grid
- [[Grid-TCOD-Integration]] - FOV, pathfinding, walkability grid.add_layer(terrain)
- [[Grid-Entity-Lifecycle]] - Entity creation, movement, removal
# Set individual tiles
## Common Tasks terrain.set((5, 3), 42) # Set sprite index at cell (5, 3)
index = terrain.at((5, 3)) # Get sprite index: 42
### Creating a Grid terrain.fill(0) # Fill entire layer with sprite 0
terrain.set((5, 3), -1) # Set to -1 for transparent (no tile drawn)
```python ```
import mcrfpy
### ColorLayer
# Create grid with default tile layer
grid = mcrfpy.Grid( Renders per-cell RGBA colors (fog of war, highlights, overlays):
grid_size=(50, 50), # 50x50 cells
pos=(100, 100), # Screen position ```python
size=(400, 400), # Viewport size in pixels fog = mcrfpy.ColorLayer(name="fog", z_index=1)
texture=my_texture # Texture for default TileLayer grid.add_layer(fog)
)
# Set individual cells
# Or create with specific layer configuration fog.set((5, 3), mcrfpy.Color(0, 0, 0, 200)) # Dark fog
grid = mcrfpy.Grid( fog.set((5, 3), mcrfpy.Color(0, 0, 0, 0)) # Clear (transparent)
grid_size=(50, 50), color = fog.at((5, 3)) # Get color
pos=(100, 100), fog.fill(mcrfpy.Color(0, 0, 0, 255)) # Fill entire layer black
size=(400, 400), ```
layers={"terrain": "tile", "highlights": "color"}
) ### z_index Semantics
# Or start empty and add layers manually - **Negative z_index:** Renders **below** entities
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(100, 100), size=(400, 400), layers={}) - **Zero or positive z_index:** Renders **above** entities
terrain = grid.add_layer("tile", z_index=-1, texture=my_texture) - Lower z_index renders first (behind higher z_index)
# Add to scene ```python
mcrfpy.sceneUI("game").append(grid) # Typical layer stack
``` background = mcrfpy.ColorLayer(name="bg", z_index=-3) # Furthest back
terrain = mcrfpy.TileLayer(name="terrain", z_index=-2) # Terrain tiles
### Setting Tile Properties items = mcrfpy.TileLayer(name="items", z_index=-1) # Items below entities
# --- entities render here (z_index = 0) ---
```python fog = mcrfpy.ColorLayer(name="fog", z_index=1) # Fog above entities
# Access layers for visual properties ```
tile_layer = grid.layers[0] # First layer (usually tiles)
tile_layer.set(x, y, 42) # Set sprite index at cell ### Managing Layers
color_layer = grid.add_layer("color", z_index=-2) ```python
color_layer.set(x, y, mcrfpy.Color(64, 64, 128)) # Blue tint # List all layers
for layer in grid.layers:
# Access grid point for world properties print(f"{type(layer).__name__} '{layer.name}' at z={layer.z_index}")
point = grid.at(x, y)
point.walkable = True # Can entities walk here? # Get layer by name
point.transparent = True # Can see through for FOV? terrain = grid.layer("terrain")
```
# Remove a layer
### FOV and Pathfinding grid.remove_layer(fog)
```
```python
# Compute field of view from position ---
grid.compute_fov(entity.grid_x, entity.grid_y, radius=10)
## Cell Properties (GridPoint)
# Check if cell is visible
if grid.is_in_fov(target_x, target_y): Each cell has world-state properties accessed via `grid.at(x, y)`:
print("Target is visible!")
```python
# A* pathfinding point = grid.at(10, 15)
path = grid.find_path(start_x, start_y, end_x, end_y) point.walkable = True # Can entities walk here?
point.transparent = True # Can see through for FOV?
# Dijkstra maps for AI
grid.compute_dijkstra([(goal_x, goal_y)]) # Read properties
distance = grid.get_dijkstra_distance(entity.grid_x, entity.grid_y) print(point.walkable) # True
path_to_goal = grid.get_dijkstra_path(entity.grid_x, entity.grid_y) print(point.transparent) # True
```
# List entities at this cell
### Mouse Events entities_here = point.entities # List of Entity objects
```
Grids support mouse interaction at both the grid level and cell level:
---
```python
# Grid-level events (screen coordinates) ## FOV (Field of View)
def on_grid_click(x, y, button):
print(f"Grid clicked at pixel ({x}, {y}), button {button}") ```python
# Set up transparent/opaque cells
grid.on_click = on_grid_click for x in range(50):
grid.on_enter = lambda: print("Mouse entered grid") for y in range(50):
grid.on_exit = lambda: print("Mouse left grid") grid.at(x, y).transparent = True
grid.on_move = lambda x, y: print(f"Mouse at ({x}, {y})")
# Mark walls as opaque
# Cell-level events (grid coordinates) grid.at(5, 5).transparent = False
def on_cell_click(grid_x, grid_y):
print(f"Cell ({grid_x}, {grid_y}) clicked!") # Compute FOV from position
point = grid.at(grid_x, grid_y) grid.compute_fov((10, 10), radius=8)
# Do something with the cell...
# Query visibility
grid.on_cell_click = on_cell_click if grid.is_in_fov((12, 14)):
grid.on_cell_enter = lambda x, y: print(f"Entered cell ({x}, {y})") print("Cell is visible!")
grid.on_cell_exit = lambda x, y: print(f"Left cell ({x}, {y})") ```
# Query currently hovered cell ### FOV Algorithms (`mcrfpy.FOV`)
if grid.hovered_cell:
hx, hy = grid.hovered_cell - `FOV.BASIC` - Simple raycasting
print(f"Hovering over ({hx}, {hy})") - `FOV.DIAMOND` - Diamond-shaped
``` - `FOV.SHADOW` - Shadow casting (recommended)
- `FOV.PERMISSIVE_0` through `FOV.PERMISSIVE_8` - Permissive variants
### Camera Control - `FOV.RESTRICTIVE` - Restrictive precise angle
```python ```python
# Center viewport on a position (pixel coordinates within grid space) grid.compute_fov((10, 10), radius=10, algorithm=mcrfpy.FOV.SHADOW)
grid.center = (player.grid_x * 16 + 8, player.grid_y * 16 + 8) ```
# Or set components individually ---
grid.center_x = player.grid_x * 16 + 8
grid.center_y = player.grid_y * 16 + 8 ## Pathfinding
# Zoom (1.0 = normal, 2.0 = 2x zoom in, 0.5 = zoom out) ### A* Pathfinding
grid.zoom = 1.5
``` ```python
# Set walkable cells
## Performance Characteristics for x in range(50):
for y in range(50):
**Implemented Optimizations:** grid.at(x, y).walkable = True
- **Chunk-based rendering** ([#123](../issues/123)): Large grids divided into chunks, only visible chunks rendered grid.at(5, 5).walkable = False # Wall
- **Dirty flag system** ([#148](../issues/148)): Layers track changes, skip redraw when unchanged
- **RenderTexture caching**: Each chunk cached to texture, reused until dirty # Find path (returns AStarPath object)
- **Viewport culling**: Only cells within viewport are processed path = grid.find_path((0, 0), (10, 10))
**Current Performance:** if path:
- Grids of 1000x1000+ cells render efficiently print(f"Path length: {len(path)} steps")
- Static scenes near-zero CPU (cached textures reused) print(f"Origin: {path.origin}")
- Entity rendering: O(visible entities) print(f"Destination: {path.destination}")
**Profiling:** Use the F3 overlay or `mcrfpy.setTimer()` with `mcrfpy.getMetrics()` to measure grid performance. See [[Performance-and-Profiling]]. # Iterate all steps
for step in path:
## Known Issues & Limitations print(f" Step: ({step.x}, {step.y})")
**Current Limitations:** # Or walk step-by-step
- **FOV Python access limited:** `compute_fov()` works but fog-of-war overlay must be managed manually via color layers. Per-entity perspective (`UIGridPointState`) not yet exposed to Python. See [#113 discussion](../issues/113). next_step = path.walk() # Advances and returns next Vector
- No tile-level animation yet (planned: [#124](../issues/124)) upcoming = path.peek() # Look at next step without advancing
- Entity spatial queries are O(n) (planned: SpatialHash [#115](../issues/115)) remaining = path.remaining # Steps remaining
```
**Workarounds:**
- For fog of war: Use a `ColorLayer` with positive z_index, update cell colors based on `is_in_fov()` results ### Dijkstra Maps
- For entity queries: Use list comprehension filtering on `grid.entities`
For multi-target or AI pathfinding:
## Related Systems
```python
- [[UI-Component-Hierarchy]] - Grid inherits from UIDrawable # Create Dijkstra map from a root position
- [[Animation-System]] - Grid properties are animatable (pos, zoom, center, etc.) dm = grid.get_dijkstra_map((5, 5))
- [[Performance-and-Profiling]] - Grid rendering instrumented with metrics
- [[Entity-Management]] - Entities live within Grid containers # Query distance from root to any position
distance = dm.distance((10, 10)) # Float distance
--- print(f"Distance: {distance}")
*Last updated: 2025-11-29* # Get path from a position back to the root
path = dm.path_from((10, 10)) # List of Vector objects
for step in path:
print(f" ({step.x}, {step.y})")
# Get single next step toward root
next_step = dm.step_from((10, 10)) # Single Vector
# Clear cached maps when grid changes
grid.clear_dijkstra_maps()
```
---
## Entity Management
Entities live on grids via the `entities` collection:
```python
# Create and add entity
player = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=0, name="player")
grid.entities.append(player)
print(player.grid == grid) # True
# Query entities
for entity in grid.entities:
print(f"{entity.name} at ({entity.grid_x}, {entity.grid_y})")
# Spatial queries (SpatialHash - O(k) complexity)
nearby = grid.entities_in_radius((10, 10), 5.0)
for entity in nearby:
print(f"Nearby: {entity.name}")
# Remove entity
player.die() # Removes from grid and SpatialHash
```
See [[Entity-Management]] for detailed entity documentation.
---
## Camera Control
```python
# Center viewport on pixel coordinates within grid space
grid.center = (player.grid_x * 16 + 8, player.grid_y * 16 + 8)
# Or set components individually
grid.center_x = player.grid_x * 16 + 8
grid.center_y = player.grid_y * 16 + 8
# Center on tile coordinates (convenience method)
grid.center_camera((14.5, 8.5)) # Centers on middle of tile (14, 8)
# Zoom (1.0 = normal, 2.0 = 2x zoom in, 0.5 = zoom out)
grid.zoom = 1.5
# Animate camera movement
grid.animate("center_x", target_x, 0.5, mcrfpy.Easing.EASE_IN_OUT)
grid.animate("center_y", target_y, 0.5, mcrfpy.Easing.EASE_IN_OUT)
grid.animate("zoom", 2.0, 0.3, mcrfpy.Easing.EASE_OUT_QUAD)
```
---
## Mouse Events
Grids support mouse interaction at both element and cell levels:
```python
# Element-level events (screen coordinates)
def on_grid_click(pos, button, action):
if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED:
print(f"Grid clicked at pixel ({pos.x}, {pos.y})")
grid.on_click = on_grid_click
grid.on_enter = lambda pos: print("Mouse entered grid")
grid.on_exit = lambda pos: print("Mouse left grid")
# Cell-level events (grid coordinates)
def on_cell_click(cell_pos, button, action):
if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED:
x, y = int(cell_pos.x), int(cell_pos.y)
point = grid.at(x, y)
point.walkable = not point.walkable # Toggle walkability
grid.on_cell_click = on_cell_click
grid.on_cell_enter = lambda cell_pos: highlight_cell(cell_pos)
grid.on_cell_exit = lambda cell_pos: clear_highlight(cell_pos)
# Query currently hovered cell
if grid.hovered_cell:
hx, hy = grid.hovered_cell
print(f"Hovering over ({hx}, {hy})")
```
See [[Input-and-Events]] for callback signature details.
---
## Perspective System
Set a perspective entity to enable FOV-based rendering:
```python
# Set perspective (fog of war from this entity's viewpoint)
grid.perspective = player_entity
# Disable perspective (show everything)
grid.perspective = None
```
---
## Level Import Integration
Grids integrate with external level editors via Tiled and LDtk import systems:
### Tiled Import
```python
tileset = mcrfpy.TileSetFile("assets/dungeon.tsj")
tilemap = mcrfpy.TileMapFile("assets/level1.tmj")
# Apply tilemap layers to grid
for layer_data in tilemap.layers:
tile_layer = mcrfpy.TileLayer(name=layer_data.name, z_index=-1, texture=tileset.texture)
grid.add_layer(tile_layer)
# ... apply tile data
```
### Wang Tile Terrain Generation
```python
wang_set = tileset.wang_set("Terrain")
terrain_data = wang_set.resolve(intgrid) # Resolve terrain to tile indices
wang_set.apply(tile_layer, terrain_data) # Apply to layer
```
---
## Performance Characteristics
**Implemented Optimizations:**
- **Chunk-based rendering** ([#123](../issues/123)): Large grids divided into chunks
- **Dirty flag system** ([#148](../issues/148)): Layers track changes, skip redraw when unchanged
- **RenderTexture caching**: Each chunk cached to texture, reused until dirty
- **Viewport culling**: Only cells within viewport are processed
- **SpatialHash** ([#115](../issues/115)): O(k) entity queries instead of O(n)
**Current Performance:**
- Grids of 1000x1000+ cells render efficiently
- Static scenes near-zero CPU (cached textures reused)
- Entity queries: O(k) where k is nearby entities (not total)
---
## Grid Properties Reference
| Property | Type | Description |
|----------|------|-------------|
| `grid_size` | `(int, int)` | Grid dimensions (read-only) |
| `grid_w`, `grid_h` | int | Width/height in cells (read-only) |
| `pos` | Vector | Screen position |
| `size` | Vector | Viewport size in pixels |
| `center` | Vector | Camera center (pixel coordinates) |
| `center_x`, `center_y` | float | Camera center components |
| `zoom` | float | Camera zoom level |
| `fill_color` | Color | Background color |
| `perspective` | Entity or None | FOV perspective entity |
| `entities` | EntityCollection | Entities on this grid |
| `layers` | list | Rendering layers (sorted by z_index) |
| `children` | UICollection | UI overlays |
| `hovered_cell` | `(x, y)` or None | Currently hovered cell (read-only) |
---
## Related Systems
- [[Entity-Management]] - Entities live within Grid containers
- [[Grid-Rendering-Pipeline]] - How grid renders each frame
- [[Grid-TCOD-Integration]] - FOV, pathfinding, walkability details
- [[Grid-Interaction-Patterns]] - Click handling, selection, context menus
- [[Animation-System]] - Grid properties are animatable (pos, zoom, center)
- [[Input-and-Events]] - Mouse callback signatures
---
*Last updated: 2026-02-07*