Builtin Library
- ..
- .metadata17 B
- README.md7.4 KB
- actions.module
- arming.module
- autoTick.module
- axes.module
- back.module
- bindings.module
- chords.module
- clock.module
- conflicts.module
- context.module
- controllers.module
- emulation.module
- events.module
- gamepad.module
- gestures.module
- init.luau30.1 KB
- input.module
- map.module
- observe.module
- pointer.module
- profile.module
- quiesce.module
- rebind.module
- scheme.module
- state.module
- surface.module
- test.module
- text.module
- touch.module
- touchControls.module
- utils.module
- virtual.module
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 andon/offedge subscriptions.M.input—onBegan/onEnded/onChanged/onTextInputlisteners andlastInputType().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 andtouch.*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, andpressto 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/fpscontroller 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--!deprecatedmessage 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 rightdisconnect.
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 callZin.tick(dt)from itsupdate(dt)hook, andZin.autoTick.start()remains available directly; both stay idempotent alongside the self-arming path.Zin.testengagement 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 inM.tickand the autoTick worker keeps this from happening. - The handle namespace is unified: every handle from
Zin.input.on*andZin.actions.bindis drawn from a single counter, andZin.disconnectroutes any of them to the correctdisconnect.Zin.input.disconnectandZin.actions.disconnectare 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_inputis an internal namespace — reachable from Luau but hidden from/docs/api/; production game code goes through Zin.