add-interactivity
Adding Interactivity to Decentraland Scenes
Decision Tree
| Need | Approach | API |
|---|---|---|
| Click/hover on a specific entity | Pointer events | pointerEventsSystem.onPointerDown() |
| Detect player entering an area | Trigger area | TriggerArea + triggerAreaEventsSystem |
| Poll key state every frame | Global input | inputSystem.isTriggered() / isPressed() |
| Detect objects in a direction | Raycasting | raycastSystem or Raycast component |
| Read cursor position / lock state | Cursor state | PointerLock, PrimaryPointerInfo |
Pointer Events (Click / Hover)
Using the Helper System (Recommended)
import { engine, Transform, MeshRenderer, pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'
const cube = engine.addEntity()
Transform.create(cube, { position: Vector3.create(8, 1, 8) })
MeshRenderer.setBox(cube)
// Add click handler
pointerEventsSystem.onPointerDown(
{
entity: cube,
opts: {
button: InputAction.IA_POINTER, // Left click
hoverText: 'Click me!',
maxDistance: 10
}
},
(event) => {
console.log('Cube clicked!', event.hit?.position)
}
)
All Input Actions
InputAction.IA_POINTER // Left mouse button
InputAction.IA_PRIMARY // E key
InputAction.IA_SECONDARY // F key
InputAction.IA_ACTION_3 // 1 key
InputAction.IA_ACTION_4 // 2 key
InputAction.IA_ACTION_5 // 3 key
InputAction.IA_ACTION_6 // 4 key
InputAction.IA_JUMP // Space key
InputAction.IA_FORWARD // W key
InputAction.IA_BACKWARD // S key
InputAction.IA_LEFT // A key
InputAction.IA_RIGHT // D key
InputAction.IA_WALK // Shift key
All Event Types
PointerEventType.PET_DOWN // Button pressed
PointerEventType.PET_UP // Button released
PointerEventType.PET_HOVER_ENTER // Cursor enters entity
PointerEventType.PET_HOVER_LEAVE // Cursor leaves entity
Pointer Up (Release)
pointerEventsSystem.onPointerDown(
{ entity: cube, opts: { button: InputAction.IA_POINTER, hoverText: 'Hold me' } },
() => { console.log('Pressed!') }
)
pointerEventsSystem.onPointerUp(
{ entity: cube, opts: { button: InputAction.IA_POINTER } },
() => { console.log('Released!') }
)
Removing Handlers
pointerEventsSystem.removeOnPointerDown(cube)
pointerEventsSystem.removeOnPointerUp(cube)
Important: Colliders Required
Pointer events only work on entities with a collider. Add one if your entity doesn't have a mesh:
import { MeshCollider } from '@dcl/sdk/ecs'
MeshCollider.setBox(entity) // Invisible box collider
For GLTF models, set the collision mask:
GltfContainer.create(entity, {
src: 'models/button.glb',
visibleMeshesCollisionMask: ColliderLayer.CL_POINTER
})
Trigger Areas (Proximity Detection)
Detect when the player enters, exits, or stays inside an area:
import { engine, Transform, TriggerArea } from '@dcl/sdk/ecs'
import { triggerAreaEventsSystem } from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'
const area = engine.addEntity()
TriggerArea.setBox(area) // or TriggerArea.setSphere(area)
Transform.create(area, {
position: Vector3.create(8, 0, 8),
scale: Vector3.create(4, 4, 4) // Size the area via Transform.scale
})
// Register enter/exit/stay events
triggerAreaEventsSystem.onTriggerEnter(area, (event) => {
console.log('Entity entered trigger:', event.trigger.entity)
})
triggerAreaEventsSystem.onTriggerExit(area, () => {
console.log('Entity exited trigger')
})
triggerAreaEventsSystem.onTriggerStay(area, () => {
// Called every frame while an entity is inside
})
By default, trigger areas react to the player layer. Use ColliderLayer to restrict which entities activate the area:
import { ColliderLayer, MeshCollider } from '@dcl/sdk/ecs'
// Area that only reacts to custom layers
TriggerArea.setBox(area, ColliderLayer.CL_CUSTOM1 | ColliderLayer.CL_CUSTOM2)
// Mark a moving entity to activate the area
const mover = engine.addEntity()
Transform.create(mover, { position: Vector3.create(8, 0, 8) })
MeshCollider.setBox(mover, ColliderLayer.CL_CUSTOM1)
Raycasting
Raycast Direction Types
Four direction modes are available:
// 1. Local direction — relative to entity rotation
{ $case: 'localDirection', localDirection: Vector3.Forward() }
// 2. Global direction — world-space, ignores entity rotation
{ $case: 'globalDirection', globalDirection: Vector3.Down() }
// 3. Global target — aim at a world position
{ $case: 'globalTarget', globalTarget: Vector3.create(10, 0, 10) }
// 4. Target entity — aim at another entity
{ $case: 'targetEntity', targetEntity: entityId }
Callback-Based Raycasting (Recommended)
import { raycastSystem, RaycastQueryType, ColliderLayer } from '@dcl/sdk/ecs'
// Local direction raycast
raycastSystem.registerLocalDirectionRaycast(
{ entity: myEntity, opts: { queryType: RaycastQueryType.RQT_HIT_FIRST, direction: Vector3.Forward(), maxDistance: 16, collisionMask: ColliderLayer.CL_POINTER } },
(result) => {
if (result.hits.length > 0) {
console.log('Hit:', result.hits[0].entityId)
}
}
)
// Global direction raycast
raycastSystem.registerGlobalDirectionRaycast(
{ entity: myEntity, opts: { queryType: RaycastQueryType.RQT_HIT_FIRST, direction: Vector3.Down(), maxDistance: 20 } },
(result) => { /* handle hits */ }
)
// Target position raycast
raycastSystem.registerGlobalTargetRaycast(
{ entity: myEntity, opts: { globalTarget: Vector3.create(8, 0, 8), maxDistance: 20 } },
(result) => { /* handle result */ }
)
// Target entity raycast
raycastSystem.registerTargetEntityRaycast(
{ entity: sourceEntity, opts: { targetEntity: targetEntity, maxDistance: 15 } },
(result) => { /* handle result */ }
)
// Remove raycast from entity
raycastSystem.removeRaycasterEntity(myEntity)
Component-Based Raycasting
import { engine, Raycast, RaycastResult, RaycastQueryType } from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'
const rayEntity = engine.addEntity()
Raycast.create(rayEntity, {
direction: { $case: 'localDirection', localDirection: Vector3.Forward() },
maxDistance: 16,
queryType: RaycastQueryType.RQT_HIT_FIRST,
continuous: false // Set true for continuous raycasting
})
// Check results
engine.addSystem(() => {
const result = RaycastResult.getOrNull(rayEntity)
if (result && result.hits.length > 0) {
const hit = result.hits[0]
console.log('Hit entity:', hit.entityId, 'at', hit.position)
}
})
Camera Raycast
Cast a ray from the camera to detect what the player is looking at:
raycastSystem.registerGlobalDirectionRaycast(
{
entity: engine.CameraEntity,
opts: {
direction: Vector3.rotate(Vector3.Forward(), Transform.get(engine.CameraEntity).rotation),
maxDistance: 16
}
},
(result) => {
if (result.hits.length > 0) console.log('Looking at:', result.hits[0].entityId)
}
)
Global Input Handling
Listen for key presses anywhere (not entity-specific):
import { inputSystem, InputAction, PointerEventType } from '@dcl/sdk/ecs'
engine.addSystem(() => {
// Check if E key was just pressed this frame
if (inputSystem.isTriggered(InputAction.IA_PRIMARY, PointerEventType.PET_DOWN)) {
console.log('E key pressed!')
}
// Check if a key is currently held down
if (inputSystem.isPressed(InputAction.IA_SECONDARY)) {
console.log('F key is held!')
}
// Entity-specific input via system
const clickData = inputSystem.getInputCommand(
InputAction.IA_POINTER,
PointerEventType.PET_DOWN,
myEntity
)
if (clickData) {
console.log('Entity clicked via system:', clickData.hit.entityId)
}
})
Cursor State
import { PointerLock, PrimaryPointerInfo } from '@dcl/sdk/ecs'
// Check if cursor is locked
const isLocked = PointerLock.get(engine.CameraEntity).isPointerLocked
// Get cursor position and world ray
const pointerInfo = PrimaryPointerInfo.get(engine.RootEntity)
console.log('Cursor position:', pointerInfo.screenCoordinates)
console.log('World ray direction:', pointerInfo.worldRayDirection)
Toggle Pattern (Click to Switch States)
Common pattern for toggleable objects:
let doorOpen = false
pointerEventsSystem.onPointerDown(
{ entity: door, opts: { button: InputAction.IA_POINTER, hoverText: 'Toggle door' } },
() => {
doorOpen = !doorOpen
const mutableTransform = Transform.getMutable(door)
mutableTransform.rotation = doorOpen
? Quaternion.fromEulerDegrees(0, 90, 0)
: Quaternion.fromEulerDegrees(0, 0, 0)
}
)
Best Practices
- Always set
maxDistanceon pointer events (8-16m is typical) - Always set
hoverTextso users know they can interact - Clean up handlers when entities are removed
- Use
MeshColliderfor invisible trigger surfaces - For complex interactions, use a system with state tracking
- Test interactions in preview — hover text should be visible and clear
- Set
continuous: falseon raycasts unless you need per-frame results - Design for both desktop and mobile — mobile has no keyboard, rely on pointer and on-screen buttons
For the full input action list and advanced patterns, see {baseDir}/references/input-reference.md.
More from dcl-regenesislabs/opendcl
optimize-scene
Optimize Decentraland scene performance. Scene limit formulas (triangles, entities, materials, textures, height per parcel count), object pooling, LOD patterns, texture optimization, system throttling, and asset preloading. Use when the user wants to optimize, improve performance, fix lag, reduce load time, check limits, or reduce entity/triangle count. Do NOT use for deployment (see deploy-scene).
51game-design
Plan and design Decentraland games and interactive experiences. Scene limit formulas, performance budgets, texture requirements, asset preloading, state management patterns (module-level, component-based, state machines), object pooling, UX/UI guidelines, input design, and MVP planning. Use when the user wants game design advice, scene architecture, performance planning, or help structuring a game. Do NOT use for specific implementation (see add-interactivity, build-ui, multiplayer-sync).
30audio-video
Add sound effects, music, audio streaming, and video players to Decentraland scenes. Covers AudioSource (local files), AudioStream (streaming URLs), VideoPlayer (video surfaces), video events, and media permissions. Use when the user wants sound, music, audio, video screens, radio, or media playback. Do NOT use for 3D model animations (see animations-tweens).
30add-3d-models
Add 3D models (.glb/.gltf) to a Decentraland scene using GltfContainer. Covers loading, positioning, scaling, colliders, parenting, and browsing 5,700+ free assets from the OpenDCL catalog. Use when the user wants to add models, import GLB files, find free 3D assets, or set up model colliders. Do NOT use for materials/textures (see advanced-rendering) or model animations (see animations-tweens).
28nft-blockchain
NFT display and blockchain interaction in Decentraland. NftShape (framed NFT artwork), wallet checks (getPlayer, isGuest), signedFetch (authenticated requests), smart contract interaction (eth-connect, createEthereumProvider), and RPC calls. Use when the user wants NFTs, blockchain, wallet, smart contracts, Web3, crypto, or token gating. Do NOT use for player avatar data or emotes (see player-avatar).
27build-ui
Build 2D screen-space UI for Decentraland scenes using React-ECS (JSX). Create HUDs, menus, health bars, scoreboards, dialogs, buttons, inputs, and dropdowns. Use when the user wants screen overlays, on-screen UI, HUD elements, menus, or form inputs. Do NOT use for 3D in-world text (see advanced-rendering) or clickable 3D objects (see add-interactivity).
26