关于 add-interactivity
面向 Decentraland 场景交互开发的技能,指导开发者为实体添加点击、悬停、接近触发、输入和射线检测等行为。内容涵盖碰撞器、距离规则及常见事件处理陷阱。
主要功能
- 01点击与悬停事件
- 02接近事件和触发区域
- 03全局输入与射线检测
- 04碰撞器配置指导
- 05事件距离与处理陷阱
适用场景
- 01为场景物体添加点击操作
- 02在玩家进入区域时触发行为
- 03实现按键输入响应
- 04排查实体交互未触发的问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ decentraland/ sdk-skills --skill add-interactivity安装后在 Claude Code 中输入/add-interactivity确认已加载。
附带文件references/input-reference.mdreferences/interactivity-patterns.md
详细介绍
为 Decentraland 场景添加交互性
规则:获取组合实体,切勿重新创建
如果要添加交互性的实体定义在 assets/scene/main.composite 中,请在 index.ts 中按名称或标签查找。不要调用 engine.addEntity() 并创建组件——那样会创建重复实体。
import { engine, pointerEventsSystem, InputAction } from "@dcl/sdk/ecs";
import { EntityNames } from "../assets/scene/entity-names";
export function main() {
// By name (type-safe via auto-generated EntityNames enum)
const door = engine.getEntityOrNullByName(EntityNames.Door_1);
if (door) {
pointerEventsSystem.onPointerDown(
{
entity: door,
opts: { button: InputAction.IA_PRIMARY, hoverText: "Open" },
},
() => {
/* open door */
}
);
}
// By tag (batch operations on groups of composite entities)
const crystals = engine.getEntitiesByTag("Crystal");
for (const crystal of crystals) {
pointerEventsSystem.onPointerDown(
{
entity: crystal,
opts: { button: InputAction.IA_PRIMARY, hoverText: "Collect" },
},
() => {
/* collect crystal */
}
);
}
}
这些查找必须在 main() 或 main() 之后调用的函数中执行——在此之前不会实例化组合实体。
决策树
| 需求 | 方案 | API |
|---|---|---|
| 单击/悬停特定实体 | 指针事件 | pointerEventsSystem.onPointerDown() |
| 玩家靠近时按下按钮(无需瞄准) | 邻近事件 | pointerEventsSystem.onProximityDown() |
| 检测玩家进入某个区域 | 触发区域 | TriggerArea + triggerAreaEventsSystem |
| 每帧轮询按键状态 | 全局输入 | inputSystem.isTriggered() / isPressed() |
| 检测某个方向上的物体 | 射线检测 | raycastSystem 或 Raycast 组件 |
| 读取光标位置/锁定状态 | 光标状态 | PointerLock、PrimaryPointerInfo |
指针事件(单击/悬停)
使用 pointerEventsSystem.onPointerDown() 为实体添加单击处理程序。还可使用:.onPointerUp()、.onPointerHoverEnter()、.onPointerHoverLeave()。在 opts 中设置 maxDistance(通常为 8–16m)以限制交互范围——距离是从化身而非摄像机测量的(见下文“距离规则”)——并设置 hoverText,让玩家知道交互结果。使用 .removeOnPointerDown(entity) 等方法移除——实体移除时清理处理程序。
在拥有碰撞体的实体上注册处理程序。 指针事件会传递到射线命中的碰撞体所属实体——注册在父组上的处理程序,或注册在自身没有碰撞体的子实体上的处理程序,都不会触发。如果模型是一个空父实体,碰撞网格位于子实体上,请在子实体上注册(或为父实体添加自己的 MeshCollider)。玩家无法单击的一个头戴式设备(2026-10-12),问题就是处理程序注册在碰撞体实体的父实体上。碰撞体层级:见下文必需的碰撞体。
距离规则(maxDistance / maxCameraDistance / maxPlayerDistance)
maxDistance 是从 AVATAR 起算的距离,不是相机距离。 较早的 SDK/协议注释将其称为相机距离,这是错误的;Unity/Godot 始终测量的是玩家距离。已验证:协议 pointer_events.proto(0e82dcd)、@dcl/ecs EventSystemOptions(6a98834a)、测试场景 34,20-pointer-events-distances。
| 选项 | 测量起点 | 默认值 |
|---|---|---|
maxDistance | avatar/玩家的位置 | 10 |
maxCameraDistance | 当前活动相机的原点 | 无(未设置) |
maxPlayerDistance | [DEPRECATED],maxDistance 的别名(含义相同) | 无 |
两个生效阈值的组合方式:
| 设置 | 允许交互的条件 |
|---|---|
仅设置 maxDistance | 玩家距离 <= maxDistance |
仅设置 maxCameraDistance | 相机距离 <= maxCameraDistance |
| 两者都设置 | 玩家距离 <= maxDistance 或 相机距离 <= maxCameraDistance(OR,不是 AND) |
| 两者都未设置 | 玩家距离 <= 10(隐式默认值) |
- 如果同时设置
maxDistance和maxPlayerDistance,两者中较大的值将作为玩家阈值。 - 优先使用
maxDistance;仅当检查确实应针对相机时(例如,固定的VirtualCamera正对着 avatar 永远不会走近的物体),才使用maxCameraDistance。默认第三人称相机与 avatar 相距约 7 米,因此实际上,仅基于相机的 10 米规则比表面上看起来严格得多。 - 邻近事件完全忽略
maxCameraDistance——请参见“邻近事件”一节。
maxCameraDistance(EventSystemOptions.maxCameraDistance)要求使用 @dcl/sdk 7.28.0+。[未验证:渲染器支持情况——已在 Bevy explorer 和 unity-explorer PR #9902 中实现;如果该 PR 未包含在玩家所用的 Unity 构建中,maxCameraDistance 在该环境下会被忽略,只有玩家距离条件生效。发布依赖仅相机规则的场景前,请确认已部署的 explorer 支持情况。]
测量起点(当阈值结果与计算值相差约 1 米时,这一点很重要):
- 光标玩家距离:从 avatar 根节点(脚部,
Transform.get(engine.PlayerEntity).position)到碰撞体表面的射线命中点。 - 光标相机距离:从相机到命中点的射线长度(
hitInfo.distance)。 - 邻近距离:从胶囊体中心(脚部上方约 1 米)起算,而不是从脚部或命中点起算。因此,即使站在同一位置,邻近距离和光标距离的读数也预计会相差约 1 米。
反馈选项:showFeedback 控制 hoverText
已通过 30,20-pointer-events-feedback 验证:
showFeedback(默认true)——启用悬停 UI。只有showFeedback为 true 时,才会显示hoverText。 设置showFeedback: false时,无论hoverText如何设置,都不会显示悬停文本。showHighlight(默认true)——悬停时实体边缘/轮廓高亮。该选项独立于文本;要显示高亮,必须启用showFeedback。- 当
showFeedback: true且hoverText未设置或为空时,explorer 会显示默认操作提示(例如按钮图标),而不是不显示任何内容。
轮廓的绘制范围: 拥有被命中碰撞体的实体的渲染器(该实体还必须带有 PointerEvents),以及它的所有后代;绝不会绘制其父级或同级实体。只有 MeshRenderer 图元和 GLB 会显示轮廓;TextShape 不会(unity-explorer InteractionHighlightSystem,main 2026-10)。因此:
- 不可见的命中框(有碰撞体和
PointerEvents,没有网格)不会显示任何轮廓,除非可见部分是它的子级。将命中框放在模型旁边或作为模型的子级,都可以触发事件并显示悬停文本,但不会显示轮廓。 - 由多个实体组成、且每个实体都有自己的碰撞体和处理器的道具,一次只会高亮其中一个部分。将一个碰撞体和
PointerEvents放在根节点上,并将可见部分设为其子级;或者将道具制作为单个 GLB。
陷阱:切勿在指针处理程序自身的回调中(重新)注册它
在某个实体自身的指针回调中调用该实体的 onPointerDown / removeOnPointerDown(或 Up / Hover 的 on/remove 变体),会导致同一次点击多次触发处理程序(观察到:一次点击触发 3 次,因为状态机在每次触发时都会重新注册)。
原因(已验证——@dcl/ecs/dist/systems/events.js):EventSystem 每帧都会遍历一个按实体划分的处理程序 Map。onPointerDown 会执行 removeEvent(entity, EventType.Down),然后执行 getEvent(entity).set(EventType.Down, …),这会将 Down 键重新插入同一个 Map。在 Map 自身的 for…of 迭代期间重新插入键,会导致它在同一轮中再次被访问,而且 inputSystem.getInputCommand(...) 仍会返回同一个缓冲的按下命令 → 回调再次触发。
修复——要动态更改悬停文本,请原地修改现有的 PointerEvents 组件,而不是重新注册:
import { PointerEvents } from '@dcl/sdk/ecs'
function setHoverText(entity: Entity, hoverText: string) {
const pe = PointerEvents.getMutableOrNull(entity)
if (!pe) return
for (const entry of pe.pointerEvents) {
if (entry.eventInfo) entry.eventInfo.hoverText = hoverText
}
}
只注册一次处理程序(例如,在创建实体时);切勿在点击/悬停回调或逐帧系统中再次调用它。
重要:必须有碰撞体——指针事件仅适用于具有使用 ColliderLayer.CL_POINTER 层的碰撞体的实体。对于不可见的碰撞体,请使用 MeshCollider.setBox(entity);或者在 GltfContainer 上设置 visibleMeshesCollisionMask: ColliderLayer.CL_POINTER。
GltfContainer 的可点击性——应使用哪个掩码(以及蒙皮网格例外)
GltfContainer 的默认值(已验证——@dcl/ecs gltf_container.gen.d.ts):
visibleMeshesCollisionMask默认值:0(默认情况下,可见网格不可点击/不可碰撞)invisibleMeshesCollisionMask默认值:CL_POINTER | CL_PHYSICS(GLB 的*_collider网格会获得这两个层)
根据模型的制作方式选择:
| 模型 | 如何使其可点击 |
|---|---|
有 _collider 网格 | 默认情况下已通过不可见网格设置位于 CL_POINTER 层。若要拆分层,请显式设置 invisibleMeshesCollisionMask,并将 visibleMeshesCollisionMask: 0 保持不变。 |
静态网格,无 _collider | 设置 visibleMeshesCollisionMask: ColliderLayer.CL_POINTER——可见网格将成为点击目标。 |
蒙皮/骨架绑定(NPC、角色、幽灵),无 _collider | visibleMeshesCollisionMask 无法让其变为可点击。请添加一个显式的不可见子碰撞体(见下文)。 |
陷阱:visibleMeshesCollisionMask 无法让蒙皮/绑定的 GLB 支持指针点击。 在一个 SDK7 场景中观察到:一个没有 _collider 网格且设置了 visibleMeshesCollisionMask: ColliderLayer.CL_POINTER 的 NPC/幽灵 GLB 没有接收指针事件。SDK6 的 GLTFShape 无论是否绑定骨架,都会使可见网格可点击;而在 SDK7 中,本例的可见网格掩码无法对蒙皮/动画可见网格进行指针事件射线检测。(这是观察到的渲染器行为——协议中未记录具体的骨架绑定条件;请逐个模型验证。)
修复——在 CL_POINTER 层添加一个不可见的盒形碰撞体,并将其设为实体的子对象,使其跟随移动;然后在碰撞体上注册处理程序:
import { engine, MeshCollider, ColliderLayer, pointerEventsSystem, InputAction, Transform } from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'
// `npc` is the rigged GLB entity (with GltfContainer + Animator).
const clicker = engine.addEntity()
// scale to the avatar bounds; parent so it follows the NPC as it moves
Transform.create(clicker, { position: Vector3.create(0, 0.45, 0), scale: Vector3.create(2, 2.9, 2), parent: npc })
MeshCollider.setBox(clicker, ColliderLayer.CL_POINTER)
pointerEventsSystem.onPointerDown(
{ entity: clicker, opts: { button: InputAction.IA_PRIMARY, hoverText: 'Talk', showFeedback: true } },
() => { /* handle click */ }
)
GltfContainer 本身可以继续使用 visibleMeshesCollisionMask: 0。对于 AvatarShape NPC(完全没有网格碰撞体),也可以采用相同的子碰撞体方案——参见 [[npcs]] 选项 A。
所有输入操作
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; // Control key
InputAction.IA_MODIFIER; // 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
PointerEventType.PET_PROXIMITY_ENTER; // Player walks within entity's proximity range
PointerEventType.PET_PROXIMITY_LEAVE; // Player moves out of entity's proximity range
移除处理器时会保留 PointerEvents 条目(SDK 7.28.1+)
已在 @dcl/ecs 中修复(js-sdk-toolchain #1577,e3d4af57)。今后请依赖以下行为:
removeOnPointerDown/removeOnClick/removeOnPointerUp/ 悬停和邻近事件的对应变体现在都会**移除注册时创建的PointerEvents条目,无论是否传入了hoverText。**此前,只有注册时带有hoverText的处理器才会移除条目;其他所有移除操作都会泄漏条目,因此渲染器仍会公布一个回调已不存在的交互,而且每次重新注册都会让数组增加一个条目(通过 CRDT 重新发送整个数组)。请勿再添加虚设的hoverText作为变通方法——该方法已不再需要。- 回调映射以
eventType:interactionType为键,因此**onProximityDown不再会逐出同一实体上的onPointerDown。现在支持为同一事件类型同时注册光标处理器和邻近处理器,且满足两者条件时会触发两个回调。** - 移除操作只会移除其自身注册对应的条目:
removeOnClick不再会夺走onPointerDown的描述符(两者都会写入PET_DOWN+CURSOR)。
@dcl/sdk 7.28.0(当前稳定版)早于此修复——该修复已发布在 @next(7.28.1-…commit-e3d4af5)。在 7.28.0 及更早版本中,请假设不带 hoverText 的移除操作会泄漏条目。
邻近事件(无需瞄准的近距离交互)
邻近事件允许实体在玩家靠近且大致面向实体时响应按键,**无需玩家将光标瞄准实体。**交互区域是从 Avatar 位置向前延伸的宽阔三角形区域——重要的是 Avatar 的朝向,而非摄像机方向。
邻近事件触发必须同时通过以下三个条件(已根据 34,20-pointer-events-distances 和 Unity explorer 的邻近事件路径验证):
- 距离 — 玩家距离 <=
maxDistance(默认值为 10),从胶囊体中心(约在脚部上方 1 m)测量。 - 朝向锥形区域 — 实体必须位于 Avatar 前方约 120 度的水平锥形区域内。
- 遮挡射线检测 — 玩家与实体之间不能有任何物体。
**注意 — 3 m 的粗略检测范围。**Unity explorer 的邻近事件粗略检测范围是硬编码的 3 m
OverlapSphere。邻近条目上的maxDistance大于 3 时,可能会被静默限制到约 3 m,因此要求 8 m 范围的邻近处理器可能会在约 3 m 处失效。请将邻近交互设计为近距离交互;需要超出约 3 m 的范围时,请使用TriggerArea(或光标指针事件)。
如果玩家既处于带有邻近交互的实体附近,又瞄准了带有指针交互的实体,**指针交互始终优先。**在范围内的多个邻近实体中,只会激活最近的实体(或优先级最高的实体)。
使用 pointerEventsSystem.onProximityDown() 和 .onProximityUp()——签名与指针事件相同。范围由 maxDistance 决定(Avatar 距离,默认值为 10);maxPlayerDistance 是已弃用的别名。每个实体的每种事件类型只能注册一个邻近处理器。请勿在系统循环内调用。
使用 .onProximityEnter() 和 .onProximityLeave() 检测玩家进入或离开范围——适用于音效、动画或 UI 提示。
多个实体重叠时,使用 priority 选项(数值越大,优先级越高)。优先级相同时,最近的实体胜出。使用 .removeOnProximityDown(entity) 等方法移除。
邻近选项
button:要监听的按钮(与指针事件相同)maxDistance:玩家的 avatar 与实体之间的最大距离(默认值为10)——即邻近范围maxPlayerDistance:[DEPRECATED]maxDistance的别名;如果两者都已设置,则取较大的值maxCameraDistance:在邻近路径中会被忽略。 仅应用玩家距离阈值。在邻近条目中设置此项不会产生任何效果。hoverText:玩家靠近时显示的文本showHighlight:进入范围时显示边缘高亮(默认值:true)showFeedback:在实体中心周围显示悬停反馈(默认值:true)priority:用于解决冲突——值越大越优先;值相同时,距离最近的优先
如需采用基于系统的方法(在同一实体上结合指针和邻近交互),请将 InteractionType.PROXIMITY 与 PointerEvents 组件和 inputSystem.isTriggered() 配合使用。
触发区域(邻近检测)
用于检测实体何时进入某个区域的原生 ECS 组件。优先使用此组件,而不是手动编写“每帧检查玩家位置”的系统,也不要使用旧版 @dcl-sdk/utils triggers.addTrigger() 辅助函数——这些方法可作为后备方案,但 TriggerArea 才是标准的 SDK7 基元(ADR-258)。
体积的大小、位置和旋转由实体的 Transform 决定。 Transform.scale 定义一个位于实体姿态处的单位立方体(或以 scale.x 为半径的球体),并遵循整个父级链。
场景是否在 Creator Hub 中打开? utils 目录中有一个名为 “Trigger Area” 的 Smart Item(e9fa0eab-44de-4efe-af77-a71171a1a73f)——用户可放置并调整大小的不可见立方体或球体,其中有一个控件,可在 AI 助手的编辑框中预填关于进入/离开时应执行什么操作的提示词。请使用它,而不是手动创建实体:它自带形状参数、碰撞体筛选器和 Reaction 连线;而且现在编辑参数不会再清除这些 Reaction(已在 creator-hub e43441b5 中修复)。旧的非脚本条目和临时使用的“Trigger Area (Script)”标签都已在同一提交中移除——现在只有一个条目。参见 creator-hub-mcp。
陷阱——触发器太薄,永远不会触发。 建模为约 0.1m 薄板的压力板或地面标记,其触发体积在玩家站在上面时,永远不会与 avatar 胶囊体重叠,因此
TriggerArea会悄无声息地不触发。这看起来像是组件损坏,但实际上并非如此。修复方法: 将薄板作为视觉模型保留,并在其上方放置一个独立的不可见方框,高约 2m(已在
149,149-synthetic-input-showcase的 S2 和 S3 站点验证):// 视觉板保持原位。触发器使用独立实体。 const plateTrigger = engine.addEntity() Transform.create(plateTrigger, { position: Vector3.create(plate.x, plate.y + 1, plate.z), // 向上居中 1m scale: Vector3.create(3, 2, 3), // 高 2m }) TriggerArea.setBox(plateTrigger)注意事项——区域重叠:根据占用状态重新计算,切勿在处理器中直接写入。 当两个触发区域各自设置某些状态(
InputModifier、相机模式、环境音轨)时,不要在onTriggerEnter中设置状态,并在onTriggerExit中清除状态。传送(movePlayerTo)跨越两个区域时,目标区域的 enter 事件可能先于起始区域的 exit 事件触发,随后过时的 exit 事件就会清除 enter 事件刚设置的状态。事件顺序两个方向都不确定——同一会话中测得过两种顺序,分别对应两个移动方向,因此不能依赖其中任何一种顺序编写代码。**修复方法:**处理器只切换占用标记;由一个函数根据所有标记计算状态,并在每次 enter 和 exit 后调用:
let insideZoneA = false let insideZoneB = false function applyModifierFromOccupancy() { if (insideZoneA) InputModifier.createOrReplace(engine.PlayerEntity, { mode: { $case: 'standard', standard: { disableAll: true } } }) else if (insideZoneB) InputModifier.createOrReplace(engine.PlayerEntity, { mode: { $case: 'standard', standard: { disableRun: true } } }) else InputModifier.deleteFrom(engine.PlayerEntity) } // 每个 enter/exit 处理器:设置对应标记,然后调用 applyModifierFromOccupancy()这样事件顺序就无关紧要了。这种方法适用于由多个区域驱动的任何“最后写入者生效”状态。
最简示例——检测本地玩家的方框:
import {
engine,
Transform,
TriggerArea,
triggerAreaEventsSystem,
ColliderLayer
} from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'
const zone = engine.addEntity()
Transform.create(zone, {
position: Vector3.create(8, 1, 8),
scale: Vector3.create(4, 2, 4) // 4m × 2m × 4m box
})
TriggerArea.setBox(zone, ColliderLayer.CL_PLAYER)
triggerAreaEventsSystem.onTriggerEnter(zone, (result) => {
if (result.trigger?.entity !== engine.PlayerEntity) return // local player only
console.log('player entered')
})
triggerAreaEventsSystem.onTriggerExit(zone, () => {
console.log('player left')
})
球体变体:TriggerArea.setSphere(entity, ColliderLayer.CL_PLAYER)——使用统一的 Transform.scale(半径取自 scale.x)。
**碰撞掩码:**默认值为 CL_PLAYER。传入其他 ColliderLayer 值(按位或组合,例如 CL_PLAYER | CL_CUSTOM4),即可响应物理层或自定义层。
尺寸:Transform 本身就是体积——没有单独的尺寸字段。PBTriggerArea 仅包含 mesh 和 collision_mask;尺寸和旋转完全取决于实体的 Transform,且体积会跟踪实体或其父级在任意时刻发生的缩放变化。这与同类区域组件恰好相反:AvatarModifierArea 和 CameraModeArea 接受显式的 area: Vector3,在组件的整个生命周期内都会覆盖 Transform.scale,因此之后缩放这些实体不会产生任何影响。不要把一种组件的使用习惯套用到另一种组件上(参见 player-avatar 和 camera-control)。
**检测非玩家实体:**对于自身碰撞体位于区域监听层上的任何实体,TriggerArea 都会触发。若要让场景实体触发某个区域,请为其添加位于匹配自定义层上的 MeshCollider/GltfContainer 碰撞体,并设置区域掩码以包含该层(已在 75,-9-trigger-areas 中验证:GltfContainer 的 visibleMeshesCollisionMask: ColliderLayer.CL_CUSTOM4 会触发通过 TriggerArea.setBox(entity, ColliderLayer.CL_PLAYER | ColliderLayer.CL_CUSTOM4) 创建的区域)。你也可以将 TriggerArea 添加到移动或下落的实体上,以检测它何时进入其他区域。
回调:
triggerAreaEventsSystem.onTriggerEnter(entity, cb)——进入时触发一次triggerAreaEventsSystem.onTriggerStay(entity, cb)——在区域内时每个 tick 都会触发(由 SDK 根据 ENTER/EXIT 状态机合成)triggerAreaEventsSystem.onTriggerExit(entity, cb)——退出时触发一次- 使用
removeOnTriggerEnter/Stay/Exit(entity)分离
回调参数结构——常见误区:
回调接收一个 PBTriggerAreaResult。result.trigger?.entity 是进入区域的实体(与 engine.PlayerEntity 比较,以筛选本地玩家)。result.triggeredEntity 是触发区域本身——将其与玩家比较总会得到 true,因此条件判断永远不会触发。命名确实很容易让人误解——triggeredEntity 听起来像是“触发事件的实体”,但实际上指的是“其触发区域被激活的实体”。参见 {baseDir}/references/input-reference.md#trigger-area-callback-fields。
多人游戏提示: 使用 CL_PLAYER 时,每位进入的玩家都会触发事件,包括远程玩家。务必使用 if (result.trigger?.entity !== engine.PlayerEntity) return 防止产生物理/UI 副作用。
底层组件: TriggerArea(配置)和 TriggerAreaResult(CRDT 结果)。通常不需要直接读取 TriggerAreaResult,请使用事件系统。
射线检测
四种方向模式:本地方向(相对于实体旋转)、全局方向(世界空间)、全局目标(瞄准某个位置)、目标实体(瞄准另一个实体)。
基于回调的方式(推荐):raycastSystem.registerLocalDirectionRaycast()、.registerGlobalDirectionRaycast()、.registerGlobalTargetRaycast()、.registerTargetEntityRaycast()。使用 .removeRaycasterEntity() 移除。
基于组件的方式:创建 Raycast 组件,并在系统中读取 RaycastResult。除非确实需要逐帧获取结果,否则请设置 continuous: false(设为 true 会逐帧重新检测,并产生相应开销)。字段(已通过 77,-1-raycast-unit-tests 验证):direction: { $case: 'localDirection'|'globalDirection'|'globalTarget'|'targetEntity', ... }、originOffset、maxDistance、queryType、continuous、timestamp。RaycastResult(稍后一帧添加)包含 globalOrigin、归一化的 direction、hits[]、timestamp 和 tickNumber。
射线原点遵循世界变换: 射线从实体的世界位置(已应用父级层级变换)加上 originOffset 处开始,因此,嵌套在已移动或缩放父级下的射线检测实体会从其解析后的世界位姿发射射线。localDirection 会根据实体的世界旋转进行旋转;globalDirection 则忽略该旋转。
注意:RaycastQueryType.RQT_HIT_FIRST 会选取范围内的第一个命中项,不一定是最近的(已通过枚举文档验证)。如果需要最近的命中项,请使用 RQT_QUERY_ALL 并按距离排序。RQT_NONE 会跳过检测并返回空的命中项。
摄像机射线检测: 使用 engine.CameraEntity 作为实体,以检测玩家正在看向什么。若要沿光标方向发射射线(锁定或未锁定时均适用),请使用 PrimaryPointerInfo.worldRayDirection 作为方向(参见 advanced-input)。
全局输入处理
使用 inputSystem.isTriggered()(本帧刚按下)和 inputSystem.isPressed()(当前按住),在 engine.addSystem() 中监听任意位置的按键,而非特定实体的按键。通过系统处理实体专属输入时,请使用 inputSystem.getInputCommand()。
同时为桌面端和移动端进行设计——移动端没有键盘,因此应依赖指针事件和屏幕 UI 按钮,而不是按键。
光标状态
使用 PointerLock.get(engine.CameraEntity).isPointerLocked 读取指针锁定状态。使用 PrimaryPointerInfo.get(engine.RootEntity) 获取光标位置和世界射线。
切换模式
常见模式:在模块级布尔值中跟踪状态,在点击处理程序中切换该值,并相应更新实体。
对于复杂交互(多步序列、冷却时间、多个实体响应共享状态),不要只用单个布尔值:改用模块级对象或自定义组件跟踪状态,并通过系统驱动更新。
Smart Items:行为是脚本代码,而非触发器图
大多数目录中的 Smart Items 已从 Actions/Triggers 无代码图迁移(creator-hub 658f4daf),改为使用带 @action 方法的 asset-packs::Script 代码。如果打开已迁移的项目并期待找到 asset-packs::Triggers,则不会找到——行为位于 .ts 文件中,“When X”钩子是可选的 ActionCallback 参数(onClick、onActivate/onDeactivate、onReachStart/onReachEnd、onRing、onOpen/onClose),其状态则是同步的 asset-packs::States。旧的公共 action 名称会保留为 call_script_method 指针,因此现有连线仍可正常工作。完整详情、未迁移列表,以及 world_teleport -> teleport 合并信息,请参见 script-components。
示例场景
用于测试这些 API 的引擎团队场景(实际行为基准):
- https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/3,2-proximity-interactions —
onProximityDown/Enter/Leave、光标与近距离交互的优先级、包含两个按钮的声明式PointerEvents,以及近距离触发的门。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/30,20-pointer-events-feedback —
showFeedback/showHighlight/hoverText的组合(悬停文本需要showFeedback: true)。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/34,20-pointer-events-distances — 距离规则:八条测试通道验证
maxDistance(玩家)、已弃用的maxPlayerDistance别名(取较大值)、maxCameraDistance、OR 组合、隐式的 10m 默认值,以及近距离交互会忽略maxCameraDistance。每个立方体都会根据先解析再分支的逻辑,每帧预测 LIVE/DEAD,因此与场景内行为不一致时,就说明存在问题。其中还包含一个探针,用于测试 Explorer 硬编码的 3m 近距离广相位。固定使用@dcl/sdk@next。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/149,149-synthetic-input-showcase — 高而不可见的
TriggerArea修复(S2/S3)和 occupancy-recomputeInputModifier模式,通过 Unity Explorer MCP 实时驱动。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/75,-9-trigger-areas —
TriggerArea.setBox/setSphere、onTriggerEnter/Stay/Exit、result.trigger?.entity,以及用于让非玩家实体触发区域的自定义碰撞体层掩码。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/0,0-cube-spawner — 在系统中通过
inputSystem.isTriggered(IA_POINTER, PET_DOWN, entity)读取声明式PointerEvents。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/77,-1-raycast-unit-tests — 使用基于组件的
Raycast/RaycastResult测试全部四种方向模式,包括起点位于经过变换的父级链下方的情况。
完整代码示例和实现模式请参阅 {baseDir}/references/interactivity-patterns.md。输入操作参考表和声明式 PointerEvents 组件请参阅 {baseDir}/references/input-reference.md。