DEMIENGINEDOCUMENTATION Everything is free
Create

In this guide

Action maps, bindings, contexts, gamepads, touch, and deterministic replay.

Input

Input is action-based and contextual. Keyboard, mouse, gamepads, touch, gestures, and HUD virtual controls all feed the same action queries, so gameplay code never cares which device produced the signal.

Declare actions in the project

"input": {
  "presets": ["wasd_arrows", "confirm", "gamepad_confirm", "move_3d"],
  "actions": {
    "move": {
      "type": "vector2",
      "context": "gameplay",
      "bindings": [
        { "input": "key:a", "vector": [-1, 0] },
        { "input": "key:d", "vector": [1, 0] },
        { "input": "gamepad:stick:left", "deadzone": 0.18 },
        { "input": "virtual:move" }
      ]
    },
    "jump": {
      "type": "button",
      "context": "gameplay",
      "bindings": [
        { "input": "key:space" },
        { "input": "gamepad:south" }
      ]
    }
  }
}

Each action is a named logical control. Bindings list the physical inputs that feed it:

  • type is button (pressed or not), axis1d (one scalar −1…1), or vector2 (two scalars).
  • context groups actions so menus can disable gameplay without touching scripts. Defaults to gameplay.
  • vector on a key binding maps a key to a direction (used for WASD).
  • deadzone (0…0.99) ignores small stick deflection.
  • scale (−1…1) and invert flip or shrink an axis.
  • normalize normalizes that binding’s own contribution to unit length.
  • player routes the binding to a local player slot.

Prefer the input presets — wasd_arrows, confirm, gamepad_confirm, move_3d — and override only what you need.

Device names

PatternExamples
key:<key>key:w, key:space, key:left_shift
mouse:<button>mouse:left, mouse:right, mouse:middle
gamepad:<button>gamepad:south, gamepad:start, gamepad:dpad_up
gamepad:axis:<axis>gamepad:axis:left_trigger, gamepad:axis:right_x
gamepad:stick:left|right2D stick, respects deadzone
virtual:<control>virtual:move, virtual:move:x, virtual:fire

Names are case-insensitive. HUD virtual_button and virtual_stick nodes publish virtual:<control> bindings.

Action queries

local Input = require("demi.input")

if Input.pressed("jump") then
    -- true only on the frame the action went down
end
if Input.down("fire") then
    -- true every frame while held
end
if Input.released("fire") then
    -- true only on the frame it came up
end

All three return a plain boolean. Call them once per frame and store the result if you need it in several places.

local strength = Input.value("accelerate")  -- −1 … 1 (axis1d)
local x, y = Input.vector("move")           -- two scalars, clamped to a unit circle
local rx, ry = Input.raw_vector("move")     -- two scalars, unclamped
local source = Input.source("jump")         -- "key:space", "gamepad:south", …
  • value is how hard an analog control is pressed; buttons report 0 or 1.
  • vector is safe to feed straight into movement (already normalized).
  • raw_vector keeps the unnormalized values when you want speed to scale with stick deflection.
  • source names the binding that produced the current value — useful for “press [key]” prompts.

Raw key helpers bypass actions and read hardware directly. Use them for debug tools and editors:

if Input.key_down("left_shift") then end
if Input.key_pressed("f1") then end
if Input.key_released("f1") then end

Need a one-dimensional value from two buttons? There is no Input.axis helper. Declare two axis1d actions, or check the buttons in Lua and combine them yourself:

"input": {
  "actions": {
    "turn_left":  { "type": "axis1d", "bindings": [ { "input": "key:a", "scale": 1 } ] },
    "turn_right": { "type": "axis1d", "bindings": [ { "input": "key:d", "scale": 1 } ] }
  }
}
local turn = Input.value("turn_right") - Input.value("turn_left")

Contexts

Input.enable_context("menu")      -- menu actions become live
Input.disable_context("gameplay") -- gameplay actions go silent
if Input.context_enabled("gameplay") then end

Only actions whose context is currently enabled respond. An empty enabled set means every action reads as released. This is how a modal menu takes over without editing gameplay scripts.

Gamepads, rebinding, and persistence

Input.assign_gamepad(1, 0)          -- device index 1 → player 0; true when applied
local count = Input.gamepad_count()  -- how many are connected

Input.rebind("jump", 1, "key:e", 0)  -- action, binding index (one-based), new control, player
local ok = Input.save_bindings("input/bindings.json")   -- true on success, plus an error string
local ok = Input.load_bindings("input/bindings.json")   -- true when loaded

save_bindings and load_bindings both return a success boolean and an error string. Relative paths resolve below Application.user_data_path(), so player remaps survive updates and never write into the game install; absolute paths are used as supplied.

Touch and gestures

for _, t in ipairs(Input.touches()) do
    -- t.id, t.phase ("began"|"moved"|"stationary"|"ended"|"cancelled"),
    -- t.x, t.y, t.dx, t.dy, t.pressure
end

for _, g in ipairs(Input.gestures()) do
    -- g.type ("tap"|"double_tap"|"long_press"|"drag"|"pinch"|"rotate"),
    -- g.pointer_id, g.x, g.y, g.dx, g.dy, g.value
end

print(Input.touch_count())

Both return arrays (empty when nothing happened this frame). Related helpers: Input.mouse_down, mouse_position, mouse_delta, mouse_world_position, viewport_size, ui_pointer_captured (true when a HUD control owns the pointer), text_entered, set_text_input_active.

Deterministic replay

Replay fixtures record action frames and re-apply them through the authored context filter. Enabled recorded actions take precedence over physical bindings. The project fixed timestep must match the fixture.

demi run \
  --input-replay fixtures/smoke.replay.json

See Testing & debugging for E2E-driven input and replay workflows.