Update Grid-Rendering-Pipeline for chunk-based caching, layer system, FOV transition status
parent
6b212e8995
commit
0d178594ff
|
|
@ -2,518 +2,373 @@
|
|||
|
||||
## Overview
|
||||
|
||||
The Grid rendering pipeline is McRogueFace's most complex rendering subsystem. It handles viewport culling, zoom, entity rendering, and FOV overlays - all within a single `sf::RenderTexture` for optimal performance.
|
||||
The Grid rendering pipeline handles multi-layer tilemap rendering with chunk-based caching for optimal performance. It supports arbitrary numbers of rendering layers, viewport culling, zoom, and entity rendering.
|
||||
|
||||
**Parent Page:** [[Grid-System]]
|
||||
|
||||
**Related Pages:**
|
||||
- [[Performance-and-Profiling]] - ScopedTimer instrumentation points
|
||||
- [[Performance-and-Profiling]] - Metrics and profiling tools
|
||||
- [[Entity-Management]] - Entity rendering within grids
|
||||
- [[UI-Component-Hierarchy]] - Where Grid fits in render hierarchy
|
||||
- [[Grid-TCOD-Integration]] - FOV and pathfinding (partially in transition)
|
||||
|
||||
**Key Files:**
|
||||
- `src/UIGrid.cpp::render()` - Main rendering entry point (lines 97-310)
|
||||
- `src/GameEngine.cpp` - ProfilingMetrics struct for tracking render times
|
||||
- `src/Profiler.h` - ScopedTimer RAII helper
|
||||
- `src/UIGrid.cpp::render()` - Main rendering orchestration
|
||||
- `src/GridLayers.cpp` - ColorLayer and TileLayer rendering
|
||||
- `src/UIGrid.cpp::renderChunk()` - Per-chunk RenderTexture management
|
||||
|
||||
**Related Issues:**
|
||||
- [#116](../../issues/116) - Dirty flag system (avoid re-rendering static content)
|
||||
- [#115](../../issues/115) - SpatialHash for entity culling optimization
|
||||
- [#113](../../issues/113) - Batch operations API
|
||||
- [#123](../../issues/123) - Chunk-based Grid Rendering (Closed - Implemented)
|
||||
- [#148](../../issues/148) - Dirty Flag RenderTexture Caching (Closed - Implemented)
|
||||
- [#147](../../issues/147) - Dynamic Layer System (Closed - Implemented)
|
||||
- [#113](../../issues/113) - Batch Operations API (Open - includes FOV access discussion)
|
||||
- [#115](../../issues/115) - SpatialHash for entity culling (Open)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Layer-Based Rendering
|
||||
|
||||
Grids render content through **layers** rather than per-cell properties. Each layer is either a `ColorLayer` (solid colors) or `TileLayer` (texture sprites).
|
||||
|
||||
**Render order is determined by z_index:**
|
||||
- Layers with `z_index < 0` render **below** entities
|
||||
- Entities render at the z=0 boundary
|
||||
- Layers with `z_index >= 0` render **above** entities (overlays)
|
||||
|
||||
Within each group, lower z_index values render first (behind higher values).
|
||||
|
||||
```
|
||||
z_index: -3 -2 -1 0 +1 +2
|
||||
↓ ↓ ↓ ↓ ↓ ↓
|
||||
[background] [tiles] [ENTITIES] [fog] [UI overlay]
|
||||
```
|
||||
|
||||
**Implementation:** See `UIGrid::render()` which iterates layers in z_index order, inserting entity rendering at the 0 boundary.
|
||||
|
||||
### Chunk-Based Caching
|
||||
|
||||
Large grids are divided into **chunks** (regions of cells). Each chunk maintains its own `sf::RenderTexture` that caches the rendered result.
|
||||
|
||||
**Key concepts:**
|
||||
- Chunk size is implementation-defined (currently ~256 cells per dimension)
|
||||
- Only chunks intersecting the viewport are considered for rendering
|
||||
- Each chunk tracks whether its content is "dirty" (needs redraw)
|
||||
- Static content renders once, then the cached texture is reused
|
||||
|
||||
**Implementation:** See `UIGrid::renderChunk()` for chunk texture management.
|
||||
|
||||
### Dirty Flag Propagation
|
||||
|
||||
When layer content changes, only affected chunks are marked dirty:
|
||||
|
||||
1. `layer.set(x, y, value)` marks the containing chunk as dirty
|
||||
2. On next render, dirty chunks redraw to their RenderTexture
|
||||
3. Clean chunks simply blit their cached texture
|
||||
|
||||
This means a 1000x1000 grid with one changing cell redraws only ~1 chunk, not 1,000,000 cells.
|
||||
|
||||
**Implementation:** Dirty flags propagate through `UIGrid::markDirty()` and are checked in `UIGrid::render()`.
|
||||
|
||||
---
|
||||
|
||||
## Render Pipeline Stages
|
||||
|
||||
The pipeline executes in **4 stages** during each frame:
|
||||
### Stage 1: Viewport Calculation
|
||||
|
||||
```
|
||||
1. Viewport Calculation (culling bounds)
|
||||
↓
|
||||
2. Base Layer Rendering (tiles + background colors)
|
||||
↓
|
||||
3. Entity Layer Rendering (visible entities)
|
||||
↓
|
||||
4. FOV Overlay Rendering (discovered/visible states)
|
||||
```
|
||||
Calculate which chunks and cells are visible based on camera position, zoom, and grid dimensions.
|
||||
|
||||
All rendering happens into an `sf::RenderTexture`, then the final texture is drawn to the window in a single operation.
|
||||
**Key properties:**
|
||||
- `center` / `center_x`, `center_y` - Camera position in pixel coordinates (within grid space)
|
||||
- `zoom` - Scale factor (1.0 = normal, 2.0 = 2x magnification)
|
||||
- `size` - Viewport dimensions in screen pixels
|
||||
|
||||
**Implementation:** Viewport bounds calculated at start of `UIGrid::render()`.
|
||||
|
||||
### Stage 2: Below-Entity Layers
|
||||
|
||||
For each layer with `z_index < 0`, sorted by z_index:
|
||||
1. Determine which chunks intersect viewport
|
||||
2. For each visible chunk:
|
||||
- If dirty: redraw layer content to chunk's RenderTexture
|
||||
- Draw chunk's cached texture to output
|
||||
|
||||
### Stage 3: Entity Rendering
|
||||
|
||||
Entities render at the z=0 boundary:
|
||||
1. Iterate entity collection
|
||||
2. Cull entities outside viewport bounds
|
||||
3. Draw visible entity sprites at interpolated positions
|
||||
|
||||
**Note:** Entity culling is currently O(n). SpatialHash optimization planned in [#115](../../issues/115).
|
||||
|
||||
### Stage 4: Above-Entity Layers
|
||||
|
||||
For each layer with `z_index >= 0`, sorted by z_index:
|
||||
- Same chunk-based rendering as Stage 2
|
||||
- These layers appear as overlays (fog, highlights, UI elements)
|
||||
|
||||
### Stage 5: Final Compositing
|
||||
|
||||
All rendered content exists in the grid's output RenderTexture, which is drawn to the window in a single operation.
|
||||
|
||||
---
|
||||
|
||||
## Stage 1: Viewport Calculation
|
||||
## Performance Characteristics
|
||||
|
||||
### Purpose
|
||||
Calculate which cells are visible based on camera position, zoom, and grid size.
|
||||
**Static Grids:**
|
||||
- Near-zero CPU cost after initial render
|
||||
- Cached chunk textures reused frame-to-frame
|
||||
- Only viewport calculation and texture blitting
|
||||
|
||||
### Implementation
|
||||
```cpp
|
||||
// Get cell dimensions from texture
|
||||
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
||||
**Dynamic Grids:**
|
||||
- Cost proportional to number of dirty chunks
|
||||
- Single-cell changes affect only one chunk
|
||||
- Bulk operations should batch changes before render
|
||||
|
||||
// Camera center in grid coordinates
|
||||
float center_x_sq = center_x / cell_width;
|
||||
float center_y_sq = center_y / cell_height;
|
||||
**Large Grids:**
|
||||
- 1000x1000+ grids render efficiently
|
||||
- Only visible chunks processed
|
||||
- Memory scales with grid size (chunk textures)
|
||||
|
||||
// Viewport size in cells
|
||||
float width_sq = box.getSize().x / (cell_width * zoom);
|
||||
float height_sq = box.getSize().y / (cell_height * zoom);
|
||||
**Profiling:** Use `mcrfpy.getMetrics()` or the F3 overlay to see render times. See [[Performance-and-Profiling]].
|
||||
|
||||
// Visible cell range
|
||||
float left_edge = center_x_sq - (width_sq / 2.0);
|
||||
float top_edge = center_y_sq - (height_sq / 2.0);
|
||||
int x_limit = left_edge + width_sq + 2; // +2 for margin
|
||||
int y_limit = top_edge + height_sq + 2;
|
||||
```
|
||||
---
|
||||
|
||||
**Key Insight:** The +2 margin ensures partially-visible cells at viewport edges are rendered completely.
|
||||
## FOV and Perspective System (In Transition)
|
||||
|
||||
### Camera Properties
|
||||
**Current Status:** The FOV computation (`compute_fov()`, `is_in_fov()`) works correctly for pathfinding and visibility queries. However, the automatic fog-of-war overlay system is **not currently connected** to the new layer architecture.
|
||||
|
||||
- `center_x`, `center_y` - Camera position in pixel coordinates
|
||||
- `zoom` - Scale factor (1.0 = normal, 2.0 = 2x zoom)
|
||||
- `box.getSize()` - Viewport dimensions in pixels
|
||||
**What works:**
|
||||
- `grid.compute_fov(x, y, radius)` - Computes which cells are visible
|
||||
- `grid.is_in_fov(x, y)` - Queries visibility of a specific cell
|
||||
- Pathfinding uses walkable/transparent properties correctly
|
||||
|
||||
### Python API
|
||||
**What's in transition:**
|
||||
- Per-entity perspective (`UIGridPointState`) not yet exposed to Python
|
||||
- Automatic fog overlay rendering disconnected from layer system
|
||||
- Batch FOV data access being designed ([#113 discussion](../../issues/113))
|
||||
|
||||
**Current workaround - Manual fog layer:**
|
||||
|
||||
```python
|
||||
grid.center_x = player.x * cell_width # Follow player
|
||||
grid.center_y = player.y * cell_height
|
||||
grid.zoom = 2.0 # Zoom in
|
||||
# Create a fog overlay layer (positive z_index = above entities)
|
||||
fog_layer = grid.add_layer("color", z_index=1)
|
||||
|
||||
# After computing FOV, update fog colors
|
||||
grid.compute_fov(player.grid_x, player.grid_y, radius=10)
|
||||
|
||||
for x in range(grid.grid_x):
|
||||
for y in range(grid.grid_y):
|
||||
if not grid.is_in_fov(x, y):
|
||||
# Dim color for non-visible cells
|
||||
fog_layer.set(x, y, mcrfpy.Color(0, 0, 0, 192))
|
||||
else:
|
||||
# Transparent for visible cells
|
||||
fog_layer.set(x, y, mcrfpy.Color(0, 0, 0, 0))
|
||||
```
|
||||
|
||||
**Limitation:** This O(n²) iteration is inefficient for large grids. Batch operations ([#113](../../issues/113)) will address this with patterns like `cells_in_radius()` iterators.
|
||||
|
||||
---
|
||||
|
||||
## Stage 2: Base Layer Rendering
|
||||
## Layer Techniques
|
||||
|
||||
### Purpose
|
||||
Render the "ground" layer: background colors and tile sprites.
|
||||
### Multiple Overlay Layers
|
||||
|
||||
### Loop Structure
|
||||
```cpp
|
||||
// Profile this stage
|
||||
ScopedTimer gridTimer(Resources::game->metrics.gridRenderTime);
|
||||
|
||||
int cellsRendered = 0;
|
||||
for (int x = left_edge - 1; x < x_limit; x++) {
|
||||
for (int y = top_edge - 1; y < y_limit; y++) {
|
||||
// Calculate pixel position within renderTexture
|
||||
auto pixel_pos = sf::Vector2f(
|
||||
(x * cell_width - left_spritepixels) * zoom,
|
||||
(y * cell_height - top_spritepixels) * zoom
|
||||
);
|
||||
|
||||
auto gridpoint = at(x, y);
|
||||
|
||||
// 1. Draw background color
|
||||
r.setPosition(pixel_pos);
|
||||
r.setFillColor(gridpoint.color);
|
||||
renderTexture.draw(r);
|
||||
|
||||
// 2. Draw tile sprite (if texture exists and tilesprite set)
|
||||
if (ptex && gridpoint.tilesprite != -1) {
|
||||
sprite = ptex->sprite(gridpoint.tilesprite, pixel_pos,
|
||||
sf::Vector2f(zoom, zoom));
|
||||
renderTexture.draw(sprite);
|
||||
}
|
||||
|
||||
cellsRendered++;
|
||||
}
|
||||
}
|
||||
|
||||
// Record metrics for profiling overlay
|
||||
Resources::game->metrics.gridCellsRendered += cellsRendered;
|
||||
```
|
||||
|
||||
### Per-Cell Data
|
||||
|
||||
Each `UIGridPoint` has:
|
||||
- `color` - RGBA background color (drawn first)
|
||||
- `tilesprite` - Sprite index from texture sheet (-1 = no sprite)
|
||||
- `walkable` - Affects pathfinding (not used in rendering)
|
||||
|
||||
### Performance Characteristics
|
||||
|
||||
**Current Performance:**
|
||||
- 100x100 grid, ~1000 visible cells
|
||||
- Grid render time: 8-12ms (without dirty flags)
|
||||
- Target: <2ms after dirty flag optimization (#116)
|
||||
|
||||
**Bottlenecks:**
|
||||
- Individual `renderTexture.draw()` calls per cell
|
||||
- No caching of static content
|
||||
- Python/C++ boundary crossings for `at(x, y)` calls
|
||||
|
||||
---
|
||||
|
||||
## Stage 3: Entity Layer Rendering
|
||||
|
||||
### Purpose
|
||||
Render entities that exist on the grid at their interpolated positions.
|
||||
|
||||
### Loop Structure
|
||||
```cpp
|
||||
{
|
||||
ScopedTimer entityTimer(Resources::game->metrics.entityRenderTime);
|
||||
int entitiesRendered = 0;
|
||||
int totalEntities = entities->size();
|
||||
|
||||
for (auto e : *entities) {
|
||||
// Viewport culling: skip out-of-bounds entities
|
||||
if (e->position.x < left_edge - 1 ||
|
||||
e->position.x >= left_edge + width_sq + 1 ||
|
||||
e->position.y < top_edge - 1 ||
|
||||
e->position.y >= top_edge + height_sq + 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get entity's sprite
|
||||
auto& drawent = e->sprite;
|
||||
drawent.setScale(sf::Vector2f(zoom, zoom));
|
||||
|
||||
// Calculate pixel position (supports fractional positions)
|
||||
auto pixel_pos = sf::Vector2f(
|
||||
(e->position.x * cell_width - left_spritepixels) * zoom,
|
||||
(e->position.y * cell_height - top_spritepixels) * zoom
|
||||
);
|
||||
|
||||
// Render entity sprite
|
||||
drawent.render(pixel_pos, renderTexture);
|
||||
|
||||
entitiesRendered++;
|
||||
}
|
||||
|
||||
// Record metrics
|
||||
Resources::game->metrics.entitiesRendered += entitiesRendered;
|
||||
Resources::game->metrics.totalEntities += totalEntities;
|
||||
}
|
||||
```
|
||||
|
||||
### Entity Positions
|
||||
|
||||
Entities have **floating-point positions** (`entity.position.x`, `entity.position.y`), allowing smooth sub-cell movement:
|
||||
You can pre-create multiple overlay layers and toggle between them:
|
||||
|
||||
```python
|
||||
# Entity at fractional position
|
||||
entity.x = 10.5 # Halfway between cells 10 and 11
|
||||
entity.y = 20.75
|
||||
# Create several overlay options
|
||||
highlight_layer = grid.add_layer("color", z_index=1)
|
||||
danger_layer = grid.add_layer("color", z_index=2)
|
||||
fog_layer = grid.add_layer("color", z_index=3)
|
||||
|
||||
# Populate each with different data...
|
||||
# highlight_layer shows selected cells
|
||||
# danger_layer shows enemy threat zones
|
||||
# fog_layer shows visibility
|
||||
|
||||
# Toggle visibility to switch which overlay shows
|
||||
def show_danger_zones():
|
||||
highlight_layer.visible = False
|
||||
danger_layer.visible = True
|
||||
fog_layer.visible = False
|
||||
|
||||
def show_fog_of_war():
|
||||
highlight_layer.visible = False
|
||||
danger_layer.visible = False
|
||||
fog_layer.visible = True
|
||||
```
|
||||
|
||||
### Performance Considerations
|
||||
### Z-Index Reordering
|
||||
|
||||
**Current Performance:**
|
||||
- 50 entities: ~2-3ms render time
|
||||
- 500 entities: ~20-30ms (all visible)
|
||||
- With culling: only visible entities rendered
|
||||
|
||||
**Optimization Opportunities:**
|
||||
- **#115**: SpatialHash for O(1) visibility queries
|
||||
- **#117**: Memory pool to reduce allocation overhead
|
||||
- Sort by Z-order before rendering (not yet implemented)
|
||||
|
||||
---
|
||||
|
||||
## Stage 4: FOV Overlay Rendering
|
||||
|
||||
### Purpose
|
||||
If `perspective_enabled` is true, overlay cells based on entity's discovered/visible state.
|
||||
|
||||
### Three Visibility States
|
||||
|
||||
1. **Visible** - Entity can currently see this cell → No overlay
|
||||
2. **Discovered but not visible** - Entity has seen this cell before → Dark gray overlay (RGBA: 32, 32, 40, 192)
|
||||
3. **Undiscovered** - Entity has never seen this cell → Black overlay (RGBA: 0, 0, 0, 255)
|
||||
|
||||
### Loop Structure
|
||||
```cpp
|
||||
if (perspective_enabled) {
|
||||
ScopedTimer fovTimer(Resources::game->metrics.fovOverlayTime);
|
||||
auto entity = perspective_entity.lock(); // weak_ptr to entity
|
||||
|
||||
sf::RectangleShape overlay;
|
||||
overlay.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom));
|
||||
|
||||
if (entity) {
|
||||
// Valid entity - use its gridstate
|
||||
for (int x = left_edge - 1; x < x_limit; x++) {
|
||||
for (int y = top_edge - 1; y < y_limit; y++) {
|
||||
int idx = y * grid_x + x;
|
||||
const auto& state = entity->gridstate[idx];
|
||||
|
||||
overlay.setPosition(pixel_pos);
|
||||
|
||||
if (!state.discovered) {
|
||||
// Black - never seen
|
||||
overlay.setFillColor(sf::Color(0, 0, 0, 255));
|
||||
renderTexture.draw(overlay);
|
||||
} else if (!state.visible) {
|
||||
// Dark gray - seen before, not currently visible
|
||||
overlay.setFillColor(sf::Color(32, 32, 40, 192));
|
||||
renderTexture.draw(overlay);
|
||||
}
|
||||
// else: visible and discovered - no overlay
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Entity destroyed/invalid - all cells black
|
||||
// ... render full black overlays ...
|
||||
}
|
||||
}
|
||||
// else: omniscient view (perspective_enabled = false) - no overlays
|
||||
```
|
||||
|
||||
### Perspective System
|
||||
|
||||
See [[Grid-TCOD-Integration]] for details on how `compute_fov()` updates entity gridstate.
|
||||
|
||||
**Python API:**
|
||||
```python
|
||||
# Enable perspective view from entity's POV
|
||||
grid.set_perspective(player_entity)
|
||||
|
||||
# Disable perspective (omniscient view)
|
||||
grid.clear_perspective()
|
||||
|
||||
# Check if perspective enabled
|
||||
if grid.perspective_enabled:
|
||||
print("Using entity perspective")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Final Output
|
||||
|
||||
### RenderTexture to Window
|
||||
|
||||
After all 4 stages complete:
|
||||
|
||||
```cpp
|
||||
renderTexture.display(); // Finalize texture
|
||||
|
||||
// output sprite uses the renderTexture
|
||||
output.setTextureRect(sf::IntRect(0, 0, box.getSize().x, box.getSize().y));
|
||||
output.setPosition(box.getPosition() + offset);
|
||||
|
||||
// Single draw call to window
|
||||
target.draw(output);
|
||||
```
|
||||
|
||||
**Performance Benefit:** Entire grid (1000s of cells + entities + overlays) rendered to window in **one draw call**.
|
||||
|
||||
---
|
||||
|
||||
## Profiling Integration
|
||||
|
||||
The rendering pipeline is instrumented with `ScopedTimer` for F3 overlay:
|
||||
|
||||
```cpp
|
||||
void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) {
|
||||
ScopedTimer gridTimer(Resources::game->metrics.gridRenderTime);
|
||||
|
||||
// Stage 2: Base layer
|
||||
// ... rendering ...
|
||||
|
||||
// Stage 3: Entities
|
||||
{
|
||||
ScopedTimer entityTimer(Resources::game->metrics.entityRenderTime);
|
||||
// ... entity rendering ...
|
||||
}
|
||||
|
||||
// Stage 4: FOV overlay
|
||||
if (perspective_enabled) {
|
||||
ScopedTimer fovTimer(Resources::game->metrics.fovOverlayTime);
|
||||
// ... overlay rendering ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Press **F3** in-game to see:
|
||||
- Grid render time
|
||||
- Entity render time
|
||||
- FOV overlay time
|
||||
- Cells/entities rendered
|
||||
|
||||
See [[Performance-and-Profiling]] for details on using the profiler.
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimization Workflow
|
||||
|
||||
### Current Bottlenecks (October 2025)
|
||||
|
||||
1. **Grid re-renders every frame** even if static
|
||||
- **Solution**: #116 (Dirty flag system)
|
||||
- Expected improvement: 8-12ms → <2ms for static content
|
||||
|
||||
2. **Entity culling is O(n)**
|
||||
- **Solution**: #115 (SpatialHash)
|
||||
- Expected improvement: 10,000 entities with 50 visible → 1-2ms
|
||||
|
||||
3. **Individual draw calls per cell**
|
||||
- **Solution**: Vertex arrays or instanced rendering
|
||||
- Expected improvement: 50% reduction in draw call overhead
|
||||
|
||||
### Profiling a Grid Performance Issue
|
||||
|
||||
Example workflow using [[Performance-Optimization-Workflow]]:
|
||||
Change layer order at runtime by modifying z_index:
|
||||
|
||||
```python
|
||||
# 1. Create benchmark scene
|
||||
import mcrfpy
|
||||
# Bring a layer to front
|
||||
important_layer.z_index = 100
|
||||
|
||||
mcrfpy.createScene("test")
|
||||
grid = mcrfpy.Grid(grid_size=(100, 100), pos=(0, 0), size=(1024, 768))
|
||||
|
||||
# Fill grid with varied content
|
||||
for x in range(100):
|
||||
for y in range(100):
|
||||
cell = grid.at((x, y))
|
||||
cell.tilesprite = (x + y) % 10
|
||||
cell.color = (50 + x, 50 + y, 100, 255)
|
||||
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
ui.append(grid)
|
||||
mcrfpy.setScene("test")
|
||||
|
||||
# 2. Press F3 to see profiler overlay
|
||||
# 3. Check metrics:
|
||||
# - Grid render time: X ms
|
||||
# - Cells rendered: Y
|
||||
# - FPS: Z
|
||||
# Send a layer behind entities
|
||||
background_layer.z_index = -10
|
||||
```
|
||||
|
||||
**Interpreting Results:**
|
||||
- Grid render time > 10ms → Consider dirty flags
|
||||
- Entities rendered << total entities → Culling is working
|
||||
- FOV overlay time > 5ms → Large visible area or complex state
|
||||
**Note:** Changing z_index marks the layer dirty, triggering re-render of affected chunks.
|
||||
|
||||
### Constructor `layers={}` Limitations
|
||||
|
||||
The `layers={}` constructor argument is convenient but limited:
|
||||
|
||||
```python
|
||||
# This creates layers, but ALL get negative z_index (below entities)
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(50, 50),
|
||||
layers={"ground": "color", "terrain": "tile", "overlay": "color"}
|
||||
)
|
||||
# Result: ground=-3, terrain=-2, overlay=-1 (all below entities!)
|
||||
```
|
||||
|
||||
**For overlays above entities, use `add_layer()` explicitly:**
|
||||
|
||||
```python
|
||||
grid = mcrfpy.Grid(grid_size=(50, 50), layers={})
|
||||
ground = grid.add_layer("color", z_index=-2)
|
||||
terrain = grid.add_layer("tile", z_index=-1)
|
||||
overlay = grid.add_layer("color", z_index=1) # Above entities!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration from Legacy API
|
||||
|
||||
Prior to the layer system, grids had built-in per-cell rendering via `UIGridPoint` properties. Here's how to recreate that behavior:
|
||||
|
||||
### Legacy Pattern (Pre-November 2025)
|
||||
|
||||
```python
|
||||
# OLD API (no longer works):
|
||||
grid = mcrfpy.Grid(50, 50, 16, 16)
|
||||
cell = grid.at(x, y)
|
||||
cell.tilesprite = 42 # Sprite index
|
||||
cell.color = (255, 0, 0) # Background color
|
||||
```
|
||||
|
||||
### Equivalent Modern Pattern
|
||||
|
||||
```python
|
||||
# NEW API - explicit layers:
|
||||
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(400, 400))
|
||||
|
||||
# Grid creates a default TileLayer; access it:
|
||||
tile_layer = grid.layers[0]
|
||||
tile_layer.set(x, y, 42) # Sprite index
|
||||
|
||||
# For background colors, add a ColorLayer behind tiles:
|
||||
color_layer = grid.add_layer("color", z_index=-2) # Behind default tile layer
|
||||
color_layer.set(x, y, mcrfpy.Color(255, 0, 0))
|
||||
```
|
||||
|
||||
### Recreating Old Three-Layer Rendering
|
||||
|
||||
The legacy system rendered: background color → tile sprite → FOV overlay.
|
||||
|
||||
```python
|
||||
# Create grid with no default layers
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(50, 50),
|
||||
pos=(100, 100),
|
||||
size=(400, 400),
|
||||
texture=tileset,
|
||||
layers={}
|
||||
)
|
||||
|
||||
# Layer 1: Background colors (z=-2, behind everything)
|
||||
background = grid.add_layer("color", z_index=-2)
|
||||
|
||||
# Layer 2: Tile sprites (z=-1, above background, below entities)
|
||||
tiles = grid.add_layer("tile", z_index=-1, texture=tileset)
|
||||
|
||||
# Layer 3: FOV overlay (z=+1, above entities)
|
||||
fog = grid.add_layer("color", z_index=1)
|
||||
|
||||
# Now populate layers as needed
|
||||
background.fill(mcrfpy.Color(20, 20, 30)) # Dark blue background
|
||||
|
||||
for x in range(50):
|
||||
for y in range(50):
|
||||
tiles.set(x, y, calculate_tile_index(x, y))
|
||||
|
||||
# FOV overlay updated after compute_fov() calls
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Issue: Grid Renders Off-Center
|
||||
### Issue: Layer Changes Don't Appear
|
||||
|
||||
**Cause:** `center_x`/`center_y` values incorrect relative to camera follow logic.
|
||||
**Cause:** Layer content changed but chunk not marked dirty.
|
||||
|
||||
**Fix:**
|
||||
**Fix:** Use layer methods (`set()`, `fill()`) which automatically mark dirty. Direct property manipulation may bypass dirty flagging.
|
||||
|
||||
### Issue: Overlay Appears Behind Entities
|
||||
|
||||
**Cause:** Layer z_index is negative.
|
||||
|
||||
**Fix:** Use `z_index >= 0` for overlays:
|
||||
```python
|
||||
# Ensure center is in pixel coordinates, not cell coordinates
|
||||
grid.center_x = player.x * cell_width
|
||||
grid.center_y = player.y * cell_height
|
||||
overlay = grid.add_layer("color", z_index=1) # Not z_index=-1
|
||||
```
|
||||
|
||||
### Issue: Entities Flicker at Viewport Edge
|
||||
### Issue: Performance Degrades with Many Changes
|
||||
|
||||
**Cause:** Culling margin too small (entities disappear before fully off-screen).
|
||||
|
||||
**Fix:** The -1/+1 margins in culling logic should handle this, but if needed:
|
||||
```cpp
|
||||
// Increase margin from 1 to 2
|
||||
if (e->position.x < left_edge - 2 || e->position.x >= left_edge + width_sq + 2)
|
||||
```
|
||||
|
||||
### Issue: Zoom Breaks Entity Positions
|
||||
|
||||
**Cause:** `zoom` not applied consistently to entity pixel positions.
|
||||
|
||||
**Fix:** Ensure both entity position calculation AND sprite scale use zoom:
|
||||
```cpp
|
||||
auto pixel_pos = sf::Vector2f(
|
||||
(e->position.x * cell_width - left_spritepixels) * zoom,
|
||||
(e->position.y * cell_height - top_spritepixels) * zoom
|
||||
);
|
||||
drawent.setScale(sf::Vector2f(zoom, zoom));
|
||||
```
|
||||
|
||||
### Issue: FOV Overlay Has Gaps
|
||||
|
||||
**Cause:** `gridstate` not updated after `compute_fov()`.
|
||||
|
||||
**Fix:** See [[Grid-TCOD-Integration]] for proper FOV computation sequence.
|
||||
|
||||
---
|
||||
|
||||
## Future Improvements
|
||||
|
||||
### Dirty Flag System (#116)
|
||||
|
||||
Only re-render cells that changed:
|
||||
|
||||
```cpp
|
||||
// Proposed implementation
|
||||
bool grid_dirty = true; // Set when cells change
|
||||
|
||||
void UIGrid::render() {
|
||||
if (!grid_dirty && cachedTexture.valid()) {
|
||||
// Use cached texture
|
||||
target.draw(cachedOutput);
|
||||
return;
|
||||
}
|
||||
|
||||
// Full re-render
|
||||
// ... existing pipeline ...
|
||||
|
||||
grid_dirty = false;
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Static grids: 8-12ms → <1ms (just copy cached texture)
|
||||
- Animated grids: Only re-render changed cells
|
||||
|
||||
### Batch Cell Updates (#113)
|
||||
|
||||
Expose NumPy-style batch operations:
|
||||
**Cause:** Each `set()` call can mark a chunk dirty; many scattered changes = many chunk redraws.
|
||||
|
||||
**Fix:** Batch logically-related changes. For bulk updates, consider:
|
||||
```python
|
||||
# Current: O(n) Python/C++ crossings
|
||||
# Less efficient: 10,000 individual calls
|
||||
for x in range(100):
|
||||
for y in range(100):
|
||||
grid.at((x, y)).color = (255, 0, 0, 255)
|
||||
layer.set(x, y, value)
|
||||
|
||||
# Proposed: O(1) crossings
|
||||
grid.set_colors(colors_array) # NumPy array or Python list
|
||||
# More efficient when available: bulk fill
|
||||
layer.fill(mcrfpy.Color(0, 0, 0)) # Single operation
|
||||
```
|
||||
|
||||
### SpatialHash Entity Culling (#115)
|
||||
|
||||
Replace linear entity scan with spatial queries:
|
||||
|
||||
```cpp
|
||||
// Current: O(n)
|
||||
for (auto e : *entities) {
|
||||
if (in_viewport(e)) render(e);
|
||||
}
|
||||
|
||||
// Proposed: O(visible)
|
||||
auto visible_entities = spatial_hash.query(viewport_bounds);
|
||||
for (auto e : visible_entities) {
|
||||
render(e);
|
||||
}
|
||||
```
|
||||
Batch operations API ([#113](../../issues/113)) will provide more patterns.
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
## API Quick Reference
|
||||
|
||||
See [`docs/api_reference_dynamic.html`](../../src/branch/master/docs/api_reference_dynamic.html) for complete Grid API.
|
||||
**Grid Properties:**
|
||||
- `center`, `center_x`, `center_y` - Camera position (pixels in grid space)
|
||||
- `zoom` - Scale factor
|
||||
- `layers` - List of layer objects, sorted by z_index
|
||||
- `fill_color` - Grid background (behind all layers)
|
||||
|
||||
**Key Properties:**
|
||||
- `grid.center_x`, `grid.center_y` - Camera position (pixels)
|
||||
- `grid.zoom` - Zoom level (float, default 1.0)
|
||||
- `grid.perspective_enabled` - Read-only, check if FOV overlay active
|
||||
**Grid Methods:**
|
||||
- `add_layer(type, z_index, texture)` - Create new layer
|
||||
- `remove_layer(layer)` - Remove a layer
|
||||
|
||||
**Key Methods:**
|
||||
- `grid.set_perspective(entity)` - Enable FOV from entity's POV
|
||||
- `grid.clear_perspective()` - Disable FOV overlay
|
||||
- `grid.at((x, y))` - Get cell for modification
|
||||
**Layer Properties:**
|
||||
- `z_index` - Render order
|
||||
- `visible` - Show/hide layer
|
||||
- `grid_size` - Dimensions (read-only)
|
||||
|
||||
**Layer Methods:**
|
||||
- `set(x, y, value)` - Set cell (color or sprite index)
|
||||
- `at(x, y)` - Get cell value
|
||||
- `fill(value)` - Fill all cells
|
||||
|
||||
---
|
||||
|
||||
**Navigation:**
|
||||
- [[Grid-System]] - Parent page
|
||||
- [[Grid-System]] - Parent page, layer concepts
|
||||
- [[Grid-TCOD-Integration]] - FOV computation details
|
||||
- [[Grid-Entity-Lifecycle]] - How entities interact with rendering
|
||||
- [[Performance-and-Profiling]] - Using ScopedTimer and F3 overlay
|
||||
- [[Entity-Management]] - Entity rendering specifics
|
||||
- [[Performance-and-Profiling]] - Metrics and optimization
|
||||
- [[Entity-Management]] - Entity rendering
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2025-11-29*
|
||||
Loading…
Reference in New Issue