1 Grid System
John McCardle edited this page 2025-12-02 03:10:29 +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)

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/UIEntity.h / src/UIEntity.cpp - Entity system (lives on grid)

API Reference:

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, cost
    • 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
    • Note: Python access currently limited - see Known Issues

This architecture follows proven patterns from Caves of Qud, Cogmind, and DCSS.

Dynamic Layer System

Grids support multiple rendering layers that can be added/removed at runtime:

Layer Type Purpose Methods
ColorLayer Solid colors per cell (fog, highlights) set(x, y, color), at(x, y), fill(color)
TileLayer Texture sprites per cell (terrain, items) set(x, y, sprite_index), at(x, y), fill(index)

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)
# Create grid with no default layers
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(100, 100), size=(400, 400), layers={})

# 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
fog_overlay = grid.add_layer("color", z_index=1)    # Above entities (fog of war)

# Access existing layers
for layer in grid.layers:
    print(f"{type(layer).__name__} at z={layer.z_index}")

# Remove a layer
grid.remove_layer(fog_overlay)

Grid → Entity Relationship

Entity Lifecycle:

  • Entities live on exactly 0 or 1 grids
  • grid.entities.append(entity) sets entity.grid = grid
  • grid.entities.remove(entity) sets entity.grid = None
  • Entity removal handled automatically on grid destruction

Spatial Queries:

  • Currently: Linear iteration through entity list
  • Planned: SpatialHash for O(1) lookups (see #115)

Sub-Pages

Common Tasks

Creating a Grid

import mcrfpy

# Create grid with default tile layer
grid = mcrfpy.Grid(
    grid_size=(50, 50),      # 50x50 cells
    pos=(100, 100),          # Screen position
    size=(400, 400),         # Viewport size in pixels
    texture=my_texture       # Texture for default TileLayer
)

# Or create with specific layer configuration
grid = mcrfpy.Grid(
    grid_size=(50, 50),
    pos=(100, 100),
    size=(400, 400),
    layers={"terrain": "tile", "highlights": "color"}
)

# Or start empty and add layers manually
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(100, 100), size=(400, 400), layers={})
terrain = grid.add_layer("tile", z_index=-1, texture=my_texture)

# Add to scene
mcrfpy.sceneUI("game").append(grid)

Setting Tile Properties

# 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

color_layer = grid.add_layer("color", z_index=-2)
color_layer.set(x, y, mcrfpy.Color(64, 64, 128))  # Blue tint

# Access grid point for world properties
point = grid.at(x, y)
point.walkable = True      # Can entities walk here?
point.transparent = True   # Can see through for FOV?

FOV and Pathfinding

# Compute field of view from position
grid.compute_fov(entity.grid_x, entity.grid_y, radius=10)

# Check if cell is visible
if grid.is_in_fov(target_x, target_y):
    print("Target is visible!")

# A* pathfinding
path = grid.find_path(start_x, start_y, end_x, end_y)

# Dijkstra maps for AI
grid.compute_dijkstra([(goal_x, goal_y)])
distance = grid.get_dijkstra_distance(entity.grid_x, entity.grid_y)
path_to_goal = grid.get_dijkstra_path(entity.grid_x, entity.grid_y)

Mouse Events

Grids support mouse interaction at both the grid level and cell level:

# Grid-level events (screen coordinates)
def on_grid_click(x, y, button):
    print(f"Grid clicked at pixel ({x}, {y}), button {button}")

grid.on_click = on_grid_click
grid.on_enter = lambda: print("Mouse entered grid")
grid.on_exit = lambda: print("Mouse left grid")
grid.on_move = lambda x, y: print(f"Mouse at ({x}, {y})")

# Cell-level events (grid coordinates)
def on_cell_click(grid_x, grid_y):
    print(f"Cell ({grid_x}, {grid_y}) clicked!")
    point = grid.at(grid_x, grid_y)
    # Do something with the cell...

grid.on_cell_click = on_cell_click
grid.on_cell_enter = lambda x, y: print(f"Entered cell ({x}, {y})")
grid.on_cell_exit = lambda x, y: print(f"Left cell ({x}, {y})")

# Query currently hovered cell
if grid.hovered_cell:
    hx, hy = grid.hovered_cell
    print(f"Hovering over ({hx}, {hy})")

Camera Control

# Center viewport on a position (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

# Zoom (1.0 = normal, 2.0 = 2x zoom in, 0.5 = zoom out)
grid.zoom = 1.5

Performance Characteristics

Implemented Optimizations:

  • Chunk-based rendering (#123): Large grids divided into chunks, only visible chunks rendered
  • 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

Current Performance:

  • Grids of 1000x1000+ cells render efficiently
  • Static scenes near-zero CPU (cached textures reused)
  • Entity rendering: O(visible entities)

Profiling: Use the F3 overlay or mcrfpy.setTimer() with mcrfpy.getMetrics() to measure grid performance. See Performance-and-Profiling.

Known Issues & Limitations

Current Limitations:

  • 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.
  • No tile-level animation yet (planned: #124)
  • Entity spatial queries are O(n) (planned: SpatialHash #115)

Workarounds:

  • For fog of war: Use a ColorLayer with positive z_index, update cell colors based on is_in_fov() results
  • For entity queries: Use list comprehension filtering on grid.entities

Last updated: 2025-11-29