mcpField
A
add-interactivity

add-interactivity

github.com/dcl-regenesislabs开发1738 次安装21 周前更新
代码仓库

关于 add-interactivity

面向 Decentraland 场景开发者的交互实现技能,涵盖点击、接近触发、区域检测和射线检测,并说明静态实体与运行时处理的分工。

主要功能

  1. 01注册指针点击与悬停事件
  2. 02按玩家距离触发交互
  3. 03检测玩家或实体进入区域
  4. 04使用射线检测目标
  5. 05处理全局按键输入
  6. 06读取光标状态与位置

适用场景

  1. 01为场景中的按钮或模型添加点击行为
  2. 02实现靠近门或告示牌时的交互
  3. 03检测玩家进入或离开指定区域
  4. 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。

评论

支持 Markdown