@dcl-regenesislabs/opendcl 0.1.0-22234509684.commit-63dfd19

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.
Files changed (54) hide show
  1. package/README.md +234 -0
  2. package/context/components-reference.md +113 -0
  3. package/context/open-source-3d-assets.md +705 -0
  4. package/context/sdk7-complete-reference.md +3684 -0
  5. package/context/sdk7-examples.md +1709 -0
  6. package/dist/index.d.ts +8 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +76 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/scene-context.d.ts +97 -0
  11. package/dist/scene-context.d.ts.map +1 -0
  12. package/dist/scene-context.js +203 -0
  13. package/dist/scene-context.js.map +1 -0
  14. package/dist/utils.d.ts +2 -0
  15. package/dist/utils.d.ts.map +1 -0
  16. package/dist/utils.js +4 -0
  17. package/dist/utils.js.map +1 -0
  18. package/extensions/dcl-context.ts +123 -0
  19. package/extensions/dcl-deploy.ts +89 -0
  20. package/extensions/dcl-header.ts +162 -0
  21. package/extensions/dcl-init.ts +62 -0
  22. package/extensions/dcl-preview.ts +144 -0
  23. package/extensions/dcl-setup-ollama.ts +312 -0
  24. package/extensions/dcl-setup.ts +96 -0
  25. package/extensions/dcl-status.ts +88 -0
  26. package/extensions/dcl-tasks.ts +102 -0
  27. package/extensions/dcl-update-check.ts +79 -0
  28. package/extensions/dcl-validate.ts +80 -0
  29. package/extensions/plan-mode/index.ts +340 -0
  30. package/extensions/plan-mode/utils.ts +168 -0
  31. package/extensions/process-registry.ts +25 -0
  32. package/extensions/scene-utils.ts +31 -0
  33. package/package.json +65 -0
  34. package/prompts/explain.md +16 -0
  35. package/prompts/review.md +19 -0
  36. package/prompts/system.md +126 -0
  37. package/skills/add-3d-models/SKILL.md +115 -0
  38. package/skills/add-interactivity/SKILL.md +176 -0
  39. package/skills/advanced-input/SKILL.md +238 -0
  40. package/skills/advanced-rendering/SKILL.md +235 -0
  41. package/skills/animations-tweens/SKILL.md +173 -0
  42. package/skills/audio-video/SKILL.md +167 -0
  43. package/skills/authoritative-server/SKILL.md +329 -0
  44. package/skills/build-ui/SKILL.md +231 -0
  45. package/skills/camera-control/SKILL.md +199 -0
  46. package/skills/create-scene/SKILL.md +67 -0
  47. package/skills/deploy-scene/SKILL.md +106 -0
  48. package/skills/deploy-worlds/SKILL.md +107 -0
  49. package/skills/lighting-environment/SKILL.md +216 -0
  50. package/skills/multiplayer-sync/SKILL.md +132 -0
  51. package/skills/nft-blockchain/SKILL.md +246 -0
  52. package/skills/optimize-scene/SKILL.md +160 -0
  53. package/skills/player-avatar/SKILL.md +239 -0
  54. package/skills/smart-items/SKILL.md +181 -0
@@ -0,0 +1,3684 @@
1
+ # Decentraland SDK7 Complete Reference Guide
2
+
3
+ ## Table of Contents
4
+ 1. [Installation & Setup](#installation--setup)
5
+ 2. [Getting Started](#getting-started)
6
+ 3. [Architecture & Core Concepts](#architecture--core-concepts)
7
+ 4. [3D Essentials](#3d-essentials)
8
+ 5. [Interactivity](#interactivity)
9
+ 6. [2D UI](#2d-ui)
10
+ 7. [Blockchain Integration](#blockchain-integration)
11
+ 8. [Media](#media)
12
+ 9. [Networking](#networking)
13
+ 10. [Libraries](#libraries)
14
+ 11. [Debugging](#debugging)
15
+ 12. [Programming Patterns](#programming-patterns)
16
+ 13. [Projects](#projects)
17
+ 14. [Publishing](#publishing)
18
+ 15. [Optimization](#optimization)
19
+ 16. [Design & Experience](#design--experience)
20
+ 17. [Web Editor](#web-editor)
21
+ 18. [Advanced Topics](#advanced-topics)
22
+
23
+ ---
24
+
25
+ ## Installation & Setup
26
+
27
+ ### Creator Hub Installation
28
+ The Creator Hub is a standalone application for building Decentraland scenes with drag-and-drop interface.
29
+
30
+ Download: [https://decentraland.org/download/creator-hub](https://decentraland.org/download/creator-hub)
31
+
32
+ ### Code Editor Setup
33
+ Install Visual Studio Code: [https://code.visualstudio.com/](https://code.visualstudio.com/)
34
+ Alternative: Cursor AI: [https://www.cursor.com/](https://www.cursor.com/)
35
+
36
+ ### CLI Installation
37
+ ```bash
38
+ npm install -g @dcl/sdk-commands
39
+ ```
40
+
41
+ ### Creating a New Scene
42
+ ```bash
43
+ npx @dcl/sdk-commands init
44
+ ```
45
+
46
+ ### Basic Imports
47
+ ```typescript
48
+ import { engine } from '@dcl/sdk/ecs'
49
+ import { Transform, GltfContainer, MeshRenderer, Material } from '@dcl/sdk/ecs'
50
+ import { Vector3, Quaternion, Color4 } from '@dcl/sdk/math'
51
+ import { ReactEcsRenderer } from '@dcl/sdk/react-ecs'
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Getting Started
57
+
58
+ ### SDK Quick Start
59
+
60
+ #### Basic Scene Structure
61
+ ```typescript
62
+ export function main() {
63
+ // Create entity
64
+ const cube = engine.addEntity()
65
+
66
+ // Add transform component
67
+ Transform.create(cube, {
68
+ position: Vector3.create(8, 1, 8),
69
+ rotation: Quaternion.Zero(),
70
+ scale: Vector3.create(1, 1, 1)
71
+ })
72
+
73
+ // Add shape component
74
+ MeshRenderer.setBox(cube)
75
+ }
76
+ ```
77
+
78
+ #### Adding Materials
79
+ ```typescript
80
+ // Create material
81
+ Material.setPbrMaterial(cube, {
82
+ albedoColor: Color4.Red(),
83
+ metallic: 0.8,
84
+ roughness: 0.1
85
+ })
86
+ ```
87
+
88
+ #### Adding Interactivity
89
+ ```typescript
90
+ import { pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'
91
+
92
+ pointerEventsSystem.onPointerDown(
93
+ {
94
+ entity: cube,
95
+ opts: { button: InputAction.IA_POINTER, hoverText: 'Click me!' }
96
+ },
97
+ () => {
98
+ console.log('Cube clicked!')
99
+ }
100
+ )
101
+ ```
102
+
103
+ #### Adding Tweens
104
+ ```typescript
105
+ import { Tween, EasingFunction } from '@dcl/sdk/ecs'
106
+
107
+ Tween.create(cube, {
108
+ mode: Tween.Mode.Move({
109
+ start: Vector3.create(8, 1, 8),
110
+ end: Vector3.create(12, 1, 8)
111
+ }),
112
+ duration: 2000,
113
+ easingFunction: EasingFunction.EF_LINEAR
114
+ })
115
+ ```
116
+
117
+ ### Development Workflow
118
+
119
+ #### Preview Scene
120
+ ```bash
121
+ npm run start
122
+ ```
123
+
124
+ Preview options:
125
+ - `-- --web3`: Connect to browser wallet
126
+ - `-- --no-debug`: Disable debug panel
127
+ - `-- --explorer-alpha`: Use Decentraland Desktop client
128
+ - `-- --port <number>`: Specific port
129
+
130
+ #### Build Scene
131
+ ```bash
132
+ npm run build
133
+ ```
134
+
135
+ #### Deploy Scene
136
+ ```bash
137
+ npm run deploy
138
+ ```
139
+
140
+ ---
141
+
142
+ ## Architecture & Core Concepts
143
+
144
+ ### Entity-Component-System (ECS)
145
+
146
+ #### Entities
147
+ Entities are basic units - just IDs that group components together.
148
+
149
+ ```typescript
150
+ // Create entity
151
+ const myEntity = engine.addEntity()
152
+
153
+ // Remove entity
154
+ engine.removeEntity(myEntity)
155
+
156
+ // Remove entity with children
157
+ removeEntityWithChildren(engine, myEntity)
158
+ ```
159
+
160
+ #### Components
161
+ Components store data about entities.
162
+
163
+ ```typescript
164
+ // Create component
165
+ Transform.create(myEntity, {
166
+ position: Vector3.create(5, 1, 5)
167
+ })
168
+
169
+ // Get component (read-only)
170
+ const transform = Transform.get(myEntity)
171
+
172
+ // Get mutable component
173
+ const mutableTransform = Transform.getMutable(myEntity)
174
+ mutableTransform.position.x = 10
175
+
176
+ // Check if component exists
177
+ const hasTransform = Transform.has(myEntity)
178
+
179
+ // Remove component
180
+ Transform.deleteFrom(myEntity)
181
+
182
+ // Create or replace component
183
+ Transform.createOrReplace(myEntity, {
184
+ position: Vector3.create(3, 1, 3)
185
+ })
186
+ ```
187
+
188
+ #### Systems
189
+ Systems contain logic that runs every frame.
190
+
191
+ ```typescript
192
+ function mySystem(dt: number) {
193
+ // dt is delta time since last frame
194
+ for (const [entity] of engine.getEntitiesWith(Transform, MeshRenderer)) {
195
+ const transform = Transform.getMutable(entity)
196
+ transform.rotation = Quaternion.multiply(
197
+ transform.rotation,
198
+ Quaternion.fromAngleAxis(dt * 10, Vector3.Up())
199
+ )
200
+ }
201
+ }
202
+
203
+ // Add system to engine
204
+ engine.addSystem(mySystem)
205
+
206
+ // Add system with priority and name
207
+ engine.addSystem(mySystem, 1, "RotationSystem")
208
+
209
+ // Remove system
210
+ engine.removeSystem("RotationSystem")
211
+ ```
212
+
213
+ #### Querying Components
214
+ ```typescript
215
+ // Query entities with specific components
216
+ for (const [entity, transform, meshRenderer] of engine.getEntitiesWith(Transform, MeshRenderer)) {
217
+ // Process entities
218
+ }
219
+ ```
220
+
221
+ #### Custom Components
222
+ ```typescript
223
+ // Define custom component schema
224
+ const HealthSchema = {
225
+ current: Schemas.Number,
226
+ max: Schemas.Number
227
+ }
228
+
229
+ // Create component with default values
230
+ const defaultValues = {
231
+ current: 100,
232
+ max: 100
233
+ }
234
+
235
+ export const Health = engine.defineComponent('Health', HealthSchema, defaultValues)
236
+
237
+ // Use custom component
238
+ Health.create(player, { current: 100, max: 100 })
239
+
240
+ const health = Health.getMutable(player)
241
+ health.current -= 10
242
+
243
+ // Flag components (no data, just marking)
244
+ export const IsEnemyFlag = engine.defineComponent('isEnemyFlag', {})
245
+ IsEnemyFlag.create(enemy)
246
+
247
+ // Complex schema types
248
+ const ComplexSchema = {
249
+ simpleField: Schemas.Boolean,
250
+ numberList: Schemas.Array(Schemas.Int),
251
+ nestedObject: Schemas.Map({
252
+ nestedField1: Schemas.String,
253
+ nestedField2: Schemas.Vector3
254
+ }),
255
+ enumField: Schemas.EnumString<Color>(Color, Color.Red)
256
+ }
257
+
258
+ // Enum types
259
+ enum Color {
260
+ Red = 'red',
261
+ Green = 'green',
262
+ Blue = 'blue'
263
+ }
264
+
265
+ // OneOf types for interchangeable data
266
+ const FlexibleSchema = {
267
+ flexField: Schemas.OneOf({
268
+ vector: Schemas.Vector3,
269
+ quaternion: Schemas.Quaternion
270
+ })
271
+ }
272
+
273
+ // Usage with $case
274
+ MyComponent.create(entity, {
275
+ flexField: {
276
+ $case: 'vector',
277
+ value: Vector3.create(1, 2, 3)
278
+ }
279
+ })
280
+
281
+ // Subscribe to component changes
282
+ Health.onChange(playerEntity, (healthData) => {
283
+ if (!healthData) return
284
+ console.log('Health changed:', healthData.current)
285
+ })
286
+ ```
287
+
288
+ #### Entity Relationships
289
+ ```typescript
290
+ // Parent-child relationships
291
+ const parent = engine.addEntity()
292
+ const child = engine.addEntity()
293
+
294
+ Transform.create(child, {
295
+ position: Vector3.create(2, 0, 0),
296
+ parent: parent
297
+ })
298
+
299
+ // Get entity by name (from Scene Editor)
300
+ const door = engine.getEntityOrNullByName('door-1')
301
+ ```
302
+
303
+ #### Reserved Entities
304
+ - `engine.PlayerEntity`: Player's avatar
305
+ - `engine.CameraEntity`: Player's camera
306
+ - `engine.RootEntity`: Scene root
307
+
308
+ #### Component Change Detection
309
+ ```typescript
310
+ Transform.onChange(myEntity, (newTransform) => {
311
+ if (!newTransform) return
312
+ console.log('Transform changed:', newTransform.position)
313
+ })
314
+ ```
315
+
316
+ ---
317
+
318
+ ## 3D Essentials
319
+
320
+ ### Entity Positioning
321
+
322
+ #### Transform Component
323
+ ```typescript
324
+ Transform.create(entity, {
325
+ position: Vector3.create(8, 1, 8), // World position
326
+ rotation: Quaternion.fromEulerDegrees(0, 90, 0), // Rotation
327
+ scale: Vector3.create(2, 2, 2), // Scale
328
+ parent: parentEntity // Optional parent
329
+ })
330
+ ```
331
+
332
+ #### Position
333
+ - Measured in meters
334
+ - Scene coordinates: (0,0,0) is South-West corner at ground level
335
+ - Single parcel: 16m x 16m
336
+ - Scene center: (8, 0, 8) for single parcel
337
+
338
+ #### Rotation
339
+ ```typescript
340
+ // Using Euler angles (degrees)
341
+ const rotation = Quaternion.fromEulerDegrees(0, 90, 0)
342
+
343
+ // Using quaternion directly
344
+ const rotation = Quaternion.create(0, 0.707, 0, 0.707)
345
+
346
+ // Get Euler angles from quaternion
347
+ const eulerAngles = Quaternion.toEuler(rotation)
348
+ ```
349
+
350
+ #### Billboard Component
351
+ ```typescript
352
+ // Always face the player
353
+ Billboard.create(entity, {
354
+ billboardMode: BillboardMode.BM_Y // Only rotate on Y axis
355
+ })
356
+
357
+ // Billboard modes
358
+ BillboardMode.BM_ALL // Rotate on all axes
359
+ BillboardMode.BM_NONE // No rotation
360
+ BillboardMode.BM_X // Fixed X axis
361
+ BillboardMode.BM_Y // Fixed Y axis (most common)
362
+ BillboardMode.BM_Z // Fixed Z axis
363
+ ```
364
+
365
+ #### Face Target
366
+ ```typescript
367
+ function lookAt(entity: Entity, target: Vector3) {
368
+ const transform = Transform.getMutable(entity)
369
+ const direction = Vector3.subtract(target, transform.position)
370
+ const normalized = Vector3.normalize(direction)
371
+ transform.rotation = Quaternion.lookRotation(normalized)
372
+ }
373
+ ```
374
+
375
+ #### Avatar Attachment
376
+ ```typescript
377
+ // Attach to player
378
+ AvatarAttach.create(entity, {
379
+ anchorPointId: AvatarAnchorPointType.AAPT_RIGHT_HAND
380
+ })
381
+
382
+ // Attach to specific player
383
+ AvatarAttach.create(entity, {
384
+ avatarId: '0x123...abc',
385
+ anchorPointId: AvatarAnchorPointType.AAPT_NAME_TAG
386
+ })
387
+
388
+ // Available anchor points
389
+ AvatarAnchorPointType.AAPT_HEAD
390
+ AvatarAnchorPointType.AAPT_NECK
391
+ AvatarAnchorPointType.AAPT_LEFT_HAND
392
+ AvatarAnchorPointType.AAPT_RIGHT_HAND
393
+ AvatarAnchorPointType.AAPT_NAME_TAG
394
+ // ... many more
395
+ ```
396
+
397
+ ### Shape Components
398
+
399
+ #### Primitive Shapes
400
+ ```typescript
401
+ // Box
402
+ MeshRenderer.setBox(entity)
403
+
404
+ // Sphere
405
+ MeshRenderer.setSphere(entity)
406
+
407
+ // Plane
408
+ MeshRenderer.setPlane(entity)
409
+
410
+ // Cylinder
411
+ MeshRenderer.setCylinder(entity)
412
+
413
+ // Cone (cylinder with radiusTop = 0)
414
+ MeshRenderer.setCylinder(entity, 0, 1)
415
+ ```
416
+
417
+ #### 3D Models
418
+ ```typescript
419
+ GltfContainer.create(entity, {
420
+ src: 'models/house.glb'
421
+ })
422
+
423
+ // Check loading state
424
+ const loadingState = GltfContainerLoadingState.getOrNull(entity)
425
+ if (loadingState?.currentState === LoadingState.FINISHED) {
426
+ // Model loaded
427
+ }
428
+ ```
429
+
430
+ #### Visibility
431
+ ```typescript
432
+ // Make invisible
433
+ VisibilityComponent.create(entity, { visible: false })
434
+
435
+ // Toggle visibility
436
+ const visibility = VisibilityComponent.getMutable(entity)
437
+ visibility.visible = !visibility.visible
438
+ ```
439
+
440
+ #### UV Mapping
441
+ ```typescript
442
+ // Custom UV coordinates for plane
443
+ MeshRenderer.setPlane(entity, [
444
+ 0, 0.75, // Bottom-left
445
+ 0.25, 0.75, // Bottom-right
446
+ 0.25, 1, // Top-right
447
+ 0, 1 // Top-left
448
+ ])
449
+ ```
450
+
451
+ ### Materials
452
+
453
+ #### PBR Materials
454
+ ```typescript
455
+ Material.setPbrMaterial(entity, {
456
+ albedoColor: Color4.create(1, 0, 0, 1), // Red
457
+ metallic: 0.8,
458
+ roughness: 0.2,
459
+ emissiveColor: Color4.create(0, 1, 0, 1), // Green glow
460
+ transparencyMode: MaterialTransparencyMode.MTM_ALPHA_BLEND
461
+ })
462
+ ```
463
+
464
+ #### Basic Materials (Unlit)
465
+ ```typescript
466
+ Material.setBasicMaterial(entity, {
467
+ diffuseColor: Color4.Red()
468
+ })
469
+ ```
470
+
471
+ #### Textures
472
+ ```typescript
473
+ Material.setPbrMaterial(entity, {
474
+ texture: Material.Texture.Common({
475
+ src: 'assets/textures/wood.png',
476
+ filterMode: TextureFilterMode.TFM_BILINEAR,
477
+ wrapMode: TextureWrapMode.TWM_REPEAT
478
+ })
479
+ })
480
+ ```
481
+
482
+ #### Multi-layer Textures
483
+ ```typescript
484
+ Material.setPbrMaterial(entity, {
485
+ texture: Material.Texture.Common({ src: 'assets/diffuse.png' }),
486
+ bumpTexture: Material.Texture.Common({ src: 'assets/normal.png' }),
487
+ emissiveTexture: Material.Texture.Common({ src: 'assets/emissive.png' })
488
+ })
489
+ ```
490
+
491
+ #### Avatar Portraits
492
+ ```typescript
493
+ Material.setPbrMaterial(entity, {
494
+ texture: Material.Texture.Avatar({
495
+ userId: '0x123...abc'
496
+ })
497
+ })
498
+ ```
499
+
500
+ #### Texture Animation
501
+ ```typescript
502
+ // Animate texture offset
503
+ Tween.setTextureMove(entity,
504
+ Vector2.create(0, 0),
505
+ Vector2.create(1, 0),
506
+ 2000
507
+ )
508
+
509
+ // Loop texture animation
510
+ TweenSequence.create(entity, { sequence: [], loop: TweenLoop.TL_RESTART })
511
+ ```
512
+
513
+ #### Transparency
514
+ ```typescript
515
+ // Alpha blend transparency
516
+ Material.setPbrMaterial(entity, {
517
+ albedoColor: Color4.create(1, 0, 0, 0.5), // 50% transparent red
518
+ transparencyMode: MaterialTransparencyMode.MTM_ALPHA_BLEND
519
+ })
520
+
521
+ // Alpha test (cutout)
522
+ Material.setPbrMaterial(entity, {
523
+ texture: Material.Texture.Common({ src: 'assets/cutout.png' }),
524
+ transparencyMode: MaterialTransparencyMode.MTM_ALPHA_TEST,
525
+ alphaTest: 0.5
526
+ })
527
+ ```
528
+
529
+ #### Modify GLTF materials
530
+ ```typescript
531
+ import { GltfNodeModifiers, GltfContainer } from '@dcl/sdk/ecs'
532
+
533
+ // Override the material of an entire GLB
534
+ const model = engine.addEntity()
535
+ GltfContainer.create(model, { src: 'models/myModel.glb' })
536
+ Transform.create(model, { position: Vector3.create(4, 0, 4) })
537
+
538
+ GltfNodeModifiers.create(model, {
539
+ modifiers: [
540
+ {
541
+ path: '', // empty string = whole model
542
+ material: {
543
+ material: {
544
+ $case: 'pbr',
545
+ pbr: {
546
+ albedoColor: Color4.Red()
547
+ }
548
+ }
549
+ }
550
+ }
551
+ ]
552
+ })
553
+ ```
554
+ Tip: set `path` to a specific mesh node to target only that part; use `Material.Texture.Common({ src: '...' })` inside `pbr` to swap textures.
555
+
556
+ ### Move Entities
557
+
558
+ #### Tween helpers (concise syntax)
559
+ ```typescript
560
+ // Move between two points
561
+ Tween.setMove(entity,
562
+ Vector3.create(4, 1, 4),
563
+ Vector3.create(8, 1, 8),
564
+ 2000,
565
+ { faceDirection: false, easingFunction: EasingFunction.EF_LINEAR }
566
+ )
567
+
568
+ // Rotate between two rotations
569
+ Tween.setRotate(entity,
570
+ Quaternion.fromEulerDegrees(0, 0, 0),
571
+ Quaternion.fromEulerDegrees(0, 180, 0),
572
+ 700,
573
+ EasingFunction.EF_EASEOUTBOUNCE
574
+ )
575
+
576
+ // Scale between sizes
577
+ Tween.setScale(entity,
578
+ Vector3.create(1, 1, 1),
579
+ Vector3.create(4, 4, 4),
580
+ 2000,
581
+ EasingFunction.EF_LINEAR
582
+ )
583
+
584
+ // Continuous movement (meters/second)
585
+ Tween.setMoveContinuous(entity, Vector3.create(0, 0, 1), 0.7)
586
+
587
+ // Continuous rotation (degrees/second)
588
+ Tween.setRotateContinuous(entity, Quaternion.fromEulerDegrees(0, -1, 0), 700)
589
+ ```
590
+
591
+ #### Tween System
592
+ ```typescript
593
+ // Move between points
594
+ Tween.create(entity, {
595
+ mode: Tween.Mode.Move({
596
+ start: Vector3.create(4, 1, 4),
597
+ end: Vector3.create(8, 1, 8)
598
+ }),
599
+ duration: 2000,
600
+ easingFunction: EasingFunction.EF_LINEAR
601
+ })
602
+
603
+ // Rotate
604
+ Tween.create(entity, {
605
+ mode: Tween.Mode.Rotate({
606
+ start: Quaternion.fromEulerDegrees(0, 0, 0),
607
+ end: Quaternion.fromEulerDegrees(0, 180, 0)
608
+ }),
609
+ duration: 1000,
610
+ easingFunction: EasingFunction.EF_EASEOUTBOUNCE
611
+ })
612
+
613
+ // Scale
614
+ Tween.create(entity, {
615
+ mode: Tween.Mode.Scale({
616
+ start: Vector3.create(1, 1, 1),
617
+ end: Vector3.create(2, 2, 2)
618
+ }),
619
+ duration: 1500,
620
+ easingFunction: EasingFunction.EF_EASEINEXPO
621
+ })
622
+ ```
623
+
624
+ #### Tween Sequences
625
+ ```typescript
626
+ // Back and forth movement
627
+ Tween.create(entity, {
628
+ mode: Tween.Mode.Move({
629
+ start: Vector3.create(4, 1, 4),
630
+ end: Vector3.create(8, 1, 8)
631
+ }),
632
+ duration: 2000,
633
+ easingFunction: EasingFunction.EF_LINEAR
634
+ })
635
+
636
+ TweenSequence.create(entity, {
637
+ sequence: [],
638
+ loop: TweenLoop.TL_YOYO // Back and forth
639
+ })
640
+
641
+ // Complex sequence
642
+ TweenSequence.create(entity, {
643
+ sequence: [
644
+ {
645
+ mode: Tween.Mode.Move({
646
+ start: Vector3.create(8, 1, 8),
647
+ end: Vector3.create(8, 3, 8)
648
+ }),
649
+ duration: 1000,
650
+ easingFunction: EasingFunction.EF_LINEAR
651
+ },
652
+ {
653
+ mode: Tween.Mode.Rotate({
654
+ start: Quaternion.fromEulerDegrees(0, 0, 0),
655
+ end: Quaternion.fromEulerDegrees(0, 360, 0)
656
+ }),
657
+ duration: 1000,
658
+ easingFunction: EasingFunction.EF_LINEAR
659
+ }
660
+ ],
661
+ loop: TweenLoop.TL_RESTART
662
+ })
663
+ ```
664
+
665
+ #### Tween Control
666
+ ```typescript
667
+ // Pause/resume tween
668
+ const tweenData = Tween.getMutable(entity)
669
+ tweenData.playing = false // Pause
670
+ tweenData.playing = true // Resume
671
+
672
+ // Remove tween
673
+ Tween.deleteFrom(entity)
674
+ TweenSequence.deleteFrom(entity)
675
+
676
+ // Detect tween completion
677
+ engine.addSystem(() => {
678
+ if (tweenSystem.tweenCompleted(entity)) {
679
+ console.log('Tween finished!')
680
+ }
681
+ })
682
+ ```
683
+
684
+ #### Manual Movement via Systems
685
+ ```typescript
686
+ // Linear interpolation movement
687
+ function moveSystem(dt: number) {
688
+ for (const [entity, moveData] of engine.getEntitiesWith(MoveComponent)) {
689
+ const transform = Transform.getMutable(entity)
690
+ const data = MoveComponent.getMutable(entity)
691
+
692
+ if (data.fraction < 1) {
693
+ data.fraction += dt * data.speed
694
+ transform.position = Vector3.lerp(data.start, data.end, data.fraction)
695
+ }
696
+ }
697
+ }
698
+
699
+ engine.addSystem(moveSystem)
700
+ ```
701
+
702
+ ### Colliders
703
+
704
+ #### Mesh Colliders
705
+ ```typescript
706
+ // Add collider to primitive
707
+ MeshCollider.setBox(entity)
708
+ MeshCollider.setSphere(entity)
709
+ MeshCollider.setPlane(entity)
710
+ MeshCollider.setCylinder(entity)
711
+
712
+ // Custom collision layer
713
+ MeshCollider.setBox(entity, ColliderLayer.CL_CUSTOM1)
714
+ ```
715
+
716
+ #### GLTF Model Colliders
717
+ ```typescript
718
+ // Use visible geometry as collider
719
+ GltfContainer.create(entity, {
720
+ src: 'models/house.glb',
721
+ visibleMeshesCollisionMask: ColliderLayer.CL_PHYSICS,
722
+ invisibleMeshesCollisionMask: ColliderLayer.CL_NONE
723
+ })
724
+ ```
725
+
726
+ #### Collision Layers
727
+ ```typescript
728
+ // Available collision layers
729
+ ColliderLayer.CL_NONE
730
+ ColliderLayer.CL_POINTER // Pointer events
731
+ ColliderLayer.CL_PHYSICS // Player movement blocking
732
+ ColliderLayer.CL_PLAYER // Player avatar body
733
+ ColliderLayer.CL_CUSTOM1 // Custom layer 1
734
+ ColliderLayer.CL_CUSTOM2 // Custom layer 2
735
+ // ... up to CL_CUSTOM8
736
+
737
+ // Combine layers
738
+ const combinedLayers = ColliderLayer.CL_PHYSICS | ColliderLayer.CL_POINTER
739
+ ```
740
+
741
+ ### Trigger Areas
742
+
743
+ Detect when the player or any entity enters, stays in, or exits a shaped area. Shapes: box or sphere. Size the area via `Transform.scale`. By default, reacts to the player layer; customize with `ColliderLayer`.
744
+
745
+ ```typescript
746
+ import { engine, Transform, TriggerArea, MeshRenderer, MeshCollider, ColliderLayer } from '@dcl/sdk/ecs'
747
+ import { Vector3 } from '@dcl/sdk/math'
748
+ import { triggerAreaEventsSystem } from '@dcl/sdk/ecs'
749
+
750
+ // Create a box trigger at (8,0,8), size 4x2x4
751
+ const area = engine.addEntity()
752
+ TriggerArea.setBox(area) // or TriggerArea.setSphere(area)
753
+ Transform.create(area, { position: Vector3.create(8, 0, 8), scale: Vector3.create(4, 2, 4) })
754
+
755
+ // Optional: visualize area for debugging
756
+ MeshRenderer.setBox(area)
757
+
758
+ // Events
759
+ triggerAreaEventsSystem.onTriggerEnter(area, (e) => {
760
+ console.log('Enter by entity', e.trigger.entity)
761
+ })
762
+ triggerAreaEventsSystem.onTriggerExit(area, () => {
763
+ console.log('Exit')
764
+ })
765
+ triggerAreaEventsSystem.onTriggerStay(area, () => {
766
+ // Called every frame while inside
767
+ })
768
+
769
+ // Layers: restrict which entities activate the area
770
+ TriggerArea.setBox(area, ColliderLayer.CL_CUSTOM1 | ColliderLayer.CL_CUSTOM2)
771
+
772
+ // Mark a moving entity to activate the area
773
+ const mover = engine.addEntity()
774
+ Transform.create(mover, { position: Vector3.create(8, 0, 8) })
775
+ MeshCollider.setBox(mover, ColliderLayer.CL_CUSTOM1)
776
+ ```
777
+
778
+ Result payload (enter/exit/stay callback parameter):
779
+ - `triggeredEntity`: area entity id
780
+ - `eventType`: ENTER | EXIT | STAY
781
+ - `trigger.entity`: entering entity id
782
+ - `trigger.layer`, `trigger.position`, `trigger.rotation`, `trigger.scale`
783
+
784
+ ### Sounds
785
+
786
+ #### Audio Sources
787
+ ```typescript
788
+ // Create audio source
789
+ AudioSource.create(entity, {
790
+ audioClipUrl: 'sounds/effect.mp3',
791
+ playing: true,
792
+ loop: false,
793
+ volume: 0.8,
794
+ pitch: 1.0
795
+ })
796
+
797
+ // Control audio
798
+ const audio = AudioSource.getMutable(entity)
799
+ audio.playing = true
800
+ audio.volume = 0.5
801
+ ```
802
+
803
+ #### Audio Streaming
804
+ ```typescript
805
+ // Stream audio from URL
806
+ AudioStream.create(entity, {
807
+ url: 'https://example.com/stream.mp3',
808
+ playing: true,
809
+ volume: 0.7
810
+ })
811
+ ```
812
+
813
+ ### Text
814
+
815
+ #### Text Shape
816
+ ```typescript
817
+ TextShape.create(entity, {
818
+ text: 'Hello World!',
819
+ fontSize: 24,
820
+ fontWeight: 'bold',
821
+ color: Color4.White(),
822
+ outlineColor: Color4.Black(),
823
+ outlineWidth: 0.1,
824
+ textAlign: TextAlignMode.TAM_MIDDLE_CENTER
825
+ })
826
+
827
+ // Text alignment options
828
+ TextAlignMode.TAM_TOP_LEFT
829
+ TextAlignMode.TAM_TOP_CENTER
830
+ TextAlignMode.TAM_TOP_RIGHT
831
+ TextAlignMode.TAM_MIDDLE_LEFT
832
+ TextAlignMode.TAM_MIDDLE_CENTER
833
+ TextAlignMode.TAM_MIDDLE_RIGHT
834
+ TextAlignMode.TAM_BOTTOM_LEFT
835
+ TextAlignMode.TAM_BOTTOM_CENTER
836
+ TextAlignMode.TAM_BOTTOM_RIGHT
837
+ ```
838
+
839
+ ### Camera
840
+
841
+ #### Camera Control
842
+ ```typescript
843
+ // Get camera mode
844
+ const cameraMode = CameraMode.get(engine.CameraEntity)
845
+ if (cameraMode.mode === CameraType.CT_FIRST_PERSON) {
846
+ // First person
847
+ } else if (cameraMode.mode === CameraType.CT_THIRD_PERSON) {
848
+ // Third person
849
+ }
850
+
851
+ // Virtual camera
852
+ VirtualCamera.create(entity, {
853
+ defaultTransition: {
854
+ transitionMode: CameraTransition.CT_SPEED,
855
+ speed: 1.0
856
+ }
857
+ })
858
+ ```
859
+
860
+ ### Animations
861
+
862
+ #### GLTF Animations
863
+ ```typescript
864
+ // Play animation
865
+ Animator.create(entity, {
866
+ states: [
867
+ {
868
+ clip: 'Walk',
869
+ playing: true,
870
+ loop: true,
871
+ speed: 1.0
872
+ }
873
+ ]
874
+ })
875
+
876
+ // Control animation
877
+ const animator = Animator.getMutable(entity)
878
+ animator.states[0].playing = false
879
+ ```
880
+
881
+ ### Lights
882
+
883
+ #### Dynamic Lights
884
+ ```typescript
885
+ import { LightSource } from '@dcl/sdk/ecs'
886
+
887
+ // Point light
888
+ const point = engine.addEntity()
889
+ Transform.create(point, { position: Vector3.create(10, 3, 10) })
890
+ LightSource.create(point, {
891
+ type: LightSource.Type.Point({}),
892
+ color: Color3.White(),
893
+ intensity: 300 // candela
894
+ })
895
+
896
+ // Spot light with shadows
897
+ const spot = engine.addEntity()
898
+ Transform.create(spot, {
899
+ position: Vector3.create(8, 4, 8),
900
+ rotation: Quaternion.fromEulerDegrees(-90, 0, 0)
901
+ })
902
+ LightSource.create(spot, {
903
+ type: LightSource.Type.Spot({ innerAngle: 25, outerAngle: 45 }),
904
+ shadow: true,
905
+ intensity: 800
906
+ })
907
+
908
+ // Toggle a light on/off
909
+ const lightData = LightSource.getMutable(point)
910
+ lightData.active = !lightData.active
911
+
912
+ // Limit range (optional)
913
+ LightSource.getMutable(point).range = 20
914
+
915
+ // Light mask (gobo) for spot/point
916
+ LightSource.getMutable(spot).shadowMaskTexture = Material.Texture.Common({
917
+ src: 'assets/scene/images/lightmask1.png'
918
+ })
919
+ ```
920
+
921
+ Notes:
922
+ - One active light per parcel maximum; overall lights/shadows are auto-culled based on quality and proximity (up to ~3 shadowed lights visible at once).
923
+ - Intensity is in candela; visible distance roughly grows with (sqrt(intensity)).
924
+
925
+ ---
926
+
927
+ ## Interactivity
928
+
929
+ ### User Data
930
+
931
+ #### Player Position & Rotation
932
+ ```typescript
933
+ function getPlayerData() {
934
+ if (!Transform.has(engine.PlayerEntity)) return
935
+
936
+ const playerTransform = Transform.get(engine.PlayerEntity)
937
+ const cameraTransform = Transform.get(engine.CameraEntity)
938
+
939
+ console.log('Player position:', playerTransform.position)
940
+ console.log('Player rotation:', playerTransform.rotation)
941
+ console.log('Camera position:', cameraTransform.position)
942
+ console.log('Camera rotation:', cameraTransform.rotation)
943
+ }
944
+
945
+ engine.addSystem(getPlayerData)
946
+ ```
947
+
948
+ #### Get Player Profile
949
+ ```typescript
950
+ import { getPlayer } from '@dcl/sdk/src/players'
951
+
952
+ function main() {
953
+ const player = getPlayer()
954
+ if (player) {
955
+ console.log('Name:', player.name)
956
+ console.log('User ID:', player.userId)
957
+ console.log('Is Guest:', player.isGuest)
958
+ console.log('Wearables:', player.wearables)
959
+ console.log('Avatar shape:', player.avatar?.bodyShapeUrn)
960
+ }
961
+ }
962
+ ```
963
+
964
+ #### Get All Players
965
+ ```typescript
966
+ for (const [entity, data, transform] of engine.getEntitiesWith(PlayerIdentityData, Transform)) {
967
+ console.log('Player:', data.address, 'Position:', transform.position)
968
+ }
969
+ ```
970
+
971
+ #### Camera Mode
972
+ ```typescript
973
+ function checkCameraMode() {
974
+ if (!CameraMode.has(engine.CameraEntity)) return
975
+
976
+ const cameraMode = CameraMode.get(engine.CameraEntity)
977
+ if (cameraMode.mode === CameraType.CT_FIRST_PERSON) {
978
+ console.log('First person camera')
979
+ } else {
980
+ console.log('Third person camera')
981
+ }
982
+ }
983
+
984
+ engine.addSystem(checkCameraMode)
985
+ ```
986
+
987
+ #### Trigger Emotes
988
+ ```typescript
989
+ import { triggerEmote, triggerSceneEmote } from '~system/RestrictedActions'
990
+
991
+ // Default emote
992
+ triggerEmote({ predefinedEmote: 'robot' })
993
+
994
+ // Custom emote (file must end with _emote.glb)
995
+ triggerSceneEmote({ src: 'animations/Snowball_Throw_emote.glb', loop: false })
996
+ ```
997
+ Notes:
998
+ - Plays only while the player is still; walking/jumping interrupts.
999
+
1000
+ #### Cursor State
1001
+ ```typescript
1002
+ // Check if cursor is locked
1003
+ const isLocked = PointerLock.get(engine.CameraEntity).isPointerLocked
1004
+
1005
+ // Get cursor position
1006
+ const pointerInfo = PrimaryPointerInfo.get(engine.RootEntity)
1007
+ console.log('Cursor position:', pointerInfo.screenCoordinates)
1008
+ console.log('Cursor delta:', pointerInfo.screenDelta)
1009
+ console.log('World ray direction:', pointerInfo.worldRayDirection)
1010
+ ```
1011
+
1012
+ ### Button Events
1013
+
1014
+ #### Click Events
1015
+ ```typescript
1016
+ // Simple click handler
1017
+ pointerEventsSystem.onPointerDown(
1018
+ {
1019
+ entity: myEntity,
1020
+ opts: {
1021
+ button: InputAction.IA_POINTER,
1022
+ hoverText: 'Click me!',
1023
+ maxDistance: 10
1024
+ }
1025
+ },
1026
+ (event) => {
1027
+ console.log('Entity clicked!', event.hit.position)
1028
+ }
1029
+ )
1030
+
1031
+ // Multiple button support
1032
+ pointerEventsSystem.onPointerDown(
1033
+ {
1034
+ entity: myEntity,
1035
+ opts: {
1036
+ button: InputAction.IA_PRIMARY, // E key
1037
+ hoverText: 'Press E'
1038
+ }
1039
+ },
1040
+ () => console.log('E key pressed!')
1041
+ )
1042
+
1043
+ pointerEventsSystem.onPointerDown(
1044
+ {
1045
+ entity: myEntity,
1046
+ opts: {
1047
+ button: InputAction.IA_SECONDARY, // F key
1048
+ hoverText: 'Press F'
1049
+ }
1050
+ },
1051
+ () => console.log('F key pressed!')
1052
+ )
1053
+ ```
1054
+
1055
+ #### Available Input Actions
1056
+ ```typescript
1057
+ InputAction.IA_POINTER // Left mouse button
1058
+ InputAction.IA_PRIMARY // E key
1059
+ InputAction.IA_SECONDARY // F key
1060
+ InputAction.IA_ACTION_3 // 1 key
1061
+ InputAction.IA_ACTION_4 // 2 key
1062
+ InputAction.IA_ACTION_5 // 3 key
1063
+ InputAction.IA_ACTION_6 // 4 key
1064
+ InputAction.IA_JUMP // Space key
1065
+ InputAction.IA_FORWARD // W key
1066
+ InputAction.IA_BACKWARD // S key
1067
+ InputAction.IA_LEFT // A key
1068
+ InputAction.IA_RIGHT // D key
1069
+ InputAction.IA_WALK // Shift key
1070
+ ```
1071
+
1072
+ #### System-based Input Events
1073
+ ```typescript
1074
+ function inputSystem() {
1075
+ // Check for specific input on specific entity
1076
+ const clickData = inputSystem.getInputCommand(
1077
+ InputAction.IA_POINTER,
1078
+ PointerEventType.PET_DOWN,
1079
+ myEntity
1080
+ )
1081
+
1082
+ if (clickData) {
1083
+ console.log('Entity clicked via system:', clickData.hit.entityId)
1084
+ }
1085
+
1086
+ // Global input check
1087
+ if (inputSystem.isTriggered(InputAction.IA_PRIMARY, PointerEventType.PET_DOWN)) {
1088
+ console.log('E key pressed globally')
1089
+ }
1090
+ }
1091
+
1092
+ engine.addSystem(inputSystem)
1093
+ ```
1094
+
1095
+ #### Event Types
1096
+ ```typescript
1097
+ PointerEventType.PET_DOWN // Button pressed
1098
+ PointerEventType.PET_UP // Button released
1099
+ PointerEventType.PET_HOVER_ENTER // Cursor enters entity
1100
+ PointerEventType.PET_HOVER_LEAVE // Cursor leaves entity
1101
+ ```
1102
+
1103
+ ### Raycasting
1104
+
1105
+ #### Basic Raycasting
1106
+ ```typescript
1107
+ // Raycast from entity in local direction
1108
+ raycastSystem.registerLocalDirectionRaycast(
1109
+ {
1110
+ entity: myEntity,
1111
+ opts: {
1112
+ direction: Vector3.Forward(),
1113
+ maxDistance: 10,
1114
+ queryType: RaycastQueryType.RQT_HIT_FIRST
1115
+ }
1116
+ },
1117
+ (result) => {
1118
+ if (result.hits.length > 0) {
1119
+ console.log('Hit entity:', result.hits[0].entityId)
1120
+ console.log('Hit position:', result.hits[0].position)
1121
+ }
1122
+ }
1123
+ )
1124
+
1125
+ // Global direction raycast
1126
+ raycastSystem.registerGlobalDirectionRaycast(
1127
+ {
1128
+ entity: myEntity,
1129
+ opts: {
1130
+ direction: Vector3.Down(),
1131
+ maxDistance: 5,
1132
+ queryType: RaycastQueryType.RQT_QUERY_ALL
1133
+ }
1134
+ },
1135
+ (result) => {
1136
+ console.log('All hits:', result.hits)
1137
+ }
1138
+ )
1139
+
1140
+ // Target position raycast
1141
+ raycastSystem.registerGlobalTargetRaycast(
1142
+ {
1143
+ entity: myEntity,
1144
+ opts: {
1145
+ globalTarget: Vector3.create(8, 0, 8),
1146
+ maxDistance: 20
1147
+ }
1148
+ },
1149
+ (result) => {
1150
+ // Handle result
1151
+ }
1152
+ )
1153
+
1154
+ // Target entity raycast
1155
+ raycastSystem.registerTargetEntityRaycast(
1156
+ {
1157
+ entity: sourceEntity,
1158
+ opts: {
1159
+ targetEntity: targetEntity,
1160
+ maxDistance: 15
1161
+ }
1162
+ },
1163
+ (result) => {
1164
+ // Handle result
1165
+ }
1166
+ )
1167
+ ```
1168
+
1169
+ #### Raycast Options
1170
+ ```typescript
1171
+ const raycastOptions = {
1172
+ direction: Vector3.Forward(),
1173
+ maxDistance: 16,
1174
+ queryType: RaycastQueryType.RQT_HIT_FIRST, // or RQT_QUERY_ALL
1175
+ originOffset: Vector3.create(0, 0.5, 0), // Offset from entity origin
1176
+ collisionMask: ColliderLayer.CL_PHYSICS | ColliderLayer.CL_CUSTOM1,
1177
+ continuous: false // Set to true for continuous raycasting
1178
+ }
1179
+ ```
1180
+
1181
+ #### Collision Layers for Raycasting
1182
+ ```typescript
1183
+ // Only check specific layers
1184
+ raycastSystem.registerLocalDirectionRaycast(
1185
+ {
1186
+ entity: myEntity,
1187
+ opts: {
1188
+ direction: Vector3.Forward(),
1189
+ collisionMask: ColliderLayer.CL_CUSTOM1 | ColliderLayer.CL_CUSTOM2
1190
+ }
1191
+ },
1192
+ (result) => {
1193
+ // Only hits entities on CUSTOM1 or CUSTOM2 layers
1194
+ }
1195
+ )
1196
+ ```
1197
+
1198
+ #### Remove Raycast
1199
+ ```typescript
1200
+ // Remove continuous raycast
1201
+ raycastSystem.removeRaycasterEntity(myEntity)
1202
+ ```
1203
+
1204
+ #### Raycast from Player/Camera
1205
+ ```typescript
1206
+ // Raycast from camera forward
1207
+ raycastSystem.registerGlobalDirectionRaycast(
1208
+ {
1209
+ entity: engine.CameraEntity,
1210
+ opts: {
1211
+ direction: Vector3.rotate(
1212
+ Vector3.Forward(),
1213
+ Transform.get(engine.CameraEntity).rotation
1214
+ ),
1215
+ maxDistance: 16
1216
+ }
1217
+ },
1218
+ (result) => {
1219
+ if (result.hits.length > 0) {
1220
+ console.log('Player looking at:', result.hits[0].entityId)
1221
+ }
1222
+ }
1223
+ )
1224
+ ```
1225
+
1226
+ ### Event Listeners
1227
+
1228
+ #### Player Events
1229
+ ```typescript
1230
+ // Player connects/disconnects
1231
+ engine.addSystem(() => {
1232
+ for (const [entity] of engine.getEntitiesWith(PlayerIdentityData)) {
1233
+ // New player joined
1234
+ if (!processedPlayers.has(entity)) {
1235
+ processedPlayers.add(entity)
1236
+ console.log('Player joined:', entity)
1237
+ }
1238
+ }
1239
+ })
1240
+
1241
+ // Cursor lock/unlock
1242
+ PointerLock.onChange(engine.CameraEntity, (pointerLock) => {
1243
+ if (pointerLock?.isPointerLocked) {
1244
+ console.log('Cursor locked')
1245
+ } else {
1246
+ console.log('Cursor unlocked')
1247
+ }
1248
+ })
1249
+ ```
1250
+
1251
+ ### Avatar Modifiers
1252
+
1253
+ #### Avatar Modifier Areas
1254
+ ```typescript
1255
+ // Create modifier area
1256
+ const modifierArea = engine.addEntity()
1257
+ Transform.create(modifierArea, {
1258
+ position: Vector3.create(8, 0, 8),
1259
+ scale: Vector3.create(4, 3, 4) // Area size
1260
+ })
1261
+
1262
+ AvatarModifierArea.create(modifierArea, {
1263
+ area: { box: Vector3.create(4, 3, 4) },
1264
+ modifiers: [AvatarModifierType.AMT_HIDE_AVATARS],
1265
+ excludeIds: ['0x123...abc'] // Optional: exclude specific players
1266
+ })
1267
+
1268
+ // Available modifiers
1269
+ AvatarModifierType.AMT_HIDE_AVATARS
1270
+ AvatarModifierType.AMT_DISABLE_PASSPORTS
1271
+ ```
1272
+
1273
+ #### Movement Constraints
1274
+ ```typescript
1275
+ // Create movement constraint area
1276
+ const constraintArea = engine.addEntity()
1277
+ Transform.create(constraintArea, {
1278
+ position: Vector3.create(8, 0, 8)
1279
+ })
1280
+
1281
+ // Prevent jumping in area
1282
+ AvatarModifierArea.create(constraintArea, {
1283
+ area: { box: Vector3.create(6, 10, 6) },
1284
+ modifiers: [AvatarModifierType.AMT_DISABLE_JUMPING]
1285
+ })
1286
+ ```
1287
+
1288
+ ### NPC Avatars
1289
+
1290
+ #### Display only wearables
1291
+ ```typescript
1292
+ import { AvatarShape } from '@dcl/sdk/ecs'
1293
+
1294
+ const mannequin = engine.addEntity()
1295
+ AvatarShape.create(mannequin, {
1296
+ id: 'npc-1',
1297
+ name: 'NPC',
1298
+ wearables: [
1299
+ 'urn:decentraland:matic:collections-v2:0x90e5cb2d673699be8f28d339c818a0b60144c494:0'
1300
+ ],
1301
+ show_only_wearables: true
1302
+ })
1303
+
1304
+ Transform.create(mannequin, {
1305
+ position: Vector3.create(4, 0.25, 5),
1306
+ scale: Vector3.create(1.2, 1.2, 1.2)
1307
+ })
1308
+ ```
1309
+ Use this to showcase items (e.g., storefront mannequins).
1310
+
1311
+ ### Input Modifiers
1312
+ ```typescript
1313
+ import { InputModifier } from '@dcl/sdk/ecs'
1314
+
1315
+ // Freeze player
1316
+ InputModifier.create(engine.PlayerEntity, {
1317
+ mode: InputModifier.Mode.Standard({ disableAll: true })
1318
+ })
1319
+
1320
+ // Restrict specific locomotion
1321
+ InputModifier.createOrReplace(engine.PlayerEntity, {
1322
+ mode: InputModifier.Mode.Standard({
1323
+ disableRun: true,
1324
+ disableJump: true,
1325
+ disableEmote: true
1326
+ })
1327
+ })
1328
+ ```
1329
+ Note: Supported in the DCL 2.0 desktop client; only affects the local player inside scene bounds.
1330
+
1331
+ ### Move Player
1332
+
1333
+ #### Teleport Player
1334
+ ```typescript
1335
+ // Move player to position
1336
+ const playerTransform = Transform.getMutable(engine.PlayerEntity)
1337
+ playerTransform.position = Vector3.create(8, 0, 8)
1338
+
1339
+ // Move player with rotation
1340
+ playerTransform.position = Vector3.create(8, 0, 8)
1341
+ playerTransform.rotation = Quaternion.fromEulerDegrees(0, 180, 0)
1342
+ ```
1343
+
1344
+ #### Restrict Player Movement
1345
+ ```typescript
1346
+ // System to keep player in bounds
1347
+ function boundarySystem() {
1348
+ const playerTransform = Transform.getMutable(engine.PlayerEntity)
1349
+ const pos = playerTransform.position
1350
+
1351
+ // Keep within scene bounds
1352
+ if (pos.x < 0) playerTransform.position.x = 0
1353
+ if (pos.x > 16) playerTransform.position.x = 16
1354
+ if (pos.z < 0) playerTransform.position.z = 0
1355
+ if (pos.z > 16) playerTransform.position.z = 16
1356
+ }
1357
+
1358
+ engine.addSystem(boundarySystem)
1359
+ ```
1360
+
1361
+ ### Runtime Data
1362
+
1363
+ #### Scene Information
1364
+ ```typescript
1365
+ import { getRealm } from '~system/Runtime'
1366
+
1367
+ executeTask(async () => {
1368
+ const realm = await getRealm({})
1369
+ console.log('Server:', realm.realmInfo?.serverName)
1370
+ console.log('Base URL:', realm.realmInfo?.baseUrl)
1371
+ })
1372
+ ```
1373
+
1374
+ #### Environment Data
1375
+ ```typescript
1376
+ // Get current time and other runtime info
1377
+ function runtimeSystem() {
1378
+ const time = Date.now()
1379
+ const sceneInfo = {
1380
+ time: time,
1381
+ players: Array.from(engine.getEntitiesWith(PlayerIdentityData)).length
1382
+ }
1383
+ console.log('Scene info:', sceneInfo)
1384
+ }
1385
+
1386
+ engine.addSystem(runtimeSystem)
1387
+ ```
1388
+
1389
+ #### Scene Metadata (getSceneInformation)
1390
+ ```typescript
1391
+ import { getSceneInformation } from '~system/Runtime'
1392
+
1393
+ executeTask(async () => {
1394
+ const info = await getSceneInformation({})
1395
+ if (!info) return
1396
+ const sceneJson = JSON.parse(info.metadataJson)
1397
+ console.log(sceneJson.scene?.parcels, sceneJson.spawnPoints)
1398
+ })
1399
+ ```
1400
+
1401
+ ### Skybox Control
1402
+
1403
+ #### Fixed time of day (scene.json)
1404
+ ```json
1405
+ "skyboxConfig": {
1406
+ "fixedTime": 36000
1407
+ }
1408
+ ```
1409
+
1410
+ #### Read current world time
1411
+ ```typescript
1412
+ import { getWorldTime } from '~system/Runtime'
1413
+
1414
+ executeTask(async () => {
1415
+ const time = await getWorldTime({})
1416
+ console.log('Seconds since midnight:', time.seconds)
1417
+ })
1418
+ ```
1419
+
1420
+ #### Change time dynamically
1421
+ ```typescript
1422
+ import { SkyboxTime, TransitionMode } from '~system/Runtime'
1423
+
1424
+ // Must target root entity
1425
+ SkyboxTime.create(engine.RootEntity, { fixed_time: 36000 })
1426
+
1427
+ // Optional transition direction
1428
+ SkyboxTime.createOrReplace(engine.RootEntity, {
1429
+ fixed_time: 54000,
1430
+ direction: TransitionMode.TM_BACKWARD
1431
+ })
1432
+ ```
1433
+
1434
+ ---
1435
+
1436
+ ## 2D UI
1437
+
1438
+ ### Basic UI Setup
1439
+
1440
+ #### Rendering UI
1441
+ ```typescript
1442
+ // ui.tsx
1443
+ import { UiEntity, ReactEcs } from '@dcl/sdk/react-ecs'
1444
+ import { Color4 } from '@dcl/sdk/math'
1445
+
1446
+ export const uiMenu = () => (
1447
+ <UiEntity
1448
+ uiTransform={{
1449
+ width: 400,
1450
+ height: 300,
1451
+ position: { top: '10%', left: '10%' }
1452
+ }}
1453
+ uiBackground={{ color: Color4.create(0, 0, 0, 0.8) }}
1454
+ >
1455
+ <UiEntity
1456
+ uiTransform={{
1457
+ width: '100%',
1458
+ height: 50,
1459
+ alignItems: 'center',
1460
+ justifyContent: 'center'
1461
+ }}
1462
+ uiText={{ value: 'Hello World!', fontSize: 24 }}
1463
+ />
1464
+ </UiEntity>
1465
+ )
1466
+
1467
+ // index.ts
1468
+ import { ReactEcsRenderer } from '@dcl/sdk/react-ecs'
1469
+ import { uiMenu } from './ui'
1470
+
1471
+ export function main() {
1472
+ ReactEcsRenderer.setUiRenderer(uiMenu)
1473
+ }
1474
+ ```
1475
+
1476
+ ### UI Transform
1477
+
1478
+ #### Positioning
1479
+ ```typescript
1480
+ // Absolute positioning
1481
+ uiTransform={{
1482
+ positionType: 'absolute',
1483
+ position: { top: '10px', left: '20px' },
1484
+ width: 200,
1485
+ height: 100
1486
+ }}
1487
+
1488
+ // Relative positioning
1489
+ uiTransform={{
1490
+ positionType: 'relative',
1491
+ margin: { top: '10px', left: '20px' },
1492
+ width: '50%',
1493
+ height: '30%'
1494
+ }}
1495
+
1496
+ // Flexbox layout
1497
+ uiTransform={{
1498
+ flexDirection: 'column', // 'row' or 'column'
1499
+ alignItems: 'center', // 'flex-start', 'center', 'flex-end', 'stretch'
1500
+ justifyContent: 'space-between', // 'flex-start', 'center', 'flex-end', 'space-between', 'space-around'
1501
+ flexWrap: 'wrap' // 'nowrap', 'wrap'
1502
+ }}
1503
+ ```
1504
+
1505
+ #### Size and Spacing
1506
+ ```typescript
1507
+ uiTransform={{
1508
+ width: 300, // Fixed width in pixels
1509
+ height: '50%', // Percentage height
1510
+ minWidth: 100, // Minimum width
1511
+ maxWidth: 500, // Maximum width
1512
+ padding: { top: 10, bottom: 10, left: 15, right: 15 },
1513
+ margin: { top: '5px', bottom: '5px' }
1514
+ }}
1515
+ ```
1516
+
1517
+ ### UI Background
1518
+
1519
+ #### Colors and Images
1520
+ ```typescript
1521
+ // Solid color background
1522
+ uiBackground={{ color: Color4.create(1, 0, 0, 0.8) }}
1523
+
1524
+ // Texture background
1525
+ uiBackground={{
1526
+ texture: { src: 'assets/ui/background.png' },
1527
+ textureMode: 'stretch' // 'stretch', 'center', 'repeat'
1528
+ }}
1529
+
1530
+ // Nine-slice background
1531
+ uiBackground={{
1532
+ texture: { src: 'assets/ui/panel.png' },
1533
+ textureSlices: {
1534
+ top: 10,
1535
+ bottom: 10,
1536
+ left: 10,
1537
+ right: 10
1538
+ }
1539
+ }}
1540
+ ```
1541
+
1542
+ ### UI Text
1543
+
1544
+ #### Text Properties
1545
+ ```typescript
1546
+ uiText={{
1547
+ value: 'Hello World!',
1548
+ fontSize: 18,
1549
+ color: Color4.White(),
1550
+ textAlign: 'middle-center', // 'top-left', 'top-center', 'top-right', etc.
1551
+ font: 'serif', // 'sans-serif', 'serif', 'monospace'
1552
+ fontWeight: 'bold' // 'normal', 'bold'
1553
+ }}
1554
+
1555
+ // Rich text with line breaks
1556
+ uiText={{
1557
+ value: 'Line 1\nLine 2\nLine 3',
1558
+ fontSize: 16,
1559
+ textAlign: 'top-left'
1560
+ }}
1561
+ ```
1562
+
1563
+ ### UI Button Events
1564
+
1565
+ #### Click Events
1566
+ ```typescript
1567
+ <UiEntity
1568
+ uiTransform={{
1569
+ width: 150,
1570
+ height: 50,
1571
+ alignItems: 'center',
1572
+ justifyContent: 'center'
1573
+ }}
1574
+ uiBackground={{ color: Color4.Blue() }}
1575
+ uiText={{ value: 'Click Me!', fontSize: 18 }}
1576
+ onMouseDown={() => {
1577
+ console.log('Button clicked!')
1578
+ }}
1579
+ />
1580
+ ```
1581
+
1582
+ #### Hover Effects
1583
+ ```typescript
1584
+ const [isHovered, setIsHovered] = useState(false)
1585
+
1586
+ <UiEntity
1587
+ uiTransform={{
1588
+ width: 150,
1589
+ height: 50,
1590
+ alignItems: 'center',
1591
+ justifyContent: 'center'
1592
+ }}
1593
+ uiBackground={{
1594
+ color: isHovered ? Color4.Green() : Color4.Blue()
1595
+ }}
1596
+ uiText={{ value: 'Hover Me!', fontSize: 18 }}
1597
+ onMouseEnter={() => setIsHovered(true)}
1598
+ onMouseLeave={() => setIsHovered(false)}
1599
+ onMouseDown={() => {
1600
+ console.log('Button clicked!')
1601
+ }}
1602
+ />
1603
+ ```
1604
+
1605
+ ### Dynamic UI
1606
+
1607
+ #### State Management
1608
+ ```typescript
1609
+ import { useState } from 'react'
1610
+
1611
+ export const DynamicUI = () => {
1612
+ const [count, setCount] = useState(0)
1613
+ const [isVisible, setIsVisible] = useState(true)
1614
+
1615
+ return (
1616
+ <UiEntity
1617
+ uiTransform={{
1618
+ width: 300,
1619
+ height: 200,
1620
+ position: { top: '10%', left: '10%' },
1621
+ flexDirection: 'column',
1622
+ alignItems: 'center',
1623
+ justifyContent: 'center'
1624
+ }}
1625
+ uiBackground={{ color: Color4.create(0, 0, 0, 0.8) }}
1626
+ >
1627
+ {isVisible && (
1628
+ <UiEntity
1629
+ uiTransform={{ width: '100%', height: 50 }}
1630
+ uiText={{
1631
+ value: `Count: ${count}`,
1632
+ fontSize: 20,
1633
+ textAlign: 'middle-center'
1634
+ }}
1635
+ />
1636
+ )}
1637
+
1638
+ <UiEntity
1639
+ uiTransform={{
1640
+ width: 100,
1641
+ height: 40,
1642
+ margin: { top: 10 }
1643
+ }}
1644
+ uiBackground={{ color: Color4.Green() }}
1645
+ uiText={{ value: '+', fontSize: 24, textAlign: 'middle-center' }}
1646
+ onMouseDown={() => setCount(count + 1)}
1647
+ />
1648
+
1649
+ <UiEntity
1650
+ uiTransform={{
1651
+ width: 100,
1652
+ height: 40,
1653
+ margin: { top: 10 }
1654
+ }}
1655
+ uiBackground={{ color: Color4.Red() }}
1656
+ uiText={{ value: 'Toggle', fontSize: 16, textAlign: 'middle-center' }}
1657
+ onMouseDown={() => setIsVisible(!isVisible)}
1658
+ />
1659
+ </UiEntity>
1660
+ )
1661
+ }
1662
+ ```
1663
+
1664
+ #### Game HUD Example
1665
+ ```typescript
1666
+ export const GameHUD = () => {
1667
+ const [health, setHealth] = useState(100)
1668
+ const [score, setScore] = useState(0)
1669
+ const [ammo, setAmmo] = useState(30)
1670
+
1671
+ return (
1672
+ <UiEntity
1673
+ uiTransform={{
1674
+ width: '100%',
1675
+ height: '100%',
1676
+ positionType: 'absolute'
1677
+ }}
1678
+ >
1679
+ {/* Health Bar */}
1680
+ <UiEntity
1681
+ uiTransform={{
1682
+ width: 200,
1683
+ height: 20,
1684
+ position: { top: '10px', left: '10px' }
1685
+ }}
1686
+ uiBackground={{ color: Color4.Red() }}
1687
+ >
1688
+ <UiEntity
1689
+ uiTransform={{
1690
+ width: `${health}%`,
1691
+ height: '100%'
1692
+ }}
1693
+ uiBackground={{ color: Color4.Green() }}
1694
+ />
1695
+ </UiEntity>
1696
+
1697
+ {/* Score */}
1698
+ <UiEntity
1699
+ uiTransform={{
1700
+ width: 150,
1701
+ height: 30,
1702
+ position: { top: '10px', right: '10px' }
1703
+ }}
1704
+ uiText={{
1705
+ value: `Score: ${score}`,
1706
+ fontSize: 18,
1707
+ textAlign: 'middle-right'
1708
+ }}
1709
+ />
1710
+
1711
+ {/* Ammo */}
1712
+ <UiEntity
1713
+ uiTransform={{
1714
+ width: 100,
1715
+ height: 30,
1716
+ position: { bottom: '10px', right: '10px' }
1717
+ }}
1718
+ uiText={{
1719
+ value: `Ammo: ${ammo}`,
1720
+ fontSize: 16,
1721
+ textAlign: 'middle-right'
1722
+ }}
1723
+ />
1724
+ </UiEntity>
1725
+ )
1726
+ }
1727
+ ```
1728
+
1729
+ ### UI Layout Examples
1730
+
1731
+ #### Modal Dialog
1732
+ ```typescript
1733
+ export const ModalDialog = ({ isOpen, onClose }: { isOpen: boolean, onClose: () => void }) => {
1734
+ if (!isOpen) return null
1735
+
1736
+ return (
1737
+ <UiEntity
1738
+ uiTransform={{
1739
+ width: '100%',
1740
+ height: '100%',
1741
+ positionType: 'absolute',
1742
+ alignItems: 'center',
1743
+ justifyContent: 'center'
1744
+ }}
1745
+ uiBackground={{ color: Color4.create(0, 0, 0, 0.5) }}
1746
+ onMouseDown={onClose}
1747
+ >
1748
+ <UiEntity
1749
+ uiTransform={{
1750
+ width: 400,
1751
+ height: 300,
1752
+ flexDirection: 'column',
1753
+ alignItems: 'center',
1754
+ justifyContent: 'space-between',
1755
+ padding: { top: 20, bottom: 20, left: 20, right: 20 }
1756
+ }}
1757
+ uiBackground={{ color: Color4.create(0.2, 0.2, 0.2, 1) }}
1758
+ onMouseDown={(e) => e.stopPropagation()}
1759
+ >
1760
+ <UiEntity
1761
+ uiText={{
1762
+ value: 'Dialog Title',
1763
+ fontSize: 24,
1764
+ textAlign: 'middle-center'
1765
+ }}
1766
+ />
1767
+
1768
+ <UiEntity
1769
+ uiText={{
1770
+ value: 'This is the dialog content.',
1771
+ fontSize: 16,
1772
+ textAlign: 'middle-center'
1773
+ }}
1774
+ />
1775
+
1776
+ <UiEntity
1777
+ uiTransform={{
1778
+ width: 100,
1779
+ height: 40,
1780
+ alignItems: 'center',
1781
+ justifyContent: 'center'
1782
+ }}
1783
+ uiBackground={{ color: Color4.Blue() }}
1784
+ uiText={{ value: 'Close', fontSize: 16 }}
1785
+ onMouseDown={onClose}
1786
+ />
1787
+ </UiEntity>
1788
+ </UiEntity>
1789
+ )
1790
+ }
1791
+ ```
1792
+
1793
+ #### Inventory Grid
1794
+ ```typescript
1795
+ export const InventoryGrid = () => {
1796
+ const items = Array.from({ length: 20 }, (_, i) => `Item ${i + 1}`)
1797
+
1798
+ return (
1799
+ <UiEntity
1800
+ uiTransform={{
1801
+ width: 400,
1802
+ height: 400,
1803
+ position: { top: '10%', left: '10%' },
1804
+ flexDirection: 'column',
1805
+ padding: { top: 10, bottom: 10, left: 10, right: 10 }
1806
+ }}
1807
+ uiBackground={{ color: Color4.create(0.1, 0.1, 0.1, 0.9) }}
1808
+ >
1809
+ <UiEntity
1810
+ uiTransform={{
1811
+ width: '100%',
1812
+ height: 40,
1813
+ alignItems: 'center',
1814
+ justifyContent: 'center'
1815
+ }}
1816
+ uiText={{ value: 'Inventory', fontSize: 20 }}
1817
+ />
1818
+
1819
+ <UiEntity
1820
+ uiTransform={{
1821
+ width: '100%',
1822
+ height: '100%',
1823
+ flexDirection: 'row',
1824
+ flexWrap: 'wrap',
1825
+ alignItems: 'flex-start',
1826
+ justifyContent: 'flex-start'
1827
+ }}
1828
+ >
1829
+ {items.map((item, index) => (
1830
+ <UiEntity
1831
+ key={index}
1832
+ uiTransform={{
1833
+ width: 70,
1834
+ height: 70,
1835
+ margin: { top: 5, bottom: 5, left: 5, right: 5 },
1836
+ alignItems: 'center',
1837
+ justifyContent: 'center'
1838
+ }}
1839
+ uiBackground={{ color: Color4.create(0.3, 0.3, 0.3, 1) }}
1840
+ uiText={{ value: item, fontSize: 10 }}
1841
+ onMouseDown={() => console.log(`Clicked ${item}`)}
1842
+ />
1843
+ ))}
1844
+ </UiEntity>
1845
+ </UiEntity>
1846
+ )
1847
+ }
1848
+ ```
1849
+
1850
+ ---
1851
+
1852
+ ## Blockchain Integration
1853
+
1854
+ ### Wallet Connection
1855
+
1856
+ #### Check Player Wallet
1857
+ ```typescript
1858
+ import { getPlayer } from '@dcl/sdk/src/players'
1859
+
1860
+ function checkWallet() {
1861
+ const player = getPlayer()
1862
+ if (player && !player.isGuest) {
1863
+ console.log('Player wallet address:', player.userId)
1864
+ } else {
1865
+ console.log('Player is guest (no wallet)')
1866
+ }
1867
+ }
1868
+ ```
1869
+
1870
+ ### NFT Display
1871
+
1872
+ #### Display Certified NFT
1873
+ ```typescript
1874
+ import { NftShape } from '@dcl/sdk/ecs'
1875
+
1876
+ // Display NFT
1877
+ NftShape.create(entity, {
1878
+ urn: 'urn:decentraland:ethereum:erc721:0x06012c8cf97bead5deae237070f9587f8e7a266d:558536',
1879
+ color: Color4.White(),
1880
+ style: NftFrameType.NFT_CLASSIC
1881
+ })
1882
+
1883
+ // Available frame styles
1884
+ NftFrameType.NFT_CLASSIC
1885
+ NftFrameType.NFT_BAROQUE_ORNAMENT
1886
+ NftFrameType.NFT_DIAMOND_ORNAMENT
1887
+ NftFrameType.NFT_MINIMAL_WIDE
1888
+ NftFrameType.NFT_MINIMAL_GREY
1889
+ NftFrameType.NFT_BLOCKY
1890
+ NftFrameType.NFT_GOLD_EDGES
1891
+ NftFrameType.NFT_GOLD_CARVED
1892
+ NftFrameType.NFT_GOLD_WIDE
1893
+ NftFrameType.NFT_GOLD_ROUNDED
1894
+ NftFrameType.NFT_METAL_MEDIUM
1895
+ NftFrameType.NFT_METAL_WIDE
1896
+ NftFrameType.NFT_METAL_SLIM
1897
+ NftFrameType.NFT_METAL_ROUNDED
1898
+ NftFrameType.NFT_PINS
1899
+ NftFrameType.NFT_MINIMAL_BLACK
1900
+ NftFrameType.NFT_MINIMAL_WHITE
1901
+ NftFrameType.NFT_TAPE
1902
+ NftFrameType.NFT_WOOD_SLIM
1903
+ NftFrameType.NFT_WOOD_WIDE
1904
+ NftFrameType.NFT_WOOD_TWIGS
1905
+ NftFrameType.NFT_CANVAS
1906
+ NftFrameType.NFT_NONE
1907
+ ```
1908
+
1909
+ ### Blockchain Transactions
1910
+
1911
+ #### Sign Message
1912
+ ```typescript
1913
+ import { signedFetch } from '@dcl/sdk/signed-fetch'
1914
+
1915
+ executeTask(async () => {
1916
+ try {
1917
+ const response = await signedFetch('https://example.com/api/action', {
1918
+ method: 'POST',
1919
+ headers: { 'Content-Type': 'application/json' },
1920
+ body: JSON.stringify({
1921
+ action: 'claimReward',
1922
+ amount: 100
1923
+ })
1924
+ })
1925
+
1926
+ const result = await response.json()
1927
+ console.log('Transaction result:', result)
1928
+ } catch (error) {
1929
+ console.log('Transaction failed:', error)
1930
+ }
1931
+ })
1932
+ ```
1933
+
1934
+ #### MANA Transactions
1935
+ ```typescript
1936
+ import { manaUser } from '@dcl/sdk/ethereum'
1937
+
1938
+ executeTask(async () => {
1939
+ try {
1940
+ // Check MANA balance
1941
+ const balance = await manaUser.balance()
1942
+ console.log('MANA balance:', balance)
1943
+
1944
+ // Send MANA
1945
+ const result = await manaUser.send('0x123...abc', 100) // 100 MANA
1946
+ console.log('MANA sent:', result)
1947
+ } catch (error) {
1948
+ console.log('MANA transaction failed:', error)
1949
+ }
1950
+ })
1951
+ ```
1952
+
1953
+ ### Smart Contract Interaction
1954
+
1955
+ #### Import Contract ABI
1956
+ ```typescript
1957
+ // Store ABI in separate file (e.g., contracts/mana.ts)
1958
+ export default [
1959
+ {
1960
+ "anonymous": false,
1961
+ "inputs": [
1962
+ {
1963
+ "indexed": true,
1964
+ "name": "burner",
1965
+ "type": "address"
1966
+ },
1967
+ {
1968
+ "indexed": false,
1969
+ "name": "value",
1970
+ "type": "uint256"
1971
+ }
1972
+ ],
1973
+ "name": "Burn",
1974
+ "type": "event"
1975
+ }
1976
+ // ... rest of ABI
1977
+ ]
1978
+
1979
+ // Import in your scene
1980
+ import { abi } from '../contracts/mana'
1981
+ ```
1982
+
1983
+ #### Create Contract Instance
1984
+ ```typescript
1985
+ import { RequestManager, ContractFactory } from 'eth-connect'
1986
+ import { createEthereumProvider } from '@dcl/sdk/ethereum-provider'
1987
+ import { abi } from '../contracts/mana'
1988
+
1989
+ executeTask(async () => {
1990
+ try {
1991
+ // Create web3 provider interface
1992
+ const provider = createEthereumProvider()
1993
+
1994
+ // Create request manager for RPC messages
1995
+ const requestManager = new RequestManager(provider)
1996
+
1997
+ // Create contract factory
1998
+ const factory = new ContractFactory(requestManager, abi)
1999
+
2000
+ // Instance contract at specific address
2001
+ const contract = await factory.at('0x2a8fd99c19271f4f04b1b7b9c4f7cf264b626edb') as any
2002
+
2003
+ // Call contract methods
2004
+ const result = await contract.balanceOf('0x123...abc')
2005
+ console.log('Balance:', result)
2006
+
2007
+ } catch (error) {
2008
+ console.log('Contract interaction failed:', error)
2009
+ }
2010
+ })
2011
+ ```
2012
+
2013
+ #### Gas Price Checking
2014
+ ```typescript
2015
+ import { RequestManager } from 'eth-connect'
2016
+ import { createEthereumProvider } from '@dcl/sdk/ethereum-provider'
2017
+
2018
+ executeTask(async () => {
2019
+ const provider = createEthereumProvider()
2020
+ const requestManager = new RequestManager(provider)
2021
+
2022
+ // Check current gas price
2023
+ const gasPrice = await requestManager.eth_gasPrice()
2024
+ console.log('Current gas price:', gasPrice)
2025
+
2026
+ // Get account balance
2027
+ const balance = await requestManager.eth_getBalance('0x123...abc', 'latest')
2028
+ console.log('Account balance:', balance)
2029
+ })
2030
+ ```
2031
+
2032
+ #### Contract Method Calls
2033
+ ```typescript
2034
+ executeTask(async () => {
2035
+ try {
2036
+ const userData = getPlayer()
2037
+ if (userData.isGuest) return
2038
+
2039
+ // Write operation (requires gas)
2040
+ const writeResult = await contract.transfer(
2041
+ '0xRecipientAddress',
2042
+ 100, // amount
2043
+ {
2044
+ from: userData.userId,
2045
+ gas: 100000,
2046
+ gasPrice: await requestManager.eth_gasPrice()
2047
+ }
2048
+ )
2049
+ console.log('Transaction hash:', writeResult)
2050
+
2051
+ // Read operation (no gas required)
2052
+ const balance = await contract.balanceOf(userData.userId)
2053
+ console.log('Current balance:', balance)
2054
+
2055
+ } catch (error) {
2056
+ console.log('Transaction failed:', error)
2057
+ }
2058
+ })
2059
+ ```
2060
+
2061
+ #### Using Test Networks
2062
+ ```typescript
2063
+ // For Sepolia testnet testing
2064
+ // Set Metamask to Sepolia network
2065
+ // Use test URLs for preview:
2066
+ // decentraland://realm=http://127.0.0.1:8000&local-scene=true&debug=true&dclenv=zone&position=0,0
2067
+
2068
+ // Contract addresses differ between networks
2069
+ const CONTRACT_ADDRESSES = {
2070
+ mainnet: '0x0f5d2fb29fb7d3cfee444a200298f468908cc942',
2071
+ sepolia: '0x...' // Test contract address
2072
+ }
2073
+
2074
+ const currentNetwork = 'sepolia' // or determine dynamically
2075
+ const contractAddress = CONTRACT_ADDRESSES[currentNetwork]
2076
+ ```
2077
+
2078
+ ---
2079
+
2080
+ ## Media
2081
+
2082
+ ### Video Playing
2083
+
2084
+ #### Basic Video Setup
2085
+ ```typescript
2086
+ // Step 1: Create screen entity
2087
+ const screen = engine.addEntity()
2088
+ MeshRenderer.setPlane(screen)
2089
+ Transform.create(screen, { position: Vector3.create(4, 1, 4) })
2090
+
2091
+ // Step 2: Create video player
2092
+ VideoPlayer.create(screen, {
2093
+ src: 'videos/myVideo.mp4', // Local file
2094
+ playing: true,
2095
+ loop: false,
2096
+ volume: 1.0,
2097
+ playbackRate: 1.0,
2098
+ position: 0 // Start time in seconds
2099
+ })
2100
+
2101
+ // Step 3: Create video texture
2102
+ const videoTexture = Material.Texture.Video({ videoPlayerEntity: screen })
2103
+
2104
+ // Step 4: Apply to material (Basic material recommended)
2105
+ Material.setBasicMaterial(screen, {
2106
+ texture: videoTexture
2107
+ })
2108
+ ```
2109
+
2110
+ #### External Video Streaming
2111
+ ```typescript
2112
+ // Stream from external URL (must be HTTPS with CORS)
2113
+ VideoPlayer.create(screen, {
2114
+ src: 'https://player.vimeo.com/external/552481870.m3u8?s=c312c8533f97e808fccc92b0510b085c8122a875',
2115
+ playing: true
2116
+ })
2117
+
2118
+ // Supported formats: .mp4, .ogg, .webm, .m3u8
2119
+ ```
2120
+
2121
+ #### Live Streaming with Decentraland Cast
2122
+ ```typescript
2123
+ // Use Decentraland's built-in live streaming
2124
+ VideoPlayer.create(screen, {
2125
+ src: 'livekit-video://current-stream',
2126
+ playing: true
2127
+ })
2128
+
2129
+ // Requires Admin tools smart item for stream key management
2130
+ ```
2131
+
2132
+ #### Video Controls & Events
2133
+ ```typescript
2134
+ // Interactive video controls
2135
+ pointerEventsSystem.onPointerDown(
2136
+ {
2137
+ entity: screen,
2138
+ opts: { button: InputAction.IA_POINTER, hoverText: 'Play/Pause' }
2139
+ },
2140
+ () => {
2141
+ const video = VideoPlayer.getMutable(screen)
2142
+ video.playing = !video.playing
2143
+ }
2144
+ )
2145
+
2146
+ // Stop and rewind
2147
+ pointerEventsSystem.onPointerDown(
2148
+ {
2149
+ entity: stopButton,
2150
+ opts: { button: InputAction.IA_POINTER, hoverText: 'Stop' }
2151
+ },
2152
+ () => {
2153
+ const video = VideoPlayer.getMutable(screen)
2154
+ video.playing = false
2155
+ video.position = 0
2156
+ }
2157
+ )
2158
+
2159
+ // Video event handling
2160
+ import { videoEventsSystem, VideoState } from '@dcl/sdk/ecs'
2161
+
2162
+ videoEventsSystem.registerVideoEventsEntity(screen, (videoEvent) => {
2163
+ console.log('Video state:', videoEvent.state)
2164
+ console.log('Current time:', videoEvent.currentOffset)
2165
+ console.log('Video length:', videoEvent.videoLength)
2166
+
2167
+ switch (videoEvent.state) {
2168
+ case VideoState.VS_PLAYING:
2169
+ console.log('Video started playing')
2170
+ break
2171
+ case VideoState.VS_PAUSED:
2172
+ console.log('Video paused')
2173
+ break
2174
+ case VideoState.VS_READY:
2175
+ console.log('Video ready to play')
2176
+ break
2177
+ case VideoState.VS_ERROR:
2178
+ console.log('Video error occurred')
2179
+ break
2180
+ }
2181
+ })
2182
+
2183
+ // Get latest video state
2184
+ const latestEvent = videoEventsSystem.getVideoState(screen)
2185
+ if (latestEvent) {
2186
+ console.log('Latest state:', latestEvent.state)
2187
+ }
2188
+ ```
2189
+
2190
+ #### Enhanced Video Materials
2191
+ ```typescript
2192
+ // PBR material with enhanced video appearance
2193
+ Material.setPbrMaterial(screen, {
2194
+ texture: videoTexture,
2195
+ roughness: 1.0,
2196
+ specularIntensity: 0,
2197
+ metallic: 0,
2198
+ emissiveTexture: videoTexture,
2199
+ emissiveIntensity: 0.6,
2200
+ emissiveColor: Color3.White()
2201
+ })
2202
+
2203
+ // Basic material (recommended for performance)
2204
+ Material.setBasicMaterial(screen, {
2205
+ texture: videoTexture
2206
+ })
2207
+ ```
2208
+
2209
+ #### Multiple Video Screens
2210
+ ```typescript
2211
+ // Share one video across multiple screens
2212
+ const screen1 = engine.addEntity()
2213
+ const screen2 = engine.addEntity()
2214
+
2215
+ MeshRenderer.setPlane(screen1)
2216
+ MeshRenderer.setPlane(screen2)
2217
+ Transform.create(screen1, { position: Vector3.create(4, 1, 4) })
2218
+ Transform.create(screen2, { position: Vector3.create(6, 1, 4) })
2219
+
2220
+ // Only one VideoPlayer component needed
2221
+ VideoPlayer.create(screen1, {
2222
+ src: 'videos/shared-video.mp4',
2223
+ playing: true
2224
+ })
2225
+
2226
+ // Same texture applied to both screens
2227
+ const sharedTexture = Material.Texture.Video({ videoPlayerEntity: screen1 })
2228
+ Material.setBasicMaterial(screen1, { texture: sharedTexture })
2229
+ Material.setBasicMaterial(screen2, { texture: sharedTexture })
2230
+ ```
2231
+
2232
+ #### Circular Video Screens
2233
+ ```typescript
2234
+ // Create circular video screen with alpha mask
2235
+ const videoTexture = Material.Texture.Video({ videoPlayerEntity: screen })
2236
+ const alphaMask = Material.Texture.Common({
2237
+ src: 'assets/circle_mask.png',
2238
+ wrapMode: TextureWrapMode.TWM_MIRROR
2239
+ })
2240
+
2241
+ Material.setBasicMaterial(screen, {
2242
+ texture: videoTexture,
2243
+ alphaTexture: alphaMask
2244
+ })
2245
+ ```
2246
+
2247
+ #### Performance Considerations
2248
+ ```typescript
2249
+ // Video performance limits:
2250
+ // - Low quality: 1 simultaneous video
2251
+ // - Medium quality: 5 simultaneous videos
2252
+ // - High quality: 10 simultaneous videos
2253
+
2254
+ // Check if video should play based on distance
2255
+ function videoPerformanceSystem() {
2256
+ const playerPos = Transform.get(engine.PlayerEntity).position
2257
+
2258
+ for (const [entity, video] of engine.getEntitiesWith(VideoPlayer)) {
2259
+ const screenPos = Transform.get(entity).position
2260
+ const distance = Vector3.distance(playerPos, screenPos)
2261
+
2262
+ const videoMutable = VideoPlayer.getMutable(entity)
2263
+
2264
+ // Only play video when player is close
2265
+ if (distance < 10 && !videoMutable.playing) {
2266
+ videoMutable.playing = true
2267
+ } else if (distance > 15 && videoMutable.playing) {
2268
+ videoMutable.playing = false
2269
+ }
2270
+ }
2271
+ }
2272
+
2273
+ engine.addSystem(videoPerformanceSystem)
2274
+ ```
2275
+
2276
+ ### Audio Streaming
2277
+
2278
+ #### Stream Audio
2279
+ ```typescript
2280
+ AudioStream.create(entity, {
2281
+ url: 'https://example.com/stream.mp3',
2282
+ playing: true,
2283
+ volume: 0.7
2284
+ })
2285
+
2286
+ // Control stream
2287
+ const stream = AudioStream.getMutable(entity)
2288
+ stream.playing = false
2289
+ stream.volume = 0.3
2290
+ ```
2291
+
2292
+ ---
2293
+
2294
+ ## Networking
2295
+
2296
+ ### Network Connections
2297
+
2298
+ #### REST API Calls
2299
+ ```typescript
2300
+ executeTask(async () => {
2301
+ try {
2302
+ const response = await fetch('https://api.example.com/data')
2303
+ const data = await response.json()
2304
+ console.log('API response:', data)
2305
+ } catch (error) {
2306
+ console.log('API call failed:', error)
2307
+ }
2308
+ })
2309
+ ```
2310
+
2311
+ #### POST Requests
2312
+ ```typescript
2313
+ executeTask(async () => {
2314
+ try {
2315
+ const response = await fetch('https://api.example.com/submit', {
2316
+ method: 'POST',
2317
+ headers: {
2318
+ 'Content-Type': 'application/json'
2319
+ },
2320
+ body: JSON.stringify({
2321
+ username: 'player123',
2322
+ score: 1500
2323
+ })
2324
+ })
2325
+
2326
+ const result = await response.json()
2327
+ console.log('Submission result:', result)
2328
+ } catch (error) {
2329
+ console.log('Submission failed:', error)
2330
+ }
2331
+ })
2332
+ ```
2333
+
2334
+ ### Multiplayer Sync
2335
+
2336
+ #### Synced Entities
2337
+ ```typescript
2338
+ import { syncEntity } from '@dcl/sdk/network'
2339
+
2340
+ // Method 1: Sync predefined entities with explicit IDs
2341
+ enum EntityIds {
2342
+ DOOR = 1,
2343
+ ELEVATOR = 2,
2344
+ DRAWBRIDGE = 3
2345
+ }
2346
+
2347
+ const door = engine.addEntity()
2348
+ Transform.create(door, { position: Vector3.create(8, 1, 8) })
2349
+ MeshRenderer.setBox(door)
2350
+
2351
+ // Sync specific components with unique ID
2352
+ syncEntity(door, [Transform.componentId, MeshRenderer.componentId], EntityIds.DOOR)
2353
+
2354
+ // Method 2: Sync player-created entities (auto-assigned ID)
2355
+ function createProjectile() {
2356
+ const projectile = engine.addEntity()
2357
+ Transform.create(projectile, { position: Vector3.create(4, 1, 4) })
2358
+ MeshRenderer.setSphere(projectile)
2359
+
2360
+ // No explicit ID needed for player-created entities
2361
+ syncEntity(projectile, [Transform.componentId])
2362
+ return projectile
2363
+ }
2364
+ ```
2365
+
2366
+ #### Parent-Child Relationships in Multiplayer
2367
+ ```typescript
2368
+ import { syncEntity, parentEntity, getParent, getChildren } from '@dcl/sdk/network'
2369
+
2370
+ // Create parent and child entities
2371
+ const parent = engine.addEntity()
2372
+ const child = engine.addEntity()
2373
+
2374
+ // Both must be synced
2375
+ syncEntity(parent, [Transform.componentId], 1)
2376
+ syncEntity(child, [Transform.componentId], 2)
2377
+
2378
+ // Use parentEntity() instead of Transform.parent for synced entities
2379
+ parentEntity(child, parent)
2380
+
2381
+ // Helper functions
2382
+ const parentRef = getParent(child) // Returns parent entity
2383
+ const childrenArray = Array.from(getChildren(parent)) // Returns [child]
2384
+
2385
+ // Remove parent relationship
2386
+ removeParent(child) // Child becomes child of root entity
2387
+ ```
2388
+
2389
+ #### Check Sync State
2390
+ ```typescript
2391
+ import { isStateSyncronized } from '@dcl/sdk/network'
2392
+
2393
+ function gameStateSystem() {
2394
+ const isSynced = isStateSyncronized()
2395
+
2396
+ if (isSynced) {
2397
+ // Player is synchronized, allow interactions
2398
+ enableGameControls()
2399
+ } else {
2400
+ // Player not synchronized, disable interactions
2401
+ disableGameControls()
2402
+ showSyncingMessage()
2403
+ }
2404
+ }
2405
+
2406
+ engine.addSystem(gameStateSystem)
2407
+ ```
2408
+
2409
+ #### Message Bus
2410
+ ```typescript
2411
+ import { MessageBus } from '@dcl/sdk/message-bus'
2412
+
2413
+ const sceneMessageBus = new MessageBus()
2414
+
2415
+ // Send messages with payload
2416
+ sceneMessageBus.emit('player-action', {
2417
+ playerId: 'player123',
2418
+ action: 'jump',
2419
+ timestamp: Date.now(),
2420
+ position: Vector3.create(8, 1, 8)
2421
+ })
2422
+
2423
+ // Listen for messages
2424
+ type PlayerAction = {
2425
+ playerId: string
2426
+ action: string
2427
+ timestamp: number
2428
+ position: Vector3
2429
+ }
2430
+
2431
+ sceneMessageBus.on('player-action', (data: PlayerAction) => {
2432
+ console.log(`Player ${data.playerId} performed ${data.action}`)
2433
+
2434
+ // Handle the action for all players
2435
+ handlePlayerAction(data)
2436
+ })
2437
+
2438
+ // Complex multiplayer interaction example
2439
+ function createMultiplayerCube() {
2440
+ const cube = engine.addEntity()
2441
+ Transform.create(cube, { position: Vector3.create(8, 1, 8) })
2442
+ MeshRenderer.setBox(cube)
2443
+ Material.setPbrMaterial(cube, { albedoColor: Color4.Blue() })
2444
+
2445
+ syncEntity(cube, [Transform.componentId, Material.componentId], 100)
2446
+
2447
+ pointerEventsSystem.onPointerDown(
2448
+ {
2449
+ entity: cube,
2450
+ opts: { button: InputAction.IA_POINTER, hoverText: 'Change Color' }
2451
+ },
2452
+ () => {
2453
+ // Send message to all players about color change
2454
+ const newColor = Color4.create(Math.random(), Math.random(), Math.random(), 1)
2455
+
2456
+ sceneMessageBus.emit('cube-color-change', {
2457
+ cubeId: 100,
2458
+ color: newColor,
2459
+ timestamp: Date.now()
2460
+ })
2461
+ }
2462
+ )
2463
+ }
2464
+
2465
+ // Handle color change message
2466
+ sceneMessageBus.on('cube-color-change', (data: any) => {
2467
+ // Find the cube and update its color
2468
+ for (const [entity] of engine.getEntitiesWith(Transform, Material)) {
2469
+ // You'd need to track which entity has which ID
2470
+ const material = Material.getMutable(entity)
2471
+ material.albedoColor = data.color
2472
+ }
2473
+ })
2474
+ ```
2475
+
2476
+ #### Test Multiplayer Locally
2477
+ ```typescript
2478
+ // Open multiple browser windows to test multiplayer:
2479
+ // 1. Use Creator Hub Preview button multiple times
2480
+ // 2. Or use URL: decentraland://realm=http://127.0.0.1:8000&local-scene=true&debug=true
2481
+
2482
+ // Track multiple players for testing
2483
+ function multiplayerTestSystem() {
2484
+ const players = Array.from(engine.getEntitiesWith(PlayerIdentityData))
2485
+ console.log(`Active players: ${players.length}`)
2486
+
2487
+ players.forEach(([entity, playerData]) => {
2488
+ const transform = Transform.getOrNull(entity)
2489
+ if (transform) {
2490
+ console.log(`Player ${playerData.address} at position:`, transform.position)
2491
+ }
2492
+ })
2493
+ }
2494
+
2495
+ engine.addSystem(multiplayerTestSystem)
2496
+ ```
2497
+
2498
+ #### Single Player Mode (Worlds)
2499
+ ```typescript
2500
+ // For Decentraland Worlds, configure scene.json for single player
2501
+ /*
2502
+ {
2503
+ "worldConfiguration": {
2504
+ "name": "my-world.dcl.eth",
2505
+ "fixedAdapter": "offline:offline"
2506
+ }
2507
+ }
2508
+ */
2509
+
2510
+ // Players won't see each other and no sync is needed
2511
+ function singlePlayerScene() {
2512
+ // No need for syncEntity or MessageBus in offline mode
2513
+ const entity = engine.addEntity()
2514
+ Transform.create(entity, { position: Vector3.create(8, 1, 8) })
2515
+ MeshRenderer.setBox(entity)
2516
+
2517
+ // Direct state changes work fine in single player
2518
+ pointerEventsSystem.onPointerDown(
2519
+ { entity, opts: { button: InputAction.IA_POINTER } },
2520
+ () => {
2521
+ const transform = Transform.getMutable(entity)
2522
+ transform.position.y += 1
2523
+ }
2524
+ )
2525
+ }
2526
+ ```
2527
+
2528
+ ---
2529
+
2530
+ ## Libraries
2531
+
2532
+ ### Managing Dependencies
2533
+
2534
+ #### Update SDK
2535
+ ```bash
2536
+ npm install @dcl/sdk@latest
2537
+ ```
2538
+
2539
+ #### Add External Libraries
2540
+ ```bash
2541
+ npm install some-library
2542
+ ```
2543
+
2544
+ #### Package.json Example
2545
+ ```json
2546
+ {
2547
+ "dependencies": {
2548
+ "@dcl/sdk": "latest"
2549
+ },
2550
+ "devDependencies": {
2551
+ "@dcl/js-runtime": "latest"
2552
+ }
2553
+ }
2554
+ ```
2555
+
2556
+ ---
2557
+
2558
+ ## Debugging
2559
+
2560
+ ### Debug in Preview
2561
+
2562
+ #### Console Logging
2563
+ ```typescript
2564
+ // Basic logging
2565
+ console.log('Debug message:', data)
2566
+
2567
+ // Structured logging
2568
+ console.log('Entity transform:', {
2569
+ entity: entity,
2570
+ position: Transform.get(entity).position,
2571
+ rotation: Transform.get(entity).rotation
2572
+ })
2573
+ ```
2574
+
2575
+ #### Debug UI Overlay
2576
+ ```typescript
2577
+ // Debug info display
2578
+ export const DebugUI = () => {
2579
+ const [debugInfo, setDebugInfo] = useState({
2580
+ entities: 0,
2581
+ fps: 0,
2582
+ players: 0
2583
+ })
2584
+
2585
+ // Update debug info
2586
+ useEffect(() => {
2587
+ const interval = setInterval(() => {
2588
+ setDebugInfo({
2589
+ entities: Array.from(engine.getEntitiesWith(Transform)).length,
2590
+ fps: Math.round(1 / engine.deltaTime),
2591
+ players: Array.from(engine.getEntitiesWith(PlayerIdentityData)).length
2592
+ })
2593
+ }, 1000)
2594
+
2595
+ return () => clearInterval(interval)
2596
+ }, [])
2597
+
2598
+ return (
2599
+ <UiEntity
2600
+ uiTransform={{
2601
+ width: 200,
2602
+ height: 100,
2603
+ position: { top: '10px', right: '10px' },
2604
+ flexDirection: 'column'
2605
+ }}
2606
+ uiBackground={{ color: Color4.create(0, 0, 0, 0.7) }}
2607
+ >
2608
+ <UiEntity uiText={{ value: `Entities: ${debugInfo.entities}`, fontSize: 12 }} />
2609
+ <UiEntity uiText={{ value: `FPS: ${debugInfo.fps}`, fontSize: 12 }} />
2610
+ <UiEntity uiText={{ value: `Players: ${debugInfo.players}`, fontSize: 12 }} />
2611
+ </UiEntity>
2612
+ )
2613
+ }
2614
+ ```
2615
+
2616
+ #### Performance Monitoring
2617
+ ```typescript
2618
+ function performanceSystem(dt: number) {
2619
+ if (dt > 0.033) { // More than 30ms per frame
2620
+ console.log('Performance warning: Frame time:', dt * 1000, 'ms')
2621
+ }
2622
+ }
2623
+
2624
+ engine.addSystem(performanceSystem)
2625
+ ```
2626
+
2627
+ ### Troubleshooting
2628
+
2629
+ #### Common Issues and Solutions
2630
+
2631
+ **Entity not visible:**
2632
+ ```typescript
2633
+ // Check if entity has required components
2634
+ if (!MeshRenderer.has(entity)) {
2635
+ console.log('Entity missing MeshRenderer')
2636
+ }
2637
+ if (!Transform.has(entity)) {
2638
+ console.log('Entity missing Transform')
2639
+ }
2640
+ ```
2641
+
2642
+ **Click events not working:**
2643
+ ```typescript
2644
+ // Ensure entity has collider
2645
+ if (!MeshCollider.has(entity) && !GltfContainer.has(entity)) {
2646
+ console.log('Entity needs collider for click events')
2647
+ // Use pointer layer so raycasts/clicks hit this entity
2648
+ MeshCollider.setBox(entity, ColliderLayer.CL_POINTER)
2649
+ }
2650
+ ```
2651
+
2652
+ **Scene bounds checking:**
2653
+ ```typescript
2654
+ function checkBounds(entity: Entity) {
2655
+ const transform = Transform.get(entity)
2656
+ const pos = transform.position
2657
+
2658
+ if (pos.x < 0 || pos.x > 16 || pos.z < 0 || pos.z > 16) {
2659
+ console.log('Entity outside scene bounds:', pos)
2660
+ }
2661
+ }
2662
+ ```
2663
+
2664
+ ---
2665
+
2666
+ ## Programming Patterns
2667
+
2668
+ ### Async Functions
2669
+
2670
+ #### executeTask
2671
+ ```typescript
2672
+ // Use executeTask for async operations
2673
+ executeTask(async () => {
2674
+ try {
2675
+ const data = await fetch('https://api.example.com/data')
2676
+ const result = await data.json()
2677
+
2678
+ // Use the result in your scene
2679
+ updateSceneWithData(result)
2680
+ } catch (error) {
2681
+ console.log('Async operation failed:', error)
2682
+ }
2683
+ })
2684
+ ```
2685
+
2686
+ #### Timers and Delays
2687
+ ```typescript
2688
+ // Delay execution
2689
+ executeTask(async () => {
2690
+ await new Promise(resolve => setTimeout(resolve, 2000)) // Wait 2 seconds
2691
+ console.log('Delayed action executed')
2692
+ })
2693
+
2694
+ // Recurring timer
2695
+ let timerRunning = true
2696
+ executeTask(async () => {
2697
+ while (timerRunning) {
2698
+ await new Promise(resolve => setTimeout(resolve, 1000)) // Wait 1 second
2699
+ console.log('Timer tick')
2700
+ }
2701
+ })
2702
+ ```
2703
+
2704
+ ### Game Objects Pattern
2705
+
2706
+ #### Game Object Class
2707
+ ```typescript
2708
+ class GameObject {
2709
+ entity: Entity
2710
+
2711
+ constructor(position: Vector3) {
2712
+ this.entity = engine.addEntity()
2713
+ Transform.create(this.entity, { position })
2714
+ }
2715
+
2716
+ setPosition(position: Vector3) {
2717
+ Transform.getMutable(this.entity).position = position
2718
+ }
2719
+
2720
+ getPosition(): Vector3 {
2721
+ return Transform.get(this.entity).position
2722
+ }
2723
+
2724
+ destroy() {
2725
+ engine.removeEntity(this.entity)
2726
+ }
2727
+ }
2728
+
2729
+ class Enemy extends GameObject {
2730
+ health: number = 100
2731
+
2732
+ constructor(position: Vector3) {
2733
+ super(position)
2734
+ MeshRenderer.setBox(this.entity)
2735
+ Material.setPbrMaterial(this.entity, { albedoColor: Color4.Red() })
2736
+ }
2737
+
2738
+ takeDamage(amount: number) {
2739
+ this.health -= amount
2740
+ if (this.health <= 0) {
2741
+ this.destroy()
2742
+ }
2743
+ }
2744
+ }
2745
+
2746
+ // Usage
2747
+ const enemy = new Enemy(Vector3.create(8, 1, 8))
2748
+ enemy.takeDamage(50)
2749
+ ```
2750
+
2751
+ ### Component Factories
2752
+
2753
+ #### Reusable Component Creation
2754
+ ```typescript
2755
+ function createProjectile(start: Vector3, direction: Vector3, speed: number): Entity {
2756
+ const projectile = engine.addEntity()
2757
+
2758
+ Transform.create(projectile, {
2759
+ position: start,
2760
+ scale: Vector3.create(0.1, 0.1, 0.1)
2761
+ })
2762
+
2763
+ MeshRenderer.setSphere(projectile)
2764
+ Material.setPbrMaterial(projectile, {
2765
+ albedoColor: Color4.Yellow(),
2766
+ emissiveColor: Color4.Yellow()
2767
+ })
2768
+
2769
+ // Add movement component
2770
+ const MovementSchema = {
2771
+ velocity: Schemas.Vector3,
2772
+ speed: Schemas.Number
2773
+ }
2774
+ const Movement = engine.defineComponent('Movement', MovementSchema)
2775
+
2776
+ Movement.create(projectile, {
2777
+ velocity: Vector3.normalize(direction),
2778
+ speed: speed
2779
+ })
2780
+
2781
+ return projectile
2782
+ }
2783
+
2784
+ // Movement system for projectiles
2785
+ function projectileSystem(dt: number) {
2786
+ for (const [entity, movement] of engine.getEntitiesWith(Movement)) {
2787
+ const transform = Transform.getMutable(entity)
2788
+ const velocity = Vector3.scale(movement.velocity, movement.speed * dt)
2789
+ transform.position = Vector3.add(transform.position, velocity)
2790
+
2791
+ // Remove if out of bounds
2792
+ if (Vector3.length(transform.position) > 50) {
2793
+ engine.removeEntity(entity)
2794
+ }
2795
+ }
2796
+ }
2797
+
2798
+ engine.addSystem(projectileSystem)
2799
+ ```
2800
+
2801
+ ### State Machines
2802
+
2803
+ #### Simple State Machine
2804
+ ```typescript
2805
+ enum NPCState {
2806
+ IDLE = 'idle',
2807
+ WALKING = 'walking',
2808
+ ATTACKING = 'attacking',
2809
+ DEAD = 'dead'
2810
+ }
2811
+
2812
+ const NPCStateSchema = {
2813
+ currentState: Schemas.EnumString(NPCState, NPCState.IDLE),
2814
+ stateTimer: Schemas.Number
2815
+ }
2816
+
2817
+ const NPCStateMachine = engine.defineComponent('NPCStateMachine', NPCStateSchema)
2818
+
2819
+ function npcStateMachineSystem(dt: number) {
2820
+ for (const [entity, stateMachine] of engine.getEntitiesWith(NPCStateMachine)) {
2821
+ const state = NPCStateMachine.getMutable(entity)
2822
+ state.stateTimer += dt
2823
+
2824
+ switch (state.currentState) {
2825
+ case NPCState.IDLE:
2826
+ if (state.stateTimer > 3) {
2827
+ state.currentState = NPCState.WALKING
2828
+ state.stateTimer = 0
2829
+ }
2830
+ break
2831
+
2832
+ case NPCState.WALKING:
2833
+ // Move the NPC
2834
+ if (state.stateTimer > 5) {
2835
+ state.currentState = NPCState.IDLE
2836
+ state.stateTimer = 0
2837
+ }
2838
+ break
2839
+ }
2840
+ }
2841
+ }
2842
+
2843
+ engine.addSystem(npcStateMachineSystem)
2844
+ ```
2845
+
2846
+ ---
2847
+
2848
+ ## Projects
2849
+
2850
+ ### Scene Metadata
2851
+
2852
+ #### scene.json Configuration
2853
+ ```json
2854
+ {
2855
+ "display": {
2856
+ "title": "My Awesome Scene",
2857
+ "description": "An amazing Decentraland experience",
2858
+ "author": "Your Name",
2859
+ "version": "1.0.0",
2860
+ "navmapThumbnail": "images/scene-thumbnail.png",
2861
+ "favicon": "favicon_asset"
2862
+ },
2863
+ "contact": {
2864
+ "name": "Your Name",
2865
+ "email": "your.email@example.com"
2866
+ },
2867
+ "main": "bin/game.js",
2868
+ "tags": ["game", "interactive", "multiplayer"],
2869
+ "scene": {
2870
+ "parcels": ["0,0"],
2871
+ "base": "0,0"
2872
+ },
2873
+ "spawningPoints": [
2874
+ {
2875
+ "name": "spawn1",
2876
+ "default": true,
2877
+ "position": { "x": 8, "y": 0, "z": 8 },
2878
+ "cameraTarget": { "x": 8, "y": 1, "z": 12 }
2879
+ }
2880
+ ],
2881
+ "requiredPermissions": [
2882
+ "ALLOW_TO_MOVE_PLAYER_INSIDE_SCENE",
2883
+ "ALLOW_TO_TRIGGER_AVATAR_EMOTE"
2884
+ ]
2885
+ }
2886
+ ```
2887
+
2888
+ #### Multiple Parcels
2889
+ ```json
2890
+ {
2891
+ "scene": {
2892
+ "parcels": [
2893
+ "0,0", "1,0", "0,1", "1,1"
2894
+ ],
2895
+ "base": "0,0"
2896
+ }
2897
+ }
2898
+ ```
2899
+
2900
+ ### Smart Wearables
2901
+
2902
+ #### Smart Wearable Setup
2903
+ ```typescript
2904
+ // Smart wearable entry point
2905
+ export function main() {
2906
+ // Initialize wearable logic
2907
+ initializeWearable()
2908
+ }
2909
+
2910
+ function initializeWearable() {
2911
+ // Attach effects to player
2912
+ const effect = engine.addEntity()
2913
+
2914
+ Transform.create(effect, {
2915
+ parent: engine.PlayerEntity,
2916
+ position: Vector3.create(0, 0.5, 0)
2917
+ })
2918
+
2919
+ MeshRenderer.setSphere(effect)
2920
+ Material.setPbrMaterial(effect, {
2921
+ albedoColor: Color4.create(1, 1, 0, 0.5),
2922
+ emissiveColor: Color4.Yellow()
2923
+ })
2924
+ }
2925
+ ```
2926
+
2927
+ ### Portable Experiences
2928
+
2929
+ #### Portable Experience Structure
2930
+ ```typescript
2931
+ // Portable experience that follows player
2932
+ export function main() {
2933
+ createPortableUI()
2934
+
2935
+ // Listen for realm changes
2936
+ engine.addSystem(() => {
2937
+ // Update portable experience based on current realm
2938
+ })
2939
+ }
2940
+
2941
+ function createPortableUI() {
2942
+ // Create UI that's always available
2943
+ ReactEcsRenderer.setUiRenderer(() => (
2944
+ <UiEntity
2945
+ uiTransform={{
2946
+ position: { top: '10px', left: '10px' },
2947
+ width: 200,
2948
+ height: 50
2949
+ }}
2950
+ uiBackground={{ color: Color4.create(0, 0, 0, 0.8) }}
2951
+ uiText={{ value: 'Portable Experience Active', fontSize: 12 }}
2952
+ />
2953
+ ))
2954
+ }
2955
+ ```
2956
+
2957
+ ---
2958
+
2959
+ ## Publishing
2960
+
2961
+ ### Deployment Process
2962
+
2963
+ #### Build and Deploy
2964
+ ```bash
2965
+ # Build the scene
2966
+ npm run build
2967
+
2968
+ # Deploy to Decentraland
2969
+ npm run deploy
2970
+ ```
2971
+
2972
+ #### Deploy to Test Server
2973
+ ```bash
2974
+ # Deploy to test environment
2975
+ npm run deploy -- --target-content https://peer-testing.decentraland.org/content
2976
+ ```
2977
+
2978
+ #### Deploy to Custom World
2979
+ ```bash
2980
+ # Deploy to specific world
2981
+ npm run deploy -- --target worlds-content-server.decentraland.org/world/your-world-name
2982
+ ```
2983
+
2984
+ ### Publishing Requirements
2985
+
2986
+ #### LAND Ownership
2987
+ - Own LAND tokens
2988
+ - Have Decentraland NAME
2989
+ - Have ENS name
2990
+ - Get permissions from LAND owner
2991
+
2992
+ #### Content Validation
2993
+ - Scene must fit within parcel bounds
2994
+ - All assets must be under size limits
2995
+ - No prohibited content
2996
+ - Performance requirements met
2997
+
2998
+ #### Metadata Requirements
2999
+ - Title and description
3000
+ - Preview image (scene thumbnail)
3001
+ - Author information
3002
+ - Spawn points defined
3003
+
3004
+ ---
3005
+
3006
+ ## Optimization
3007
+
3008
+ ### Performance Guidelines
3009
+
3010
+ #### Entity Limits
3011
+ - Maximum entities per scene: ~10,000
3012
+ - Maximum polygons: Varies by parcel count
3013
+ - Texture memory: 64MB per parcel
3014
+ - Materials: 20 per scene recommended
3015
+
3016
+ #### Optimization Techniques
3017
+ ```typescript
3018
+ // Object pooling for projectiles
3019
+ class ProjectilePool {
3020
+ private pool: Entity[] = []
3021
+ private active: Entity[] = []
3022
+
3023
+ getProjectile(): Entity {
3024
+ if (this.pool.length > 0) {
3025
+ const projectile = this.pool.pop()!
3026
+ this.active.push(projectile)
3027
+ return projectile
3028
+ }
3029
+
3030
+ return this.createProjectile()
3031
+ }
3032
+
3033
+ releaseProjectile(projectile: Entity) {
3034
+ const index = this.active.indexOf(projectile)
3035
+ if (index > -1) {
3036
+ this.active.splice(index, 1)
3037
+ this.pool.push(projectile)
3038
+
3039
+ // Hide the projectile
3040
+ Transform.getMutable(projectile).position = Vector3.create(0, -100, 0)
3041
+ }
3042
+ }
3043
+
3044
+ private createProjectile(): Entity {
3045
+ const projectile = engine.addEntity()
3046
+ MeshRenderer.setSphere(projectile)
3047
+ Material.setPbrMaterial(projectile, { albedoColor: Color4.Yellow() })
3048
+ this.active.push(projectile)
3049
+ return projectile
3050
+ }
3051
+ }
3052
+ ```
3053
+
3054
+ #### LOD (Level of Detail)
3055
+ ```typescript
3056
+ function lodSystem() {
3057
+ const playerPos = Transform.get(engine.PlayerEntity).position
3058
+
3059
+ for (const [entity, transform] of engine.getEntitiesWith(Transform, MeshRenderer)) {
3060
+ const distance = Vector3.distance(playerPos, transform.position)
3061
+
3062
+ if (distance > 30) {
3063
+ // Far: hide entity
3064
+ VisibilityComponent.createOrReplace(entity, { visible: false })
3065
+ } else if (distance > 15) {
3066
+ // Medium: show simple version
3067
+ VisibilityComponent.createOrReplace(entity, { visible: true })
3068
+ // Could switch to lower poly model here
3069
+ } else {
3070
+ // Close: show full detail
3071
+ VisibilityComponent.createOrReplace(entity, { visible: true })
3072
+ }
3073
+ }
3074
+ }
3075
+
3076
+ engine.addSystem(lodSystem)
3077
+ ```
3078
+
3079
+ #### Texture Optimization
3080
+ ```typescript
3081
+ // Use compressed texture formats
3082
+ Material.setPbrMaterial(entity, {
3083
+ texture: Material.Texture.Common({
3084
+ src: 'assets/compressed_texture.webp', // Use WebP instead of PNG
3085
+ filterMode: TextureFilterMode.TFM_TRILINEAR
3086
+ })
3087
+ })
3088
+
3089
+ // Share textures between materials
3090
+ const sharedTexture = Material.Texture.Common({
3091
+ src: 'assets/shared_texture.webp'
3092
+ })
3093
+
3094
+ Material.setPbrMaterial(entity1, { texture: sharedTexture })
3095
+ Material.setPbrMaterial(entity2, { texture: sharedTexture })
3096
+ ```
3097
+
3098
+ ### Scene Limitations
3099
+
3100
+ #### Parcel-based Limits
3101
+ - 1 parcel: 16m x 16m area, ~20m height
3102
+ - More parcels = higher height limit
3103
+ - Materials: 20 per scene recommended
3104
+ - Textures: 512x512 recommended, 1024x1024 max
3105
+
3106
+ #### Performance Targets
3107
+ - 30 FPS minimum
3108
+ - <100ms system execution per frame
3109
+ - Reasonable memory usage
3110
+
3111
+ ---
3112
+
3113
+ ## Design & Experience
3114
+
3115
+ ### UX Guidelines
3116
+
3117
+ #### Player Onboarding
3118
+ ```typescript
3119
+ // Welcome sequence
3120
+ function createWelcomeSequence() {
3121
+ // Show welcome message
3122
+ ui.displayAnnouncement('Welcome to the scene!')
3123
+
3124
+ // Highlight interactive objects
3125
+ for (const [entity] of engine.getEntitiesWith(PointerEvents)) {
3126
+ addGlowEffect(entity)
3127
+ }
3128
+
3129
+ // Remove highlights after delay
3130
+ setTimeout(() => {
3131
+ for (const [entity] of engine.getEntitiesWith(GlowEffect)) {
3132
+ GlowEffect.deleteFrom(entity)
3133
+ }
3134
+ }, 10000)
3135
+ }
3136
+ ```
3137
+
3138
+ #### Clear Visual Feedback
3139
+ ```typescript
3140
+ // Hover feedback for interactive objects
3141
+ pointerEventsSystem.onPointerDown(
3142
+ {
3143
+ entity: button,
3144
+ opts: {
3145
+ button: InputAction.IA_POINTER,
3146
+ hoverText: 'Click to activate',
3147
+ maxDistance: 8,
3148
+ showFeedback: true // Shows outline when hovered
3149
+ }
3150
+ },
3151
+ () => {
3152
+ // Provide immediate feedback
3153
+ ui.displayAnnouncement('Activated!')
3154
+
3155
+ // Visual feedback
3156
+ const transform = Transform.getMutable(button)
3157
+ Tween.create(button, {
3158
+ mode: Tween.Mode.Scale({
3159
+ start: transform.scale,
3160
+ end: Vector3.scale(transform.scale, 1.2)
3161
+ }),
3162
+ duration: 200,
3163
+ easingFunction: EasingFunction.EF_EASEOUTBOUNCE
3164
+ })
3165
+ }
3166
+ )
3167
+ ```
3168
+
3169
+ #### Accessibility Considerations
3170
+ ```typescript
3171
+ // Text legibility
3172
+ TextShape.create(entity, {
3173
+ text: 'Important Information',
3174
+ fontSize: 24,
3175
+ color: Color4.White(),
3176
+ outlineColor: Color4.Black(),
3177
+ outlineWidth: 0.1 // Improves readability
3178
+ })
3179
+
3180
+ // Audio cues
3181
+ function playAudioCue(sound: string) {
3182
+ const audioEntity = engine.addEntity()
3183
+ AudioSource.create(audioEntity, {
3184
+ audioClipUrl: `sounds/${sound}.mp3`,
3185
+ playing: true,
3186
+ volume: 0.8
3187
+ })
3188
+
3189
+ // Clean up after playing
3190
+ setTimeout(() => {
3191
+ engine.removeEntity(audioEntity)
3192
+ }, 3000)
3193
+ }
3194
+ ```
3195
+
3196
+ ### Game Design Patterns
3197
+
3198
+ #### Quest System
3199
+ ```typescript
3200
+ interface Quest {
3201
+ id: string
3202
+ title: string
3203
+ description: string
3204
+ objectives: QuestObjective[]
3205
+ completed: boolean
3206
+ }
3207
+
3208
+ interface QuestObjective {
3209
+ id: string
3210
+ description: string
3211
+ completed: boolean
3212
+ }
3213
+
3214
+ class QuestManager {
3215
+ private quests: Map<string, Quest> = new Map()
3216
+
3217
+ addQuest(quest: Quest) {
3218
+ this.quests.set(quest.id, quest)
3219
+ }
3220
+
3221
+ completeObjective(questId: string, objectiveId: string) {
3222
+ const quest = this.quests.get(questId)
3223
+ if (!quest) return
3224
+
3225
+ const objective = quest.objectives.find(o => o.id === objectiveId)
3226
+ if (objective) {
3227
+ objective.completed = true
3228
+
3229
+ // Check if all objectives completed
3230
+ if (quest.objectives.every(o => o.completed)) {
3231
+ quest.completed = true
3232
+ this.onQuestCompleted(quest)
3233
+ }
3234
+ }
3235
+ }
3236
+
3237
+ private onQuestCompleted(quest: Quest) {
3238
+ ui.displayAnnouncement(`Quest completed: ${quest.title}`)
3239
+ // Award rewards, etc.
3240
+ }
3241
+ }
3242
+ ```
3243
+
3244
+ #### Inventory System
3245
+ ```typescript
3246
+ interface InventoryItem {
3247
+ id: string
3248
+ name: string
3249
+ icon: string
3250
+ quantity: number
3251
+ }
3252
+
3253
+ class Inventory {
3254
+ private items: Map<string, InventoryItem> = new Map()
3255
+ private maxSlots: number = 20
3256
+
3257
+ addItem(itemId: string, quantity: number = 1): boolean {
3258
+ if (this.items.size >= this.maxSlots && !this.items.has(itemId)) {
3259
+ return false // Inventory full
3260
+ }
3261
+
3262
+ const existingItem = this.items.get(itemId)
3263
+ if (existingItem) {
3264
+ existingItem.quantity += quantity
3265
+ } else {
3266
+ // Add new item (would need item definition lookup)
3267
+ this.items.set(itemId, {
3268
+ id: itemId,
3269
+ name: 'Item Name',
3270
+ icon: 'item_icon.png',
3271
+ quantity: quantity
3272
+ })
3273
+ }
3274
+
3275
+ this.updateInventoryUI()
3276
+ return true
3277
+ }
3278
+
3279
+ removeItem(itemId: string, quantity: number = 1): boolean {
3280
+ const item = this.items.get(itemId)
3281
+ if (!item || item.quantity < quantity) {
3282
+ return false
3283
+ }
3284
+
3285
+ item.quantity -= quantity
3286
+ if (item.quantity <= 0) {
3287
+ this.items.delete(itemId)
3288
+ }
3289
+
3290
+ this.updateInventoryUI()
3291
+ return true
3292
+ }
3293
+
3294
+ private updateInventoryUI() {
3295
+ // Update UI to reflect inventory changes
3296
+ }
3297
+ }
3298
+ ```
3299
+
3300
+ ---
3301
+
3302
+ ## Web Editor
3303
+
3304
+ ### Smart Items
3305
+
3306
+ #### Creating Smart Items
3307
+ ```typescript
3308
+ // Smart Item definition
3309
+ export interface SmartItemProps {
3310
+ enabled: boolean
3311
+ clickText: string
3312
+ onActivate?: () => void
3313
+ }
3314
+
3315
+ export function SmartButton({ enabled, clickText, onActivate }: SmartItemProps) {
3316
+ const entity = engine.addEntity()
3317
+
3318
+ MeshRenderer.setBox(entity)
3319
+ Material.setPbrMaterial(entity, {
3320
+ albedoColor: enabled ? Color4.Green() : Color4.Gray()
3321
+ })
3322
+
3323
+ if (enabled && onActivate) {
3324
+ pointerEventsSystem.onPointerDown(
3325
+ {
3326
+ entity: entity,
3327
+ opts: { button: InputAction.IA_POINTER, hoverText: clickText }
3328
+ },
3329
+ onActivate
3330
+ )
3331
+ }
3332
+
3333
+ return entity
3334
+ }
3335
+ ```
3336
+
3337
+ #### Smart Item Actions
3338
+ ```typescript
3339
+ // Available actions that can be triggered
3340
+ export enum SmartItemAction {
3341
+ ACTIVATE = 'activate',
3342
+ DEACTIVATE = 'deactivate',
3343
+ TOGGLE = 'toggle',
3344
+ MOVE_TO = 'moveTo',
3345
+ ROTATE_TO = 'rotateTo',
3346
+ SCALE_TO = 'scaleTo',
3347
+ CHANGE_COLOR = 'changeColor',
3348
+ PLAY_SOUND = 'playSound',
3349
+ SHOW_TEXT = 'showText'
3350
+ }
3351
+
3352
+ // Action implementation
3353
+ export function executeAction(entity: Entity, action: SmartItemAction, parameters: any) {
3354
+ switch (action) {
3355
+ case SmartItemAction.MOVE_TO:
3356
+ Tween.create(entity, {
3357
+ mode: Tween.Mode.Move({
3358
+ start: Transform.get(entity).position,
3359
+ end: parameters.position
3360
+ }),
3361
+ duration: parameters.duration || 2000,
3362
+ easingFunction: EasingFunction.EF_LINEAR
3363
+ })
3364
+ break
3365
+
3366
+ case SmartItemAction.CHANGE_COLOR:
3367
+ Material.setPbrMaterial(entity, {
3368
+ albedoColor: parameters.color
3369
+ })
3370
+ break
3371
+
3372
+ case SmartItemAction.PLAY_SOUND:
3373
+ AudioSource.createOrReplace(entity, {
3374
+ audioClipUrl: parameters.soundUrl,
3375
+ playing: true,
3376
+ volume: parameters.volume || 1.0
3377
+ })
3378
+ break
3379
+ }
3380
+ }
3381
+ ```
3382
+
3383
+ ### Combine Scene Editor with Code
3384
+
3385
+ Link your scene code to entities created and configured via the Creator Hub.
3386
+
3387
+ #### Reference entities by name
3388
+ ```typescript
3389
+ import { EntityNames } from '../assets/scene/entity-names'
3390
+
3391
+ export function main() {
3392
+ // Get by enum (generated by Creator Hub)
3393
+ const door1 = engine.getEntityOrNullByName(EntityNames.Door_1)
3394
+
3395
+ // Get by string name (as shown in the Scene Editor tree)
3396
+ const door2 = engine.getEntityOrNullByName('Door 2')
3397
+
3398
+ if (door1 && door2) {
3399
+ pointerEventsSystem.onPointerDown(
3400
+ { entity: door1, opts: { button: InputAction.IA_PRIMARY, hoverText: 'Open' } },
3401
+ () => {
3402
+ // custom logic
3403
+ }
3404
+ )
3405
+ }
3406
+ }
3407
+ ```
3408
+
3409
+ Validate existence at compile-time with a generic:
3410
+ ```typescript
3411
+ import { EntityNames } from '../assets/scene/entity-names'
3412
+
3413
+ const door = engine.getEntityByName<EntityNames>(EntityNames.Door_1)
3414
+ // No null-check needed
3415
+ console.log(Transform.get(door).position.x)
3416
+ ```
3417
+
3418
+ Only reference by name inside `main()`, systems, or functions called after `main()` to ensure entities are instantiated.
3419
+
3420
+ #### Iterate named entities and fetch children
3421
+ ```typescript
3422
+ import { Name } from '@dcl/sdk/ecs'
3423
+
3424
+ // Iterate all named entities
3425
+ for (const [entity, name] of engine.getEntitiesWith(Name)) {
3426
+ console.log({ entity, name })
3427
+ }
3428
+
3429
+ // Helper to get all children of a parent entity
3430
+ function getChildren(parent: Entity): Entity[] {
3431
+ const childEntities: Entity[] = []
3432
+ for (const [entity, transform] of engine.getEntitiesWith(Transform)) {
3433
+ if (transform.parent === parent) childEntities.push(entity)
3434
+ }
3435
+ return childEntities
3436
+ }
3437
+ ```
3438
+
3439
+ #### Fetch entities by tag
3440
+ ```typescript
3441
+ import { engine } from '@dcl/sdk/ecs'
3442
+
3443
+ export function main() {
3444
+ const tagged = engine.getEntitiesByTag('myTag')
3445
+ for (const entity of tagged) {
3446
+ // Handle each tagged entity
3447
+ }
3448
+ }
3449
+ ```
3450
+
3451
+ Add or remove tags from code:
3452
+ ```typescript
3453
+ import { Tags } from '@dcl/sdk/ecs'
3454
+
3455
+ Tags.add(entity, 'myTag')
3456
+ Tags.remove(entity, 'myTag')
3457
+ ```
3458
+
3459
+ #### Smart item triggers (Creator Hub asset-packs)
3460
+ ```typescript
3461
+ import { getTriggerEvents } from '@dcl/asset-packs/dist/events'
3462
+ import { TriggerType } from '@dcl/asset-packs'
3463
+ import { EntityNames } from '../assets/scene/entity-names'
3464
+
3465
+ export function main() {
3466
+ const restart = engine.getEntityOrNullByName(EntityNames.Restart_Button)
3467
+ if (restart) {
3468
+ const triggers = getTriggerEvents(restart)
3469
+ triggers.on(TriggerType.ON_CLICK, () => {
3470
+ // restartGame()
3471
+ })
3472
+ }
3473
+ }
3474
+ ```
3475
+
3476
+ #### Smart item actions (listen and emit)
3477
+ ```typescript
3478
+ import { getTriggerEvents, getActionEvents } from '@dcl/asset-packs/dist/events'
3479
+ import { TriggerType } from '@dcl/asset-packs'
3480
+ import { EntityNames } from '../assets/scene/entity-names'
3481
+
3482
+ export function main() {
3483
+ const button = engine.getEntityOrNullByName(EntityNames.Red_Button)
3484
+ const door = engine.getEntityOrNullByName(EntityNames.Wooden_Door)
3485
+ if (button && door) {
3486
+ const buttonTriggers = getTriggerEvents(button)
3487
+ const doorActions = getActionEvents(door)
3488
+
3489
+ // Listen to actions
3490
+ doorActions.on('Open', () => {
3491
+ console.log('Door opened!')
3492
+ })
3493
+
3494
+ // Emit an action when button is triggered
3495
+ buttonTriggers.on(TriggerType.ON_INPUT_ACTION, () => {
3496
+ doorActions.emit('Open', {})
3497
+ })
3498
+ }
3499
+ }
3500
+ ```
3501
+
3502
+ #### Read other smart item components
3503
+ ```typescript
3504
+ import { getComponents } from '@dcl/asset-packs'
3505
+ import { getTriggerEvents } from '@dcl/asset-packs/dist/events'
3506
+ import { TriggerType } from '@dcl/asset-packs'
3507
+ import { EntityNames } from '../assets/scene/entity-names'
3508
+
3509
+ export function main() {
3510
+ const chest = engine.getEntityOrNullByName(EntityNames.chest)
3511
+ if (chest) {
3512
+ const chestTriggers = getTriggerEvents(chest)
3513
+ chestTriggers.on(TriggerType.ON_INPUT_ACTION, () => {
3514
+ const { States } = getComponents(engine)
3515
+ const current = States.getMutableOrNull(chest)?.currentValue
3516
+ console.log('chest new state', current)
3517
+ })
3518
+ }
3519
+ }
3520
+ ```
3521
+
3522
+ ---
3523
+
3524
+ ## Advanced Topics
3525
+
3526
+ ### Custom Shaders (Not Currently Supported)
3527
+ *Note: Custom shaders are not currently supported in Decentraland SDK7, but this section provides context for future features.*
3528
+
3529
+ ### Physics Simulation
3530
+ *Note: Advanced physics beyond basic colliders are not currently available in SDK7.*
3531
+
3532
+ ### WebAssembly Integration
3533
+ *Note: WASM support is limited in the current SDK7 implementation.*
3534
+
3535
+ ### Scene Analytics
3536
+
3537
+ #### Basic Analytics
3538
+ ```typescript
3539
+ // Track player interactions
3540
+ function trackInteraction(action: string, object: string) {
3541
+ console.log(`Analytics: ${action} on ${object}`)
3542
+
3543
+ // Send to analytics service
3544
+ executeTask(async () => {
3545
+ try {
3546
+ await fetch('https://analytics.example.com/track', {
3547
+ method: 'POST',
3548
+ headers: { 'Content-Type': 'application/json' },
3549
+ body: JSON.stringify({
3550
+ action: action,
3551
+ object: object,
3552
+ timestamp: Date.now(),
3553
+ scene: 'my-scene-id'
3554
+ })
3555
+ })
3556
+ } catch (error) {
3557
+ console.log('Analytics tracking failed:', error)
3558
+ }
3559
+ })
3560
+ }
3561
+
3562
+ // Usage
3563
+ pointerEventsSystem.onPointerDown(
3564
+ { entity: button, opts: { button: InputAction.IA_POINTER } },
3565
+ () => {
3566
+ trackInteraction('click', 'main-button')
3567
+ }
3568
+ )
3569
+ ```
3570
+
3571
+ ### Scene Boundaries and Validation
3572
+
3573
+ #### Boundary Checking
3574
+ ```typescript
3575
+ function boundaryCheckSystem() {
3576
+ for (const [entity, transform] of engine.getEntitiesWith(Transform)) {
3577
+ const pos = transform.position
3578
+
3579
+ // Check if entity is outside scene bounds
3580
+ if (pos.x < 0 || pos.x > 16 || pos.z < 0 || pos.z > 16 || pos.y > 20) {
3581
+ console.log('Entity outside bounds:', entity, pos)
3582
+
3583
+ // Optionally move back to bounds
3584
+ const mutableTransform = Transform.getMutable(entity)
3585
+ mutableTransform.position = Vector3.create(
3586
+ Math.max(0, Math.min(16, pos.x)),
3587
+ Math.max(0, Math.min(20, pos.y)),
3588
+ Math.max(0, Math.min(16, pos.z))
3589
+ )
3590
+ }
3591
+ }
3592
+ }
3593
+
3594
+ engine.addSystem(boundaryCheckSystem)
3595
+ ```
3596
+
3597
+ ---
3598
+
3599
+ ## Common Patterns and Best Practices
3600
+
3601
+ ### Entity Management
3602
+ ```typescript
3603
+ // Entity factory pattern
3604
+ class EntityFactory {
3605
+ static createPlayer(position: Vector3): Entity {
3606
+ const player = engine.addEntity()
3607
+ Transform.create(player, { position })
3608
+ MeshRenderer.setBox(player)
3609
+ Material.setPbrMaterial(player, { albedoColor: Color4.Blue() })
3610
+ return player
3611
+ }
3612
+
3613
+ static createPickup(position: Vector3, type: string): Entity {
3614
+ const pickup = engine.addEntity()
3615
+ Transform.create(pickup, { position })
3616
+ MeshRenderer.setSphere(pickup)
3617
+
3618
+ // Add pickup component
3619
+ const PickupSchema = { type: Schemas.String }
3620
+ const Pickup = engine.defineComponent('Pickup', PickupSchema)
3621
+ Pickup.create(pickup, { type })
3622
+
3623
+ return pickup
3624
+ }
3625
+ }
3626
+ ```
3627
+
3628
+ ### Component Composition
3629
+ ```typescript
3630
+ // Composable behavior components
3631
+ const HealthSchema = { current: Schemas.Number, max: Schemas.Number }
3632
+ const MovementSchema = { speed: Schemas.Number, direction: Schemas.Vector3 }
3633
+ const AISchema = { state: Schemas.String, target: Schemas.Entity }
3634
+
3635
+ const Health = engine.defineComponent('Health', HealthSchema)
3636
+ const Movement = engine.defineComponent('Movement', MovementSchema)
3637
+ const AI = engine.defineComponent('AI', AISchema)
3638
+
3639
+ // Create different entity types by combining components
3640
+ function createPlayer(position: Vector3) {
3641
+ const player = engine.addEntity()
3642
+ Transform.create(player, { position })
3643
+ Health.create(player, { current: 100, max: 100 })
3644
+ Movement.create(player, { speed: 5, direction: Vector3.Zero() })
3645
+ // No AI component - player controlled
3646
+ }
3647
+
3648
+ function createEnemy(position: Vector3) {
3649
+ const enemy = engine.addEntity()
3650
+ Transform.create(enemy, { position })
3651
+ Health.create(enemy, { current: 50, max: 50 })
3652
+ Movement.create(enemy, { speed: 2, direction: Vector3.Zero() })
3653
+ AI.create(enemy, { state: 'patrol', target: 0 as Entity })
3654
+ }
3655
+ ```
3656
+
3657
+ ### Error Handling
3658
+ ```typescript
3659
+ // Safe component access
3660
+ function safeGetTransform(entity: Entity): Vector3 | null {
3661
+ try {
3662
+ if (Transform.has(entity)) {
3663
+ return Transform.get(entity).position
3664
+ }
3665
+ return null
3666
+ } catch (error) {
3667
+ console.log('Error getting transform:', error)
3668
+ return null
3669
+ }
3670
+ }
3671
+
3672
+ // Graceful degradation
3673
+ function attemptAction(entity: Entity, action: () => void) {
3674
+ try {
3675
+ action()
3676
+ } catch (error) {
3677
+ console.log('Action failed:', error)
3678
+ // Provide fallback behavior
3679
+ ui.displayAnnouncement('Action temporarily unavailable')
3680
+ }
3681
+ }
3682
+ ```
3683
+
3684
+ This comprehensive reference covers all major aspects of Decentraland SDK7 development, from basic setup to advanced patterns and optimization techniques. Use this as a complete reference for building scenes, implementing interactivity, managing assets, and creating engaging experiences in Decentraland.