Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

README.md

zinput

zinput is the engine's Luau-side input framework and the only public Luau input API. It sits on top of the internal __zero_input.* FFI (snapshot, events, frameId, pointer-lock, simulate*) and builds the control model above it in pure Luau: control assets grouped into maps, the live set of activated maps and its per-frame dispatch, per-device-class binding evaluation, the on-screen touch overlay, gesture recognisers, and the tick coordinator that advances all of it.

The top-level module re-exports every submodule under a single table and owns the tick coordinator (Zin.tick) plus the unified handle namespace for Zin.disconnect.

The control model

A control is a <name>.inputBinding/ folder inside a <name>.inputMap/ folder. It carries a label, a kind (button / axis1 / axis2), and a binding for each of the three device classes — kbm, gamepad, touch. A class set to false is a deliberate refusal.

Nothing is live until something activates a map. A world activates the map it needs and subscribes to the controls it uses:

public = {
    inputMap = Field.assetRef("inputMap", "@builtin::inputMaps.default", NoSync, "The controls this component reads."),
    speed = Field.number(5, NoSync, "World units per second at full deflection."),
    jumps = Field.number(0, NoSync, "How many times the jump control was pressed."),
}

local controls

function awake()
    controls = public.inputMap:activate()

    controls.move:onInput(function(v, dt, active)
        if not active then return end
        local p = public.entity.position
        local step = public.speed * dt
        public.entity.position = { p.x + v.x * step, p.y, p.z - v.y * step }
    end)

    controls.jump:onPressed(function() public.jumps += 1 end)
end

function onDestroy()
    public.inputMap:deactivate(controls)
end

What activate returned is this holder's claim on the map, and handing it back to deactivate disconnects the subscriptions made through it while the map stays live for whoever else holds it. Every activation reads the map's binding children again, so a control edited since it came up reaches the running set and the holders already standing on it.

Several maps are live at once and compose: the player's surface is the union of them, so picking up a gun adds Fire beside the movement stick and dropping it takes away Fire alone. A map declares the group its controls belong to and the groups it suppresses while live, which is how a car stands walking down without owning the walk map. Zin.scheme holds the live set and runs the dispatch.

man topics/input is the full walkthrough.

Exports

Subsystems (re-exported from the corresponding submodule):

  • M.scheme — the live set of activated maps and their per-frame dispatch.
  • M.bindings — binding constructors, evaluators, and the legend formatter.
  • M.state — per-frame snapshot reads plus held-time and synthetic repeat.
  • M.events — frame-event readers and on/off edge subscriptions.
  • M.input — onBegan / onEnded / onChanged / onTextInput listeners and lastInputType().
  • M.context — the input context stack.
  • M.chords — sequence and simultaneous recognisers.
  • M.gestures — tap / double-tap / long-press / swipe / pinch / pan recognisers.
  • M.touch — per-finger contacts and touch.* listeners.
  • M.surface — touch-vs-kbm device-class classification.
  • M.gamepad, connected pads, family, button legends, each device's elements (device), the haptic outputs it offers (capabilities), and rumble.
  • M.virtual — the per-tick virtual-control store on-screen controls write.
  • M.touchControls — the on-screen touch overlay (stick, drag-look zone, buttons) and the API that adds a control while a world runs.
  • M.emulation — the map-driven key-emulation floor.
  • M.pointer — pointer-lock mutations.
  • M.back: Back, claims a world holds while Back means something to it, and press to issue one. The engine routes each press to the topmost dismissible overlay, else the most recent claim, else the platform's own default (an Android app moves to the background).
  • M.test — synchronous test-input helpers.
  • M.controllers — free / orbit / fps controller factories.
  • M.profile — binding profiles, saved as JSON and loadable from @builtin::profiles.<name>.
  • M.rebind — interactive binding-capture sessions.
  • M.conflicts — the binding-collision reverse index.
  • M.autoTick — the opt-in per-frame tick worker.
  • M.utils — pure helpers.
  • M.actions / M.axes / M.map — the name-registered action, axis and map registry. A control asset replaced them; each carries its own --!deprecated message naming the replacement.

Tick coordinator:

  • M.tick(dt: number?) — advance every stateful subsystem.
  • M.lastTickAt() -> number / M.lastTickFrameId() -> number — test/debug accessors.
  • M._resetTickGuard() / M._resetLiveArmed() — test-only.

Handles:

  • M.disconnect(handle: number) -> boolean — accepts any Zin handle (input listener OR action handler) and routes to the right disconnect.

Constants:

  • M.VERSION — the library version string.
  • M.SPEC — the VFS path of the architecture spec.
  • M.ROADMAP — the VFS path of the roadmap document.

Raw state

Zin.state and Zin.events are the snapshot and event layer that bindings evaluate against — the layer the emulation floor, rebind capture, and dev tooling operate on directly. Gameplay code subscribes to controls; polling Zin.state.keyDown from a world makes that world keyboard-only and draws the engine-raw-key-poll diagnostic at author time.

Notes

  • Zin.tick(dt) is the single hub that advances every stateful subsystem each frame. Stateless features (state / events / utils / bindings / context) work without ticking; the live binding dispatch, axes, chords, subscriptions, held-time, repeat, and rebind are dormant until something ticks.
  • The first real input read arms the input system on its own: it starts Zin.autoTick, so dispatch, the touch overlay and the emulation floor advance with no host component and no explicit tick call. A host script component can still call Zin.tick(dt) from its update(dt) hook, and Zin.autoTick.start() remains available directly; both stay idempotent alongside the self-arming path. Zin.test engagement suppresses the arming so suites keep explicit control over tick cadence.
  • Same-frame re-tick dedup: __zero_input.events() is non-draining, so running the observe-and-dispatch pipeline twice in one engine frame would double-fire subscribers. The frame-id guard in M.tick and the autoTick worker keeps this from happening.
  • The handle namespace is unified: every handle from Zin.input.on* and Zin.actions.bind is drawn from a single counter, and Zin.disconnect routes any of them to the correct disconnect. Zin.input.disconnect and Zin.actions.disconnect are also wired to the unified dispatcher.
  • Library users can either grab the whole table (require("@builtin::modules.zinput")) or pull one submodule (require("@builtin::modules.zinput.state")).
  • __zero_input is an internal namespace — reachable from Luau but hidden from /docs/api/; production game code goes through Zin.