Input and Movement

drive a node from the keyboard with input actions.

▶ Run in browser

Tags: tutorial beginner input

Input and Movement

Now make something move when the player presses a key. SimVX does not ask you to check raw key codes scattered through your game. Instead you name an action (“move_right”) and bind keys to it once; your gameplay reads the action. Rebinding keys or adding a gamepad later never touches the movement code.

By the end you will drive a square around the window with WASD or the arrow keys.

Step 1: Declare the actions on the root node

Put an input_actions dictionary at class scope on your root node. Each action name maps to a list of bindings (here, two keys each):

class MovementDemo(Node2D):
    input_actions = {
        "move_left":  [Key.A, Key.LEFT],
        "move_right": [Key.D, Key.RIGHT],
        "move_up":    [Key.W, Key.UP],
        "move_down":  [Key.S, Key.DOWN],
    }

Why the class attribute, not main()? SimVX registers input_actions when the scene starts, before the first frame. Registering actions inside main() works on the desktop but is silently skipped by the web export (which never runs main()), so your web build would have no input. The class attribute is the one path that works everywhere.

Step 2: Read the actions and move

Input.get_strength(action) returns how hard an action is held, from 0 to 1. It reads identically for a key (0 or 1) or a gamepad axis (anything between), so the same line of code handles both. Subtract opposite directions to get a signed axis:

class Player(Node2D):
    SPEED = 360.0

    def on_update(self, dt):
        dx = Input.get_strength("move_right") - Input.get_strength("move_left")
        dy = Input.get_strength("move_down")  - Input.get_strength("move_up")
        self.position += Vec2(dx, dy) * self.SPEED * dt

Step 3: Stay frame-rate independent

Multiplying by dt (seconds since the last frame) is what keeps the speed the same on a 60 fps and a 144 fps machine. Never add a fixed amount per frame: the game would run faster on faster hardware.

Run it

uv run python examples/tutorials/input_and_movement/main.py

What’s next

  • Bouncing BallsProperty descriptors and spawning many children.

  • Pong – put input, signals, and collision together into a full game.

Source

 1"""Input and Movement: drive a node from the keyboard with input actions.
 2
 3The third lesson: turn key presses into movement. Instead of checking raw keys,
 4SimVX uses named *input actions* ("move_left") that you bind to one or more keys.
 5Your game logic reads the action, so rebinding keys (or adding a gamepad) never
 6touches gameplay code.
 7
 8Move the square with WASD or the arrow keys.
 9
10# /// simvx
11# tags = ["tutorial", "beginner", "input"]
12# web = { root = "MovementDemo", width = 800, height = 600, responsive = true }
13# ///
14
15## What you will learn
16
17- **Input actions** -- name an intent ("move_right") and bind keys to it.
18- **`input_actions` class attribute** -- the web-safe way to register actions on the
19  root node (registering in `main()` is skipped by the web export, so actions there
20  are silently lost).
21- **`Input.get_strength(action)`** -- read how hard an action is held (0..1), which
22  reads the same for a key or a gamepad axis.
23- **Frame-rate-independent movement** -- multiply speed by `dt` so motion is the same
24  on any machine.
25
26## How it works
27
28`MovementDemo` (the root) declares `input_actions` at class scope, binding each
29direction to two keys. The `Player` child reads the four actions every frame,
30turns them into a velocity vector, and moves itself by `velocity * speed * dt`,
31clamped to the window. No raw key codes appear in the movement code.
32"""
33
34from simvx.core import Input, Key, Node2D, Vec2
35from simvx.graphics import App
36
37WIDTH, HEIGHT = 800, 600
38
39
40class Player(Node2D):
41    """Reads the named actions and moves. Knows nothing about which keys are bound."""
42
43    SIZE = 56
44    SPEED = 360.0
45
46    def on_update(self, dt: float):
47        # get_strength returns 0..1 per action; subtracting opposite directions
48        # gives a signed axis that works for keys and gamepad sticks alike.
49        dx = Input.get_strength("move_right") - Input.get_strength("move_left")
50        dy = Input.get_strength("move_down") - Input.get_strength("move_up")
51        self.position += Vec2(dx, dy) * self.SPEED * dt
52
53        half = self.SIZE / 2
54        self.position.x = max(half, min(WIDTH - half, self.position.x))
55        self.position.y = max(half, min(HEIGHT - half, self.position.y))
56
57    def on_draw(self, renderer):
58        half = self.SIZE / 2
59        top_left = (self.position.x - half, self.position.y - half)
60        renderer.draw_rect(top_left, (self.SIZE, self.SIZE), colour=(0.4, 0.8, 1.0, 1.0), filled=True)
61
62
63class MovementDemo(Node2D):
64    """Root node: declares the input actions and spawns the player."""
65
66    # The canonical, web-safe registration path. SimVX reads this when the scene
67    # starts, before the first frame. Each action maps to a list of bindings.
68    input_actions = {
69        "move_left": [Key.A, Key.LEFT],
70        "move_right": [Key.D, Key.RIGHT],
71        "move_up": [Key.W, Key.UP],
72        "move_down": [Key.S, Key.DOWN],
73    }
74
75    def on_ready(self):
76        self.player = self.add_child(Player(position=Vec2(WIDTH / 2, HEIGHT / 2)))
77
78    def on_draw(self, renderer):
79        renderer.draw_text("Input and Movement", (20, 20), scale=2, colour=(1, 1, 1))
80        renderer.draw_text("WASD or arrow keys to move", (20, 52), scale=1, colour=(0.7, 0.7, 0.7))
81
82
83if __name__ == "__main__":
84    App(title="Input and Movement", width=WIDTH, height=HEIGHT).run(MovementDemo())