Input System

Master keyboard, mouse, gamepad, and touch input in Web Engine. Learn about input contexts, action mapping, and cross-platform input handling.

Web Engine provides a unified input system that handles keyboard, mouse, gamepad, and touch input. The system is context-aware, supports virtual controls, and provides both polling and event-based input.

Keyboard

WASD movement, key bindings, modifiers

Mouse

Click, movement, pointer lock, delta

Gamepad

Buttons, analog sticks, triggers, rumble

Touch

Touch input, virtual joysticks, gestures

Basic Input Access#

In scripts, access input through the api.input object. This provides normalized, cross-platform input values.

// Input state object
interface InputState {
move: Vector2; // Movement (WASD/left stick)
look: Vector2; // Mouse delta (or right stick)
jump: boolean; // Jump key held
jumpDown: boolean; // Jump pressed this frame (edge trigger)
sprint: boolean; // Sprint key held
}
export default (api) => {
return (dt, time) => {
const input = api.input;
// Movement (normalized -1 to 1)
if (input.move.x !== 0 || input.move.y !== 0) {
api.log(`Move: ${input.move.x}, ${input.move.y}`);
}
// Mouse look (delta in pixels)
if (input.look.x !== 0 || input.look.y !== 0) {
api.log(`Look delta: ${input.look.x}, ${input.look.y}`);
}
// Jump (edge trigger - fires once per press)
if (input.jumpDown) {
api.log("Jump pressed!");
}
// Sprint (continuous - true while held)
if (input.sprint) {
api.log("Sprinting...");
}
};
};

Polling vs Events

Input is polled, not event-based. This means you check the current state every frame. Edge triggers like jumpDown are automatically handled by the input system.

Keyboard Input#

Keyboard input is mapped to actions via the InputRegistry. Default bindings use standard FPS controls.

ActionDefault KeysDescription
MoveForwardW, ArrowUpMove forward
MoveBackS, ArrowDownMove backward
MoveLeftA, ArrowLeftMove left (strafe)
MoveRightD, ArrowRightMove right (strafe)
JumpSpaceJump / accept
SprintShiftSprint / run
CrouchC, CtrlCrouch
FireMouse0 (left click)Primary action / shoot
InteractEInteract with objects
InventoryI, TabOpen inventory

Movement keys are automatically combined into the move vector:

// WASD input automatically combines into move vector
const input = api.input;
// input.move.x ranges from -1 (left) to 1 (right)
// input.move.y ranges from -1 (back) to 1 (forward)
// Example: Pressing W+D gives:
// input.move.x = 1.0 (right)
// input.move.y = 1.0 (forward)
// Diagonal movement is automatically normalized

Mouse Input#

Mouse input provides delta movement (for camera control) and button states. Web Engine supports pointer lock for first-person games.

Mouse Look (Delta)#

export default (api) => {
let yaw = 0;
let pitch = 0;
const sensitivity = 0.002;
return (dt, time) => {
const input = api.input;
// Mouse delta is accumulated per frame
if (input.look.x !== 0 || input.look.y !== 0) {
yaw -= input.look.x * sensitivity;
pitch -= input.look.y * sensitivity;
// Clamp pitch to prevent over-rotation
pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitch));
// Apply rotation
const rot = api.rotation;
rot.x = pitch;
rot.y = yaw;
api.rotation = rot;
}
};
};

The look delta is reset to zero at the end of each frame, so it accumulates movement only for the current frame.

Pointer Lock#

Pointer lock captures the mouse cursor and provides unlimited mouse movement. Essential for first-person games.

// Request pointer lock (typically on click or game start)
// This is handled by the engine, but you can trigger it via:
// InputManager.getInstance().requestPointerLock();
// In your script, mouse delta is automatically available when locked
export default (api) => {
return (dt, time) => {
// Mouse delta works automatically when pointer is locked
// No special handling needed
const look = api.input.look;
// ...use look delta for camera rotation
};
};

Pointer Lock Activation

Browsers require a user gesture (click) to activate pointer lock. The engine handles this automatically when entering Play Mode.

Gamepad Input#

Web Engine automatically detects and supports gamepads. Analog sticks map to movement and look, buttons map to actions.

ActionGamepad BindingDescription
MoveXLeft Stick XHorizontal movement
MoveYLeft Stick YVertical movement
LookXRight Stick XHorizontal camera
LookYRight Stick YVertical camera
JumpButton 0 (A/Cross)Jump
SprintLeft TriggerSprint
FireRight TriggerPrimary action
InteractButton 1 (B/Circle)Interact

Gamepad input is automatically merged with keyboard/mouse input:

// Gamepad and keyboard inputs are unified
export default (api) => {
return (dt, time) => {
const input = api.input;
// Works with both keyboard WASD and gamepad left stick
const moveX = input.move.x; // Keyboard: -1/0/1, Gamepad: -1.0 to 1.0
const moveY = input.move.y;
// Gamepad look uses right stick (mapped to look)
const lookX = input.look.x; // Mouse delta OR right stick
const lookY = input.look.y;
// Jump works with keyboard Space OR gamepad A button
if (input.jumpDown) {
// Fires regardless of input method
}
};
};

Analog Stick Deadzone#

Analog sticks have a configurable deadzone (default 0.1) to prevent drift:

// Values below the deadzone (0.1) are treated as 0
// This is handled automatically by the InputManager
// In your script, you can add additional deadzone checking:
export default (api) => {
const customDeadzone = 0.15;
return (dt, time) => {
const input = api.input;
// Check if movement exceeds custom deadzone
const moveLen = Math.sqrt(input.move.x ** 2 + input.move.y ** 2);
if (moveLen > customDeadzone) {
// Apply movement
}
};
};

Gamepad Haptics (Rumble)#

// Note: Haptics are handled by InputManager, not directly in scripts
// You would need to extend the ScriptAPI to expose haptics
// Example (if implemented):
// api.input.vibrate(duration, weakMagnitude, strongMagnitude);
// For now, haptics are triggered automatically on certain events
// like firing (in InputManager.updateState())

Touch Input#

Touch input is automatically converted to movement and look input. The left half of the screen controls movement, the right half controls camera.

// Touch input is transparent - same API as keyboard/mouse
export default (api) => {
return (dt, time) => {
const input = api.input;
// Works with:
// - WASD (keyboard)
// - Left stick (gamepad)
// - Touch drag on left half of screen (mobile)
const move = input.move;
// Works with:
// - Mouse delta (desktop)
// - Right stick (gamepad)
// - Touch drag on right half of screen (mobile)
const look = input.look;
};
};

Virtual Controls

For more advanced mobile controls, you can implement virtual joystick UI and use InputManager.setVirtualAxis() to inject input values.

Input Contexts#

Input contexts allow different input mappings for different game states (gameplay, UI, vehicles, etc.). The engine provides these built-in contexts:

  • Gameplay — Standard FPS controls (WASD, mouse look, jump)
  • Vehicle — Driving controls (accelerate, brake, steer)
  • UI — Menu navigation (arrow keys, enter, escape)
  • Menu — Pause menu controls

Scripts automatically use the current context. The engine manages context switching based on game state.

Advanced: Direct Action Access#

For advanced use cases, you can access the raw InputManager to query actions directly:

// Note: This requires extending ScriptAPI or using InputManager directly
// Not available in standard scripts by default
// Example (conceptual):
export default (api) => {
return (dt, time) => {
// Standard script API
const input = api.input;
// Access raw actions (if exposed)
// const fireAction = InputManager.getInstance().getAction("Fire");
// const firePressed = InputManager.getInstance().isActionPressed("Fire");
// const fireDown = InputManager.getInstance().isActionDown("Fire");
};
};

Input Extension

The standard api.input provides the most common input needs. For custom actions or input remapping, you'll need to extend the InputRegistry and ScriptAPI.

Best Practices#

  • Use edge triggers for actions: Use jumpDown instead of jump for single-press actions to avoid repeated triggering.
  • Normalize diagonal movement: When using keyboard input, diagonal movement (W+D) can be faster than cardinal directions. The engine handles this automatically for api.input.move.
  • Apply sensitivity scaling: Multiply mouse delta by a sensitivity factor (e.g., 0.002) to control camera rotation speed.
  • Check for zero input: Before applying movement,check if move.x !== 0 || move.y !== 0 to avoid unnecessary calculations.
  • Handle input state changes: Use onStart to initialize input-dependent state, and onDestroy to clean up.

Complete Input Example#

CompleteInputHandler.ts
typescript
export default (api) => {
// Configuration
const moveSpeed = 5.0;
const sprintMultiplier = 2.0;
const mouseSensitivity = 0.002;
const gamepadLookSensitivity = 3.0;
// State
let yaw = 0;
let pitch = 0;
const maxPitch = Math.PI / 3;
return (dt, time) => {
const input = api.input;
// === Mouse Look ===
if (input.look.x !== 0 || input.look.y !== 0) {
// Mouse delta is in pixels, gamepad is -1 to 1
// Detect gamepad by checking if values are small (normalized)
const isGamepad = Math.abs(input.look.x) <= 1.0 && Math.abs(input.look.y) <= 1.0;
const sensitivity = isGamepad ? gamepadLookSensitivity * dt : mouseSensitivity;
yaw -= input.look.x * sensitivity;
pitch -= input.look.y * sensitivity;
pitch = Math.max(-maxPitch, Math.min(maxPitch, pitch));
const rot = api.rotation;
rot.y = yaw;
api.rotation = rot;
}
// === Movement ===
const moveLen = Math.sqrt(input.move.x ** 2 + input.move.y ** 2);
if (moveLen > 0.01) {
const speed = input.sprint ? moveSpeed * sprintMultiplier : moveSpeed;
// Calculate movement relative to camera rotation
const sin = Math.sin(yaw);
const cos = Math.cos(yaw);
const fwdX = -sin * input.move.y;
const fwdZ = -cos * input.move.y;
const rightX = cos * input.move.x;
const rightZ = -sin * input.move.x;
api.controller.move({
x: (fwdX + rightX) * speed,
y: 0,
z: (fwdZ + rightZ) * speed
});
}
// === Jump ===
if (input.jumpDown && api.controller.isGrounded) {
api.applyImpulse({ x: 0, y: 10, z: 0 });
}
};
};
Input System - Web Engine Docs | Web Engine Docs