---
title: "inputBinding assetType"
description: "One control in a scheme — jump, move, boost — expressed as an asset."
section: "Types"
slug: "types-inputbinding"
canonical: "https://origozero.ai/docs/types-inputbinding"
updated: "2026-10-06T23:13:49.155406104+00:00"
tags: ["asset-type", "reference"]
---

# inputBinding assetType

Containment is what records the reference, so a scene that carries its map
carries every control the map declares. A scene pulled into another world
cannot arrive somewhere `boost` binds to nothing.

```luau
-- racer.inputMap/boost.inputBinding/init.luau
local B = require("@builtin::modules.zinput.bindings")

return {
    label = "Boost",
    kind = "button",

    kbm     = { B.key("ShiftLeft") },
    gamepad = { B.padButton("left_shoulder") },
    touch   = { B.touchButton({ zone = "right-lower" }) },
}
```

## Every device class, always

`kbm`, `gamepad` and `touch` are all required. A control bound on one device
and not another is how a world ships playable on a desktop and dead on a
phone, so a record missing a class is refused at activation, naming the
binding and the class it lacks.

Setting a class to `false` suppresses it deliberately — a text-entry control
with no touch counterpart says so, and says it in the record where a reader
sees it.

The cost of that rule is paid at authoring time rather than by the author:
the `inputAuthor` toolbox stamps canonical defaults for a control whose name
the catalog knows, so a complete three-class binding is one call.

## Kinds

| `kind` | Shape per class | What it is |
|---|---|---|
| `button` | an **array** of bindings — any one satisfies it | A press: jump, fire, interact |
| `axis1` | **one** binding | A scalar: throttle, zoom, lean |
| `axis2` | **one** binding | A vector: movement, look, aim |

A button takes an array because a control usually has more than one way to
be pressed on the same device (`ShiftLeft` and `ShiftRight`). An axis takes
one because an axis evaluates exactly one binding per class.

```luau
-- move.inputBinding/init.luau
return {
    label = "Move",
    kind = "axis2",

    kbm     = B.wasd(),
    gamepad = B.padStick("left"),
    touch   = B.touchStick({ zone = "left" }),
}
```

## Shaping an axis

An `axis1` / `axis2` control conditions its reading before a consumer sees
it. Four optional fields, applied every tick in this order:

| Field | What it does |
|---|---|
| `deadzone` | A magnitude below it reads 0. On an `axis1` that is the value; on an `axis2` it is **radial** — the magnitude of the pair — so a diagonal held past the threshold keeps both components |
| `curve` | `"linear"` (the default), `"quadratic"` (squares, keeping the sign), `"cubic"`, or a function taking the reading and returning the shaped one |
| `invert` | `true` negates the reading — both components, for an `axis2` |
| `smoothing` | The time constant, in seconds, the reading approaches its target over. `0` (the default) arrives at once |

```luau
-- look.inputBinding/init.luau
return {
    label = "Look",
    kind = "axis2",

    deadzone = 0.12,
    curve = "quadratic",
    smoothing = 0.05,

    kbm     = B.mouseDelta(),
    gamepad = B.padStick("right", { as = "delta", unitsPerSecond = 1200 }),
    touch   = B.touchDrag({ zone = "right" }),
}
```

The shaping lands on the reading the classes produced, after `as` /
`unitsPerSecond` / `scale` have brought each of them into the control's own
unit — so one `deadzone` is written once, in that unit, and covers the
stick, the mouse and the finger together.

`smoothing` carries the same value toward whatever the devices report next,
so a control whose context leaves, whose group stands down, or whose gate
closes drains to neutral rather than cutting to it, and reports its drop
frame when it arrives.

Shaping is refused on a `button`: it is held or it is not, so there is no
magnitude to cut, bend or approach. Say when a button may read at all with
`gate`.

## Events

A binding exposes the events a controller subscribes to. Handlers take
positional arguments, so a movement handler reads them directly rather than
unpacking a table every frame.

| Event | Fires |
|---|---|
| `input` | Every frame the binding is active, carrying `(value, dt, active)` — and once more on the frame it goes inactive, with `active = false` and the neutral value |
| `pressed` | The press edge of a button |
| `released` | The release edge of a button |
| `changed` | When the value changes |

`input` firing on the drop frame is what lets one handler both start and
stop motion. Inside a component, whose hooks reach its fields through
`public`:

```luau
map.move:onInput(function(v, dt, active)
    public.moving = 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)
```

## Ref methods

- `:record()` — the validated binding record. Raises when a class is
  missing or the record is malformed, naming what is wrong.
- `:problems()` — every problem with the record, without raising. What an
  authoring tool reports so a whole map's faults surface at once.
- `:inspect()` — `{ label, kind, classes }` for `asset.inspect`.
- `.events` — the subscribe-only view over the four events above.

## Creating one

`asset.create("inputBinding", name, opts)` writes a record carrying every
device class, so a binding is complete the moment it exists:

```luau
asset.create("inputBinding", "boost", {
    label = "Boost",
    kind = "button",
    kbm = '{ B.key("ShiftLeft") }',
    gamepad = '{ B.padButton("left_shoulder") }',
    touch = '{ B.touchButton({ zone = "right-lower" }) }',
})
```

## In the Inspector

Binding: the binding's label, its kind and the device classes it binds, every problem its definition has, and **Open** for its source.
