Table of Contents
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.
Quick Reference
Related Issues:
- #113 - Batch Operations for Grid (Open - Tier 1)
- #124 - Grid Point Animation (Open - Tier 1)
- #150 - User-driven Layer Rendering (Closed - Implemented)
- #148 - Dirty Flag RenderTexture Caching (Closed - Implemented)
- #147 - Dynamic Layer System (Closed - Implemented)
- #123 - Chunk-based Grid Rendering (Closed - Implemented)
- #115 - SpatialHash for Entity Queries (Closed - Implemented)
Key Files:
src/UIGrid.h/src/UIGrid.cpp- Main grid implementationsrc/GridLayers.h/src/GridLayers.cpp- ColorLayer and TileLayersrc/UIGridPoint.h- Individual grid cell (walkability, transparency)src/UIGridPointState.h- Per-entity perspective/knowledgesrc/SpatialHash.h/src/SpatialHash.cpp- Spatial indexing for entities
Architecture Overview
Three-Layer Design
The Grid System uses a three-layer architecture for sophisticated roguelike features:
-
Visual Layer (Rendering Layers)
- What's displayed: tile sprites, colors, overlays
- Implemented via
ColorLayerandTileLayerobjects - Multiple layers per grid with z_index ordering
- Files:
src/GridLayers.h,src/GridLayers.cpp
-
World State Layer (
TCODMap)- Physical properties: walkable, transparent
- Used for pathfinding and FOV calculations
- Integration: libtcod via
src/UIGrid.cpp
-
Perspective Layer (
UIGridPointState)- Per-entity knowledge: what each entity has seen/explored
- Enables fog of war, asymmetric information
- File:
src/UIGridPointState.h
Creating a Grid
import mcrfpy
# Basic grid (gets a default TileLayer at z_index=-1)
grid = mcrfpy.Grid(
grid_size=(50, 50), # 50x50 cells
pos=(100, 100), # Screen position
size=(400, 400) # Viewport size in pixels
)
# Grid with specific layers passed at creation
terrain = mcrfpy.TileLayer(name="terrain", z_index=-1)
fog = mcrfpy.ColorLayer(name="fog", z_index=1)
grid = mcrfpy.Grid(
grid_size=(50, 50),
pos=(100, 100),
size=(400, 400),
layers=[terrain, fog]
)
# Grid with no layers (add them later)
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(100, 100), size=(400, 400), layers=[])
# Add to scene
scene = mcrfpy.Scene("game")
scene.children.append(grid)
mcrfpy.current_scene = scene
Layer System
Layers are standalone objects created independently, then added to grids:
TileLayer
Renders per-cell sprite indices from a texture atlas:
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png")
# Create standalone layer
terrain = mcrfpy.TileLayer(name="terrain", z_index=-1, texture=texture)
# Add to grid
grid.add_layer(terrain)
# Set individual tiles
terrain.set((5, 3), 42) # Set sprite index at cell (5, 3)
index = terrain.at((5, 3)) # Get sprite index: 42
terrain.fill(0) # Fill entire layer with sprite 0
terrain.set((5, 3), -1) # Set to -1 for transparent (no tile drawn)
ColorLayer
Renders per-cell RGBA colors (fog of war, highlights, overlays):
fog = mcrfpy.ColorLayer(name="fog", z_index=1)
grid.add_layer(fog)
# Set individual cells
fog.set((5, 3), mcrfpy.Color(0, 0, 0, 200)) # Dark fog
fog.set((5, 3), mcrfpy.Color(0, 0, 0, 0)) # Clear (transparent)
color = fog.at((5, 3)) # Get color
fog.fill(mcrfpy.Color(0, 0, 0, 255)) # Fill entire layer black
z_index Semantics
- Negative z_index: Renders below entities
- Zero or positive z_index: Renders above entities
- Lower z_index renders first (behind higher z_index)
# Typical layer stack
background = mcrfpy.ColorLayer(name="bg", z_index=-3) # Furthest back
terrain = mcrfpy.TileLayer(name="terrain", z_index=-2) # Terrain tiles
items = mcrfpy.TileLayer(name="items", z_index=-1) # Items below entities
# --- entities render here (z_index = 0) ---
fog = mcrfpy.ColorLayer(name="fog", z_index=1) # Fog above entities
Managing Layers
# List all layers
for layer in grid.layers:
print(f"{type(layer).__name__} '{layer.name}' at z={layer.z_index}")
# Get layer by name
terrain = grid.layer("terrain")
# Remove a layer
grid.remove_layer(fog)
Cell Properties (GridPoint)
Each cell has world-state properties accessed via grid.at(x, y):
point = grid.at(10, 15)
point.walkable = True # Can entities walk here?
point.transparent = True # Can see through for FOV?
# Read properties
print(point.walkable) # True
print(point.transparent) # True
# List entities at this cell
entities_here = point.entities # List of Entity objects
FOV (Field of View)
# Set up transparent/opaque cells
for x in range(50):
for y in range(50):
grid.at(x, y).transparent = True
# Mark walls as opaque
grid.at(5, 5).transparent = False
# Compute FOV from position
grid.compute_fov((10, 10), radius=8)
# Query visibility
if grid.is_in_fov((12, 14)):
print("Cell is visible!")
FOV Algorithms (mcrfpy.FOV)
FOV.BASIC- Simple raycastingFOV.DIAMOND- Diamond-shapedFOV.SHADOW- Shadow casting (recommended)FOV.PERMISSIVE_0throughFOV.PERMISSIVE_8- Permissive variantsFOV.RESTRICTIVE- Restrictive precise angle
grid.compute_fov((10, 10), radius=10, algorithm=mcrfpy.FOV.SHADOW)
Pathfinding
A* Pathfinding
# Set walkable cells
for x in range(50):
for y in range(50):
grid.at(x, y).walkable = True
grid.at(5, 5).walkable = False # Wall
# Find path (returns AStarPath object)
path = grid.find_path((0, 0), (10, 10))
if path:
print(f"Path length: {len(path)} steps")
print(f"Origin: {path.origin}")
print(f"Destination: {path.destination}")
# Iterate all steps
for step in path:
print(f" Step: ({step.x}, {step.y})")
# Or walk step-by-step
next_step = path.walk() # Advances and returns next Vector
upcoming = path.peek() # Look at next step without advancing
remaining = path.remaining # Steps remaining
Dijkstra Maps
For multi-target or AI pathfinding:
# Create Dijkstra map from a root position
dm = grid.get_dijkstra_map((5, 5))
# Query distance from root to any position
distance = dm.distance((10, 10)) # Float distance
print(f"Distance: {distance}")
# 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:
# 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
# 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:
# 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:
# 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
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
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): Large grids divided into chunks
- Dirty flag system (#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): 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