2 Grid System
John McCardle edited this page 2026-02-07 22:16:05 +00:00

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 implementation
  • src/GridLayers.h / src/GridLayers.cpp - ColorLayer and TileLayer
  • src/UIGridPoint.h - Individual grid cell (walkability, transparency)
  • src/UIGridPointState.h - Per-entity perspective/knowledge
  • src/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:

  1. Visual Layer (Rendering Layers)

    • What's displayed: tile sprites, colors, overlays
    • Implemented via ColorLayer and TileLayer objects
    • Multiple layers per grid with z_index ordering
    • Files: src/GridLayers.h, src/GridLayers.cpp
  2. World State Layer (TCODMap)

    • Physical properties: walkable, transparent
    • Used for pathfinding and FOV calculations
    • Integration: libtcod via src/UIGrid.cpp
  3. 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 raycasting
  • FOV.DIAMOND - Diamond-shaped
  • FOV.SHADOW - Shadow casting (recommended)
  • FOV.PERMISSIVE_0 through FOV.PERMISSIVE_8 - Permissive variants
  • FOV.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)


Last updated: 2026-02-07