Log inGet started

inputMap assetType

Updated October 6, 2026
racer.inputMap/
  init.luau                    -- what the map is, and how it composes
  boost.inputBinding/
    init.luau
  steer.inputBinding/
    init.luau
-- racer.inputMap/init.luau
return {
    name = "racer",
    description = "Throttle, steering and boost.",

    -- The group these controls belong to, and the groups this map stands
    -- down while it is live. Declaring neither composes with everything.
    group = "vehicle",
    suppresses = { "player" },
}

Each control carries kbm, gamepad and touch; the inputBinding assetType covers the record shape.

Reading it

A controller activates the map it needs and subscribes to the controls it drives. Nothing is live until something asks for it.

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. Keep it and hand it back: releasing the claim disconnects the subscriptions made through it, and the map stays live for whoever else holds it.

A control the map does not declare reads as nil, so a typo raises at the subscribe call. A controller written against more than one map tests for an optional control first — if controls.crouch then controls.crouch:onInput(...) end.

Several maps can be live at once, and the player's surface is the union of them. group / suppresses are how one map stands another down — a car takes the player over by naming the player group, and walking resumes the moment the car releases. Zin.scheme.live() reports each live map's group, suppresses and suppressedBy, and heldBy — who opened each activation still standing on it, so a control that goes quiet under a suppression leads to the activation doing it rather than to the map.

Whose keys a map reads

A key belongs to the surface it was pressed on. In the editor, where the scene shares the window with panels, the scene holds the keys pressed while it was the active input context — the last click landed on the viewport, or play just started. A key pressed after a click on a panel is the editor's, and a game's controls never see it. A map reads the scene's keys unless it declares otherwise:

return {
    label = "Editor",
    group = "editor",
    surface = "editor",   -- every key no focused widget took
}

surface = "editor" is for controls that belong to the editor around the scene — its hotkeys, its viewport camera — and answer wherever the author last clicked. Pads and touch belong to no panel and read the same on either surface. Zin.scheme.live() reports each map's surface.

Ref methods

  • :activate(label?) — load every <name>.inputBinding/ child, validate them, add them to the live binding set, and return the handles keyed by control name. Every fault across every child is reported in one error. Activating a map already live adds a holder, so two components sharing one map read one set of controls — each through its own handles, which are the claim it releases by. Every activation reads the children again, so a binding added, edited or removed since the map came up reaches the running set here and reaches the holders already standing on it. label names this activation in Zin.scheme.live().
  • :deactivate(claim) — release one holder's claim, disconnecting every subscription that claim made. The map stays live while another holder remains; the last one out takes the controls out of the live set. Name no claim and the caller's own most recent one goes.
  • :controls() — { name, label, kind, classes, problems } per child, read without activating anything, so an authoring tool can show a map's whole surface and everything wrong with it before it goes live.
  • :record() — the raw record its init.luau returns.
  • :effective() — the materialized effective map, without activating it.
  • :bindingsFor(name, class) — the bindings a named entry has for one device class in this map's effective form.

Writing one

The inputAuthor toolbox writes a map and its controls together, filling in canonical defaults for a control whose name it recognises:

tools.use("inputAuthor", "map", "racer", { "throttle", "steer", "boost" },
    { group = "vehicle", suppresses = { "player" } })
tools.use("inputAuthor", "check", "/zero/source/inputMaps/racer.inputMap")

asset.create("inputMap", name, { record = ... }) bakes a record into both shapes a map is read in — init.luau beside one <name>.inputBinding/ child per entry, each carrying all three device classes, a class the record had nothing for written as false with the reason beside it. That is the hook Zin.map.bake(name) drives to turn the active effective map into an editable asset.

The engine's built-in schemes live at @builtin::inputMaps.default, .editor, .editorMeta, .vehicle and .flight.

In the Inspector

Controls: whether this map is the one taking input now, how many controls it declares, and each control's name, label, kind, device classes and problem count.

  • asset-type
  • reference