@dcl-regenesislabs/opendcl 0.2.1-26165320302.commit-e6effe4 → 0.2.1-26238928766.commit-28648d7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/context/sdk7-cheat-sheet.md +4 -0
- package/dist/index.js +0 -12
- package/dist/index.js.map +1 -1
- package/extensions/dcl-init.ts +58 -6
- package/package.json +3 -3
- package/prompts/system.md +71 -41
- package/skills/add-3d-models/SKILL.md +120 -70
- package/skills/add-interactivity/SKILL.md +74 -2
- package/skills/advanced-input/SKILL.md +34 -1
- package/skills/advanced-rendering/SKILL.md +82 -9
- package/skills/animations-tweens/SKILL.md +203 -98
- package/skills/audio-analysis/SKILL.md +164 -0
- package/skills/audio-video/SKILL.md +184 -83
- package/skills/build-ui/SKILL.md +25 -2
- package/skills/camera-control/SKILL.md +78 -7
- package/skills/create-scene/SKILL.md +56 -13
- package/skills/deploy-scene/SKILL.md +12 -0
- package/skills/deploy-worlds/SKILL.md +35 -0
- package/skills/editor-gizmo/.gitignore +11 -0
- package/skills/editor-gizmo/SKILL.md +222 -0
- package/skills/editor-gizmo/src/__editor/camera.ts +277 -0
- package/skills/editor-gizmo/src/__editor/discovery.ts +186 -0
- package/skills/editor-gizmo/src/__editor/drag.ts +265 -0
- package/skills/editor-gizmo/src/__editor/gizmo.ts +496 -0
- package/skills/editor-gizmo/src/__editor/history.ts +72 -0
- package/skills/editor-gizmo/src/__editor/index.ts +137 -0
- package/skills/editor-gizmo/src/__editor/input.ts +55 -0
- package/skills/editor-gizmo/src/__editor/math-utils.ts +114 -0
- package/skills/editor-gizmo/src/__editor/persistence.ts +113 -0
- package/skills/editor-gizmo/src/__editor/selection.ts +157 -0
- package/skills/editor-gizmo/src/__editor/state.ts +117 -0
- package/skills/editor-gizmo/src/__editor/ui.tsx +697 -0
- package/skills/game-design/SKILL.md +1 -2
- package/skills/lighting-environment/SKILL.md +103 -56
- package/skills/multiplayer-sync/SKILL.md +31 -2
- package/skills/nft-blockchain/SKILL.md +45 -40
- package/skills/npcs/SKILL.md +180 -0
- package/skills/optimize-scene/SKILL.md +7 -2
- package/skills/particle-system/SKILL.md +222 -0
- package/skills/player-avatar/SKILL.md +133 -7
- package/skills/player-physics/SKILL.md +93 -0
- package/skills/scene-runtime/SKILL.md +9 -5
- package/skills/visual-feedback/SKILL.md +1 -0
- package/extensions/dcl-setup-ollama.ts +0 -312
|
@@ -5,6 +5,11 @@ description: Add sound effects, music, audio streaming, and video players to Dec
|
|
|
5
5
|
|
|
6
6
|
# Audio and Video in Decentraland
|
|
7
7
|
|
|
8
|
+
## Authoring split
|
|
9
|
+
|
|
10
|
+
- **`AudioSource`** (local audio files), **`AudioStream`** (streaming URLs), and **`VideoPlayer`** are all supported in `main-entities.ts` — declare the speaker / radio / screen entity fully there with the streaming/playback config.
|
|
11
|
+
- Volume / play / pause toggles at runtime happen in `src/index.ts` via `getMutable`.
|
|
12
|
+
|
|
8
13
|
## When to Use Which Media Component
|
|
9
14
|
|
|
10
15
|
| Need | Component | Key Difference |
|
|
@@ -20,22 +25,26 @@ description: Add sound effects, music, audio streaming, and video players to Dec
|
|
|
20
25
|
|
|
21
26
|
## Audio Source (Sound Effects & Music)
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
Declare the speaker in `main-entities.ts`:
|
|
24
29
|
|
|
25
30
|
```typescript
|
|
26
|
-
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
// main-entities.ts
|
|
32
|
+
import type { Scene } from '@dcl/sdk/scene-types'
|
|
33
|
+
|
|
34
|
+
export const scene = {
|
|
35
|
+
speaker: {
|
|
36
|
+
components: {
|
|
37
|
+
Transform: { position: { x: 8, y: 1, z: 8 } },
|
|
38
|
+
AudioSource: {
|
|
39
|
+
audioClipUrl: 'sounds/music.mp3',
|
|
40
|
+
playing: true,
|
|
41
|
+
loop: true,
|
|
42
|
+
volume: 0.5, // 0 to 1
|
|
43
|
+
pitch: 1.0 // Playback speed (0.5 = half speed, 2.0 = double)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} satisfies Scene
|
|
39
48
|
```
|
|
40
49
|
|
|
41
50
|
### Supported Formats
|
|
@@ -43,6 +52,26 @@ AudioSource.create(speaker, {
|
|
|
43
52
|
- `.ogg`
|
|
44
53
|
- `.wav`
|
|
45
54
|
|
|
55
|
+
### Spatial vs Non-Spatial Audio
|
|
56
|
+
|
|
57
|
+
`AudioSource` defaults to spatial (volume falls off with distance). For background music / radio / non-positional sound effects, set `global: true`:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// main-entities.ts
|
|
61
|
+
bg_music: {
|
|
62
|
+
components: {
|
|
63
|
+
Transform: { position: { x: 0, y: 0, z: 0 } }, // ignored when global
|
|
64
|
+
AudioSource: {
|
|
65
|
+
audioClipUrl: 'sounds/bg.mp3',
|
|
66
|
+
playing: true,
|
|
67
|
+
loop: true,
|
|
68
|
+
volume: 0.5,
|
|
69
|
+
global: true // heard everywhere in the scene at constant volume
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
46
75
|
### File Organization
|
|
47
76
|
```
|
|
48
77
|
project/
|
|
@@ -55,97 +84,127 @@ project/
|
|
|
55
84
|
└── scene.json
|
|
56
85
|
```
|
|
57
86
|
|
|
58
|
-
### Play/Stop/Toggle
|
|
87
|
+
### Play/Stop/Toggle (runtime, in `src/index.ts`)
|
|
59
88
|
```typescript
|
|
60
|
-
|
|
61
|
-
|
|
89
|
+
import { engine, AudioSource } from '@dcl/sdk/ecs'
|
|
90
|
+
|
|
91
|
+
export function main() {
|
|
92
|
+
const speaker = engine.getEntityOrNullByName('speaker')
|
|
93
|
+
if (!speaker) return
|
|
62
94
|
|
|
63
|
-
//
|
|
64
|
-
AudioSource.getMutable(speaker).playing = false
|
|
95
|
+
AudioSource.getMutable(speaker).playing = true // play
|
|
96
|
+
AudioSource.getMutable(speaker).playing = false // stop
|
|
65
97
|
|
|
66
|
-
//
|
|
67
|
-
const audio = AudioSource.getMutable(speaker)
|
|
68
|
-
audio.playing = !audio.playing
|
|
98
|
+
// toggle
|
|
99
|
+
const audio = AudioSource.getMutable(speaker)
|
|
100
|
+
audio.playing = !audio.playing
|
|
101
|
+
}
|
|
69
102
|
```
|
|
70
103
|
|
|
71
104
|
### Play on Click
|
|
72
|
-
```typescript
|
|
73
|
-
import { pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'
|
|
74
|
-
|
|
75
|
-
const button = engine.addEntity()
|
|
76
|
-
// ... set up transform and mesh ...
|
|
77
|
-
|
|
78
|
-
const audioEntity = engine.addEntity()
|
|
79
|
-
Transform.create(audioEntity, { position: Vector3.create(8, 1, 8) })
|
|
80
|
-
AudioSource.create(audioEntity, {
|
|
81
|
-
audioClipUrl: 'sounds/click.mp3',
|
|
82
|
-
playing: false,
|
|
83
|
-
loop: false,
|
|
84
|
-
volume: 0.8
|
|
85
|
-
})
|
|
86
105
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
Static entities (the button mesh and the click-sfx speaker) go in `main-entities.ts`. `PointerEvents` and the click handler are runtime — they live in `src/index.ts`.
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// main-entities.ts
|
|
110
|
+
sfx_button: {
|
|
111
|
+
components: {
|
|
112
|
+
Transform: { position: { x: 8, y: 1, z: 8 } },
|
|
113
|
+
MeshRenderer: { mesh: { $case: 'box', box: { uvs: [] } } }
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
click_sfx: {
|
|
117
|
+
components: {
|
|
118
|
+
Transform: { position: { x: 8, y: 1, z: 8 } },
|
|
119
|
+
AudioSource: {
|
|
120
|
+
audioClipUrl: 'sounds/click.mp3',
|
|
121
|
+
playing: false,
|
|
122
|
+
loop: false,
|
|
123
|
+
volume: 0.8
|
|
124
|
+
}
|
|
94
125
|
}
|
|
95
|
-
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// src/index.ts
|
|
131
|
+
import { engine, AudioSource, pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'
|
|
132
|
+
|
|
133
|
+
export function main() {
|
|
134
|
+
const button = engine.getEntityOrNullByName('sfx_button')
|
|
135
|
+
const sfx = engine.getEntityOrNullByName('click_sfx')
|
|
136
|
+
if (!button || !sfx) return
|
|
137
|
+
|
|
138
|
+
pointerEventsSystem.onPointerDown(
|
|
139
|
+
{ entity: button, opts: { button: InputAction.IA_POINTER, hoverText: 'Play sound' } },
|
|
140
|
+
() => {
|
|
141
|
+
// Reset and play
|
|
142
|
+
const audio = AudioSource.getMutable(sfx)
|
|
143
|
+
audio.playing = false
|
|
144
|
+
audio.playing = true
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
}
|
|
96
148
|
```
|
|
97
149
|
|
|
98
150
|
## Audio Streaming
|
|
99
151
|
|
|
100
|
-
|
|
152
|
+
`AudioStream` is supported in `main-entities.ts` — declare the radio entity with its streaming config in one place:
|
|
101
153
|
|
|
102
154
|
```typescript
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
155
|
+
// main-entities.ts
|
|
156
|
+
radio: {
|
|
157
|
+
components: {
|
|
158
|
+
Transform: { position: { x: 8, y: 1, z: 8 } },
|
|
159
|
+
GltfContainer: { src: 'models/radio.glb' },
|
|
160
|
+
AudioStream: {
|
|
161
|
+
url: 'https://example.com/stream.mp3',
|
|
162
|
+
playing: true,
|
|
163
|
+
volume: 0.3
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
114
167
|
```
|
|
115
168
|
|
|
169
|
+
Toggling play / volume at runtime is the same `getMutable` pattern as `AudioSource`.
|
|
170
|
+
|
|
116
171
|
## Video Player
|
|
117
172
|
|
|
118
|
-
|
|
173
|
+
`VideoPlayer`, `MeshRenderer`, and the screen Transform all go in `main-entities.ts`. The video **texture binding** in `Material` needs a runtime Entity ID, not a name — the build only resolves `Transform.parent` by name. So `Material` is set at runtime in `src/index.ts`:
|
|
119
174
|
|
|
120
175
|
```typescript
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
})
|
|
176
|
+
// main-entities.ts
|
|
177
|
+
video_screen: {
|
|
178
|
+
components: {
|
|
179
|
+
Transform: {
|
|
180
|
+
position: { x: 8, y: 3, z: 15.9 },
|
|
181
|
+
scale: { x: 8, y: 4.5, z: 1 } // 16:9 ratio
|
|
182
|
+
},
|
|
183
|
+
MeshRenderer: { mesh: { $case: 'plane', plane: { uvs: [] } } },
|
|
184
|
+
VideoPlayer: {
|
|
185
|
+
src: 'https://example.com/video.mp4',
|
|
186
|
+
playing: true,
|
|
187
|
+
loop: true,
|
|
188
|
+
volume: 0.5,
|
|
189
|
+
playbackRate: 1.0,
|
|
190
|
+
position: 0 // start time in seconds
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
141
195
|
|
|
142
|
-
|
|
143
|
-
|
|
196
|
+
```typescript
|
|
197
|
+
// src/index.ts
|
|
198
|
+
import { engine, Material } from '@dcl/sdk/ecs'
|
|
144
199
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
200
|
+
export function main() {
|
|
201
|
+
const screen = engine.getEntityOrNullByName('video_screen')
|
|
202
|
+
if (!screen) return
|
|
203
|
+
|
|
204
|
+
const videoTexture = Material.Texture.Video({ videoPlayerEntity: screen })
|
|
205
|
+
// Basic material — better performance than PBR for video surfaces
|
|
206
|
+
Material.setBasicMaterial(screen, { texture: videoTexture })
|
|
207
|
+
}
|
|
149
208
|
```
|
|
150
209
|
|
|
151
210
|
### Video Controls
|
|
@@ -182,6 +241,48 @@ Material.setPbrMaterial(screen, {
|
|
|
182
241
|
})
|
|
183
242
|
```
|
|
184
243
|
|
|
244
|
+
### Video on a GLTF Surface (Curved Screens, TVs, Monitors)
|
|
245
|
+
|
|
246
|
+
When the "screen" is part of a model (a TV in a living room scene, a curved arena display), keep the GLTF and override its screen material with the video texture via `GltfNodeModifiers` at runtime:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
// main-entities.ts — declare the TV model
|
|
250
|
+
tv: {
|
|
251
|
+
components: {
|
|
252
|
+
Transform: { position: { x: 8, y: 1.5, z: 8 } },
|
|
253
|
+
GltfContainer: { src: 'models/tv.glb' },
|
|
254
|
+
VideoPlayer: { src: 'https://example.com/show.mp4', playing: true, loop: true }
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// src/index.ts — bind the video texture to the screen sub-mesh by path
|
|
261
|
+
import { engine, Material, GltfNodeModifiers } from '@dcl/sdk/ecs'
|
|
262
|
+
|
|
263
|
+
export function main() {
|
|
264
|
+
const tv = engine.getEntityOrNullByName('tv')
|
|
265
|
+
if (!tv) return
|
|
266
|
+
|
|
267
|
+
const videoTexture = Material.Texture.Video({ videoPlayerEntity: tv })
|
|
268
|
+
GltfNodeModifiers.createOrReplace(tv, {
|
|
269
|
+
modifiers: [
|
|
270
|
+
{
|
|
271
|
+
path: 'TV/Screen', // GLTF node path to the screen sub-mesh
|
|
272
|
+
material: {
|
|
273
|
+
material: {
|
|
274
|
+
$case: 'unlit',
|
|
275
|
+
unlit: { texture: videoTexture }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
]
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Use `path: ''` (empty) to apply the video material to every node of the model — useful when the whole model is the screen (e.g., a flat billboard mesh exported from Blender).
|
|
285
|
+
|
|
185
286
|
### Video Events
|
|
186
287
|
|
|
187
288
|
Monitor video playback state:
|
package/skills/build-ui/SKILL.md
CHANGED
|
@@ -38,7 +38,11 @@ const MyUI = () => (
|
|
|
38
38
|
)
|
|
39
39
|
|
|
40
40
|
export function setupUi() {
|
|
41
|
-
|
|
41
|
+
// ALWAYS pass virtualWidth + virtualHeight — the renderer scales the layout
|
|
42
|
+
// to fit the player's window using these as the reference. Without them,
|
|
43
|
+
// sizes are interpreted in raw pixels and won't behave consistently across
|
|
44
|
+
// resolutions and aspect ratios.
|
|
45
|
+
ReactEcsRenderer.setUiRenderer(MyUI, { virtualWidth: 1920, virtualHeight: 1080 })
|
|
42
46
|
}
|
|
43
47
|
```
|
|
44
48
|
|
|
@@ -304,6 +308,25 @@ The `Dropdown` component supports additional props:
|
|
|
304
308
|
/>
|
|
305
309
|
```
|
|
306
310
|
|
|
311
|
+
### Multiple UI Modules (`addUiRenderer` / `removeUiRenderer`)
|
|
312
|
+
|
|
313
|
+
If you have several independent UI modules — e.g., a HUD, a dialog system, a debug overlay — combine them under a single root *or* use `addUiRenderer` to mount each module against an **owner entity**. When the owner entity is deleted, the UI renderer is removed automatically.
|
|
314
|
+
|
|
315
|
+
```tsx
|
|
316
|
+
import { engine, ReactEcsRenderer } from '@dcl/sdk/react-ecs'
|
|
317
|
+
|
|
318
|
+
const hudOwner = engine.addEntity()
|
|
319
|
+
ReactEcsRenderer.addUiRenderer(hudOwner, () => <HudOverlay />)
|
|
320
|
+
|
|
321
|
+
// later, when the HUD should disappear:
|
|
322
|
+
engine.removeEntity(hudOwner) // also removes the UI renderer
|
|
323
|
+
|
|
324
|
+
// or explicitly:
|
|
325
|
+
ReactEcsRenderer.removeUiRenderer(hudOwner)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Each `addUiRenderer` mount renders independently. Useful for dynamic UIs that should appear/disappear based on game state without manually conditioning every sub-tree of one giant root component.
|
|
329
|
+
|
|
307
330
|
## Troubleshooting
|
|
308
331
|
|
|
309
332
|
| Problem | Cause | Solution |
|
|
@@ -312,7 +335,7 @@ The `Dropdown` component supports additional props:
|
|
|
312
335
|
| UI elements overlapping | Missing `flexDirection` or wrong layout | Set `flexDirection: 'column'` on the parent container |
|
|
313
336
|
| Button clicks not registering | Missing `onMouseDown` handler | Add `onMouseDown={() => { ... }}` to the Button or UiEntity |
|
|
314
337
|
| JSX errors at compile time | File extension is `.ts` instead of `.tsx` | Rename the file to `.tsx` |
|
|
315
|
-
| Multiple UIs fighting | More than one `setUiRenderer` call |
|
|
338
|
+
| Multiple UIs fighting | More than one `setUiRenderer` call | Use ONE `setUiRenderer` for the main UI; for independent modules use `addUiRenderer(ownerEntity, ...)` instead |
|
|
316
339
|
| Text not visible | Text color matches background | Set contrasting `color` on Label or `uiText` |
|
|
317
340
|
|
|
318
341
|
> **World interactions instead of screen UI?** See the **add-interactivity** skill for click handlers and pointer events on 3D objects.
|
|
@@ -5,6 +5,59 @@ description: Control camera behavior in Decentraland scenes. CameraMode detectio
|
|
|
5
5
|
|
|
6
6
|
# Camera Control in Decentraland
|
|
7
7
|
|
|
8
|
+
## Authoring split
|
|
9
|
+
|
|
10
|
+
`CameraModeArea` and `VirtualCamera` are supported in `main-entities.ts` — both static-by-nature components belong there. `MainCamera` is NOT supported because it lives on the reserved `engine.CameraEntity`; activate a virtual camera at runtime in `src/index.ts`.
|
|
11
|
+
|
|
12
|
+
`VirtualCamera.lookAtEntity` accepts an entity **name** in `main-entities.ts` (resolved to an Entity ID at build time, same as `Transform.parent`).
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// main-entities.ts
|
|
16
|
+
cinematic_cam: {
|
|
17
|
+
components: {
|
|
18
|
+
Transform: {
|
|
19
|
+
position: { x: 12, y: 4, z: 8 },
|
|
20
|
+
rotation: { x: 0, y: 0.7071, z: 0, w: 0.7071 }
|
|
21
|
+
},
|
|
22
|
+
VirtualCamera: {
|
|
23
|
+
defaultTransition: { transitionMode: { $case: 'time', time: 2 } },
|
|
24
|
+
lookAtEntity: 'shopkeeper' // name of another entity in this file
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
first_person_zone: {
|
|
29
|
+
components: {
|
|
30
|
+
Transform: { position: { x: 8, y: 1, z: 8 } },
|
|
31
|
+
CameraModeArea: {
|
|
32
|
+
area: { x: 16, y: 2, z: 16 },
|
|
33
|
+
mode: 0 // CameraType.CT_FIRST_PERSON
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Activate the cinematic camera at runtime:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// src/index.ts
|
|
43
|
+
import { engine, MainCamera } from '@dcl/sdk/ecs'
|
|
44
|
+
|
|
45
|
+
export function main() {
|
|
46
|
+
const cam = engine.getEntityOrNullByName('cinematic_cam')
|
|
47
|
+
if (cam) MainCamera.createOrReplace(engine.CameraEntity, { virtualCameraEntity: cam })
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The reserved `engine.CameraEntity` and `engine.PlayerEntity` are engine-managed and have no representation in `main-entities.ts`.
|
|
52
|
+
|
|
53
|
+
### CameraType values for `CameraModeArea.mode`
|
|
54
|
+
|
|
55
|
+
| value | enum | meaning |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| 0 | CT_FIRST_PERSON | Force first-person inside the zone |
|
|
58
|
+
| 1 | CT_THIRD_PERSON | Force third-person inside the zone |
|
|
59
|
+
| 2 | CT_CINEMATIC | Force cinematic camera inside the zone |
|
|
60
|
+
|
|
8
61
|
## Reading Camera State
|
|
9
62
|
|
|
10
63
|
Access the camera's current position and rotation via the reserved `engine.CameraEntity`:
|
|
@@ -199,12 +252,30 @@ engine.addSystem(followNpcCamera)
|
|
|
199
252
|
|
|
200
253
|
> **Freezing player during cutscenes?** Combine VirtualCamera with `InputModifier` from the **advanced-input** skill to prevent player movement during cinematic sequences.
|
|
201
254
|
|
|
255
|
+
## Camera vs Colliders (preventing camera-through-wall)
|
|
256
|
+
|
|
257
|
+
In third-person mode the player's camera can slide through walls if the wall's collider mask doesn't include `CL_POINTER`. The camera uses the **pointer** collider mask for occlusion checks — not `CL_PHYSICS`. Set both masks on walls and architecture that the camera should bounce off:
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// main-entities.ts
|
|
261
|
+
wall: {
|
|
262
|
+
components: {
|
|
263
|
+
Transform: { position: { x: 8, y: 1.5, z: 16 } },
|
|
264
|
+
GltfContainer: {
|
|
265
|
+
src: 'models/wall.glb',
|
|
266
|
+
visibleMeshesCollisionMask: 3 // CL_PHYSICS | CL_POINTER
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
For GLBs that ship with invisible collider meshes (Creator Hub asset packs), set `invisibleMeshesCollisionMask: 3` instead. Default of `CL_PHYSICS` only would let the camera pass through.
|
|
273
|
+
|
|
202
274
|
## Best Practices
|
|
203
275
|
|
|
204
|
-
- Only one VirtualCamera should be active at a time
|
|
205
|
-
- Use `CameraModeArea` to force first-person in tight indoor spaces
|
|
206
|
-
- Keep transition speeds between 0.5 and 3.0 for comfortable camera movement
|
|
207
|
-
-
|
|
208
|
-
-
|
|
209
|
-
-
|
|
210
|
-
- Camera control is read-only outside of VirtualCamera and CameraModeArea — you cannot directly move the player's camera
|
|
276
|
+
- Only one VirtualCamera should be active at a time.
|
|
277
|
+
- Use `CameraModeArea` to force first-person in tight indoor spaces.
|
|
278
|
+
- Keep transition speeds between 0.5 and 3.0 for comfortable camera movement.
|
|
279
|
+
- Read camera state via `engine.CameraEntity` — never try to write to it directly.
|
|
280
|
+
- For look-at detection, combine camera position with raycasting (see `add-interactivity` skill).
|
|
281
|
+
- Camera control is read-only outside of VirtualCamera and CameraModeArea — you cannot directly move the player's camera.
|
|
@@ -52,26 +52,68 @@ Update the `display` fields and parcels:
|
|
|
52
52
|
- `scene.parcels` — for multi-parcel scenes, list all parcels (e.g., `["0,0", "0,1", "1,0", "1,1"]` for 2x2)
|
|
53
53
|
- `scene.base` — set to the southwest corner parcel
|
|
54
54
|
|
|
55
|
-
### src/index.ts
|
|
56
|
-
|
|
55
|
+
### `main-entities.ts` + `src/index.ts`
|
|
56
|
+
|
|
57
|
+
OpenDCL scenes use a **two-file authoring model**:
|
|
58
|
+
|
|
59
|
+
- `main-entities.ts` (scene root) — typed declarative entities + their data components, keyed by Name. Compiled to `main.crdt` at build time and preloaded by the engine before `main()` runs.
|
|
60
|
+
- `src/index.ts` — behavior only. References entities by `Name` via `engine.getEntityOrNullByName<EntityName>(name)` and attaches systems, pointer events, tweens, etc.
|
|
61
|
+
|
|
62
|
+
**Example `main-entities.ts`:**
|
|
57
63
|
|
|
58
64
|
```typescript
|
|
59
|
-
import {
|
|
60
|
-
|
|
65
|
+
import type { Scene } from '@dcl/sdk/scene-types'
|
|
66
|
+
|
|
67
|
+
export const scene = {
|
|
68
|
+
blue_cube: {
|
|
69
|
+
components: {
|
|
70
|
+
Transform: { position: { x: 8, y: 1, z: 8 }, rotation: { x: 0, y: 0, z: 0, w: 1 }, scale: { x: 1, y: 1, z: 1 } },
|
|
71
|
+
MeshRenderer: { mesh: { $case: 'box', box: { uvs: [] } } },
|
|
72
|
+
Material: {
|
|
73
|
+
material: { $case: 'pbr', pbr: { albedoColor: { r: 0.2, g: 0.5, b: 1, a: 1 } } },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
} satisfies Scene
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The `satisfies Scene` clause keeps the literal keys typed (so `keyof typeof scene` gives the typed entity-name union), while still validating the shape against the `Scene` schema.
|
|
81
|
+
|
|
82
|
+
**Example `src/index.ts`:**
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { engine, pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'
|
|
86
|
+
import type { scene } from '../main-entities'
|
|
87
|
+
|
|
88
|
+
type EntityName = keyof typeof scene
|
|
61
89
|
|
|
62
90
|
export function main() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
91
|
+
const cube = engine.getEntityOrNullByName<EntityName>('blue_cube')
|
|
92
|
+
if (cube === null) return
|
|
93
|
+
|
|
94
|
+
pointerEventsSystem.onPointerDown(
|
|
95
|
+
{ entity: cube, opts: { button: InputAction.IA_POINTER, hoverText: 'Click me' } },
|
|
96
|
+
() => console.log('clicked'),
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`tsconfig.json` should include `main-entities.ts` so it gets type-checked:
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"extends": "@dcl/sdk/types/tsconfig.ecs7.json",
|
|
105
|
+
"include": ["src/**/*.ts", "src/**/*.tsx", "main-entities.ts"]
|
|
72
106
|
}
|
|
73
107
|
```
|
|
74
108
|
|
|
109
|
+
**Rules:**
|
|
110
|
+
|
|
111
|
+
- Every editable / declared entity must have a unique Name in `main-entities.ts`.
|
|
112
|
+
- Dynamic entities created at runtime (effects, projectiles, dynamic UI markers) use `engine.addEntity()` directly. **Don't give dynamic entities a Name** — they don't go in `main-entities.ts`.
|
|
113
|
+
- Anything that's pure data (Transform, GltfContainer, MeshRenderer, MeshCollider, Material, AudioSource, VideoPlayer, TextShape, Animator config, NftShape, Billboard, VisibilityComponent) goes in `main-entities.ts`.
|
|
114
|
+
- Anything that's behavior (pointer callbacks, systems, tween triggers, conditional logic) goes in `src/`.
|
|
115
|
+
- The `scene` literal must be JSON-compatible — no function calls, no spreads, no comments inside the object.
|
|
116
|
+
|
|
75
117
|
### scene.json Reference
|
|
76
118
|
|
|
77
119
|
All valid `scene.json` fields:
|
|
@@ -147,3 +189,4 @@ After customizing the files:
|
|
|
147
189
|
- Y axis is up, minimum Y=0 (ground)
|
|
148
190
|
- The `main` field in scene.json MUST be `"bin/index.js"` — this is the compiled output path
|
|
149
191
|
- The `jsx` and `jsxImportSource` tsconfig settings are already included by `/init` — do not modify them
|
|
192
|
+
- **Never pass `undefined` values in Transform fields** (position, rotation, scale) — the SDK serializer crashes. If a field is optional, omit the key entirely instead of including it with an `undefined` value.
|
|
@@ -77,6 +77,18 @@ npx @dcl/sdk-commands deploy
|
|
|
77
77
|
}
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
### Scene Tipping (`creator`)
|
|
81
|
+
|
|
82
|
+
Let visitors send MANA tips to the scene creator. Add a `creator` field with the recipient's wallet address:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"creator": "0x1234567890123456789012345678901234567890"
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
When set, a piggy-bank icon appears in the top-left for visitors. Clicking it opens a MANA tip modal. If the address is linked to a Decentraland NAME, the name is shown in the modal. Creators receive an in-app notification for each tip. Also configurable via Creator Hub → scene Settings → Details → Creator wallet address.
|
|
91
|
+
|
|
80
92
|
### Spawn Points
|
|
81
93
|
|
|
82
94
|
Configure where players appear when entering the scene:
|
|
@@ -44,6 +44,41 @@ All Worlds are automatically listed on the [Places page](https://places.decentra
|
|
|
44
44
|
}
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
### Skybox + Minimap configuration
|
|
48
|
+
|
|
49
|
+
`worldConfiguration` accepts extra fields that aren't available for Genesis City scenes:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"worldConfiguration": {
|
|
54
|
+
"name": "my-name.dcl.eth",
|
|
55
|
+
"skyboxConfig": {
|
|
56
|
+
"fixedTime": 36000,
|
|
57
|
+
"textures": ["textures/skybox.png"]
|
|
58
|
+
},
|
|
59
|
+
"miniMapConfig": {
|
|
60
|
+
"visible": true,
|
|
61
|
+
"dataImage": "images/minimap.png",
|
|
62
|
+
"estateImage": "images/estate.png"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`skyboxConfig.fixedTime`:
|
|
69
|
+
|
|
70
|
+
| Value | Time of day |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `0` | Midnight |
|
|
73
|
+
| `18000` | 6 AM (sunrise) |
|
|
74
|
+
| `36000` | Noon |
|
|
75
|
+
| `45000` | 6 PM (sunset) |
|
|
76
|
+
| `50400` | Maximum |
|
|
77
|
+
|
|
78
|
+
Omit `fixedTime` for a dynamic day/night cycle. `textures` is an array of cubemap face textures (top/bottom/front/back/left/right) when you want a fully custom sky.
|
|
79
|
+
|
|
80
|
+
`miniMapConfig`: set `visible: true` to show the minimap inside the World; `dataImage` is the base tile, `estateImage` overlays estate boundaries.
|
|
81
|
+
|
|
47
82
|
## 2. Deploy
|
|
48
83
|
|
|
49
84
|
**Use the `/deploy` command** — it auto-detects the `worldConfiguration` in scene.json and deploys to the Worlds content server automatically.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Only track the skill doc and editor module.
|
|
2
|
+
# All test-scene scaffolds (scene.json, package.json, src/index.ts, main.json,
|
|
3
|
+
# models/, .vscode/, etc.) are auto-ignored by the *-then-allowlist below.
|
|
4
|
+
*
|
|
5
|
+
!.gitignore
|
|
6
|
+
!SKILL.md
|
|
7
|
+
!src/
|
|
8
|
+
!src/__editor/
|
|
9
|
+
!src/__editor/**
|
|
10
|
+
node_modules/
|
|
11
|
+
bin/
|