advanced-input

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Advanced Input Handling in Decentraland

Decentraland 中的高级输入处理

For basic click/hover events, see the
add-interactivity
skill. This skill covers advanced input patterns. Prefer
pointerEventsSystem.onPointerDown()
(add-interactivity) for simple entity clicks; use
inputSystem
for complex multi-key or polling patterns.
对于基础点击/悬停事件,请参阅
add-interactivity
skill。本 skill 涵盖高级输入模式。对于简单的实体点击,优先使用
pointerEventsSystem.onPointerDown()
(
add-interactivity
);对于复杂的多按键或轮询模式,请使用
inputSystem
。

Pointer Lock State

Pointer Lock 状态

Detect whether the cursor is captured (first-person mode) or free:
typescript
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)
检测光标是否已被捕获(第一人称模式)或处于自由状态:
typescript
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)

Requesting / releasing pointer lock (writable)

请求/解除 Pointer Lock(可写入)

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):
typescript
PointerLock.createOrReplace(engine.CameraEntity, { isPointerLocked: false })
// request lock (e.g. from a button)
PointerLock.getMutable(engine.CameraEntity).isPointerLocked = true
// release lock
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).
PointerLock.isPointerLocked
是一个可直接写入的普通 boolean——场景可以通过修改它来请求或解除光标捕获(已验证:
31,20-pointer-lock-control
会从点击处理器和定时系统设置该值):
typescript
PointerLock.createOrReplace(engine.CameraEntity, { isPointerLocked: false })
// request lock (e.g. from a button)
PointerLock.getMutable(engine.CameraEntity).isPointerLocked = true
// release lock
PointerLock.getMutable(engine.CameraEntity).isPointerLocked = false
PITFALL: 如果此前从未在相机上创建
PointerLock
,
getMutable(engine.CameraEntity)
会抛出异常。在修改之前,先在
main()
中调用一次
PointerLock.createOrReplace(engine.CameraEntity, { isPointerLocked: false })
。写入
true
只是发出一个请求;实际的捕获状态仍可能由客户端/玩家控制(例如按 Esc 会解除锁定)。

Pointer Lock Change Detection

检测 Pointer Lock 变化

typescript
PointerLock.onChange(engine.CameraEntity, (pointerLock) => {
  if (pointerLock?.isPointerLocked) {
    console.log('Cursor locked')
  } else {
    console.log('Cursor unlocked')
  }
})
typescript
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:
typescript
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)
获取光标的屏幕位置,以及其投射到 3D 世界中的射线:
typescript
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)

Field details

字段详情

  • screenCoordinates
    (optional Vector2) — cursor position in pixels. Origin is the bottom-left corner of the screen (positive Y = up). When the cursor is locked, freezes at the screen center.
  • screenDelta
    (optional Vector2) — how many pixels the mouse moved since the last frame. Positive
    x
    = right, positive
    y
    = up (bottom-left origin). Keeps reporting raw mouse movement while the cursor is locked — unlike
    screenCoordinates
    and
    worldRayDirection
    , which freeze at screen center. This makes
    screenDelta
    the only way to read mouse movement during pointer lock, and the correct input for mouselook / FPS camera controls (see the camera-control skill's mouselook pattern). Desktop only — always reports 0 on mobile.
  • worldRayDirection
    (optional Vector3) — direction from the camera through the cursor. Freezes at center ray while locked.
  • pointerType
    —
    0
    for none,
    1
    for mouse.
PITFALL: every field is optional — 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).
  • screenCoordinates
    (可选的 Vector2)——光标在屏幕上的位置,单位为像素。原点位于屏幕左下角(Y 轴正方向为向上)。光标锁定时,该值会固定在屏幕中心。
  • screenDelta
    (可选的 Vector2)——自上一帧以来鼠标移动的像素数。x 轴正值表示向右,y 轴正值表示向上(采用左下角原点)。光标锁定期间仍会持续报告原始鼠标移动——不同于会在屏幕中心冻结的
    screenCoordinates
    和
    worldRayDirection
    。因此,
    screenDelta
    是 Pointer Lock 期间读取鼠标移动的唯一方式,也是 mouselook / FPS 相机控制的正确输入(见 camera-control skill 中的 mouselook 模式)。仅支持桌面端——在移动端始终报告 0。
  • worldRayDirection
    (可选的 Vector3)——从相机穿过光标的方向。锁定时会固定为中心射线。
  • pointerType
    ——
    0
    表示无指针,
    1
    表示鼠标。
PITFALL: 每个字段都是可选的——已验证 schema 和
0,5-primary-cursor-info
,后者对每次读取都进行了保护(
pointerInfo.screenCoordinates?.x ?? -666
、
pointerInfo.worldRayDirection?.x.toFixed(2)
)。使用
getOrCreateMutable(engine.RootEntity)
,确保首次读取前组件已经存在,并且始终检查 null。
worldRayDirection
可直接用作相机 raycast 方向(见该场景中的“在光标处生成”模式)。

Input Polling with inputSystem

使用 inputSystem 轮询输入

Per-Entity Input Commands

单实体输入命令

Check if a specific input action occurred on a specific entity:
typescript
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)
The returned command carries
hit
data (position and entity) — use
getInputCommand()
when you need to know what was clicked.
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
).
For the Tag-based per-entity cookbook (mark entities with a Tag, fetch them with
engine.getEntitiesByTag
, and poll each with
getInputCommand
inside a system), see
{baseDir}/references/input-patterns.md
→ "Per-Entity Input Command Cookbook (Tag-based)".
检查特定实体上是否发生了特定输入操作:
typescript
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)
返回的命令包含
hit
数据(位置和实体)——需要知道点击了什么对象时,请使用
getInputCommand()
。
省略实体参数可执行全局检查(任意实体/无目标)。传入
InputAction.IA_ANY
可匹配任意操作——
getInputCommand(InputAction.IA_ANY, PointerEventType.PET_DOWN)
会返回任意被按下按键对应的命令,而
cmd.button
会指出具体是哪一个(已验证:
0,1-input-modifier
)。
有关基于 Tag 的单实体 cookbook(使用 Tag 标记实体、通过
engine.getEntitiesByTag
获取实体,并在 system 内使用
getInputCommand
轮询每个实体),请参阅
{baseDir}/references/input-patterns.md
→ “Per-Entity Input Command Cookbook (Tag-based)”。

Global Input Checks

全局输入检查

Check if a specific key was pressed, regardless of if the player's cursor was pointing at an entity or not.
Use
isTriggered()
for one-shot actions (fire a weapon, open a 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.
typescript
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)
无论玩家的光标是否指向实体,都可以检查特定按键是否被按下。
对于一次性操作(开火、开门),使用
isTriggered()
——它仅在按键首次按下的那一帧返回 true。对于持续操作(移动、举盾),使用
isPressed()
——按键按住期间,它会在每一帧返回 true。
typescript
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)

All InputAction Values

所有 InputAction 值

InputActionKey/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
)
InputAction按键/按钮
IA_POINTER
鼠标左键
IA_PRIMARY
E 键
IA_SECONDARY
F 键
IA_ACTION_3
1 键
IA_ACTION_4
2 键
IA_ACTION_5
3 键
IA_ACTION_6
4 键
IA_JUMP
Space 键
IA_FORWARD
W 键
IA_BACKWARD
S 键
IA_LEFT
A 键
IA_RIGHT
D 键
IA_WALK
Control 键
IA_MODIFIER
Shift 键(奔跑)
IA_ANY
匹配任意输入操作(通配符——配合
getInputCommand
使用)

Event Types

事件类型

typescript
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
typescript
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)

InputModifier(移动限制)

Restrict or freeze the player's movement:
typescript
import { engine, InputModifier } from '@dcl/sdk/ecs'

// Freeze player completely
InputModifier.create(engine.PlayerEntity, {
  mode: InputModifier.Mode.Standard({ disableAll: true })
})

// Restrict specific movement (all flags optional; a false/omitted flag is ignored)
InputModifier.createOrReplace(engine.PlayerEntity, {
  mode: InputModifier.Mode.Standard({
    disableWalk: true,
    disableJog: true,
    disableRun: true,
    disableJump: true,
    disableEmote: true,
    disableDoubleJump: true,
    disableGliding: true
  })
})

// Restore normal movement
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 — test with the desktop client if your scene relies on it.
限制或冻结玩家的移动:
typescript
import { engine, InputModifier } from '@dcl/sdk/ecs'

// Freeze player completely
InputModifier.create(engine.PlayerEntity, {
  mode: InputModifier.Mode.Standard({ disableAll: true })
})

// Restrict specific movement (all flags optional; a false/omitted flag is ignored)
InputModifier.createOrReplace(engine.PlayerEntity, {
  mode: InputModifier.Mode.Standard({
    disableWalk: true,
    disableJog: true,
    disableRun: true,
    disableJump: true,
    disableEmote: true,
    disableDoubleJump: true,
    disableGliding: true
  })
})

// Restore normal movement
InputModifier.deleteFrom(engine.PlayerEntity)
Standard 标志(均为可选 boolean;已验证
input_modifier.gen.d.ts
):
disableAll
、
disableWalk
、
disableJog
、
disableRun
、
disableJump
、
disableEmote
、
disableDoubleJump
、
disableGliding
。值为
false
或省略的标志会被忽略(不消耗带宽)。
InputModifier.Mode.Standard({...})
与原始的
{ $case: 'standard', standard: {...} }
形式等价(两者都出现在测试场景中)。
重要: InputModifier 仅在 DCL 2.0 桌面客户端中有效,在 Web 浏览器 explorer 中不起作用。如果场景依赖此功能,请使用桌面客户端进行测试。

Cutscene Pattern

过场动画模式

For the worked cutscene flow (freeze the player with
disableAll
during a cinematic, then restore movement with
InputModifier.deleteFrom
), see
{baseDir}/references/input-patterns.md
→ "Cutscene Pattern (freeze player during a cinematic)".
有关完整的过场动画流程(在电影播放期间使用
disableAll
冻结玩家,然后通过
InputModifier.deleteFrom
恢复移动),请参阅
{baseDir}/references/input-patterns.md
→ “Cutscene Pattern (freeze player during a cinematic)”。

WASD Movement Pattern

WASD 移动模式

For the WASD-driven custom-entity pattern (poll
IA_FORWARD
/
IA_BACKWARD
/
IA_LEFT
/
IA_RIGHT
with
isPressed
to move a
Transform
, plus the note on freezing the avatar with
InputModifier
and how polling WASD relates to player movement), see
{baseDir}/references/input-patterns.md
→ "WASD Movement Pattern (drive a custom entity)".
有关由 WASD 驱动的自定义实体模式(使用
isPressed
轮询
IA_FORWARD
/
IA_BACKWARD
/
IA_LEFT
/
IA_RIGHT
来移动
Transform
,以及如何使用
InputModifier
冻结 avatar、WASD 轮询与玩家移动之间的关系),请参阅
{baseDir}/references/input-patterns.md
→ “WASD Movement Pattern (drive a custom entity)”。

Combining Input Patterns

组合输入模式

For the action-bar / number-key pattern (map
IA_ACTION_3
–
IA_ACTION_6
to ability slots via
isTriggered
), see
{baseDir}/references/input-patterns.md
→ "Action Bar with Number Keys".
有关操作栏/数字键模式(使用
isTriggered
将
IA_ACTION_3
–
IA_ACTION_6
映射到技能槽位),请参阅
{baseDir}/references/input-patterns.md
→ “Action Bar with Number Keys”。

Platform detection

平台检测

Detect whether the scene is running on mobile to conditionally adapt controls and UI:
typescript
import { getPlatform, isMobile } from '@dcl/sdk/platform'

// getPlatform() returns 'mobile' | 'desktop' | 'web' | null
// Returns null until the explorer reports its platform (async, shortly after scene start)
// Defer platform-dependent setup until getPlatform() is non-null:
function platformCheckSystem() {
  if (getPlatform() === null) return
  engine.removeSystem(platformCheckSystem)
  if (isMobile()) {
    // mobile-specific setup here (e.g. larger UI, touch-friendly interactions)
  }
}
engine.addSystem(platformCheckSystem)
Import from
@dcl/sdk/platform
. Verified against docs commit
17ca7be
.
检测场景是否正在移动端运行,以便根据平台调整控制方式和 UI:
typescript
import { getPlatform, isMobile } from '@dcl/sdk/platform'

// getPlatform() returns 'mobile' | 'desktop' | 'web' | null
// Returns null until the explorer reports its platform (async, shortly after scene start)
// Defer platform-dependent setup until getPlatform() is non-null:
function platformCheckSystem() {
  if (getPlatform() === null) return
  engine.removeSystem(platformCheckSystem)
  if (isMobile()) {
    // mobile-specific setup here (e.g. larger UI, touch-friendly interactions)
  }
}
engine.addSystem(platformCheckSystem)
从
@dcl/sdk/platform
导入。已对照 docs commit
17ca7be
验证。

Mobile considerations

移动端注意事项

Key facts from the mobile docs expansion (commit
17ca7be
):
  • Touch-only input -- no mouse hover states, keyboard shortcuts, or right-click.
  • borderRadius
    unsupported on mobile UI
    -- avoid rounded corners in mobile-targeting scenes.
  • Static HUD -- the mobile client has fixed on-screen controls (joystick, action buttons) that cannot be repositioned or customized from the scene.
  • UI designed for desktop needs ~3x scaling for mobile readability.
  • Use
    ScreenInsetArea
    (from
    @dcl/sdk/react-ecs
    ) to keep UI inside the device's safe area (notch, home indicator).
  • SDK features not yet on mobile: ParticleSystem, scene dynamic lights (PBPointLight), AudioAnalysis, nine-slice UI tile mode. Check the docs for the latest feature parity tracker.
来自移动端 docs 扩展内容的关键事实(commit
17ca7be
):
  • 仅支持触摸输入——没有鼠标悬停状态、键盘快捷键或右键操作。
  • 移动端 UI 不支持
    borderRadius
    ——在面向移动端的场景中应避免使用圆角。
  • 静态 HUD——移动端客户端具有固定的屏幕控件(joystick、操作按钮),无法从场景中重新定位或自定义。
  • 面向桌面端设计的 UI 在移动端通常需要约 3 倍缩放才能保证可读性。
  • 使用
    ScreenInsetArea
    (来自
    @dcl/sdk/react-ecs
    ),将 UI 保持在设备的安全区域内(刘海、home indicator)。
  • 移动端尚不支持的 SDK 功能: ParticleSystem、场景动态灯光(PBPointLight)、AudioAnalysis、九宫格 UI tile 模式。请查阅 docs 获取最新的功能支持情况跟踪表。

Example scenes

示例场景

Engine-team test scenes exercising these APIs (ground truth):
Engine 团队用于验证这些 API 的测试场景(基准依据):

References

参考资料

  • {baseDir}/references/input-patterns.md
    — branch-specific worked patterns: Tag-based per-entity input cookbook, cutscene freeze/restore flow, WASD-driven custom entity, action-bar number-key mapping.
For basic pointer events and click handlers, see the
add-interactivity
skill.
  • {baseDir}/references/input-patterns.md
    ——分支专用实例模式:基于 Tag 的单实体输入 cookbook、过场动画冻结/恢复流程、WASD 驱动的自定义实体、操作栏数字键映射。
有关基础指针事件和点击处理器,请参阅
add-interactivity
skill。