关于 add-interactivity
面向 Decentraland 场景开发者的交互实现技能,涵盖点击、接近触发、区域检测和射线检测,并说明静态实体与运行时处理的分工。
主要功能
- 01注册指针点击与悬停事件
- 02按玩家距离触发交互
- 03检测玩家或实体进入区域
- 04使用射线检测目标
- 05处理全局按键输入
- 06读取光标状态与位置
适用场景
- 01为场景中的按钮或模型添加点击行为
- 02实现靠近门或告示牌时的交互
- 03检测玩家进入或离开指定区域
- 04从摄像机方向识别命中对象
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dcl-regenesislabs/ opendcl --skill add-interactivity安装后在 Claude Code 中输入/add-interactivity确认已加载。
附带文件references/input-reference.md
详细介绍
为 Decentraland 场景添加交互性
编写方式
可点击的实体(立方体、按钮、模型)是静态的——在 main-entities.ts 中声明它,并设置 Transform / Mesh / Material。可点击性始终在运行时通过 src/index.ts 中的 pointerEventsSystem.onPointerDown(...)(或相关辅助方法)添加。该辅助方法会在一次调用中写入 PointerEvents 组件并注册回调——不要同时在 main-entities.ts 中声明 PointerEvents;辅助方法会直接覆盖它,重复声明也容易导致配置不一致。
// main-entities.ts — entity only, no PointerEvents
clickable_cube: {
components: {
Transform: { position: { x: 8, y: 1, z: 8 } },
MeshRenderer: { mesh: { $case: 'box', box: { uvs: [] } } }
}
}
// src/index.ts — register clickability via the helper system
import { engine, pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'
export function main() {
const cube = engine.getEntityOrNullByName('clickable_cube')
if (cube) {
pointerEventsSystem.onPointerDown(
{ entity: cube, opts: { button: InputAction.IA_POINTER, hoverText: 'Open' } },
() => { /* what happens on click */ }
)
}
}
TriggerArea 和 Raycast 也在运行时处理——它们位于 src/index.ts 中。下方通过 engine.addEntity() 内联创建实体的代码示例用于运行时/技术实体(射线检测探针、根据数据生成的触发区域);对于静态可点击道具,应在 main-entities.ts 中声明该道具,并按上述方式在 src/index.ts 中添加处理程序。
决策树
| 需求 | 方式 | API |
|---|---|---|
| 点击/悬停特定实体 | 指针事件 | pointerEventsSystem.onPointerDown() |
| 检测玩家进入区域 | 触发区域 | TriggerArea + triggerAreaEventsSystem |
| 每帧轮询按键状态 | 全局输入 | inputSystem.isTriggered() / isPressed() |
| 检测某个方向上的物体 | 射线检测 | raycastSystem 或 Raycast 组件 |
| 读取光标位置/锁定状态 | 光标状态 | PointerLock、PrimaryPointerInfo |
指针事件(点击/悬停)
使用辅助系统(推荐)
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)
}
)
所有输入操作
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
所有事件类型
PointerEventType.PET_DOWN // Button pressed
PointerEventType.PET_UP // Button released
PointerEventType.PET_HOVER_ENTER // Cursor enters entity
PointerEventType.PET_HOVER_LEAVE // Cursor leaves entity
指针抬起(释放)
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!') }
)
移除处理程序
pointerEventsSystem.removeOnPointerDown(cube)
pointerEventsSystem.removeOnPointerUp(cube)
重要提示:需要碰撞体
指针事件仅适用于具有位于 CL_POINTER 层上的碰撞体的实体。如果实体没有网格,请为其添加碰撞体:
import { MeshCollider } from '@dcl/sdk/ecs'
MeshCollider.setBox(entity) // Invisible box collider — defaults include CL_POINTER
对于 GLTF 模型,请设置碰撞掩码:
GltfContainer.create(entity, {
src: 'models/button.glb',
visibleMeshesCollisionMask: ColliderLayer.CL_POINTER
})
邻近事件(无指针触发器)
这类事件类似于 pointerEventsSystem.onPointerDown,但会根据玩家与实体的距离触发,而不是由点击触发。适用于“靠近时按 E”这类交互,以及玩家接近时高亮的指示牌。无需碰撞体——系统会轮询玩家位置与实体变换之间的关系。
import { engine, pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'
const door = engine.getEntityOrNullByName('shop_door')
if (door) {
pointerEventsSystem.onProximityDown(
{
entity: door,
opts: {
button: InputAction.IA_PRIMARY,
hoverText: 'Open shop',
maxPlayerDistance: 3 // metres
}
},
() => { /* run when the player presses the button within range */ }
)
pointerEventsSystem.onProximityEnter(
{ entity: door, opts: { maxPlayerDistance: 5 } },
() => { /* fired once when the player enters the radius */ }
)
pointerEventsSystem.onProximityLeave(
{ entity: door, opts: { maxPlayerDistance: 5 } },
() => { /* fired once when the player leaves the radius */ }
)
}
maxPlayerDistance为必填项,距离以Avatar 根节点为起点测量,而非摄像机。priority(数值)——如果多个邻近事件重叠,则优先级较高的事件生效。- 使用
pointerEventsSystem.removeOnProximityDown(entity)等方法移除。
当实体没有可见碰撞体,或玩家不应需要瞄准实体时(例如指示牌、靠近时自动打开的门),优先使用邻近事件,而不是 pointerEventsSystem.onPointerDown。
触发区域(邻近检测)
检测玩家进入、离开或停留在某个区域内:
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
})
默认情况下,触发区域会响应玩家层。使用 ColliderLayer 限制哪些实体可以激活该区域:
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)
射线检测
射线方向类型
提供四种方向模式:
// 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 }
基于回调的射线检测(推荐)
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)
基于组件的射线检测
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)
}
})
摄像机射线检测
从摄像机发射射线,以检测玩家正在注视的对象:
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)
}
)
全局输入处理
监听任意位置的按键(不针对特定实体):
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)
}
})
光标状态
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)
切换模式(点击切换状态)
可切换对象的常见模式:
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)
}
)
最佳实践
- 始终在指针事件上设置
maxDistance(通常为 8-16m) - 始终设置
hoverText,让用户知道可以进行交互 - 移除实体时清理处理程序
- 使用
MeshCollider创建不可见的触发器表面 - 对于复杂交互,使用带有状态跟踪的系统
- 在预览中测试交互——悬停文本应清晰可见
- 除非需要逐帧获取结果,否则在射线上将
continuous: false - 同时针对桌面端和移动端进行设计——移动端没有键盘,因此要依赖指针和屏幕按钮
完整的输入操作列表和高级模式,请参阅 {baseDir}/references/input-reference.md。