EngineIntermediate

Player input and touch controls

What produces movement and look on each device, how touch sticks behave, and how authored action buttons reach Lua.

What produces look

Every look producer feeds input.lookDeltaX/lookDeltaY, but the two built-in follow modes intentionally use them differently:

Producerfirst_personthird_person
Arrow keysRotate the cameraFeed Lua look input; the starter controller turns the character and the boom follows it
Touch look stickRotate the cameraFeed Lua look input, like the arrow keys
Gamepad right stickRotate the cameraFeed Lua look input, like the arrow keys
Mouse movementRotate the camera when Desktop mouse look is enabledOrbit the boom independently when Desktop mouse look is enabled
Touch drag away from controlsRotate the cameraOrbit the boom independently

Desktop mouse look is off by default. On touch, mobile controls must be enabled for either a look stick or drag look, and the two are alternatives: a scene with a look stick uses the stick, and dragging on the screen does not turn the view. Remove the look stick and players drag anywhere outside the controls to look, and the overlay shows a Drag to look hint. A gamepad's right stick is held look, like the arrow keys; see Gamepad.

By default a first-person camera's pitch is clamped to just under ±90°, and a third_person orbit tilts at most ±60°. The camera's Minimum Pitch and Maximum Pitch change either; see Cameras and follow modes.

Players can tune input from the game menu, in Preview and in a published game, whenever the scene's player controls are enabled. Look controls sets look sensitivity from 50% to 200% and inverts either axis, and applies to every look producer. Movement controls appears when the scene has an analog movement stick. It sets the stick's dead zone (0-30%, default 15%) and its response: Precise, Linear (the default) or Quick. Both are saved in the player's browser, not in the scene, so you cannot set them for your players.

Touch drag is viewport-relative. A full-width drag turns 320°, consistently across phone and tablet. Mouse movement stays pixel-based. Vertical look runs at 0.78x horizontal.

A third-person camera composes its boom with the target's rotation. Turning the character therefore turns the view already; applying the held look axis to the boom as well would double the turn. Pointer look is separate because it moves only the boom. First-person still consumes every producer directly.

Lua should normally read the per-frame lookDeltaX/lookDeltaY. lookAxisX/lookAxisY expose held deflection, but the engine already integrates that axis into the deltas each frame, so reading both applies held input twice.

Desktop keys

On a keyboard, W, A, S and D set input.moveForward, input.moveLeft, input.moveBackward and input.moveRight. They also set input.moveAxisX and input.moveAxisY to whole steps of -1, 0 or 1, so a script written for the stick's axes still works from the keyboard. Space is input.jump, the arrow keys are held look (input.lookUp, lookDown, lookLeft, lookRight and the look axes), and left click is input.primaryFire. These work in every scene, whether or not it enables player controls. Other actions need an authored button; see Action buttons.

Gamepad

A gamepad works in Preview and in a published game, in every scene, as the keyboard does. The browser has to report it with the standard mapping, as it does for Xbox, PlayStation and most current controllers; a gamepad with any other mapping is ignored. A browser reveals a gamepad to the page only once it has been used, so press a button after the game starts. With several connected, the one used most recently drives the player, and in a multiplayer room its input is sent to the room like any other.

  • Left stick moves, setting input.moveAxisX and input.moveAxisY as the touch movement stick does. Clicking it (L3) sets input.moveSprint until the stick comes back to rest.
  • Right stick is held look, like the arrow keys and the touch look stick, so a third_person camera follows the character it turns rather than orbiting.
  • D-pad walks, like W, A, S and D, unless an action is bound to it.
  • South (A on Xbox, Cross on PlayStation) is jump and the right trigger is primary_fire, in every scene. East (B / Circle) is crouch when the scene has a crouch action.
  • Start pauses and resumes the game. It cannot be bound to an action.

Both sticks have a fixed radial dead zone of 18%, and a trigger counts as pressed at half its travel. The triggers are also analog: a trigger bound to an action reports how far it is pulled, 0 to 1, as input.actionValues.<id> — from a light touch past a small rest dead zone, not only once it counts as pressed — so throttle and brake on the triggers can be gradual. See the Actions section of the Lua scripting overview. The player's look sensitivity and invert settings apply to the right stick.

Other authored actions take the next free button of West (X / Square), North (Y / Triangle), the right bumper, the left bumper and the left trigger, in the order they are authored. To choose a button yourself, set the action's Gamepad button in Scene Settings, or give it up to two gamepadButtons through scenes_update. An authored button always wins over a default. See Action buttons.

Mobile touch controls

The overlay appears while the game is running, if the scene enables both Enable player controls and Mobile controls in Scene Settings and the device looks like a touch device: a coarse pointer, or a window 768px wide or narrower. It hides as soon as the player presses a key, uses a mouse or uses a gamepad (connecting one is not enough), and comes back on the next touch or when the last gamepad is disconnected, so a tablet with a keyboard attached switches between the two.

A scene's player controls overlay can provide one movement stick plus one look stick. The movement control can be a joystick or D-pad; a look control is always a joystick. Each stick can be placed on the left or right side of the screen, and each chooses independently how it positions itself. The two sticks always take opposite sides: moving one to a side moves the other away.

Stick placement modes

  • Drift (the default for a movement stick, and the recommended one; a look stick you add starts as Fixed) — the ring is always on screen, stays wherever the player last let go of it, and migrates most of the way toward each new touch. Over a few touches it settles where that player's thumb actually lands, on their device and in their grip, without a settings screen.
  • Dynamic — nothing is drawn until the player touches, then the ring appears under the thumb and disappears on release. The most screen space, but a new player sees no controls at all.
  • Fixed — the ring is pinned to one spot and always returns to it. Predictable and learnable, but less forgiving: a touch that lands off-centre deflects the stick immediately.

Drift and dynamic also drag the ring along once the thumb passes its rim, so a long push never runs out of travel.

The overlay sizes itself to the device. The ring's radius is a fixed number of CSS pixels, because a thumb is the same size on a phone and on a tablet, while its resting position is a fraction of the viewport, because a tablet is held with the hands further apart. Both respect the display's safe-area insets.

What a stick feeds your script

input.moveAxisX and input.moveAxisY carry the stick's actual deflection, already through the player's dead zone and response curve. input.moveForward, input.moveBackward, input.moveLeft and input.moveRight are a convenience for scripts that only need a direction, and a stick lights one only past 0.4 of deflection in that direction. input.moveSprint is true while the movement stick is pushed nearly to its rim (0.88 of full deflection, measured before the dead zone and response curve). It gives a touch player a way to run. A gamepad sets it when the player clicks the left stick (L3), until the stick comes back to rest. No keyboard key sets it, so bind a sprint action if desktop players need one too.

That threshold matters because the booleans carry no magnitude, so a script reading only them treats each as full input. Lighting them on any deflection at all made a thumb three degrees off centre indistinguishable from a deliberate push — which is invisible on a character but made vehicles weave badly, since no thumb holds an exact heading. Read the axes wherever proportional control matters, and see Vehicles, constraints, and navigation for what a driving script needs on top of them. A car can also be driven on touch with the stick alone — push it the way the car should go and the push is the throttle, with no Gas or Brake buttons; see One-stick point-and-go driving.

In a multiplayer room the same fields are sent to the room server as that player's input, and the control script of the character or vehicle the player controls reads them there as input. There the input also carries input.aimYaw and input.aimPitch: where the player was aiming when that input was sent, in world-space radians, with yaw measured from +Z toward +X and pitch positive up. They are nil outside a multiplayer room, so test for them before use.

Which device is the player using?

input.device is "keyboard" (keyboard and mouse), "gamepad" or "touch", taken from the player's most recent deliberate input, so a HUD can show "Press A", "Press Space" or a tap hint. A key, a mouse click or a mouse move that actually moves counts as the keyboard. A gamepad counts on a button press, or when a stick or trigger is pushed past half its travel, so stick drift and a resting trigger never switch it, and connecting a pad does not switch it either. Before the player has used anything it is "touch" on a phone, a tablet or a narrow window when the scene has mobile controls, and "keyboard" otherwise. The last device used is remembered across a scene change or a restart. It is the same reading that decides whether the touch overlay is shown, so a prompt always matches the controls on screen.

input.gamepadStyle is "xbox", "playstation", "nintendo" or "generic" for the last gamepad used, and nil until one is used. A Nintendo pad has A and B the other way round, and a generic one is best described by position.

-- On a GUI label: show the jump prompt for the device in use
local shown = nil

function update(deltaTime)
  local device = input.device or "keyboard"
  local prompt = "Press Space to jump"
  if device == "touch" then
    prompt = "Tap Jump"
  elseif device == "gamepad" then
    if input.gamepadStyle == "playstation" then
      prompt = "Press Cross to jump"
    elseif input.gamepadStyle == "nintendo" then
      prompt = "Press B to jump"
    else
      prompt = "Press A to jump"
    end
  end
  if prompt ~= shown then
    gameObject.setText(prompt)
    shown = prompt
  end
end

Read them in update(), not init(): like the rest of input, they are set before each update() call, and input is nil while init() runs. Both belong to the player's browser. In a multiplayer room they read nil in the scene script, control scripts and authority objects, so read them in a GUI or camera script; see Build and test a multiplayer game.

Action buttons

You can add up to six action buttons and place each one on the left, center, or right. Give every authored action a stable snake_case ID; Lua reads its state at input.actions.<id>. The action IDs jump, primary_fire, and crouch also drive the built-in booleans input.jump, input.primaryFire, and input.crouch.

For a conventional first-person setup, turn on Mobile controls and click Use FPS controls. It places a drifting movement stick on the left, a fixed look stick on the right, and the Fire and Jump buttons in the center. It replaces the scene's existing sticks and buttons.

Every action must be authored. An action must be included among the scene's authored buttons to receive a touch button or a desktop route. Each button may name up to four desktop bindings in its keys list as KeyboardEvent.code values such as KeyF, Digit1, Space, or ArrowUp, plus Mouse0, Mouse1, and Mouse2. The accepted codes are letters (KeyA-KeyZ), Digit0-Digit9, Numpad0-Numpad9, F1-F12, the arrow keys, the left and right Shift, Control, Alt and Meta keys, Space, Enter, Backspace, and the punctuation keys; any other code is dropped when the scene is saved. Escape and Tab are reserved by the browser.

Key bindings are set through scenes_update, not in the editor. The editor's button form edits Label (up to 20 characters), Action ID, Position, On touch (shown while Mobile controls is on) and Gamepad, and keeps any keys already set. Its Gamepad menu sets one button; scenes_update accepts two. An action ID starts with a lowercase letter and uses only lowercase letters, digits and underscores, up to 32 characters; each ID can appear only once.

When an action has no authored keys, non-reserved actions fall back to digits 1 through 9 by author position. jump and primary_fire keep Space and left mouse as their compatibility bindings, but either may also name explicit alternatives. An authored Mouse0 binding wins over the usual left-click primary_fire. If two actions claim the same code, the later authored action wins.

Authored actions are how a scene gets verbs beyond the built-in two. Melee, grenade, reload, and weapon-swap are all ordinary authored actions read from input.actions; there is no separate binding to request for each one. Buttons are authorable through scenes_update as well as the editor, so an agent can define them headlessly.

Not every action needs a persistent touch button. Each action's On touch setting (touchDisplay) is Button (the default), In actions menu or Hidden. Narrow containers show up to three persistent actions; wider containers show up to five when space permits. The authored button list determines priority. Extra actions enter quick access automatically, alongside actions explicitly assigned to the menu. Position is a preference within the action dock, which stays clear of movement and look sticks. A hidden action has no touch control, so hide only what a touch player can do without.

Persistent action buttons are 56 × 56 CSS pixels, with a 30% opaque background and fully opaque labels. The quick-access hub is 48 px; choices are 64 px. Resting joysticks use a faint ring and brighten while held; their gesture area and input travel are unchanged. The overlay uses its container and the device safe-area insets, including in an embedded Preview.

Change an action's touch display while the game runs

A script can move an authored action on this player's touch overlay for the rest of the run. player.setTouchDisplay(action, display) sets it to "button", "menu" or "hidden", the same three values as On touch, and player.clearTouchDisplay(action) puts the authored setting back. action is the action's ID, not its label. Both return ok, reason. A bad call changes nothing and does not raise: the reason is unknown_action when the scene authors no action with that ID, invalid_display for any other display value, and unsupported where there is no player overlay to change.

-- On a GUI label: show Reload on touch only while a weapon is held
local reloadShown = nil

function update(deltaTime)
  local show = scene.getVar("hasWeapon") == true
  if show ~= reloadShown then
    if show then
      player.clearTouchDisplay("reload")
    else
      player.setTouchDisplay("reload", "hidden")
    end
    reloadShown = show
  end
end
  • Local and presentation only. An override changes only where the action appears on this player's own touch overlay. Its keyboard keys, gamepad buttons, input.actions and what the action does are unchanged, so a hidden action still fires from its key. Nothing is sent to a multiplayer room, and other players' controls are unaffected. In a room, call these from a camera, GUI or presentation script; an authority or prediction script is refused them with an error.
  • The overlay's own rules still apply. "button" asks for a persistent button, but only three fit in a narrow container and five in a wide one, so an action past that limit still moves into quick access. An override is only visible while the touch overlay is: when Mobile controls is on and the player is using touch.
  • Overrides last for one run. Every override is cleared when the run stops or restarts and when the scene changes, and the overlay goes back to each action's authored On touch setting. Clearing an action that was never overridden does nothing.
  • To disable an action's touch control without moving it, see Disable an action's touch control while the game runs; to rename it, see Rename an action's touch control while the game runs.

Disable an action's touch control while the game runs

A script can stop this player's touch control for an authored action from activating it, without moving or hiding it. player.setTouchEnabled(action, enabled) takes false to disable it and true to enable it again, and player.clearTouchEnabled(action) goes back to the default, enabled. Both return ok, reason, change nothing when refused and never raise: unknown_action when the scene authors no action with that ID, invalid_enabled when enabled is not true or false (0, "false" and nil are refused, not converted), and unsupported where there is no player overlay.

-- On a GUI script: grey out Reload on touch while a reload is under way
local reloading = nil

function update(deltaTime)
  local busy = scene.getVar("reloading") == true
  if busy ~= reloading then
    player.setTouchEnabled("reload", not busy)
    reloading = busy
  end
end
  • It stays where it is. A disabled button keeps its place in the dock or in quick access and its label. It is drawn dimmed with a dashed border, and screen readers announce it as unavailable. Tapping, dragging onto, or activating it with a keyboard or assistive technology does nothing, and quick access stays open so another choice can be picked.
  • A held press lets go at once. Disabling an action while a finger holds its control releases that touch, and only that touch: other fingers keep moving, aiming and holding their actions. Enabling it again does not press it for a finger still resting there; the player presses again, or slides off the quick-access choice and back.
  • Touch only, and local. The action's keyboard keys and gamepad buttons still fire it, input.actions and multiplayer.sendAction are unchanged, nothing is sent to a multiplayer room, and other players' controls are unaffected. It is not a rule the game enforces, so a script that must refuse an action still has to refuse it. In a room, call these from a camera, GUI or presentation script; an authority or prediction script is refused them with an error.
  • Independent of the touch display. clearTouchDisplay leaves a disabled action disabled, and clearTouchEnabled leaves a display override in place.
  • One run. Every enabled override is cleared when the run stops or restarts and when the scene changes. The movement and look sticks and the D-pad cannot be disabled.

Rename an action's touch control while the game runs

A script can change the text on this player's touch control for an authored action for the rest of the run. player.setTouchLabel(action, label) shows label in place of the authored label, in the dock or in quick access, and player.clearTouchLabel(action) puts the authored label back. The same text is what screen readers announce, so a renamed button is heard as it is shown.

-- On a GUI script: the Fire button reads Throw while a grenade is equipped
local shown = nil

function update(deltaTime)
  local grenade = scene.getVar("weapon") == "grenade"
  if grenade ~= shown then
    if grenade then
      player.setTouchLabel("primary_fire", "Throw")
    else
      player.clearTouchLabel("primary_fire")
    end
    shown = grenade
  end
end
  • Plain text, 1 to 20 characters. The label is shown as text, never as markup. Spaces at either end are removed and every run of spaces, tabs or line breaks becomes a single space; what is left must be 1 to 20 characters, the same limit as an authored label (an emoji can count as two). A longer label is refused rather than cut short, so shorten it yourself. A button shows at most three lines of text, so a long single word ends in "…"; screen readers still hear all of it.
  • Results. Both return ok, reason and never raise. The reason is unknown_action when the scene authors no action with that ID, invalid_label for a label that is not text, is blank, is too long or contains control characters, and unsupported where there is no player overlay. A refused call keeps whatever label was showing.
  • Touch only, and local. The action's ID, so input.actions, onPlayerAction and multiplayer.sendAction, its keyboard keys and gamepad buttons, and other players' controls are unchanged, and nothing is sent to a multiplayer room. Prompts your own GUI draws are yours to update. In a room, call these from a camera, GUI or presentation script.
  • Independent of display and enabled. Moving, hiding or disabling an action keeps its label, and clearing any one override leaves the others.
  • One run. Labels clear when the run stops or restarts and when the scene changes. Sticks cannot be relabelled, and there is no translation system.

Hold and drag for quick access

Press More actions and keep holding to reveal choices around the press point. Drag onto a choice to activate it, then hold there to keep its input active. Moving away, returning to the hub or lifting releases it. A tap on the hub alone activates nothing. An action such as Reset can take effect as soon as you enter its choice; returning to the hub does not undo it. Choices fan toward available space, and another finger can keep moving or aiming. Keep continuously held or simultaneous actions among the persistent buttons.

With a keyboard, activate More actions to open focusable choices. Hold Enter or Space on a choice to hold its input; releasing closes quick access. Escape closes it and returns focus to the hub.

Keep the mobile HUD compact

Keep the current score, health, time or objective visible in one compact strip. Put longer instructions and optional telemetry behind an Info control with at least a 44 px touch target. Brief first-touch hints can disappear after a few seconds; interaction prompts, dialogue and important match messages should remain available when they matter. Use input.device in a GUI script to choose touch-specific layout and wording, while leaving the authored desktop layout intact. Keep a local Info toggle in Lua state rather than writing shared object variables from a presentation script. New projects demonstrate this pattern; see Build your first playable scene. Where the controls and the game menu sit on a phone, and where to put status, prompts and instructions around them, is in Design a HUD that works on phones.

Keys in input.actions appear only once a button has been pressed, so an untouched action reads nil rather than false. Test it for truth, not against false. A tap too quick to span a frame is not lost: the action reads true for exactly one update() and is released after that frame. Presses scripted through an MCP client's physics_run_simulation are latched the same way.

To crouch, add an action button with the ID crouch and read input.crouch. It is true for as long as the button is held, rather than for one press, because the character's collider follows it — so pass it straight to setCrouched(input.crouch), and releasing the button stands the character back up. See Characters.