Add "Input-and-Events"
parent
dab5590077
commit
612cd2c101
|
|
@ -0,0 +1,395 @@
|
|||
# Input and Events
|
||||
|
||||
## Overview
|
||||
|
||||
McRogueFace provides keyboard, mouse, and window event handling through Python callbacks. Events are dispatched through the scene system, allowing different scenes to have different input handlers.
|
||||
|
||||
**Related Pages:**
|
||||
- [[UI-Component-Hierarchy]] - UI element interaction
|
||||
- [[Entity-Management]] - Entity-based input (click handlers)
|
||||
- [[Writing-Tests]] - Testing input with automation API
|
||||
|
||||
**Key Files:**
|
||||
- `src/GameEngine.cpp::processEvent()` - Event dispatch
|
||||
- `src/Scene.cpp::input()` - Scene input handling
|
||||
- `src/McRFPy_API.cpp` - Python callback registration
|
||||
|
||||
**Related Issues:**
|
||||
- [#128](../../issues/128) - Middle-click entity selection
|
||||
- [#1](../../issues/1) - Window resize events
|
||||
|
||||
---
|
||||
|
||||
## Keyboard Input
|
||||
|
||||
### Scene-Level Keyboard Callbacks
|
||||
|
||||
Register a callback that fires on every key press/release:
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
def handle_key(key: str, pressed: bool):
|
||||
"""
|
||||
key: Key name (e.g., "W", "Space", "Escape", "Up")
|
||||
pressed: True on press, False on release
|
||||
"""
|
||||
if key == "Escape" and pressed:
|
||||
print("Escape pressed!")
|
||||
mcrfpy.setScene("menu")
|
||||
|
||||
if key == "W":
|
||||
if pressed:
|
||||
player.start_moving_north()
|
||||
else:
|
||||
player.stop_moving_north()
|
||||
|
||||
# Register for current scene
|
||||
mcrfpy.keypressScene(handle_key)
|
||||
```
|
||||
|
||||
### Key Names
|
||||
|
||||
Common key names from SFML:
|
||||
- Letters: `"A"`, `"B"`, `"C"`, ... `"Z"`
|
||||
- Numbers: `"Num0"`, `"Num1"`, ... `"Num9"`
|
||||
- Arrows: `"Up"`, `"Down"`, `"Left"`, `"Right"`
|
||||
- Special: `"Space"`, `"Enter"`, `"Escape"`, `"Tab"`, `"LShift"`, `"RShift"`, `"LControl"`, `"RControl"`
|
||||
- Function: `"F1"`, `"F2"`, ... `"F12"`
|
||||
|
||||
See `src/McRFPy_API.cpp::McRFPy_API_keypressScene()` for full key mapping.
|
||||
|
||||
---
|
||||
|
||||
## Mouse Input
|
||||
|
||||
### Click Events
|
||||
|
||||
Mouse clicks are dispatched to UI elements based on screen position:
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
# Create a frame with click handler
|
||||
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 50))
|
||||
|
||||
def on_frame_click(button: int, x: int, y: int):
|
||||
"""
|
||||
button: 0=left, 1=right, 2=middle
|
||||
x, y: Mouse position in window coordinates
|
||||
"""
|
||||
print(f"Frame clicked with button {button} at ({x}, {y})")
|
||||
|
||||
frame.click = on_frame_click
|
||||
```
|
||||
|
||||
### Entity Click Handlers
|
||||
|
||||
Entities can register click callbacks for grid-based interaction:
|
||||
|
||||
```python
|
||||
player = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=1)
|
||||
|
||||
def on_entity_click(button: int):
|
||||
if button == 0: # Left-click
|
||||
print("Selected player!")
|
||||
elif button == 1: # Right-click
|
||||
print("Context menu for player")
|
||||
elif button == 2: # Middle-click
|
||||
print("Middle-clicked entity")
|
||||
|
||||
player.click = on_entity_click
|
||||
```
|
||||
|
||||
**Issue #128** adds proper middle-click support for entity selection.
|
||||
|
||||
### Mouse Position
|
||||
|
||||
Get current mouse position in game coordinates:
|
||||
|
||||
```python
|
||||
# Window coordinates (pixels)
|
||||
window_x, window_y = mcrfpy.getMousePos()
|
||||
|
||||
# Convert to game coordinates (respects viewport)
|
||||
game_x, game_y = mcrfpy.windowToGameCoords(window_x, window_y)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Window Events
|
||||
|
||||
### Resize Events
|
||||
|
||||
**Issue #1** tracks window resize event support. Current workaround:
|
||||
|
||||
```python
|
||||
def update_on_resize(ms):
|
||||
"""Timer-based resize detection"""
|
||||
current_size = mcrfpy.getWindowSize()
|
||||
if current_size != last_size:
|
||||
on_window_resized(current_size)
|
||||
|
||||
mcrfpy.setTimer("resize_check", update_on_resize, 100)
|
||||
```
|
||||
|
||||
### Focus Events
|
||||
|
||||
Window focus/unfocus events are not currently exposed to Python. The engine does not pause automatically when focus is lost.
|
||||
|
||||
---
|
||||
|
||||
## Event Priority and Propagation
|
||||
|
||||
### Click Dispatch Order
|
||||
|
||||
Clicks are dispatched in reverse render order (top-to-bottom):
|
||||
|
||||
1. **UI elements with highest z_index** receive clicks first
|
||||
2. If a UI element handles the click, propagation stops
|
||||
3. **Entities on grids** receive clicks if no UI element handled it
|
||||
4. **Grid cells** receive clicks last
|
||||
|
||||
```python
|
||||
# High z_index UI blocks clicks to entities below
|
||||
overlay = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), z_index=100)
|
||||
overlay.click = lambda btn, x, y: True # Blocks all clicks
|
||||
|
||||
# Remove overlay to allow clicks through
|
||||
ui.remove(overlay)
|
||||
```
|
||||
|
||||
### Keyboard Priority
|
||||
|
||||
Keyboard events are dispatched only to the current scene's registered callback. There is no concept of "focused" UI elements for keyboard input.
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Modal Dialog
|
||||
|
||||
Capture all input while a dialog is open:
|
||||
|
||||
```python
|
||||
class ModalDialog:
|
||||
def __init__(self, message: str):
|
||||
self.frame = mcrfpy.Frame(pos=(300, 200), size=(400, 200), z_index=1000)
|
||||
self.caption = mcrfpy.Caption(text=message, pos=(310, 220))
|
||||
self.frame.click = self._on_click
|
||||
|
||||
# Save previous input handlers
|
||||
self.prev_keypress = mcrfpy.keypressScene(self._on_key)
|
||||
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
ui.append(self.frame)
|
||||
ui.append(self.caption)
|
||||
|
||||
def _on_click(self, btn, x, y):
|
||||
return True # Block all clicks
|
||||
|
||||
def _on_key(self, key, pressed):
|
||||
if key == "Enter" and pressed:
|
||||
self.close()
|
||||
return True # Block all keys
|
||||
|
||||
def close(self):
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
ui.remove(self.frame)
|
||||
ui.remove(self.caption)
|
||||
mcrfpy.keypressScene(self.prev_keypress) # Restore
|
||||
```
|
||||
|
||||
### WASD Movement
|
||||
|
||||
Handle movement keys with press/release state:
|
||||
|
||||
```python
|
||||
class PlayerController:
|
||||
def __init__(self, entity):
|
||||
self.entity = entity
|
||||
self.moving = {"W": False, "A": False, "S": False, "D": False}
|
||||
|
||||
def handle_key(self, key, pressed):
|
||||
if key in self.moving:
|
||||
self.moving[key] = pressed
|
||||
self._update_velocity()
|
||||
|
||||
def _update_velocity(self):
|
||||
vx = 0
|
||||
vy = 0
|
||||
if self.moving["W"]: vy -= 1
|
||||
if self.moving["S"]: vy += 1
|
||||
if self.moving["A"]: vx -= 1
|
||||
if self.moving["D"]: vx += 1
|
||||
|
||||
# Normalize diagonal movement
|
||||
import math
|
||||
magnitude = math.sqrt(vx*vx + vy*vy)
|
||||
if magnitude > 0:
|
||||
vx /= magnitude
|
||||
vy /= magnitude
|
||||
|
||||
self.entity.velocity_x = vx * 5.0
|
||||
self.entity.velocity_y = vy * 5.0
|
||||
|
||||
controller = PlayerController(player)
|
||||
mcrfpy.keypressScene(controller.handle_key)
|
||||
```
|
||||
|
||||
### Context Menu
|
||||
|
||||
Right-click to show context-sensitive options:
|
||||
|
||||
```python
|
||||
def show_context_menu(entity, x, y):
|
||||
"""Show menu at mouse position"""
|
||||
menu_frame = mcrfpy.Frame(pos=(x, y), size=(150, 100), z_index=500)
|
||||
|
||||
# Menu options
|
||||
option1 = mcrfpy.Caption(text="Examine", pos=(x+10, y+10))
|
||||
option2 = mcrfpy.Caption(text="Attack", pos=(x+10, y+30))
|
||||
option3 = mcrfpy.Caption(text="Talk", pos=(x+10, y+50))
|
||||
|
||||
def on_menu_click(btn, mx, my):
|
||||
# Determine which option was clicked
|
||||
if y+10 <= my < y+30:
|
||||
entity.examine()
|
||||
elif y+30 <= my < y+50:
|
||||
entity.attack()
|
||||
elif y+50 <= my < y+70:
|
||||
entity.talk()
|
||||
|
||||
# Close menu
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
ui.remove(menu_frame)
|
||||
ui.remove(option1)
|
||||
ui.remove(option2)
|
||||
ui.remove(option3)
|
||||
|
||||
menu_frame.click = on_menu_click
|
||||
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
ui.append(menu_frame)
|
||||
ui.append(option1)
|
||||
ui.append(option2)
|
||||
ui.append(option3)
|
||||
|
||||
# Entity right-click handler
|
||||
def entity_right_click(button):
|
||||
if button == 1: # Right-click
|
||||
mx, my = mcrfpy.getMousePos()
|
||||
show_context_menu(entity, mx, my)
|
||||
|
||||
entity.click = entity_right_click
|
||||
```
|
||||
|
||||
### Hotbar / Quick Slots
|
||||
|
||||
Number keys for quick item access:
|
||||
|
||||
```python
|
||||
hotbar = [None] * 10 # 10 slots
|
||||
|
||||
def handle_hotbar(key, pressed):
|
||||
if pressed and key.startswith("Num"):
|
||||
slot = int(key[3:]) # "Num1" -> 1
|
||||
if 0 <= slot < 10 and hotbar[slot]:
|
||||
hotbar[slot].use()
|
||||
|
||||
mcrfpy.keypressScene(handle_hotbar)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Input
|
||||
|
||||
### Automation API
|
||||
|
||||
Use the automation API to simulate input in tests:
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
# Simulate keyboard input
|
||||
automation.keypress("W", True) # Press W
|
||||
automation.keypress("W", False) # Release W
|
||||
|
||||
# Simulate mouse clicks
|
||||
automation.click(100, 200, button=0) # Left-click at (100, 200)
|
||||
automation.click(150, 250, button=1) # Right-click
|
||||
|
||||
# Wait for effects
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
|
||||
# Verify results with screenshot
|
||||
automation.screenshot("test_result.png")
|
||||
```
|
||||
|
||||
See [[Writing-Tests]] for complete testing patterns.
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Event Handler Complexity
|
||||
|
||||
Event handlers run on the main thread and block rendering:
|
||||
|
||||
```python
|
||||
# BAD: Expensive computation in event handler
|
||||
def handle_key(key, pressed):
|
||||
if key == "Space" and pressed:
|
||||
# This will freeze the game!
|
||||
for i in range(1000000):
|
||||
expensive_calculation()
|
||||
|
||||
# GOOD: Defer expensive work to timer
|
||||
def handle_key(key, pressed):
|
||||
if key == "Space" and pressed:
|
||||
mcrfpy.setTimer("deferred_work", do_expensive_work, 10)
|
||||
```
|
||||
|
||||
### Click Handler Optimization
|
||||
|
||||
Avoid creating handlers that search large collections:
|
||||
|
||||
```python
|
||||
# BAD: O(n) search on every click
|
||||
def on_click(btn, x, y):
|
||||
for entity in all_entities: # Expensive!
|
||||
if entity.contains_point(x, y):
|
||||
entity.select()
|
||||
|
||||
# GOOD: Use spatial data structures (see [[Grid-System]])
|
||||
def on_click(btn, x, y):
|
||||
grid_x, grid_y = screen_to_grid(x, y)
|
||||
entity = grid.get_entity_at(grid_x, grid_y) # O(1)
|
||||
if entity:
|
||||
entity.select()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
See [`docs/api_reference_dynamic.html`](../../src/branch/master/docs/api_reference_dynamic.html) for complete input API documentation.
|
||||
|
||||
**Key Functions:**
|
||||
- `mcrfpy.keypressScene(callback)` - Register keyboard handler
|
||||
- `mcrfpy.getMousePos() -> (int, int)` - Get mouse position
|
||||
- `mcrfpy.windowToGameCoords(x, y) -> (float, float)` - Convert coordinates
|
||||
|
||||
**UI Element Properties:**
|
||||
- `element.click = callback` - Click handler for any UIDrawable
|
||||
- `entity.click = callback` - Click handler for entities
|
||||
|
||||
---
|
||||
|
||||
**Navigation:**
|
||||
- [[Home]] - Documentation hub
|
||||
- [[Entity-Management]] - Entity interaction patterns
|
||||
- [[UI-Component-Hierarchy]] - UI element click regions
|
||||
- [[Writing-Tests]] - Testing input with automation API
|
||||
Loading…
Reference in New Issue