Input & Callbacks
The Input & Callback System handles all user interaction and timed events in McRogueFace. It provides keyboard input through scene callbacks, mouse input through per-element callbacks, and scheduled execution through timers.
Overview
McRogueFace uses a callback-based input model. Instead of polling for input in a game loop, you register callback functions that are invoked when events occur. Callbacks receive enum values (Key, InputState, MouseButton) rather than strings, so typos fail loudly instead of silently never matching.
def on_key(key, state):
if key == mcrfpy.Key.W and state == mcrfpy.InputState.PRESSED:
move_player(0, -1)
scene.on_key = on_key
Keyboard input belongs to the Scene; mouse input belongs to individual UI elements. A click is delivered to the topmost element under the cursor that has a handler, so there is no manual hit-testing in your code.
Objects
| Object | Purpose |
|---|---|
| Scene | Keyboard input via on_key, lifecycle callbacks |
| Key | Keyboard key enum |
| InputState | PRESSED / RELEASED enum |
| MouseButton | Mouse button and scroll wheel enum |
| Keyboard | Modifier key state singleton (mcrfpy.keyboard) |
| Mouse | Mouse position/button state singleton (mcrfpy.mouse) |
| Timer | Scheduled callbacks at intervals |
Keyboard Input
Keyboard input is handled through the scene’s on_key callback.
Key Callback Signature
def on_key(key: mcrfpy.Key, state: mcrfpy.InputState) -> None:
pass
scene.on_key = on_key # set to None to remove the handler
key: A Key enum value (e.g.,mcrfpy.Key.W,mcrfpy.Key.SPACE,mcrfpy.Key.ESCAPE)state: An InputState enum value:PRESSEDorRELEASED
A handler can be set on any scene, not just the active one - only the active scene’s handler receives events. Holding a key down delivers repeated PRESSED events (OS key repeat); RELEASED fires exactly once when the key comes up.
Example: Movement
import mcrfpy
scene = mcrfpy.Scene("game")
grid = mcrfpy.Grid(grid_size=(20, 15), pos=(50, 50), size=(320, 240))
scene.children.append(grid)
player = mcrfpy.Entity(grid_pos=(10, 7), sprite_index=84)
grid.entities.append(player)
# Cells default to walkable=False; open up the floor
for y in range(15):
for x in range(20):
grid.at(x, y).walkable = True
MOVES = {
mcrfpy.Key.W: (0, -1), mcrfpy.Key.UP: (0, -1),
mcrfpy.Key.S: (0, 1), mcrfpy.Key.DOWN: (0, 1),
mcrfpy.Key.A: (-1, 0), mcrfpy.Key.LEFT: (-1, 0),
mcrfpy.Key.D: (1, 0), mcrfpy.Key.RIGHT: (1, 0),
}
def on_key(key, state):
if state != mcrfpy.InputState.PRESSED:
return
if key in MOVES:
dx, dy = MOVES[key]
nx, ny = player.grid_x + dx, player.grid_y + dy
if 0 <= nx < 20 and 0 <= ny < 15 and grid.at(nx, ny).walkable:
player.grid_pos = (nx, ny)
scene.on_key = on_key
scene.activate()
Example: PRESSED vs RELEASED
InputState distinguishes key-down from key-up, which enables hold-and-release mechanics. This is a complete runnable script:
import mcrfpy
scene = mcrfpy.Scene("charge_demo")
status = mcrfpy.Caption(text="Hold SPACE to charge, release to fire", pos=(20, 20))
scene.children.append(status)
charging = False
def on_key(key, state):
global charging
if key == mcrfpy.Key.SPACE:
if state == mcrfpy.InputState.PRESSED and not charging:
# Guard with a flag: key repeat delivers PRESSED again while held
charging = True
status.text = "Charging..."
elif state == mcrfpy.InputState.RELEASED:
charging = False
status.text = "Fired! Hold SPACE to charge again"
elif key == mcrfpy.Key.ESCAPE and state == mcrfpy.InputState.PRESSED:
mcrfpy.exit()
scene.on_key = on_key
scene.activate()
Modifier Keys
Modifier keys arrive as their own Key events (LEFT_SHIFT, RIGHT_CONTROL, …), but for “is Shift held right now?” checks it is easier to poll the mcrfpy.keyboard singleton:
def on_key(key, state):
if key == mcrfpy.Key.UP and state == mcrfpy.InputState.PRESSED:
step = 10 if mcrfpy.keyboard.shift else 1
move_cursor(0, -step)
mcrfpy.keyboard exposes shift, ctrl, alt, and system (all read-only booleans, true if either the left or right key is held).
Key Names
mcrfpy.Key covers the full keyboard. Commonly used values:
- Letters:
Key.AthroughKey.Z - Numbers:
Key.NUM_0throughKey.NUM_9(numpad:Key.NUMPAD_0…) - Arrows:
Key.UP,Key.DOWN,Key.LEFT,Key.RIGHT - Modifiers:
Key.LEFT_SHIFT,Key.RIGHT_SHIFT,Key.LEFT_CONTROL,Key.LEFT_ALT, … - Special:
Key.SPACE,Key.ENTER,Key.ESCAPE,Key.TAB,Key.BACKSPACE - Function keys:
Key.F1throughKey.F15
For debugging, key.name gives the string name of any key that arrives in your handler.
Mouse Input
Mouse input is delivered to UI elements, not to the scene. Every drawable (Frame, Caption, Sprite, Grid, …) supports click and hover callbacks. Events go to the topmost element under the cursor; the first element that handles a click stops propagation.
Click Callback Signature
def on_click(pos: mcrfpy.Vector, button: mcrfpy.MouseButton, action: mcrfpy.InputState) -> None:
pass
element.on_click = on_click
pos: A Vector with the click position in scene coordinatesbutton: A MouseButton enum value:LEFT,RIGHT,MIDDLE,X1,X2- orSCROLL_UP/SCROLL_DOWNfor the wheelaction:InputState.PRESSEDorInputState.RELEASED
on_click fires for both press and release, so filter on action:
button_frame = mcrfpy.Frame(pos=(100, 100), size=(120, 40))
def on_button(pos, button, action):
if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED:
print(f"Clicked at {pos.x}, {pos.y}")
button_frame.on_click = on_button
Scroll wheel events arrive through the same callback with button set to SCROLL_UP or SCROLL_DOWN.
Hover Callbacks
Hover callbacks receive only the position:
def on_enter(pos):
panel.fill_color = mcrfpy.Color(80, 80, 120) # highlight
def on_exit(pos):
panel.fill_color = mcrfpy.Color(50, 50, 50) # restore
panel.on_enter = on_enter
panel.on_exit = on_exit
on_enter: mouse entered the element’s bounds - called with(pos)on_exit: mouse left the element’s bounds - called with(pos)on_move: mouse moved while inside - called with(pos)on every movement, so keep it cheap
Polling Mouse State
The mcrfpy.mouse singleton offers read-only state for polling instead of callbacks: pos (Vector), x, y, and left/middle/right booleans. It also controls the cursor via visible and grabbed.
Grid Cell Input
Grid adds cell-level callbacks that report positions in tile coordinates rather than pixels, accounting for the grid’s camera (center and zoom) automatically.
Cell Callback Signatures
def on_cell_click(cell_pos: mcrfpy.Vector, button: mcrfpy.MouseButton, action: mcrfpy.InputState) -> None:
pass
def on_cell_enter(cell_pos: mcrfpy.Vector) -> None:
pass
grid.on_cell_click = on_cell_click
grid.on_cell_enter = on_cell_enter
grid.on_cell_exit = lambda cell_pos: None
Example: Tile Inspection
def inspect_cell(cell_pos, button, action):
if button != mcrfpy.MouseButton.LEFT or action != mcrfpy.InputState.PRESSED:
return
x, y = int(cell_pos.x), int(cell_pos.y)
point = grid.at(x, y)
print(f"Cell ({x}, {y}): walkable={point.walkable}, entities={len(point.entities)}")
grid.on_cell_click = inspect_cell
The currently hovered cell is also available as grid.hovered_cell - an (x, y) tuple, or None when the mouse is outside the grid.
A grid also supports the standard on_click (pixel position) alongside on_cell_click (tile position); use the cell variant whenever you care about tiles rather than screen geometry.
Timers
Timers execute callbacks at specified intervals. The callback receives the timer object and the total elapsed runtime in milliseconds.
Creating Timers
def update(timer, runtime):
print(f"{timer.name} tick at {runtime} ms")
t = mcrfpy.Timer("update_timer", update, 500) # every 500 ms, starts immediately
One-Shot Timers
Pass once=True for single execution; pass start=False to create a timer without starting it:
def delayed_action(timer, runtime):
print("Delayed!")
t = mcrfpy.Timer("delay_timer", delayed_action, 1000, once=True)
One-shot timers stop themselves after firing and can be restarted with t.restart().
Timer Control
Timers are controlled through methods on the object:
t.pause() # preserve remaining time
t.resume() # continue from where it paused
t.stop() # remove from the tick loop (callback preserved)
t.start() # start a stopped timer
t.restart() # reset to the full interval and ensure running
State is inspectable via t.active, t.paused, t.stopped, and t.remaining (milliseconds until the next fire). t.interval can be changed while running.
Sprite Animation with a Timer
frames = [84, 85, 86, 87]
def animate(timer, runtime):
sprite.sprite_index = frames[(runtime // 100) % len(frames)]
mcrfpy.Timer("walk_cycle", animate, 100) # 10 FPS animation
For property transitions (position, color, opacity), prefer the Animation System over hand-rolled timer loops.
Scene Lifecycle
Scenes have lifecycle callbacks for initialization and cleanup. Unlike on_key, these are subclass methods, not assignable properties - define them by subclassing Scene:
class GameScene(mcrfpy.Scene):
def on_enter(self):
# Scene became active (via activate())
print("Game started!")
def on_exit(self):
# Another scene activated; clean up timers, save state
print("Leaving scene")
def update(self, dt):
# Called every frame with delta time in seconds
pass
def on_resize(self, new_size):
# Window was resized; realign responsive layouts
self.realign()
scene = GameScene("game")
scene.activate()
scene.activate() deactivates the current scene (firing its on_exit) and activates this one (firing on_enter). Keyboard handlers can also be defined as a subclass on_key(self, key, state) method instead of assigning the property.