Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

Touch controls

On a phone, the engine draws a control for every binding that declares touch. Declaring it is all a game does, as Roblox draws its thumbstick and jump button and a BindAction button when its bind asks for one:

  • A vector input with touch: { label }, vector2Input({ keys, touch }), gets a stick. The stick holds the input while a thumb drags it, added to its keys.
  • A button input, a command with no fields or a page action with touch: { label, icon } gets a button. The button holds a held input while the finger is down, sends a command once per press, and presses and releases a page action as its key would. A one-shot, such as a roll, is a buttonInput with edge: true, as the jump is.

A label is the control’s accessible name. A button shows its icon, one of Icon’s names, or the label’s first letter. Your first game declares the example’s Kick button in a whole plugin.

characters(), from @spawnite/engine/characters, declares a stick and a Jump button, so a game that lists it walks and jumps on a phone with no code. The stick’s walk is the same input the keys fill, so prediction, the room and a recording see one walk. A twin-stick game asks for an aim stick too, characters({ touch: { aim: { label: "Aim" } } }), which stands bottom-right and fills characters.inputs.aim. The player’s keys name both and the characters({ touch }) setting that relabels or drops either.

Every Weapon draws a fire button in the row, after the declared buttons, labelled with its name unless its touch prop says otherwise. It fires through the crosshair while the finger holds it, and the finger may drag from it to turn the look, so the player tracks a target while firing. touch={false} draws none.

A Tap of the game’s own takes look to do the same: the finger that holds it turns the camera by its travel, at the camera’s touchLookSpeed.

The first stick stands in the screen’s bottom-left slot and a second in the bottom-right, each a Slot from @spawnite/engine, as Panel places one. The buttons stand in a row in the bottom-right slot, or in the right slot when the game declares a second stick. The slots follow every declared stick, so a stick the game places elsewhere or hides moves no other control. The row runs from the corner, so the first declared button, Jump where characters() is listed first, stands in it. On a phone held sideways the row runs left from the corner across up to 60% of the screen before it wraps up, since its height holds no column of three; on one held upright it wraps up from the corner. Every control is drawn for a coarse pointer only, so a desktop sees them with a phone-sized page.

An input’s control shows only while a touch on it would fill the input. It shows while the local player controls an entity. An input that input contexts list shows only while one of them is live, and one that none lists shows whenever the local player controls an entity. A live exclusive context above it, such as a menu’s, hides it and lets go of a stick a finger holds, as it rests the input’s keys. A higher context whose inputs take every key the input has hides it too: a kart that takes Space hides the Jump. A stick with no keys, such as the aim stick, hides under a higher context that lists a stick of its own, as a kart steered by its own stick does. Input contexts say how a context rests an input.

A page action’s button shows only while a component listens to the action with useAction, as Roblox’s BindAction button stands only while its action is bound. Pass enabled: false to useAction while the action can do nothing, such as a rotate with nothing in hand. A command’s button always shows. No control shows while the loading cover is up.

Place <TouchControls /> from @spawnite/engine anywhere in a scene or in the game’s own HUD, inside a <Views> or not. While one is mounted, the engine’s own controls give way to it:

  • hidden draws none of the engine’s sticks and buttons, as Roblox’s TouchControlsEnabled = false: for a scene with none, such as a menu. A bound Joystick or Tap the game placed, below, still draws.
  • buttons.slot moves the row, such as to Slot.Right.
  • buttons.className restyles the row, such as its gap, and each button with *:, such as *:size-20.
  • buttons.hidden hides the row and keeps the sticks.
  • buttons.layout={TouchButtonLayout.Arc} stands the buttons round the thumb, as the next section says.

A row suits two or three buttons. A sports or action game with four or more stands them round the thumb instead: TouchButtonLayout.Arc puts the primary button in the corner, 80 px across, and the rest on a quarter circle round it, 64 px across, from beside it to above it. Each is one roll of the thumb from the primary, and none reaches the middle of the screen, where the play is. buttons.primary names the primary, the button a thumb rests on, and the rest follow in their declared order. Without it, the first declared stands in the corner. An arc button with no icon writes its whole label, such as HIT, where a row’s shows the first letter.

src/hud/Controls.tsx
import { characters } from "@spawnite/engine/characters";
import { TouchButtonLayout, TouchControls } from "@spawnite/engine";
export function Controls() {
return (
<TouchControls
buttons={{
layout: TouchButtonLayout.Arc,
primary: characters.inputs.jump,
}}
/>
);
}

The arc turns with its corner, so buttons.slot at Slot.BottomLeft is the same arc for a left-handed player. Rocket League Sideswipe and Brawl Stars stand their buttons this way. Unity, Godot and Unreal place each on-screen button by hand, and Roblox moves a bound action’s button with ContextActionService:SetPosition.

Its props are in TouchControlsProps.

A Joystick or a Tap bound to an input with input takes the place of the engine’s control for that input alone, while the others stay:

  • <Joystick input={characters.inputs.move} slot={Slot.Left} className="size-40" /> moves and enlarges the player’s stick.
  • <Tap input={characters.inputs.jump} slot={Slot.Right} /> moves the player’s Jump out of the row.
  • <Tap input={kicks.inputs.kick} hidden /> hides one button and keeps the rest.

A bound control takes its label and icon from the declaration, and a Tap’s children replace the icon. It shows only while its input can fill, as the engine’s do. A Joystick or a Tap with onSteer or onTap instead binds nothing and replaces nothing.

A game that wants none of the engine’s buttons places <TouchControls hidden /> and draws its own in its style. For a page action, such as a build tool’s Place, the button’s onPress calls pressAction(building.actions.place), which presses and lets go of it once, as a tap of its key.

A room and a headless run draw none of these, as they draw no view.