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 コンポーネントの書き込みとコールバックの登録を1回の呼び出しで行います。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 キーを押す」インタラクションや、近づくとハイライト表示される案内標識などに便利です。コライダーは不要です。システムがプレイヤーの位置とエンティティの Transform をポーリングします。
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は必須で、カメラではなくアバターのルートからの距離を測定します。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)
レイキャスト
レイキャストの方向タイプ
4種類の方向モードを利用できます。
// 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を参照してください。