Proposal: revamp the input system — action maps, rebinding, gamepads, touch, deterministic replay #7

Closed
opened 2026-08-29 18:13:01 +02:00 by orkun · 1 comment
Owner

Context

Ludic's input is a single call — Input.key() returns this frame's key code. That's enough for a demo, not a game. This proposes a full input system, modeled on Godot's InputMap / Input and Unity's Input System (action maps, rebinding, multiple devices), plus SDL GameController-style gamepad handling. It supersedes/expands the Input.* additions sketched in #2.

Determinism tie-in (a real feature): input is I/O, but Ludic can snapshot the per-frame action state into a compact record. Recording that stream and replaying it against the deterministic sim yields free, exact replays and lockstep netcode — a standout capability for a deterministic engine. Design the API so "read input" and "read a recorded input snapshot" are the same call.

Layer 1 — raw devices

  • Keyboard: Input.key_down(k), Input.key_pressed(k) (edge), Input.key_released(k), Input.any_key(), Input.text() (buffered text for names/chat).
  • Mouse/pointer: Input.mouse_pos() -> vec2, Input.mouse_delta() -> vec2, Input.mouse_down(b), Input.mouse_pressed(b), Input.wheel() -> int.
  • Gamepad(s): Input.pad_count(), Input.pad_button(pad, b) / pad_pressed, Input.pad_axis(pad, a) -> fixed, Input.pad_stick(pad, side) -> vec2, Input.pad_trigger(pad, side) -> fixed; hotplug events PadConnected/PadDisconnected (via event/@On). SDL-style standard button/axis names so layouts are portable.
  • Touch: Input.touch_count(), Input.touch(i) -> vec2, tap/hold; basic gestures later.

Layer 2 — action map (the important idea)

Bind named actions to many physical inputs; gameplay reads actions, not keys — which is what makes rebinding, multiple schemes, and local multiplayer clean (Godot InputMap, Unity Input System).

# declare actions + default bindings (data, editable/rebindable at runtime)
Input.action("jump",  keys: ['z', ' '], pad: [Pad.A])
Input.action("move_x", axis_keys: ['a', 'd'], pad_axis: [Pad.LeftStickX])

# gameplay reads by action — device-agnostic
if Input.action_pressed("jump") { ... }              # edge
let run = Input.action_down("attack")                 # held
let move = Input.vector("move_left","move_right","move_up","move_down")  # -> vec2, deadzoned
let x = Input.action_strength("move_x")               # analog 0..1 (fixed)
  • Analog: action_strength / axis / vector return fixed/vec2 with deadzones (per-action and global, like Godot's joy-axis deadzone).
  • Rebinding at runtime: Input.rebind("jump", event), Input.clear_binding(...), Input.bindings("jump") — for a settings/accessibility menu.
  • Multiple schemes / local multiplayer: assign a device (or set) to a player: Input.assign(player: 1, pad: 0), then Input.action_pressed("jump", player: 1).

Layer 3 — determinism & replay

  • Input.snapshot() -> InputFrame captures all action/axis states for the frame.
  • Input.record(on) / Input.replay(stream) — feed recorded snapshots so the sim reproduces exactly. Same read API in live and replay mode. Foundation for netcode rollback and diffable headless tests.

Phasing

  1. Keyboard edges (key_down/pressed/released) + mouse (pos/buttons/wheel).
  2. Action map: action, action_pressed/down, axis/vector, deadzones.
  3. Gamepad(s) + hotplug events; runtime rebinding.
  4. Touch/gestures; per-player device assignment; snapshot/record/replay.

References

  • Godot Input and InputMap (actions, action_add_event/action_erase_events rebinding, get_vector, joy-axis deadzones).
  • GDQuest Input cheatsheet; Unity Input System (action maps, rebinding, multi-device, hot-swap); SDL GameController.
  • Related: vec2 in #1; event/@On for device hotplug; Random.stream/determinism ethos in #2.

Action-map input with gamepads/touch/rebinding — and per-frame snapshots that turn the deterministic sim into free replays.

## Context Ludic's input is a single call — `Input.key()` returns this frame's key code. That's enough for a demo, not a game. This proposes a full input system, modeled on **Godot's `InputMap` / `Input`** and **Unity's Input System** (action maps, rebinding, multiple devices), plus **SDL GameController**-style gamepad handling. It supersedes/expands the `Input.*` additions sketched in #2. > **Determinism tie-in (a real feature):** input is I/O, but Ludic can snapshot the **per-frame action state** into a compact record. Recording that stream and replaying it against the deterministic sim yields **free, exact replays and lockstep netcode** — a standout capability for a deterministic engine. Design the API so "read input" and "read a recorded input snapshot" are the same call. ## Layer 1 — raw devices - **Keyboard:** `Input.key_down(k)`, `Input.key_pressed(k)` (edge), `Input.key_released(k)`, `Input.any_key()`, `Input.text()` (buffered text for names/chat). - **Mouse/pointer:** `Input.mouse_pos() -> vec2`, `Input.mouse_delta() -> vec2`, `Input.mouse_down(b)`, `Input.mouse_pressed(b)`, `Input.wheel() -> int`. - **Gamepad(s):** `Input.pad_count()`, `Input.pad_button(pad, b)` / `pad_pressed`, `Input.pad_axis(pad, a) -> fixed`, `Input.pad_stick(pad, side) -> vec2`, `Input.pad_trigger(pad, side) -> fixed`; hotplug events `PadConnected`/`PadDisconnected` (via `event`/`@On`). SDL-style standard button/axis names so layouts are portable. - **Touch:** `Input.touch_count()`, `Input.touch(i) -> vec2`, tap/hold; basic gestures later. ## Layer 2 — action map (the important idea) Bind named **actions** to many physical inputs; gameplay reads actions, not keys — which is what makes rebinding, multiple schemes, and local multiplayer clean (Godot `InputMap`, Unity Input System). ``` # declare actions + default bindings (data, editable/rebindable at runtime) Input.action("jump", keys: ['z', ' '], pad: [Pad.A]) Input.action("move_x", axis_keys: ['a', 'd'], pad_axis: [Pad.LeftStickX]) # gameplay reads by action — device-agnostic if Input.action_pressed("jump") { ... } # edge let run = Input.action_down("attack") # held let move = Input.vector("move_left","move_right","move_up","move_down") # -> vec2, deadzoned let x = Input.action_strength("move_x") # analog 0..1 (fixed) ``` - **Analog:** `action_strength` / `axis` / `vector` return `fixed`/`vec2` with **deadzones** (per-action and global, like Godot's joy-axis deadzone). - **Rebinding at runtime:** `Input.rebind("jump", event)`, `Input.clear_binding(...)`, `Input.bindings("jump")` — for a settings/accessibility menu. - **Multiple schemes / local multiplayer:** assign a device (or set) to a player: `Input.assign(player: 1, pad: 0)`, then `Input.action_pressed("jump", player: 1)`. ## Layer 3 — determinism & replay - `Input.snapshot() -> InputFrame` captures all action/axis states for the frame. - `Input.record(on)` / `Input.replay(stream)` — feed recorded snapshots so the sim reproduces exactly. Same read API in live and replay mode. Foundation for netcode rollback and diffable headless tests. ## Phasing 1. Keyboard edges (`key_down/pressed/released`) + mouse (`pos/buttons/wheel`). 2. Action map: `action`, `action_pressed/down`, `axis`/`vector`, deadzones. 3. Gamepad(s) + hotplug events; runtime rebinding. 4. Touch/gestures; per-player device assignment; `snapshot`/`record`/`replay`. ## References - [Godot `Input`](https://docs.godotengine.org/en/stable/classes/class_input.html) and `InputMap` (actions, `action_add_event`/`action_erase_events` rebinding, `get_vector`, joy-axis deadzones). - [GDQuest Input cheatsheet](https://school.gdquest.com/cheatsheets/input); Unity Input System (action maps, rebinding, multi-device, hot-swap); SDL GameController. - Related: `vec2` in #1; `event`/`@On` for device hotplug; `Random.stream`/determinism ethos in #2. _Action-map input with gamepads/touch/rebinding — and per-frame snapshots that turn the deterministic sim into free replays._
orkun added the
proposal
priority:medium
area:input
labels 2026-08-29 19:51:37 +02:00
Author
Owner

Shipped the two ideas this proposal leads with — action maps ("the important idea": gameplay reads actions, not keys) and deterministic record/replay ("a real feature… a standout capability for a deterministic engine") — in 377b6d1.

  • Action maps + runtime rebinding. Input.bind(action, key) binds a key to a named action; Input.down(action) / Input.pressed(action) read it (held vs one-shot edge); Input.rebind(action, from, to) remaps it at runtime — the primitive a rebinding menu is built on. Gameplay reads the action, so a rebind changes nothing in the game logic.
  • Deterministic replay. Input.poll() is the single per-frame input read the actions sit on. Input.record() captures the polled key each frame; Input.replay() feeds the tape back so Input.poll reads the recording instead of the device — a run reproduces exactly. As the issue asks, "read input" and "read a recorded snapshot" are the same call.

All integer and deterministic, in runtime/native/input.ludic, with seven docs/language/input/ pages. Proof: examples/library/input_actions.ludic binds actions, rebinds one at runtime, records two frames and replays them, asserting 1 0 1 1 0 1 0 — now in the regression suite (75 passed, self-host fixpoint intact, no golden drift).

The raw device layer — simultaneous held keys, Input.key_down/pressed/released, analog axis/vector with deadzones, mouse, gamepads (SDL-style), and touch — all needs a platform key-state backend (keydown+keyup into a held set) across the Cocoa/headless/wasm targets rather than the current one-key-per-frame poll, so it is a runtime/platform job tracked in #50 (which also covers extending the replay tape to the full multi-key action snapshot). Closing as the action-map + replay core is delivered.

Shipped the two ideas this proposal leads with — **action maps** ("the important idea": gameplay reads actions, not keys) and **deterministic record/replay** ("a real feature… a standout capability for a deterministic engine") — in 377b6d1. - **Action maps + runtime rebinding.** `Input.bind(action, key)` binds a key to a named action; `Input.down(action)` / `Input.pressed(action)` read it (held vs one-shot edge); `Input.rebind(action, from, to)` remaps it at runtime — the primitive a rebinding menu is built on. Gameplay reads the action, so a rebind changes nothing in the game logic. - **Deterministic replay.** `Input.poll()` is the single per-frame input read the actions sit on. `Input.record()` captures the polled key each frame; `Input.replay()` feeds the tape back so `Input.poll` reads the recording instead of the device — a run reproduces exactly. As the issue asks, "read input" and "read a recorded snapshot" are the same call. All integer and deterministic, in `runtime/native/input.ludic`, with seven `docs/language/input/` pages. **Proof:** `examples/library/input_actions.ludic` binds actions, rebinds one at runtime, records two frames and replays them, asserting `1 0 1 1 0 1 0` — now in the regression suite (75 passed, self-host fixpoint intact, no golden drift). The **raw device layer** — simultaneous held keys, `Input.key_down`/`pressed`/`released`, analog `axis`/`vector` with deadzones, mouse, gamepads (SDL-style), and touch — all needs a platform **key-state backend** (keydown+keyup into a held set) across the Cocoa/headless/wasm targets rather than the current one-key-per-frame poll, so it is a runtime/platform job tracked in #50 (which also covers extending the replay tape to the full multi-key action snapshot). Closing as the action-map + replay core is delivered.
orkun closed this issue 2026-08-31 15:04:29 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: workshopsoft/ludic#7
No description provided.