About advanced-input
A skill for Decentraland scene developers covering advanced input patterns, from pointer state and input polling to movement restrictions. It supports building interactions that go beyond basic clicks and hover events.
Key features
- 01Detect pointer lock state and changes
- 02Read cursor coordinates and world rays
- 03Poll input per entity or globally
- 04Batch entity input by tags
- 05Restrict movement or freeze the player
- 06Poll keys to control custom movement
Use cases
- 01Implement one-shot and continuous key actions
- 02Handle input for groups of similar entities
- 03Freeze the player during cutscenes
- 04Control custom entities with WASD
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ dcl-regenesislabs/ opendcl --skill advanced-inputAfter installing, type/advanced-inputin Claude Code to confirm it loaded.
Details
From the project READMEAdvanced 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) {
// Cursor is captured — player is in first-person control
} else {
// Cursor is free — player can click UI elements
}
}
engine.addSystem(checkPointerLock)
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.get(engine.RootEntity)
console.log('Cursor position:', pointerInfo.screenCoordinates)
console.log('Cursor delta:', pointerInfo.screenDelta)
console.log('World ray direction:', pointerInfo.worldRayDirection)
}
engine.addSystem(readPointer)
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() {
// Check for click on a specific entity
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)
Global Input Checks
Fires regardless of whether the player's cursor was over an entity.
function globalInputSystem() {
// Was the key just pressed this frame?
if (inputSystem.isTriggered(InputAction.IA_PRIMARY, PointerEventType.PET_DOWN)) {
console.log('E key pressed!')
}
// Is the key currently held down?
if (inputSystem.isPressed(InputAction.IA_SECONDARY)) {
console.log('F key is held!')
}
}
engine.addSystem(globalInputSystem)
Tag-Based Input Batching
If you have many similar entities that all respond to the same input (e.g., every barrel responds to E to break), tag them via the Tag component and iterate the tag query each frame:
import { engine, Tag, inputSystem, InputAction, PointerEventType } from '@dcl/sdk/ecs'
// At setup: tag the entities (or declare Tag in main-entities.ts).
for (const barrel of [b1, b2, b3]) {
Tag.createOrReplace(barrel, { value: 'breakable' })
}
engine.addSystem(() => {
for (const [entity] of engine.getEntitiesByTag('breakable')) {
const cmd = inputSystem.getInputCommand(InputAction.IA_PRIMARY, PointerEventType.PET_DOWN, entity)
if (cmd) {
// entity was the IA_PRIMARY target this frame
engine.removeEntity(entity)
}
}
})
Cleaner than registering N individual pointerEventsSystem.onPointerDown handlers when the behavior is uniform.
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 | Shift key |
Event Types
PointerEventType.PET_DOWN // Button/key pressed
PointerEventType.PET_UP // Button/key released
PointerEventType.PET_HOVER_ENTER // Cursor enters entity
PointerEventType.PET_HOVER_LEAVE // Cursor leaves entity
InputModifier (Movement Restriction)
Restrict or freeze the player's movement:
import { engine, InputModifier } from '@dcl/sdk/ecs'
// Freeze player completely
InputModifier.create(engine.PlayerEntity, {
mode: InputModifier.Mode.Standard({ disableAll: true })
})
// Restrict specific movement (every flag is optional and defaults to false)
InputModifier.createOrReplace(engine.PlayerEntity, {
mode: InputModifier.Mode.Standard({
disableWalk: false,
disableJog: false,
disableRun: true,
disableJump: true,
disableDoubleJump: true,
disableGliding: true,
disableEmote: true
})
})
// Restore normal movement
InputModifier.deleteFrom(engine.PlayerEntity)
While restricted: gravity still applies, the camera still rotates, and pointer / proximity events still fire. All restrictions auto-lift when the player leaves the scene.
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() {
// Freeze player
InputModifier.create(engine.PlayerEntity, {
mode: InputModifier.Mode.Standard({ disableAll: true })
})
// ... play cinematic with VirtualCamera ...
// After cutscene ends, restore movement
// InputModifier.deleteFrom(engine.PlayerEntity)
}
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 — useinputSystemfor 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
For basic pointer events and click handlers, see the add-interactivity skill.