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.
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 aVec2. 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.PRIMARYis 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())