Input#

There are two ways to read the player, and picking the wrong one is the most common source of input bugs in OneJS.

Use it for
Pointer events on an elementAnything the UI reacts to: buttons, dragging, hovering, hit testing against your layout
The input moduleGame input: keys, gamepads, raw mouse movement, touches, and anything polled every frame

Events are pushed to you when something happens to an element. input is polled: you ask what is held right now, from inside a frame loop.

import { input } from "onejs-unity/input"

Quick Start#

import { input } from "onejs-unity/input"
import { useAnimationFrame } from "onejs-unity/gpu"

function Player() {
    useAnimationFrame(() => {
        const move = input.keyboard.wasd()
        player.x += move.x * SPEED

        if (input.keyboard.wasKeyPressed("Space")) player.jump()
        if (input.mouse.wasLeftPressed) player.shoot()
    })

    return <View />
}

useAnimationFrame is one way to get a per-frame callback; your own requestAnimationFrame loop works just as well. The important part is that input is read inside it.

Keyboard#

input.keyboard.isKeyDown("W")        // held right now
input.keyboard.wasKeyPressed("Space") // went down this frame
input.keyboard.wasKeyReleased("Space")// came up this frame

input.keyboard.shift   // modifiers
input.keyboard.ctrl
input.keyboard.alt
input.keyboard.meta

input.keyboard.anyKeyDown
input.keyboard.anyKeyPressed

Key names are Unity's Key enum names, plus aliases for the ones people spell differently: Enter and Return, Escape and Esc, LeftArrow and Left, Shift for LeftShift. Matching is case-insensitive, so "space" and "Space" are the same key. A name that matches nothing logs a warning and reads as never pressed, so check the Console if a key seems dead.

Movement axes#

Four helpers cover the usual movement cases, so you are not writing the same four isKeyDown calls in every game:

input.keyboard.wasd()      // W A S D    -> { x, y }
input.keyboard.arrows()    // arrow keys -> { x, y }

input.keyboard.axis2D({ up: "W", down: "S", left: "A", right: ["A", "LeftArrow"] })
input.keyboard.axis({ negative: "Q", positive: "E" })   // -1, 0, or 1

Any direction takes an array, so one axis can accept WASD and the arrows together.

Two things to know about what comes back:

  • y is positive upward. This follows Unity, not the UI. UI Toolkit's top grows downward, so moving an element with the keyboard means top -= move.y * speed, not +=. Getting this wrong makes W move the player down, which looks like a bug in your maths rather than a sign.
  • Diagonals are not normalized. Holding W and D gives { x: 1, y: 1 }, which is about 1.41 long, so diagonal movement is faster than straight movement. Normalize it yourself if that matters:
const move = input.keyboard.wasd()
const length = Math.hypot(move.x, move.y) || 1
player.x += (move.x / length) * SPEED

Mouse#

input.mouse.position   // { x, y } in screen coordinates
input.mouse.delta      // movement since last frame
input.mouse.scroll     // wheel delta

input.mouse.leftButton     // held: also rightButton, middleButton,
                           // forwardButton, backButton
input.mouse.wasLeftPressed // this frame: also wasRightPressed, wasMiddlePressed
input.mouse.wasLeftReleased// this frame: also wasRightReleased, wasMiddleReleased

position is in Unity screen coordinates, which is not the space a pointer event uses. See Coordinate spaces below before you mix the two.

Gamepad#

input.gamepad is the first connected pad, or null when nothing is plugged in, so a game that supports a pad but does not require one reads it with ?.:

const gp = input.gamepad
if (gp?.wasButtonPressed("South")) {
    player.jump()
    gp.rumblePulse(0.5, 0.15)
}

const move = gp?.leftStick ?? { x: 0, y: 0 }
SticksleftStick, rightStick: { x, y } from -1 to 1
TriggersleftTrigger, rightTrigger: 0 to 1
Face buttonsbuttonSouth, buttonEast, buttonWest, buttonNorth
ShouldersleftShoulder, rightShoulder
Sticks pressedleftStickButton, rightStickButton
MenustartButton, selectButton
D-paddpad.up, dpad.down, dpad.left, dpad.right
EdgeswasButtonPressed(name), wasButtonReleased(name), isButtonDown(name)
Hapticsrumble(low, high, duration?), rumblePulse(intensity, duration), stopRumble()

Button names accept several vocabularies for the same button, so "South", "A" and "Cross" all mean the bottom face button. Also available: LeftShoulder / LB / L1, Start, Select, and the d-pad directions.

For local multiplayer, input.gamepads is the full list and input.gamepadCount its length. input.pauseHaptics() and input.resumeHaptics() stop and restart rumble everywhere at once, which is what a pause menu wants.

Touch#

for (const touch of input.touches) {
    if (touch.phase === "ended" || touch.phase === "canceled") continue
    aimAt(touch.position.x, touch.position.y)
}

Each touch carries fingerId, position, delta, and a phase of "began", "moved", "stationary", "ended" or "canceled". input.touchCount is the number of active touches.

Touch positions are in the same screen coordinates as the mouse, with the same caveat.

Three things that catch people out#

Vectors are reused, not returned fresh#

input.mouse.position, delta, scroll and the gamepad sticks all hand back the same object every time, refilled with current values. That keeps a frame loop free of allocations, and it means holding on to one does not hold on to a value:

const start = input.mouse.position     // NOT a snapshot
// ... a frame later ...
const dragged = input.mouse.position.x - start.x   // always 0: same object

Copy the numbers out when you need them to survive the frame:

const start = { x: input.mouse.position.x, y: input.mouse.position.y }

The keyboard axis helpers (wasd, arrows, axis2D) return a new object each call, so they are safe to keep. Touches are pooled like the mouse vectors.

Pressed and released are independent#

wasKeyPressed and wasKeyReleased are separate edge records, not two halves of one flag. Unity stamps each transition with the update it happened in and compares that against the current one, so both can be true in the same frame when a key goes down and comes back up quickly. Write handlers that tolerate it:

// Fine: each edge is handled on its own terms
if (input.keyboard.wasKeyPressed("Space")) startCharge()
if (input.keyboard.wasKeyReleased("Space")) fire()

// Fragile: assumes a release can never share a frame with its press
if (input.keyboard.wasKeyReleased("Space") && !charging) return

Holding a key does not re-fire wasKeyPressed. It is true on the frame the key went down and false for every frame after that, however long the key is held. If you want something to happen repeatedly while a key is held, poll isKeyDown instead and run your own timer.

Coordinate spaces#

input.mouse.position and a pointer event's e.x / e.y describe the same pointer in two different spaces:

Originy axisUnits
input.mouse.position, touch.positionbottom leftincreases upwardscreen pixels
e.x, e.y on a pointer eventtop leftincreases downwardpanel units

Mixing them flips the y axis, and under a PanelSettings scale mode that is not one-to-one with the screen it also scales the result. Nothing warns you: the pointer just ends up somewhere plausible and wrong, usually mirrored vertically.

The reliable rule is to pick one source per job rather than converting between them:

  • Positioning anything against your layout, hit testing an element, dragging: use pointer events, and subtract the element's worldBound for element-local coordinates. See Events.
  • Aiming, camera control, anything measured against the world rather than the UI: use input.mouse.

If you genuinely need to cross over, convert with RuntimePanelUtils.ScreenToPanel, and note that it handles the scale but not the flip. You supply the flip yourself:

import { RuntimePanelUtils } from "UnityEngine/UIElements"
import { Screen, Vector2 } from "UnityEngine"

/** A screen-space point (from `input`) in the space a pointer event uses. */
function toPanel(element, screenX, screenY) {
    return RuntimePanelUtils.ScreenToPanel(
        element.panel,
        new Vector2(screenX, Screen.height - screenY),
    )
}

const p = toPanel(ref.current, input.mouse.position.x, input.mouse.position.y)

Screen.height - screenY is the flip. ScreenToPanel then divides by the panel's scale, which is what makes the result correct under ScaleWithScreenSize as well as ConstantPixelSize. Two limits: Screen.height refers to the current display, so a multi-display setup needs the height of the display the panel is on, and the conversion no longer holds if your project installs a custom function with PanelSettings.SetScreenToPanelSpaceFunction, as world-space and render-texture UI does.

React Hooks#

The device hooks mirror the state into React so you can render it:

import { useKeyboard, useMouse, useGamepad } from "onejs-unity/input"

function DebugOverlay() {
    const mouse = useMouse()
    const keyboard = useKeyboard()

    return (
        <View>
            <Label text={`Mouse: ${mouse.position.x}, ${mouse.position.y}`} />
            <Label text={`Shift: ${keyboard.shift ? "on" : "off"}`} />
        </View>
    )
}
These re-render every frame. useKeyboard, useMouse, useGamepad, useTouch and useInput set state on every frame callback, so any component using one renders at frame rate along with its children. That is fine for a debug overlay or a small control-hints strip. For game logic, read input directly inside a frame loop and keep it out of React entirely.

The event hooks do not have that cost, since they only fire your callback:

HookFires
useKeyPress(key, cb)On the frame a key goes down
useKeyDown(key, cb)Every frame the key is held
useKeyRelease(key, cb)On the frame a key comes up
useMouseClick(button, cb)On a mouse button press, with the position
useGamepadButton(button, cb)On a gamepad button press, with the pad

Zero-Allocation Reading#

Every input.mouse.position call returns a cached object rather than a fresh one, but key and button names still have to be resolved on each call. For a hot loop, InputReader resolves names to integer ids once at build time and caches every value per frame:

import { useInputReader } from "onejs-unity/input"
import { useAnimationFrame } from "onejs-unity/gpu"

function Game() {
    const reader = useInputReader(b => b
        .keyAxis2D("move", {
            up: ["W", "UpArrow"], down: ["S", "DownArrow"],
            left: ["A", "LeftArrow"], right: ["D", "RightArrow"],
        })
        .mouseButton("fire", "left")
        .mouseVec2("look", "delta")
        .gamepadVec2("gamepadMove", "leftStick")
    )

    useAnimationFrame(() => {
        const move = reader.vec2("move")     // same object every frame
        if (reader.down("fire")) player.shoot()
    })
}

The hook builds the reader once, ticks it each frame, and disposes it on unmount. createReader() is the manual form, where you call reader.tick() yourself once per frame and reader.dispose() when finished.

BindingReads
key, keyPressed, keyReleasedOne key, held or on an edge
keyAxis, keyAxis2DTwo or four keys as a float or a vector
mouseButton, mouseVec2, mouseFloatA button, or position / delta / scroll
gamepadButton, gamepadVec2, gamepadFloatThe same for a pad, with an optional index

Read values back with down, pressed, released, float and vec2. vec2 returns the same object each frame, so copy it if you need to keep a value past the current frame.

If you are reading the mouse through InputReader, you can also switch off UI Toolkit's pointer-move forwarding, which allocates around 0.6 KB per frame:

input.setPointerMoveEventsEnabled(false)

This only affects onPointerMove. onPointerEnter, onPointerLeave and onClick keep working. See Zero-Allocation Interop for the wider picture.

Input Actions#

If your project already uses Unity's Input System actions, you can drive them from JavaScript instead of rebuilding the bindings:

const actions = input.loadActions(playerActionsAsset)   // injected via JSRunner globals
const jump = actions.action("Player/Jump")

if (jump.triggered) player.jump()
jump.on("performed", () => player.jump())

actions.dispose()

Actions can also be defined in JavaScript with input.createActions("Player"), and there are useAction, useActionValue and useActionCallback hooks for the React side. Direct device access covers most games; reach for actions when you need rebindable controls or a binding scheme that already exists as an asset.