@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.
- package/README.md +234 -0
- package/context/components-reference.md +113 -0
- package/context/open-source-3d-assets.md +705 -0
- package/context/sdk7-complete-reference.md +3684 -0
- package/context/sdk7-examples.md +1709 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/scene-context.d.ts +97 -0
- package/dist/scene-context.d.ts.map +1 -0
- package/dist/scene-context.js +203 -0
- package/dist/scene-context.js.map +1 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +4 -0
- package/dist/utils.js.map +1 -0
- package/extensions/dcl-context.ts +123 -0
- package/extensions/dcl-deploy.ts +89 -0
- package/extensions/dcl-header.ts +162 -0
- package/extensions/dcl-init.ts +62 -0
- package/extensions/dcl-preview.ts +144 -0
- package/extensions/dcl-setup-ollama.ts +312 -0
- package/extensions/dcl-setup.ts +96 -0
- package/extensions/dcl-status.ts +88 -0
- package/extensions/dcl-tasks.ts +102 -0
- package/extensions/dcl-update-check.ts +79 -0
- package/extensions/dcl-validate.ts +80 -0
- package/extensions/plan-mode/index.ts +340 -0
- package/extensions/plan-mode/utils.ts +168 -0
- package/extensions/process-registry.ts +25 -0
- package/extensions/scene-utils.ts +31 -0
- package/package.json +65 -0
- package/prompts/explain.md +16 -0
- package/prompts/review.md +19 -0
- package/prompts/system.md +126 -0
- package/skills/add-3d-models/SKILL.md +115 -0
- package/skills/add-interactivity/SKILL.md +176 -0
- package/skills/advanced-input/SKILL.md +238 -0
- package/skills/advanced-rendering/SKILL.md +235 -0
- package/skills/animations-tweens/SKILL.md +173 -0
- package/skills/audio-video/SKILL.md +167 -0
- package/skills/authoritative-server/SKILL.md +329 -0
- package/skills/build-ui/SKILL.md +231 -0
- package/skills/camera-control/SKILL.md +199 -0
- package/skills/create-scene/SKILL.md +67 -0
- package/skills/deploy-scene/SKILL.md +106 -0
- package/skills/deploy-worlds/SKILL.md +107 -0
- package/skills/lighting-environment/SKILL.md +216 -0
- package/skills/multiplayer-sync/SKILL.md +132 -0
- package/skills/nft-blockchain/SKILL.md +246 -0
- package/skills/optimize-scene/SKILL.md +160 -0
- package/skills/player-avatar/SKILL.md +239 -0
- 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.
|