Named Input Actions

An action is a name for what the player means (“move”, “jump”) rather than for the key that means it. A node declares its actions in an input_context dictionary on the class, and they are in force exactly while that node is in the tree; on the root they cover the whole scene. Game code then asks Input.value("move") and never needs to know which device answered.

▶ Run in browser

Tags: input actions keyboard mouse controller touch

Each list entry is one whole source, and an action’s type follows its sources:

  • "move": [Axis2D.WASD, Axis2D.ARROWS, Controller.LEFT_STICK] is a Vec2. A keyboard block or a stick each produce a whole direction on their own, a keyboard diagonal is normalised, and a stick inside its deadzone reads zero.

  • "boost": [Key.SHIFT, Controller.B, Controller.RIGHT_TRIGGER] is a float, because a bare trigger is an axis: the trigger reads its deflection and a button beside it reads 1.0.

  • "jump": [Key.SPACE, Pointer.PRIMARY, Controller.A] is buttons alone, so it is a bool. Pointer.PRIMARY is the main press of a mouse, pen or finger.

When several sources drive one action at once, the one furthest from rest wins. @on_input("jump") runs once per press, whichever device made it.

A TouchController draws a pad on the screen for touch players. It is published as a real controller (listed in Input.controllers with the source VIRTUAL), so its stick and buttons drive the same three actions with nothing further wired up, and its stick claims the finger that steers it, so steering is never also a jump.

The panel reads the map back out: InputMap.kind gives each action’s type, InputMap.get_sources its sources, and Input.label names each one the way the connected hardware prints it, so the legend always says what is bound.

Controls: WASD, the arrows, the left stick or the drawn stick move; Space, a click or tap, or pad A jumps; Shift, pad B or the right trigger boosts; R or pad Start recentres the shape and resets the jump count.

Source

  1# /// simvx
  2# tags = ["input", "actions", "keyboard", "mouse", "controller", "touch"]
  3# web = { width = 960, height = 540, responsive = true }
  4# ///
  5"""Named Input Actions
  6
  7An action is a name for what the player means ("move", "jump") rather than for
  8the key that means it. A node declares its actions in an ``input_context``
  9dictionary on the class, and they are in force exactly while that node is in
 10the tree; on the root they cover the whole scene. Game code then asks
 11``Input.value("move")`` and never needs to know which device answered.
 12
 13Each list entry is one whole source, and an action's type follows its sources:
 14
 15- ``"move": [Axis2D.WASD, Axis2D.ARROWS, Controller.LEFT_STICK]`` is a ``Vec2``.
 16  A keyboard block or a stick each produce a whole direction on their own, a
 17  keyboard diagonal is normalised, and a stick inside its deadzone reads zero.
 18- ``"boost": [Key.SHIFT, Controller.B, Controller.RIGHT_TRIGGER]`` is a float,
 19  because a bare trigger is an axis: the trigger reads its deflection and a
 20  button beside it reads 1.0.
 21- ``"jump": [Key.SPACE, Pointer.PRIMARY, Controller.A]`` is buttons alone, so
 22  it is a bool. ``Pointer.PRIMARY`` is the main press of a mouse, pen or finger.
 23
 24When several sources drive one action at once, the one furthest from rest wins.
 25``@on_input("jump")`` runs once per press, whichever device made it.
 26
 27A ``TouchController`` draws a pad on the screen for touch players. It is
 28published as a real controller (listed in ``Input.controllers`` with the source
 29``VIRTUAL``), so its stick and buttons drive the same three actions with nothing
 30further wired up, and its stick claims the finger that steers it, so steering
 31is never also a jump.
 32
 33The panel reads the map back out: ``InputMap.kind`` gives each action's type,
 34``InputMap.get_sources`` its sources, and ``Input.label`` names each one the way
 35the connected hardware prints it, so the legend always says what is bound.
 36
 37Controls: WASD, the arrows, the left stick or the drawn stick move; Space, a
 38click or tap, or pad A jumps; Shift, pad B or the right trigger boosts; R or
 39pad Start recentres the shape and resets the jump count.
 40"""
 41
 42from simvx.core import Anchor, Axis2D, CanvasLayer, Controller, Input, InputMap, Key, Node2D, Pointer, Vec2, on_input
 43from simvx.core.ui import Label, ProgressBar, TouchController, VBoxContainer
 44from simvx.graphics import App
 45
 46RADIUS = 24.0
 47SPEED = 320.0  # doubled at full boost
 48HOP_VELOCITY = -420.0
 49GRAVITY = 1400.0
 50ACTIVE, IDLE = (0.35, 0.95, 0.45, 1.0), (0.55, 0.55, 0.6, 1.0)
 51
 52
 53def show(value: bool | float | Vec2) -> str:
 54    """An action's value, printed in whatever type it has."""
 55    if isinstance(value, Vec2):
 56        return f"({value.x:+.2f}, {value.y:+.2f})"
 57    if isinstance(value, bool):
 58        return "held" if value else "--"
 59    return f"{value:+.2f}"
 60
 61
 62class InputActionsDemo(Node2D):
 63    """A shape driven entirely through named actions, never raw device state."""
 64
 65    dynamic = True
 66
 67    input_context = {
 68        "move": [Axis2D.WASD, Axis2D.ARROWS, Controller.LEFT_STICK],
 69        "jump": [Key.SPACE, Pointer.PRIMARY, Controller.A],
 70        "boost": [Key.SHIFT, Controller.B, Controller.RIGHT_TRIGGER],
 71        "reset": [Key.R, Controller.START],
 72    }
 73
 74    def on_ready(self):
 75        self.position = self.home()
 76        self.hop_y = self.hop_v = 0.0
 77        self.jumps = 0
 78
 79        hud = self.add_child(CanvasLayer())  # the readout stays put while the shape moves
 80        panel = hud.add_child(VBoxContainer(anchor=Anchor.TOP_LEFT, inset=10))
 81        panel.add_child(Label("Named Input Actions", font_size=24))
 82        self.rows = {name: panel.add_child(Label()) for name in self.input_context.actions}
 83        self.pads = panel.add_child(Label(font_size=14))
 84        self.boost_bar = panel.add_child(ProgressBar(max_value=1.0))
 85        self.counter = hud.add_child(Label(anchor=Anchor.TOP_RIGHT, inset=10))
 86
 87        self.add_child(TouchController(buttons=(Controller.A, Controller.B)))
 88
 89    def home(self) -> Vec2:
 90        width, height = self.viewport_size
 91        return Vec2(width / 2, height * 0.62)
 92
 93    @on_input("jump")
 94    def jump(self, event):
 95        if self.hop_y == 0.0:
 96            self.hop_v = HOP_VELOCITY
 97            self.jumps += 1
 98
 99    @on_input("reset")
100    def reset(self, event):
101        self.position = self.home()
102        self.jumps = 0
103
104    def on_update(self, dt: float):
105        speed = SPEED * (1.0 + Input.value("boost"))
106        step = self.position + Input.value("move") * speed * dt
107        self.position = step.clip(RADIUS, self.viewport_size - RADIUS)
108
109        self.hop_v += GRAVITY * dt
110        self.hop_y = min(self.hop_y + self.hop_v * dt, 0.0)
111        if self.hop_y == 0.0:
112            self.hop_v = 0.0
113
114        for name, row in self.rows.items():
115            legend = ", ".join(Input.label(source) for source in InputMap.get_sources(name))
116            row.text = f"{name} ({InputMap.kind(name)}): {show(Input.value(name))}   [{legend}]"
117            row.text_colour = ACTIVE if Input.pressed(name) else IDLE
118        pads = ", ".join(f"{pad.id}: {pad.source}" for pad in Input.controllers.values())
119        self.pads.text = f"controllers: {pads or 'none'}"
120        self.boost_bar.value = Input.value("boost")
121        self.counter.text = f"Jumps: {self.jumps}"
122
123    def on_draw(self, renderer):
124        boost = Input.value("boost")
125        renderer.circle((0, self.hop_y + RADIUS * 0.9), RADIUS * 0.8, fill=(0, 0, 0, 0.25))  # shadow
126        renderer.circle((0, self.hop_y), RADIUS, fill=(0.4 + 0.6 * boost, 0.8, 1.0 - 0.5 * boost, 1.0))
127
128
129if __name__ == "__main__":
130    App(title="Named Input Actions", width=960, height=540).run(InputActionsDemo())