Input System¶
SimVX provides a unified input system for keyboard, mouse, gamepad, and touch. All bindings use typed enums: no raw strings.
Input Actions¶
Actions are named bindings that decouple game logic from specific keys:
from simvx.core import InputMap, Key, MouseButton, JoyButton, Input
# Bind actions to keys (accepts Key, MouseButton, or JoyButton)
InputMap.add_action("jump", [Key.SPACE, JoyButton.A])
InputMap.add_action("shoot", [MouseButton.LEFT])
InputMap.add_action("move_left", [Key.A, Key.LEFT])
InputMap.add_action("move_right", [Key.D, Key.RIGHT])
# Query in process()
class Player(Node):
def on_update(self, dt):
if Input.is_action_just_pressed("jump"):
self.jump()
if Input.is_action_pressed("shoot"):
self.fire()
Action Queries¶
Method |
Returns |
Description |
|---|---|---|
|
|
True while any bound key is held |
|
|
True on the frame the action activates |
|
|
True on the frame the action deactivates |
|
|
0.0–1.0 (1.0 when pressed for digital keys, analog for axes) |
|
|
Alias for |
Vector Input¶
For directional movement, get_vector() and get_axis() combine multiple actions into a single value:
# Returns a normalised Vec2 from four directional actions
direction = Input.get_vector("move_left", "move_right", "move_up", "move_down")
self.position += direction * speed * dt
# Returns a float from two opposing actions (-1 to 1)
horizontal = Input.get_axis("move_left", "move_right")
InputMap¶
InputMap manages action-to-binding mappings. It is a class-level singleton.
Method |
Description |
|---|---|
|
Register an action with a list of key/button bindings |
|
Remove an action and all its bindings |
|
Add a binding to an existing action |
|
Remove a specific binding from an action |
|
Check if an action exists |
|
List all registered action names |
|
Get all bindings for an action |
|
Remove all actions |
InputBinding¶
For advanced bindings (gamepad axes, deadzones), use InputBinding directly:
from simvx.core import InputBinding, JoyAxis
InputMap.add_action("move_left", [
Key.A,
Key.LEFT,
InputBinding(joy_axis=JoyAxis.LEFT_X, joy_axis_positive=False, deadzone=0.2),
])
A binding can require a modifier, written either as a combo string or with the
ctrl / shift / alt flags:
InputMap.add_action("dedent", ["shift+tab"])
InputMap.add_action("dedent", [InputBinding(key=Key.TAB, shift=True)]) # the same thing
Two layers then read modifiers, by opposite rules, and both must agree before your code runs:
The binding, which decides whether the action matches at all. Modifiers here are required, never exclusive: a binding that names none matches whatever else is held, so
Input.is_action_pressed("jump")on a Space binding stays true while the player holds Shift to run.The
@on_inputfilter, which narrows an event the action has already matched. An unlisted modifier must be absent here, so a handler declared bare sees only the unmodified press.
The consequence to remember: a handler for a combo-bound action must open the
modifier its binding requires, or it never fires. Bare @on_input(action="dedent")
is silently dead against a shift+tab binding, because the binding demands Shift
and the default filter forbids it. Pass None for that modifier:
@on_input(action="dedent", shift=None) # the binding already decided Shift
def dedent(self, event): ...
The same holds for the Shift-to-run case: @on_input(action="jump") does not fire
while Shift is held, @on_input(action="jump", shift=None) does, and polling with
Input.is_action_pressed("jump") is unaffected because it sees only the binding.
A project file carries either spelling, and simvx.toml is what the Input Map
dialog and save_project write:
[input]
dedent = ["shift+tab"] # combo string
redo = [{key = "y", ctrl = true}] # or the flags, which is what a save writes
Direct Key/Mouse Queries¶
For non-action-based input (debug tools, editor code):
# Keyboard
Input.is_key_pressed(Key.ESCAPE)
Input.is_key_just_pressed(Key.F3)
# Mouse
Input.is_mouse_button_pressed(MouseButton.LEFT) # held this frame
Input.is_mouse_button_just_pressed(MouseButton.LEFT) # rising edge (single click)
Input.is_mouse_button_just_released(MouseButton.LEFT) # falling edge
pos = Input.mouse_position # Vec2
delta = Input.mouse_delta # Vec2 (frame-to-frame movement)
scroll_x, scroll_y = Input.scroll_delta
Mouse Capture¶
Control cursor visibility and confinement:
from simvx.core import MouseCaptureMode
Input.set_mouse_capture_mode(MouseCaptureMode.CAPTURED) # FPS-style
Input.set_mouse_capture_mode(MouseCaptureMode.VISIBLE) # Normal
Mode |
Description |
|---|---|
|
Normal cursor |
|
Cursor hidden but moves freely |
|
Cursor hidden and locked to window (FPS cameras) |
|
Cursor visible but confined to window |
Gamepad¶
from simvx.core import JoyButton, JoyAxis
# Digital buttons
if Input.is_gamepad_pressed(button=JoyButton.A):
self.jump()
# Analog sticks (returns Vec2)
stick = Input.get_gamepad_vector(stick="left")
self.velocity = Vec3(stick.x, 0, stick.y) * speed
# Raw axis value: a trigger is 0.0 released, 1.0 fully pulled
trigger = Input.get_gamepad_axis(axis=JoyAxis.RIGHT_TRIGGER)
# Which pads are readable right now; empty once the last one is unplugged
pads = Input.get_connected_gamepads()
Axis ranges¶
Every window backend maps its own library’s ranges onto one convention, so the same physical control reads the same number everywhere:
Axis |
Range |
At rest |
|---|---|---|
|
-1.0 to 1.0, |
0.0 |
|
0.0 to 1.0 |
0.0 |
A trigger is therefore bound with joy_axis_positive=True; its released
position is the bottom of its travel, not the middle.
Connection¶
get_connected_gamepads() returns the ids readable this frame, in ascending
order. A pad enters the list on the first poll that reports it and leaves on
the first poll that does not, and its held buttons release through the ordinary
just-released edge when it goes, so an action bound to a pad that was unplugged
mid-press stops reading pressed. A backend with no gamepad support (Qt) reports
nothing and prunes nothing.
Enums¶
Enum |
Examples |
|---|---|
|
|
|
|
|
|
|
|
Touch gestures¶
GestureRecognizer is a node that turns raw touch points into gestures and
emits one signal per kind (tap, long_press, swipe, pinch, rotate,
pan). Thresholds are properties: tap_timeout, long_press_timeout,
tap_max_distance, swipe_min_velocity.
gestures = root.add_child(GestureRecognizer())
gestures.tap.connect(lambda x, y: print("tap", x, y))
gestures.swipe.connect(lambda direction: print("swipe", direction))
Timings are measured against the scene clock (tree.now), so slow motion
stretches a long press, a paused scene never completes one, and the same
fixed-step input script is recognised identically every run. A recogniser
driving UI that must keep working while the game is paused (a pause menu’s
tap-and-hold) opts out:
gestures = GestureRecognizer(clock="wall") # wall time, ignores pause
gestures.update_mode = UpdateMode.ALWAYS # and it has to keep ticking
Input.set_touch_emulation(True) maps the left mouse button to finger 0, so
every single-finger gesture is reachable on desktop. See
examples/features/ui/gestures.py.
Input Events¶
For UI widgets and event-driven handling, SimVX provides event objects:
InputEventKey:key,pressed,echo,shift,ctrl,alt,handledInputEventMouse:button,pressed,position,shift,ctrl,alt,handled
Set event.handled = True to consume an event and prevent further propagation. UI widgets receive input first: if a focused widget consumes the event, game nodes don’t see it.
API Reference¶
See simvx.core.input for the complete input API.