Web Engine Docs
Preparing documentation
Use the search bar to quickly find any topic
Preparing documentation
Use the search bar to quickly find any topic
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.
WASD movement, key bindings, modifiers
Click, movement, pointer lock, delta
Buttons, analog sticks, triggers, rumble
Touch input, virtual joysticks, gestures
In scripts, access input through the api.input object. This provides normalized, cross-platform input values.
// Input state objectinterface InputState {move: Vector2; // Movement (WASD/left stick)look: Vector2; // Mouse delta (or right stick)jump: boolean; // Jump key heldjumpDown: 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 is mapped to actions via the InputRegistry. Default bindings use standard FPS controls.
| Action | Default Keys | Description |
|---|---|---|
| MoveForward | W, ArrowUp | Move forward |
| MoveBack | S, ArrowDown | Move backward |
| MoveLeft | A, ArrowLeft | Move left (strafe) |
| MoveRight | D, ArrowRight | Move right (strafe) |
| Jump | Space | Jump / accept |
| Sprint | Shift | Sprint / run |
| Crouch | C, Ctrl | Crouch |
| Fire | Mouse0 (left click) | Primary action / shoot |
| Interact | E | Interact with objects |
| Inventory | I, Tab | Open inventory |
Movement keys are automatically combined into the move vector:
// WASD input automatically combines into move vectorconst 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 provides delta movement (for camera control) and button states. Web Engine supports pointer lock for first-person games.
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 frameif (input.look.x !== 0 || input.look.y !== 0) {yaw -= input.look.x * sensitivity;pitch -= input.look.y * sensitivity;// Clamp pitch to prevent over-rotationpitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitch));// Apply rotationconst 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 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 lockedexport default (api) => {return (dt, time) => {// Mouse delta works automatically when pointer is locked// No special handling neededconst 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.
Web Engine automatically detects and supports gamepads. Analog sticks map to movement and look, buttons map to actions.
| Action | Gamepad Binding | Description |
|---|---|---|
| MoveX | Left Stick X | Horizontal movement |
| MoveY | Left Stick Y | Vertical movement |
| LookX | Right Stick X | Horizontal camera |
| LookY | Right Stick Y | Vertical camera |
| Jump | Button 0 (A/Cross) | Jump |
| Sprint | Left Trigger | Sprint |
| Fire | Right Trigger | Primary action |
| Interact | Button 1 (B/Circle) | Interact |
Gamepad input is automatically merged with keyboard/mouse input:
// Gamepad and keyboard inputs are unifiedexport default (api) => {return (dt, time) => {const input = api.input;// Works with both keyboard WASD and gamepad left stickconst moveX = input.move.x; // Keyboard: -1/0/1, Gamepad: -1.0 to 1.0const moveY = input.move.y;// Gamepad look uses right stick (mapped to look)const lookX = input.look.x; // Mouse delta OR right stickconst lookY = input.look.y;// Jump works with keyboard Space OR gamepad A buttonif (input.jumpDown) {// Fires regardless of input method}};};
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 deadzoneconst moveLen = Math.sqrt(input.move.x ** 2 + input.move.y ** 2);if (moveLen > customDeadzone) {// Apply movement}};};
// 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 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/mouseexport 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 allow different input mappings for different game states (gameplay, UI, vehicles, etc.). The engine provides these built-in contexts:
Scripts automatically use the current context. The engine manages context switching based on game state.
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 APIconst 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.
jumpDown instead of jump for single-press actions to avoid repeated triggering.api.input.move.move.x !== 0 || move.y !== 0 to avoid unnecessary calculations.onStart to initialize input-dependent state, and onDestroy to clean up.export default (api) => {// Configurationconst moveSpeed = 5.0;const sprintMultiplier = 2.0;const mouseSensitivity = 0.002;const gamepadLookSensitivity = 3.0;// Statelet 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 rotationconst 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 });}};};