Page:
Grid Interaction Patterns
Pages
AI and Pathfinding
Adding Python Bindings
Animation System
Design Proposals
Development Workflow
Entity Management
Grid Interaction Patterns
Grid Rendering Pipeline
Grid System
Grid TCOD Integration
Headless Mode
Home
Input and Events
Issue Roadmap
Performance Optimization Workflow
Performance and Profiling
Procedural Generation
Proposal: Next Gen Grid Entity System
Python Binding Layer
Rendering and Visuals
Strategic Direction
UI Component Hierarchy
UI Widget Patterns
Writing Tests
1
Grid Interaction Patterns
John McCardle edited this page 2025-11-29 23:46:07 +00:00
Grid Interaction Patterns
Patterns for handling mouse and keyboard interaction with Grids, cells, and entities. These patterns build on the grid-specific event handlers.
Related Pages:
- Grid-System - Grid architecture and layers
- Entity-Management - Entity behavior patterns
- Input-and-Events - General event handling
Grid Cell Events
Grids provide cell-level mouse events in addition to standard UIDrawable events:
| Property | Signature | Description |
|---|---|---|
on_cell_click |
(grid_x, grid_y, button) -> None |
Cell clicked (0=left, 1=right, 2=middle) |
on_cell_enter |
(grid_x, grid_y) -> None |
Mouse enters cell |
on_cell_exit |
(grid_x, grid_y) -> None |
Mouse leaves cell |
hovered_cell |
(x, y) or None |
Currently hovered cell (read-only) |
These events use grid coordinates (cell indices), not pixel coordinates.
Setup Template
Most grid interaction patterns fit into this structure:
import mcrfpy
# Scene setup
mcrfpy.createScene("game")
ui = mcrfpy.sceneUI("game")
# Create grid with layers
grid = mcrfpy.Grid(
grid_size=(20, 15),
pos=(50, 50),
size=(640, 480),
layers={}
)
grid.fill_color = mcrfpy.Color(20, 20, 30)
ui.append(grid)
# Add terrain layer
terrain = grid.add_layer("tile", z_index=-2)
# terrain.fill(0) # Fill with default tile
# Add highlight layer (above terrain, below entities)
highlight = grid.add_layer("color", z_index=-1)
# Add overlay layer (above entities, for fog/selection)
overlay = grid.add_layer("color", z_index=1)
# Create player entity
player = mcrfpy.Entity(pos=(10, 7), sprite_index=0)
grid.entities.append(player)
# Wire up events (patterns below fill these in)
# grid.on_cell_click = ...
# grid.on_cell_enter = ...
# grid.on_cell_exit = ...
mcrfpy.setScene("game")
Cell Hover Highlighting
Show which cell the mouse is over.
# Track currently highlighted cell
current_highlight = [None] # Use list for closure mutability
def on_cell_enter(x, y):
# Highlight new cell
highlight.set(x, y, mcrfpy.Color(255, 255, 255, 40))
current_highlight[0] = (x, y)
def on_cell_exit(x, y):
# Clear old highlight
highlight.set(x, y, mcrfpy.Color(0, 0, 0, 0))
current_highlight[0] = None
grid.on_cell_enter = on_cell_enter
grid.on_cell_exit = on_cell_exit
Cell Click Actions
Respond to clicks on specific cells.
def on_cell_click(x, y, button):
point = grid.at(x, y)
if button == 0: # Left click
if point.walkable:
# Move player to clicked cell
player.pos = (x, y)
elif button == 1: # Right click
# Inspect cell
show_cell_info(x, y, point)
elif button == 2: # Middle click
# Toggle walkability (for level editor)
point.walkable = not point.walkable
grid.on_cell_click = on_cell_click
WASD Movement
Track key states for smooth entity movement.
class MovementController:
def __init__(self, entity):
self.entity = entity
self.keys = {"W": False, "A": False, "S": False, "D": False}
self.move_delay = 150 # ms between moves
self.last_move = 0
def handle_key(self, key, pressed):
if key in self.keys:
self.keys[key] = pressed
def update(self, current_time):
if current_time - self.last_move < self.move_delay:
return
dx, dy = 0, 0
if self.keys["W"]: dy -= 1
if self.keys["S"]: dy += 1
if self.keys["A"]: dx -= 1
if self.keys["D"]: dx += 1
if dx == 0 and dy == 0:
return
new_x = self.entity.x + dx
new_y = self.entity.y + dy
# Check walkability
point = self.entity.grid.at(new_x, new_y)
if point and point.walkable:
self.entity.pos = (new_x, new_y)
self.last_move = current_time
# Setup
controller = MovementController(player)
mcrfpy.keypressScene(controller.handle_key)
def game_update(dt):
import time
controller.update(time.time() * 1000)
mcrfpy.setTimer("movement", game_update, 16) # ~60fps
Entity Selection
Click to select entities, show selection indicator.
class SelectionManager:
def __init__(self, grid, overlay_layer):
self.grid = grid
self.overlay = overlay_layer
self.selected = None
def select(self, entity):
# Clear previous selection
if self.selected:
self._clear_indicator(self.selected)
self.selected = entity
if entity:
self._draw_indicator(entity)
def _draw_indicator(self, entity):
x, y = entity.x, entity.y
self.overlay.set(x, y, mcrfpy.Color(255, 200, 0, 80))
def _clear_indicator(self, entity):
x, y = entity.x, entity.y
self.overlay.set(x, y, mcrfpy.Color(0, 0, 0, 0))
def update_indicator(self):
"""Call after selected entity moves."""
if self.selected:
# Clear all overlay first (simple approach)
self.overlay.fill(mcrfpy.Color(0, 0, 0, 0))
self._draw_indicator(self.selected)
selection = SelectionManager(grid, overlay)
def on_cell_click(x, y, button):
if button == 0: # Left click
# Find entity at position
for entity in grid.entities:
if entity.x == x and entity.y == y:
selection.select(entity)
return
# Clicked empty cell - deselect
selection.select(None)
grid.on_cell_click = on_cell_click
Path Preview
Show pathfinding path on hover.
class PathPreview:
def __init__(self, grid, highlight_layer, source_entity):
self.grid = grid
self.highlight = highlight_layer
self.source = source_entity
self.current_path = []
def show_path_to(self, target_x, target_y):
# Clear previous path
self.clear()
# Calculate path
path = self.source.path_to((target_x, target_y))
if not path:
return
self.current_path = path
# Draw path cells
for i, (x, y) in enumerate(path):
if i == 0:
continue # Skip source cell
alpha = 100 - (i * 5) # Fade with distance
alpha = max(30, alpha)
self.highlight.set(x, y, mcrfpy.Color(100, 200, 255, alpha))
def clear(self):
for x, y in self.current_path:
self.highlight.set(x, y, mcrfpy.Color(0, 0, 0, 0))
self.current_path = []
path_preview = PathPreview(grid, highlight, player)
def on_cell_enter(x, y):
path_preview.show_path_to(x, y)
def on_cell_exit(x, y):
pass # Path updates on enter, so no action needed
grid.on_cell_enter = on_cell_enter
Context Menu
Right-click to show context-sensitive options.
class ContextMenu:
def __init__(self, scene_ui):
self.ui = scene_ui
self.frame = None
self.options = []
def show(self, x, y, options):
"""
options: list of (label, callback) tuples
x, y: screen coordinates
"""
self.close() # Close any existing menu
height = len(options) * 24 + 8
self.frame = mcrfpy.Frame(pos=(x, y), size=(120, height))
self.frame.fill_color = mcrfpy.Color(40, 40, 50)
self.frame.outline = 1
self.frame.outline_color = mcrfpy.Color(80, 80, 100)
self.frame.z_index = 500
self.ui.append(self.frame)
for i, (label, callback) in enumerate(options):
item = mcrfpy.Frame(pos=(2, 2 + i * 24), size=(116, 22))
item.fill_color = mcrfpy.Color(40, 40, 50)
self.frame.children.append(item)
text = mcrfpy.Caption(pos=(8, 3), text=label)
text.fill_color = mcrfpy.Color(200, 200, 200)
item.children.append(text)
# Hover effect
item.on_enter = lambda i=item: setattr(i, 'fill_color', mcrfpy.Color(60, 60, 80))
item.on_exit = lambda i=item: setattr(i, 'fill_color', mcrfpy.Color(40, 40, 50))
# Click handler
def make_handler(cb):
return lambda x, y, btn: (cb(), self.close())
item.on_click = make_handler(callback)
def close(self):
if self.frame:
self.ui.remove(self.frame)
self.frame = None
context_menu = ContextMenu(ui)
def on_cell_click(x, y, button):
if button == 1: # Right click
# Find entity at position
target = None
for entity in grid.entities:
if entity.x == x and entity.y == y:
target = entity
break
# Build context menu
mouse_x, mouse_y = mcrfpy.getMousePos()
if target and target != player:
options = [
("Examine", lambda: examine(target)),
("Attack", lambda: attack(target)),
("Talk", lambda: talk(target)),
]
else:
point = grid.at(x, y)
options = [
("Move here", lambda: player.pos.__setitem__(slice(None), (x, y))),
("Examine ground", lambda: examine_cell(x, y)),
]
context_menu.show(mouse_x, mouse_y, options)
elif button == 0: # Left click closes menu
context_menu.close()
grid.on_cell_click = on_cell_click
Tile Inspector Panel
Click cell to show information panel.
class TileInspector:
def __init__(self, parent, pos):
self.frame = mcrfpy.Frame(pos=pos, size=(200, 150))
self.frame.fill_color = mcrfpy.Color(30, 30, 40, 230)
self.frame.outline = 1
self.frame.outline_color = mcrfpy.Color(80, 80, 100)
self.frame.visible = False
parent.append(self.frame)
self.title = mcrfpy.Caption(pos=(10, 8), text="Cell Info")
self.title.fill_color = mcrfpy.Color(180, 180, 200)
self.frame.children.append(self.title)
self.info_lines = []
for i in range(5):
line = mcrfpy.Caption(pos=(10, 30 + i * 20), text="")
line.fill_color = mcrfpy.Color(160, 160, 180)
self.frame.children.append(line)
self.info_lines.append(line)
def show(self, grid, x, y):
point = grid.at(x, y)
self.title.text = f"Cell ({x}, {y})"
self.info_lines[0].text = f"Walkable: {point.walkable}"
self.info_lines[1].text = f"Transparent: {point.transparent}"
# Count entities at this position
entities_here = [e for e in grid.entities if e.x == x and e.y == y]
self.info_lines[2].text = f"Entities: {len(entities_here)}"
if entities_here:
self.info_lines[3].text = f" {entities_here[0].name or 'unnamed'}"
else:
self.info_lines[3].text = ""
self.info_lines[4].text = ""
self.frame.visible = True
def hide(self):
self.frame.visible = False
inspector = TileInspector(ui, (700, 50))
def on_cell_click(x, y, button):
if button == 0:
inspector.show(grid, x, y)
grid.on_cell_click = on_cell_click
Related Pages
- Entity-Management - Entity behavior and pathfinding
- Grid-System - Layer management and rendering
- UI-Widget-Patterns - Non-grid UI patterns
- Input-and-Events - Event API reference
Last updated: 2025-11-29