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 objectinterface 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.
| 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 normalizedMouse 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 lockedexport 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.
| 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 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/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#
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
jumpDowninstead ofjumpfor 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 !== 0to avoid unnecessary calculations. - Handle input state changes: Use
onStartto initialize input-dependent state, andonDestroyto clean up.
Complete Input Example#
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 }); } };};