| name | advanced-input |
| description | System-level input polling and player movement control in Decentraland. Covers inputSystem (isTriggered/isPressed for held keys, WASD polling), InputModifier (freeze/restrict player movement), PointerLock (cursor capture detection), PrimaryPointerInfo (cursor screen coords and world ray), and number-key action bar patterns. Use when the user wants continuous key polling, WASD-controlled entities, to freeze the player during a cutscene, FPS-style cursor lock, or multi-key combo patterns. For event-driven clicks and hover on entities see add-interactivity. |
Advanced Input Handling in Decentraland
For basic click/hover events, see the add-interactivity skill. This skill covers advanced input patterns.
Pointer Lock State
Detect whether the cursor is captured (first-person mode) or free:
import { engine, PointerLock } from '@dcl/sdk/ecs'
function checkPointerLock() {
const isLocked = PointerLock.get(engine.CameraEntity).isPointerLocked
if (isLocked) {
} else {
}
}
engine.addSystem(checkPointerLock)
Requesting / releasing pointer lock (writable)
PointerLock.isPointerLocked is a plain writable boolean — a scene can request or release cursor capture by mutating it (verified: 31,20-pointer-lock-control sets it from click handlers and a timed system):
PointerLock.createOrReplace(engine.CameraEntity, { isPointerLocked: false })
PointerLock.getMutable(engine.CameraEntity).isPointerLocked = true
PointerLock.getMutable(engine.CameraEntity).isPointerLocked = false
PITFALL: getMutable(engine.CameraEntity) throws if PointerLock was never created on the camera. Call PointerLock.createOrReplace(engine.CameraEntity, { isPointerLocked: false }) once in main() before mutating. Writing true is a request; the client/player may still control actual capture (e.g. Esc unlocks).
Pointer Lock Change Detection
PointerLock.onChange(engine.CameraEntity, (pointerLock) => {
if (pointerLock?.isPointerLocked) {
console.log('Cursor locked')
} else {
console.log('Cursor unlocked')
}
})
Cursor Position and World Ray
Get the cursor's screen position and the ray it casts into the 3D world:
import { engine, PrimaryPointerInfo } from '@dcl/sdk/ecs'
function readPointer() {
const pointerInfo = PrimaryPointerInfo.getOrCreateMutable(engine.RootEntity)
console.log('Cursor position:', pointerInfo.screenCoordinates)
console.log('Cursor delta:', pointerInfo.screenDelta)
console.log('World ray direction:', pointerInfo.worldRayDirection)
}
engine.addSystem(readPointer)
PITFALL: every PrimaryPointerInfo field is optional (screenCoordinates?, screenDelta?, worldRayDirection?, pointerType?) — verified schema and 0,5-primary-cursor-info, which guards each read (pointerInfo.screenCoordinates?.x ?? -666, pointerInfo.worldRayDirection?.x.toFixed(2)). Use getOrCreateMutable(engine.RootEntity) so the component exists before first read, and always null-check the fields. worldRayDirection feeds directly into a camera raycast direction (see the "spawn at cursor" pattern in that scene).
Input Polling with inputSystem
Per-Entity Input Commands
Check if a specific input action occurred on a specific entity:
import { engine, inputSystem, InputAction, PointerEventType } from '@dcl/sdk/ecs'
function myInputSystem() {
const clickData = inputSystem.getInputCommand(
InputAction.IA_POINTER,
PointerEventType.PET_DOWN,
myEntity
)
if (clickData) {
console.log('Entity clicked via system:', clickData.hit.entityId)
}
}
engine.addSystem(myInputSystem)
Omit the entity argument to check globally (any entity / no target). Pass InputAction.IA_ANY to match any action — getInputCommand(InputAction.IA_ANY, PointerEventType.PET_DOWN) returns the command for whatever key was pressed, and cmd.button tells you which one (verified: 0,1-input-modifier).
Best practice: use the Tag component to mark all entities that share a same interaction, then iterate over them in a system.
import { engine, inputSystem, InputAction, PointerEventType, Tags } from '@dcl/sdk/ecs'
function myInputSystem() {
const taggedEntities = engine.getEntitiesByTag('myTag')
for (const entity of taggedEntities) {
const clickData = inputSystem.getInputCommand(
InputAction.IA_POINTER,
PointerEventType.PET_DOWN,
entity
)
if (clickData) {
console.log('Entity clicked via system:', clickData.hit.entityId)
}
}
}
engine.addSystem(myInputSystem)
Global Input Checks
Check if a specific key was pressed, regardless of if the player's cursor was pointing at an entity or not.
function globalInputSystem() {
if (inputSystem.isTriggered(InputAction.IA_PRIMARY, PointerEventType.PET_DOWN)) {
console.log('E key pressed!')
}
if (inputSystem.isPressed(InputAction.IA_SECONDARY)) {
console.log('F key is held!')
}
}
engine.addSystem(globalInputSystem)
All InputAction Values
| InputAction | Key/Button |
|---|
IA_POINTER | Left mouse button |
IA_PRIMARY | E key |
IA_SECONDARY | F key |
IA_ACTION_3 | 1 key |
IA_ACTION_4 | 2 key |
IA_ACTION_5 | 3 key |
IA_ACTION_6 | 4 key |
IA_JUMP | Space key |
IA_FORWARD | W key |
IA_BACKWARD | S key |
IA_LEFT | A key |
IA_RIGHT | D key |
IA_WALK | Control key |
IA_MODIFIER | Shift key (run) |
IA_ANY | Matches any input action (wildcard — use with getInputCommand) |
Event Types
PointerEventType.PET_DOWN
PointerEventType.PET_UP
PointerEventType.PET_HOVER_ENTER
PointerEventType.PET_HOVER_LEAVE
InputModifier (Movement Restriction)
Restrict or freeze the player's movement:
import { engine, InputModifier } from '@dcl/sdk/ecs'
InputModifier.create(engine.PlayerEntity, {
mode: InputModifier.Mode.Standard({ disableAll: true })
})
InputModifier.createOrReplace(engine.PlayerEntity, {
mode: InputModifier.Mode.Standard({
disableWalk: true,
disableJog: true,
disableRun: true,
disableJump: true,
disableEmote: true,
disableDoubleJump: true,
disableGliding: true
})
})
InputModifier.deleteFrom(engine.PlayerEntity)
Standard flags (all optional booleans; verified input_modifier.gen.d.ts): disableAll, disableWalk, disableJog, disableRun, disableJump, disableEmote, disableDoubleJump, disableGliding. A false/omitted flag is ignored (consumes no bandwidth). InputModifier.Mode.Standard({...}) and the raw { $case: 'standard', standard: {...} } form are equivalent (both seen in test scenes).
Important: InputModifier only works in the DCL 2.0 desktop client. It has no effect in the web browser explorer.
Cutscene Pattern
Freeze the player during a cinematic sequence:
function startCutscene() {
InputModifier.create(engine.PlayerEntity, {
mode: InputModifier.Mode.Standard({ disableAll: true })
})
}
WASD Movement Pattern
Poll movement keys to control custom entities:
import { engine, inputSystem, InputAction, PointerEventType, Transform } from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'
const MOVE_SPEED = 5
function customMovementSystem(dt: number) {
const transform = Transform.getMutable(controllableEntity)
let moveX = 0
let moveZ = 0
if (inputSystem.isPressed(InputAction.IA_FORWARD)) moveZ += 1
if (inputSystem.isPressed(InputAction.IA_BACKWARD)) moveZ -= 1
if (inputSystem.isPressed(InputAction.IA_LEFT)) moveX -= 1
if (inputSystem.isPressed(InputAction.IA_RIGHT)) moveX += 1
transform.position.x += moveX * MOVE_SPEED * dt
transform.position.z += moveZ * MOVE_SPEED * dt
}
engine.addSystem(customMovementSystem)
Combining Input Patterns
Action Bar with Number Keys
function actionBarSystem() {
if (inputSystem.isTriggered(InputAction.IA_ACTION_3, PointerEventType.PET_DOWN)) {
console.log('Slot 1 activated')
useAbility(1)
}
if (inputSystem.isTriggered(InputAction.IA_ACTION_4, PointerEventType.PET_DOWN)) {
console.log('Slot 2 activated')
useAbility(2)
}
if (inputSystem.isTriggered(InputAction.IA_ACTION_5, PointerEventType.PET_DOWN)) {
console.log('Slot 3 activated')
useAbility(3)
}
if (inputSystem.isTriggered(InputAction.IA_ACTION_6, PointerEventType.PET_DOWN)) {
console.log('Slot 4 activated')
useAbility(4)
}
}
engine.addSystem(actionBarSystem)
Best Practices
- Use
isTriggered() for one-shot actions (fire weapon, open door) — it returns true only on the frame the key is first pressed
- Use
isPressed() for continuous actions (movement, holding a shield) — it returns true every frame while held
getInputCommand() gives hit data (position, entity) — use it when you need to know what was clicked
- Prefer
pointerEventsSystem.onPointerDown() for simple entity clicks — use inputSystem for complex multi-key or polling patterns
- InputModifier only works in the DCL 2.0 desktop client — test with the desktop client if your scene relies on it
- WASD keys (
IA_FORWARD, etc.) also control player movement — polling them reads the movement state but doesn't override it
Example scenes
Engine-team test scenes exercising these APIs (ground truth):
- https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/0,1-input-modifier — InputModifier standard flags (incl.
disableWalk/disableJog), getInputCommand(IA_ANY, PET_DOWN) to read whichever key was pressed.
- https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/31,20-pointer-lock-control — writing
PointerLock.isPointerLocked to request/release cursor capture; PointerLock.onChange.
- https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/0,5-primary-cursor-info — reading
PrimaryPointerInfo (screen coords/delta/worldRayDirection) each frame; feeding worldRayDirection into a camera raycast.
- https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/2,22-virtual-cameras — WASD-driven controllable camera via
isPressed(IA_FORWARD/...); toggling InputModifier alongside a VirtualCamera.
- https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/0,0-cube-spawner — system-based per-entity click via
getEntitiesWith(Cube, PointerEvents) + inputSystem.isTriggered(IA_POINTER, PET_DOWN, entity).
For basic pointer events and click handlers, see the add-interactivity skill.