Skip to content

Input ​

api.input is your window into the keyboard, mouse and touch during Play: held and just-pressed keys and buttons, per-frame mouse-look deltas, the pointer's position and this frame's presses, the wheel and pinch, the safe area and pointer-lock state.

Input in Awaken is intentionally low-level: the engine exposes raw held keys and raw mouse movement, and your script decides what they mean. There is no "jump action" or "look axis" baked into the engine - a character controller is entirely script logic on top of these primitives (see the Character Controller).

The Input surface ​

MemberTypeDescription
isDown(key)(string) => booleanIs this key currently held? Case-insensitive.
wasKeyPressed(key)(string) => booleanDid the key go down this frame? One true per physical press, even a tap let go before the frame ran; an auto-repeat of a held key is not a press.
mouseDX / mouseDYnumberMouse movement this frame (pixels). Cleared after scripts run.
isButtonDown(b)(number) => booleanIs this mouse button held? 0 left, 1 middle, 2 right.
wasPressed(b)(number) => booleanDid it go down this frame? One true per physical click, however long it is held.
pointerLockedbooleanWhether the pointer is locked (mouse-look active).
pointerX / pointerYnumberPointer position in canvas CSS pixels, top-left origin. A state, not a delta: it persists between frames.
pointerInsidebooleanIs the pointer over the canvas? false for a lifted finger, which has no hover.
downX / downYnumberWhere the primary pointer went down this frame (the first press if there were two), canvas CSS px; NaN when it did not.
upX / upYnumberWhere it came up this frame (the last release); NaN when it did not.
presses{ x, y, time }[]Every press of the primary pointer this frame, oldest first. time is the press's own timestamp in seconds on the page's performance clock, for rhythm or double-tap timing finer than a frame.
pointerSamples{ x, y }[]Every position the pointer reported this frame, oldest first, including the moves the browser merged between frames, the press and the release. pointerX/Y is only the last of them; draw a stroke through all of them.
wheelX / wheelYnumberWheel movement this frame in CSS pixels, normalised (line mode x 40, page mode x view height); positive = right / down-away. A two-finger trackpad scroll arrives here too. wheel is an alias of wheelY.
pinchnumberTrackpad pinch this frame; positive = zoom out. Zooming by exp(pinch / 100) follows the fingers.
viewWidth / viewHeightnumberCanvas size in CSS pixels, to test the pointer against the edges.
safeArea{ top, right, bottom, left }How far in from each edge, in CSS px, UI should keep clear of a notch, Dynamic Island or home indicator. 0 on every side in the editor and on desktop. See In-Game UI.
setPointerLockEnabled(on)(boolean) => voidWhether a click on the canvas may take pointer lock (default true). A point-and-click game passes false to keep the cursor; the shipped player then also suppresses the browser context menu on the canvas. Reset to true on every Play; kept across scene swaps.
setGestureKeysEnabled(on)(boolean) => voidWhether a touch on the canvas also presses keys in the shipped player (default true). See Touch.
touches{ id, x, y, kind }[]Every pointer pressed on the game view now: each finger, a pen, and a mouse while a button is held. See Touch.
gamepad(pad?)(number) => { id, axes, buttons } | nullGame pad pad (0, the first connected one) as of this frame, or null. See Game pads.
isGamepadDown(button, pad?)(number, number) => booleanIs this pad button held (past half way, for a trigger)?
wasGamepadPressed(button, pad?)(number, number) => booleanDid this pad button go down this frame?
ts
onUpdate(api: ScriptApi, dt: number) {
  if (api.input.isDown("w")) api.translate(0, 0, -3 * dt);
  if (api.input.pointerLocked) {
    this.yaw -= api.input.mouseDX * 0.0025;
  }
}

Held keys ​

isDown(key) reports whether a key is held right now. Keys are matched case-insensitively, and the value is a standard KeyboardEvent.key string:

  • Letters and digits: "w", "a", "s", "d", "1".
  • Space is a literal space: api.input.isDown(" ").
  • Named keys: "shift", "control", "arrowup", "arrowleft", "escape".
ts
const dx = (api.input.isDown("d") ? 1 : 0) - (api.input.isDown("a") ? 1 : 0);
const dz = (api.input.isDown("s") ? 1 : 0) - (api.input.isDown("w") ? 1 : 0);

isDown is a state query - it stays true for as long as the key is held, every frame. For a one-shot action (jump, fire, open a menu) use wasKeyPressed(key): it is true in exactly one frame per physical press, so a quick tap that starts and ends between two frames is not missed, and holding the key does not repeat it.

ts
if (api.input.wasKeyPressed(" ")) this.jump();

Held keys and buttons are let go when the window loses focus or the page is hidden, because their keyups go elsewhere. On macOS a Command keyup lets go of every held key except the modifiers, since macOS sends no keyup for a key released while Command is down.

Mouse-look deltas ​

The two button calls answer different questions, and picking the wrong one is a common bug. isButtonDown is for a continuous action - a held trigger, a drag. wasPressed is for a discrete one: "swing at the block I am looking at" must fire once per click, not once per frame the button happens to be down. Pressed state is cleared with the mouse delta each frame.

mouseDX and mouseDY are per-frame deltas - how far the mouse moved since the last frame, not an absolute position. This is exactly what a mouse-look camera wants: accumulate them into yaw and pitch.

They are accumulated during the frame and reset to zero after scripts run, so each frame you read that frame's movement. If nothing moved, both are 0.

ts
// first-person look
this.yaw   -= api.input.mouseDX * this.sensitivity;
this.pitch -= api.input.mouseDY * this.sensitivity;
this.pitch = Math.max(-1.55, Math.min(1.55, this.pitch)); // clamp ±~89°

Deltas are only meaningful while the pointer is locked - otherwise the OS cursor is free and the browser reports little or no movement.

Pointer lock ​

Mouse-look needs the cursor locked to the game canvas, so it can move infinitely without hitting a screen edge. Awaken wires this up for you:

  • Clicking the game canvas during Play requests pointer lock, unless the game has called api.input.setPointerLockEnabled(false). The cursor disappears and mouse movement starts feeding mouseDX/mouseDY.
  • pointerLocked flips to true while locked.
  • Esc releases the lock (browser default), and pointerLocked returns to false.

Guard look code with pointerLocked so the camera does not swing before the player has clicked in:

ts
onUpdate(api: ScriptApi) {
  if (!api.input.pointerLocked) return; // wait for the click-to-lock
  this.yaw -= api.input.mouseDX * this.sensitivity;
}

This same wiring exists in the editor's Game view and in the standalone Player, so lock behaviour is identical in Play and in a shipped game.

Touch ​

The pointer fields (pointerX/Y, downX/Y, presses and the rest) are fed from the mouse and from the primary touch, the first finger down. A finger has no hover, so pointerInside is false once it lifts.

For more than one finger at a time, read touches: every pointer pressed on the game view right now, oldest first, each { id, x, y, kind } with x/y in canvas CSS pixels, kept current as it moves. id stays the same for one finger from down to up, and kind is "touch", "pen" or "mouse". A mouse counts while one of its buttons is held, so on-screen controls can be tried on a desktop. Two thumbs at once, one steering and one on a pedal:

ts
onUpdate(api: ScriptApi) {
  for (const t of api.input.touches) {
    const gas = api.ui.rect("Gas Pedal");
    if (gas && t.x >= gas.x && t.x <= gas.x + gas.width && t.y >= gas.y && t.y <= gas.y + gas.height) this.throttle = 1;
  }
}

The shipped player also turns touch into keys so a keyboard game plays on a phone unchanged: a swipe presses the matching arrow key and a tap presses Space, each for 100 ms. A game that reads the pointer itself should call api.input.setGestureKeysEnabled(false) in onStart. The editor's Game view has no gesture keys.

ts
onStart(api: ScriptApi) {
  api.input.setGestureKeysEnabled(false); // this game handles taps itself
}
onUpdate(api: ScriptApi) {
  for (const p of api.input.presses) this.tapAt(p.x, p.y);
}

To turn a pointer position into a world point, use api.screenToWorld(x, y), which returns a ray from the active camera (see ScriptApi Reference).

Game pads ​

gamepad(pad) returns a game pad as of this frame: pad 0 is the first connected one. Most pads use the browser's standard mapping:

IndexAxes (-1–1, down is positive)Buttons (0–1)
0left stick xA / Cross
1left stick yB / Circle
2right stick xX / Square
3right stick yY / Triangle
4, 5left and right bumpers
6, 7left and right triggers (analog)
8, 9Back, Start
10, 11left and right stick clicks
12–15d-pad up, down, left, right

isGamepadDown(button) is true while a button is held past half way, and wasGamepadPressed(button) on the frame it went down. The pads are read once a frame, before the scripts run. A browser shows a pad to the page only after one of its buttons has been pressed there, so gamepad() is null until then. Sticks rest a little off centre: ignore the first tenth or so of their travel.

ts
onUpdate(api: ScriptApi, dt: number) {
  const pad = api.input.gamepad();
  if (!pad) return;
  const x = Math.abs(pad.axes[0]) > 0.12 ? pad.axes[0] : 0;
  this.turn(x * dt);
  if (api.input.wasGamepadPressed(0)) this.jump();
}

How input reaches Play ​

The editor forwards keyboard and mouse events into a single Input object only while Play is active, mirroring the same listeners the standalone player uses. The game loop clears the mouse deltas after ScriptHost.update each frame, so your script always reads a fresh per-frame delta. Keyboard held-state persists across frames until the key is released; pressing Stop clears all input.

A movement example ​

A minimal WASD mover with mouse-look, built purely on api.input and api.moveAndSlide:

The Third-person character template in Play: the player capsule moved by WASD read through api.input.

ts
export default class implements ScriptBehavior {
  camera: Camera;            // assign your scene camera
  moveSpeed = 5;             // m/s
  sensitivity = 0.0025;      // rad per pixel
  private yaw = 0;
  private pitch = 0;

  onUpdate(api: ScriptApi, dt: number) {
    if (api.input.pointerLocked) {
      this.yaw   -= api.input.mouseDX * this.sensitivity;
      this.pitch  = Math.max(-1.55, Math.min(1.55, this.pitch - api.input.mouseDY * this.sensitivity));
    }
    const f = (api.input.isDown("w") ? 1 : 0) - (api.input.isDown("s") ? 1 : 0);
    const s = (api.input.isDown("d") ? 1 : 0) - (api.input.isDown("a") ? 1 : 0);
    // move in the yaw frame
    const mx = (Math.cos(this.yaw) * s + Math.sin(this.yaw) * f) * this.moveSpeed;
    const mz = (-Math.sin(this.yaw) * s + Math.cos(this.yaw) * f) * this.moveSpeed;
    api.moveAndSlide(mx * dt, 0, mz * dt);
  }
}

The full first-person controller - adding gravity, jump, and driving the camera entity - is on Examples and Character Controller.

See also ​

Awaken: browser-native WebGPU game engine.