@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,1709 @@
|
|
|
1
|
+
# Decentraland SDK 7 Scenes Context7 Reference
|
|
2
|
+
|
|
3
|
+
This reference documents common patterns, components, and systems used in Decentraland SDK 7 scenes based on example implementations.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- @Scene Structure
|
|
7
|
+
- @Entity-Component System
|
|
8
|
+
- @Component Reference
|
|
9
|
+
- @UI System
|
|
10
|
+
- @Runtime Data
|
|
11
|
+
- @Player Data & Camera Controls
|
|
12
|
+
- @Input Handling
|
|
13
|
+
- @Event Listeners
|
|
14
|
+
- @Movement & Animation
|
|
15
|
+
- @Lights & Visual Effects
|
|
16
|
+
- @Triggers & Interactions
|
|
17
|
+
- @Scene Optimization
|
|
18
|
+
- @Restricted Actions
|
|
19
|
+
- @Testing Framework
|
|
20
|
+
- @Network Connections
|
|
21
|
+
- @Blockchain Operations
|
|
22
|
+
|
|
23
|
+
## Scene Structure
|
|
24
|
+
|
|
25
|
+
### Basic Project Structure
|
|
26
|
+
```
|
|
27
|
+
├── src/
|
|
28
|
+
│ ├── index.ts # Main entry point
|
|
29
|
+
│ ├── components.ts # Custom component definitions
|
|
30
|
+
│ ├── systems.ts # Custom system implementations
|
|
31
|
+
│ ├── factory.ts # Entity creation functions
|
|
32
|
+
│ ├── utils.ts # Helper functions
|
|
33
|
+
│ └── ui.tsx # UI definitions with React
|
|
34
|
+
├── package.json
|
|
35
|
+
└── tsconfig.json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Main Entry Point
|
|
39
|
+
```typescript
|
|
40
|
+
// index.ts
|
|
41
|
+
import { engine } from '@dcl/sdk/ecs'
|
|
42
|
+
import { setupUi } from './ui'
|
|
43
|
+
import { mySystem } from './systems'
|
|
44
|
+
|
|
45
|
+
export function main() {
|
|
46
|
+
// Add systems to the engine
|
|
47
|
+
engine.addSystem(mySystem)
|
|
48
|
+
|
|
49
|
+
// Initialize UI
|
|
50
|
+
setupUi()
|
|
51
|
+
|
|
52
|
+
// Create initial entities
|
|
53
|
+
// ...
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Entity-Component System
|
|
58
|
+
|
|
59
|
+
### Creating Entities
|
|
60
|
+
```typescript
|
|
61
|
+
import { engine, Transform, MeshRenderer, MeshCollider } from '@dcl/sdk/ecs'
|
|
62
|
+
import { Vector3 } from '@dcl/sdk/math'
|
|
63
|
+
|
|
64
|
+
// Create a new entity
|
|
65
|
+
const entity = engine.addEntity()
|
|
66
|
+
|
|
67
|
+
// Add components to the entity
|
|
68
|
+
Transform.create(entity, {
|
|
69
|
+
position: Vector3.create(8, 1, 8),
|
|
70
|
+
scale: Vector3.create(1, 1, 1)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Add a visual mesh
|
|
74
|
+
MeshRenderer.setBox(entity) // Predefined shapes: setBox, setSphere, setPlane, etc.
|
|
75
|
+
|
|
76
|
+
// Add collision
|
|
77
|
+
MeshCollider.setBox(entity)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Defining Custom Components
|
|
81
|
+
```typescript
|
|
82
|
+
// components.ts
|
|
83
|
+
import { Schemas, engine } from '@dcl/sdk/ecs'
|
|
84
|
+
|
|
85
|
+
// Define a component with properties
|
|
86
|
+
export const Spinner = engine.defineComponent('spinner', {
|
|
87
|
+
speed: Schemas.Number
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Define a tag component (no properties)
|
|
91
|
+
export const Cube = engine.defineComponent('cube-id', {})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Creating Systems
|
|
95
|
+
```typescript
|
|
96
|
+
// systems.ts
|
|
97
|
+
import { engine, Transform } from '@dcl/sdk/ecs'
|
|
98
|
+
import { Quaternion, Vector3 } from '@dcl/sdk/math'
|
|
99
|
+
import { Spinner } from './components'
|
|
100
|
+
|
|
101
|
+
// System that rotates entities with the Spinner component
|
|
102
|
+
export function circularSystem(dt: number) {
|
|
103
|
+
// Query all entities with both Spinner and Transform components
|
|
104
|
+
const entitiesWithSpinner = engine.getEntitiesWith(Spinner, Transform)
|
|
105
|
+
|
|
106
|
+
for (const [entity, spinner, transform] of entitiesWithSpinner) {
|
|
107
|
+
// Get a mutable reference to modify the component
|
|
108
|
+
const mutableTransform = Transform.getMutable(entity)
|
|
109
|
+
|
|
110
|
+
// Apply rotation based on the spinner speed and delta time
|
|
111
|
+
mutableTransform.rotation = Quaternion.multiply(
|
|
112
|
+
mutableTransform.rotation,
|
|
113
|
+
Quaternion.fromAngleAxis(dt * spinner.speed, Vector3.Up())
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Component Reference
|
|
120
|
+
|
|
121
|
+
### Transform
|
|
122
|
+
```typescript
|
|
123
|
+
// Position, rotation, and scale
|
|
124
|
+
Transform.create(entity, {
|
|
125
|
+
position: Vector3.create(x, y, z),
|
|
126
|
+
rotation: Quaternion.fromEulerDegrees(x, y, z), // or Quaternion.create()
|
|
127
|
+
scale: Vector3.create(x, y, z),
|
|
128
|
+
parent: parentEntity // optional, for hierarchical transformations
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Update transform
|
|
132
|
+
const transform = Transform.getMutable(entity)
|
|
133
|
+
transform.position = Vector3.create(newX, newY, newZ)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Mesh Rendering
|
|
137
|
+
```typescript
|
|
138
|
+
// Basic shapes
|
|
139
|
+
MeshRenderer.setBox(entity)
|
|
140
|
+
MeshRenderer.setSphere(entity)
|
|
141
|
+
MeshRenderer.setPlane(entity)
|
|
142
|
+
|
|
143
|
+
// Material
|
|
144
|
+
import { Material, MeshRenderer } from '@dcl/sdk/ecs'
|
|
145
|
+
import { Color4 } from '@dcl/sdk/math'
|
|
146
|
+
|
|
147
|
+
// PBR material
|
|
148
|
+
Material.setPbrMaterial(entity, {
|
|
149
|
+
albedoColor: Color4.fromHexString("#FF0000"),
|
|
150
|
+
metallic: 0.5,
|
|
151
|
+
roughness: 0.5,
|
|
152
|
+
// other properties: emissiveColor, reflectivityColor, etc.
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// Basic material
|
|
156
|
+
Material.setBasicMaterial(entity, {
|
|
157
|
+
diffuseColor: Color4.White()
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 3D Models
|
|
162
|
+
```typescript
|
|
163
|
+
import { GltfContainer } from '@dcl/sdk/ecs'
|
|
164
|
+
|
|
165
|
+
// Load a 3D model
|
|
166
|
+
GltfContainer.create(entity, {
|
|
167
|
+
src: 'models/model.glb',
|
|
168
|
+
visibleMeshesCollisionMask: ColliderLayer.CL_POINTER // Optional
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// Check loading state
|
|
172
|
+
if (GltfContainerLoadingState.get(entity).currentState === LoadingState.FINISHED) {
|
|
173
|
+
// Model is loaded
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Colliders
|
|
178
|
+
```typescript
|
|
179
|
+
import { MeshCollider, ColliderLayer } from '@dcl/sdk/ecs'
|
|
180
|
+
|
|
181
|
+
// Basic colliders
|
|
182
|
+
MeshCollider.setBox(entity)
|
|
183
|
+
MeshCollider.setSphere(entity)
|
|
184
|
+
MeshCollider.setPlane(entity)
|
|
185
|
+
|
|
186
|
+
// With specific collision layer
|
|
187
|
+
MeshCollider.setBox(entity, ColliderLayer.CL_PHYSICS)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Text
|
|
191
|
+
```typescript
|
|
192
|
+
import { TextShape } from '@dcl/sdk/ecs'
|
|
193
|
+
import { Color4 } from '@dcl/sdk/math'
|
|
194
|
+
|
|
195
|
+
TextShape.create(entity, {
|
|
196
|
+
text: 'Hello Decentraland',
|
|
197
|
+
fontSize: 3,
|
|
198
|
+
textColor: Color4.White(),
|
|
199
|
+
outlineWidth: 0.1,
|
|
200
|
+
outlineColor: Color4.Black(),
|
|
201
|
+
width: 4,
|
|
202
|
+
height: 2,
|
|
203
|
+
textWrapping: true
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Billboard
|
|
208
|
+
```typescript
|
|
209
|
+
import { Billboard } from '@dcl/sdk/ecs'
|
|
210
|
+
|
|
211
|
+
// Makes an entity always face the camera
|
|
212
|
+
Billboard.create(entity)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Audio
|
|
216
|
+
```typescript
|
|
217
|
+
import { AudioSource } from '@dcl/sdk/ecs'
|
|
218
|
+
|
|
219
|
+
AudioSource.create(entity, {
|
|
220
|
+
audioClipUrl: 'sounds/mySound.mp3',
|
|
221
|
+
playing: true,
|
|
222
|
+
loop: false,
|
|
223
|
+
volume: 1.0
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
// Play sound
|
|
227
|
+
AudioSource.getMutable(entity).playing = true
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## UI System
|
|
231
|
+
|
|
232
|
+
### Setting Up React UI
|
|
233
|
+
```typescript
|
|
234
|
+
// ui.tsx
|
|
235
|
+
import ReactEcs, { ReactEcsRenderer, UiEntity, Label, Button } from '@dcl/sdk/react-ecs'
|
|
236
|
+
import { Color4 } from '@dcl/sdk/math'
|
|
237
|
+
|
|
238
|
+
export function setupUi() {
|
|
239
|
+
ReactEcsRenderer.setUiRenderer(uiComponent)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const uiComponent = () => (
|
|
243
|
+
<UiEntity
|
|
244
|
+
uiTransform={{
|
|
245
|
+
width: 400,
|
|
246
|
+
height: 230,
|
|
247
|
+
margin: '16px 0 8px 270px',
|
|
248
|
+
padding: 4
|
|
249
|
+
}}
|
|
250
|
+
uiBackground={{ color: Color4.create(0.5, 0.8, 0.1, 0.6) }}
|
|
251
|
+
>
|
|
252
|
+
<Label
|
|
253
|
+
value="Hello Decentraland"
|
|
254
|
+
color={Color4.White()}
|
|
255
|
+
fontSize={24}
|
|
256
|
+
/>
|
|
257
|
+
<Button
|
|
258
|
+
value="Click Me"
|
|
259
|
+
variant="primary"
|
|
260
|
+
fontSize={14}
|
|
261
|
+
onMouseDown={() => {
|
|
262
|
+
console.log('Button clicked')
|
|
263
|
+
}}
|
|
264
|
+
/>
|
|
265
|
+
</UiEntity>
|
|
266
|
+
)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### UI Components
|
|
270
|
+
|
|
271
|
+
#### UiEntity
|
|
272
|
+
```typescript
|
|
273
|
+
<UiEntity
|
|
274
|
+
uiTransform={{
|
|
275
|
+
width: 400, // Pixels or percentage (e.g. '100%')
|
|
276
|
+
height: 300,
|
|
277
|
+
position: { top: 10, left: 10 }, // For absolute positioning
|
|
278
|
+
positionType: 'absolute', // 'absolute' or 'relative'
|
|
279
|
+
display: 'flex', // 'flex' or 'none'
|
|
280
|
+
flexDirection: 'column', // 'column' or 'row'
|
|
281
|
+
alignItems: 'center', // 'center', 'flex-start', 'flex-end'
|
|
282
|
+
justifyContent: 'center', // 'center', 'flex-start', 'flex-end', 'space-between'
|
|
283
|
+
margin: 5, // Or { top: 5, right: 10, bottom: 5, left: 10 }
|
|
284
|
+
padding: 5 // Same as margin
|
|
285
|
+
}}
|
|
286
|
+
uiBackground={{
|
|
287
|
+
color: Color4.White(),
|
|
288
|
+
texture: { src: 'images/image.png' },
|
|
289
|
+
textureMode: 'stretch', // 'stretch', 'nine-slices', 'center'
|
|
290
|
+
avatarTexture: { userId: 'user-id' } // For rendering avatar
|
|
291
|
+
}}
|
|
292
|
+
/>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
#### Label
|
|
296
|
+
```typescript
|
|
297
|
+
<Label
|
|
298
|
+
value="Text content"
|
|
299
|
+
color={Color4.Black()}
|
|
300
|
+
fontSize={18}
|
|
301
|
+
textAlign="middle-center" // 'top-left', 'middle-right', etc.
|
|
302
|
+
font="serif" // 'serif', 'monospace', or default sans-serif
|
|
303
|
+
uiTransform={{ width: 200, height: 50 }}
|
|
304
|
+
/>
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
#### Button
|
|
308
|
+
```typescript
|
|
309
|
+
<Button
|
|
310
|
+
value="Click Me"
|
|
311
|
+
variant="primary" // 'primary', 'secondary', etc.
|
|
312
|
+
fontSize={14}
|
|
313
|
+
color={Color4.White()} // Text color
|
|
314
|
+
uiTransform={{ width: 100, height: 40 }}
|
|
315
|
+
onMouseDown={() => { /* action */ }}
|
|
316
|
+
uiBackground={{ color: Color4.Blue() }} // Override default button style
|
|
317
|
+
/>
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
#### Input
|
|
321
|
+
```typescript
|
|
322
|
+
<Input
|
|
323
|
+
placeholder="Enter text..."
|
|
324
|
+
placeholderColor={Color4.Gray()}
|
|
325
|
+
color={Color4.Black()} // Text color
|
|
326
|
+
fontSize={16}
|
|
327
|
+
onChange={(value) => { console.log('Value changing: ' + value) }}
|
|
328
|
+
onSubmit={(value) => { console.log('Submitted: ' + value) }}
|
|
329
|
+
uiTransform={{ width: 200, height: 40 }}
|
|
330
|
+
/>
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
#### Dropdown
|
|
334
|
+
```typescript
|
|
335
|
+
<Dropdown
|
|
336
|
+
options={['Option 1', 'Option 2', 'Option 3']}
|
|
337
|
+
onChange={(index) => { console.log('Selected option: ' + index) }}
|
|
338
|
+
fontSize={16}
|
|
339
|
+
color={Color4.Black()}
|
|
340
|
+
uiTransform={{ width: 200, height: 40 }}
|
|
341
|
+
acceptEmpty={true}
|
|
342
|
+
emptyLabel="-- Select an option --"
|
|
343
|
+
/>
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Canvas Information
|
|
347
|
+
```typescript
|
|
348
|
+
import { UiCanvasInformation, engine } from '@dcl/sdk/ecs'
|
|
349
|
+
|
|
350
|
+
// Get screen info
|
|
351
|
+
const canvasInfo = UiCanvasInformation.get(engine.RootEntity)
|
|
352
|
+
const screenWidth = canvasInfo.width
|
|
353
|
+
const screenHeight = canvasInfo.height
|
|
354
|
+
const pixelRatio = canvasInfo.devicePixelRatio
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Runtime Data
|
|
358
|
+
|
|
359
|
+
### World Time
|
|
360
|
+
```typescript
|
|
361
|
+
import { getWorldTime } from '~system/Runtime'
|
|
362
|
+
|
|
363
|
+
// Get the current time in the Decentraland world
|
|
364
|
+
executeTask(async () => {
|
|
365
|
+
const time = await getWorldTime({})
|
|
366
|
+
console.log(`Current time: ${time.seconds} seconds since day start`)
|
|
367
|
+
|
|
368
|
+
// Convert to hours (24-hour cycle)
|
|
369
|
+
const hours = (time.seconds / 3600) % 24
|
|
370
|
+
console.log(`Current hour: ${hours.toFixed(2)}`)
|
|
371
|
+
|
|
372
|
+
// Check if it's night time (between 19:50 and 6:15)
|
|
373
|
+
const isNight = time.seconds > 19.833 * 3600 || time.seconds < 6.25 * 3600
|
|
374
|
+
if (isNight) {
|
|
375
|
+
console.log('It is night time')
|
|
376
|
+
}
|
|
377
|
+
})
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Realm Information
|
|
381
|
+
```typescript
|
|
382
|
+
import { getRealm } from '~system/Runtime'
|
|
383
|
+
|
|
384
|
+
// Get information about the current realm
|
|
385
|
+
executeTask(async () => {
|
|
386
|
+
const { realmInfo } = await getRealm({})
|
|
387
|
+
console.log(`Current realm: ${realmInfo.realmName}`)
|
|
388
|
+
console.log(`Network ID: ${realmInfo.networkId}`)
|
|
389
|
+
console.log(`Is preview: ${realmInfo.isPreview}`)
|
|
390
|
+
|
|
391
|
+
// Check if connected to scene room
|
|
392
|
+
if (realmInfo.isConnectedSceneRoom) {
|
|
393
|
+
console.log('Connected to scene room')
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Platform Detection
|
|
399
|
+
```typescript
|
|
400
|
+
import { getPlatform } from '~system/EnvironmentApi'
|
|
401
|
+
|
|
402
|
+
// Detect the platform the player is using
|
|
403
|
+
executeTask(async () => {
|
|
404
|
+
const { platform } = await getPlatform()
|
|
405
|
+
|
|
406
|
+
if (platform === 'BROWSER') {
|
|
407
|
+
console.log('Running in browser')
|
|
408
|
+
// Optimize for browser performance
|
|
409
|
+
} else if (platform === 'DESKTOP') {
|
|
410
|
+
console.log('Running in desktop app')
|
|
411
|
+
// Enable higher quality features
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Engine Information
|
|
417
|
+
```typescript
|
|
418
|
+
import { EngineInfo, engine } from '@dcl/sdk/ecs'
|
|
419
|
+
|
|
420
|
+
// Access engine information
|
|
421
|
+
engine.addSystem((deltaTime) => {
|
|
422
|
+
const engineInfo = EngineInfo.getOrNull(engine.RootEntity)
|
|
423
|
+
if (!engineInfo) return
|
|
424
|
+
|
|
425
|
+
// Get current frame number
|
|
426
|
+
const currentFrame = engineInfo.frameNumber
|
|
427
|
+
|
|
428
|
+
// Get total runtime in seconds
|
|
429
|
+
const runtime = engineInfo.totalRuntime
|
|
430
|
+
|
|
431
|
+
// Get current tick number
|
|
432
|
+
const currentTick = engineInfo.tickNumber
|
|
433
|
+
|
|
434
|
+
// Example: Log every 100 frames
|
|
435
|
+
if (currentFrame % 100 === 0) {
|
|
436
|
+
console.log(`Runtime: ${runtime.toFixed(2)}s, Frame: ${currentFrame}, Tick: ${currentTick}`)
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## Player Data & Camera Controls
|
|
442
|
+
|
|
443
|
+
### Player Position and Rotation
|
|
444
|
+
```typescript
|
|
445
|
+
import { engine, Transform } from '@dcl/sdk/ecs'
|
|
446
|
+
import { Vector3, Quaternion } from '@dcl/sdk/math'
|
|
447
|
+
|
|
448
|
+
// Get player position and rotation
|
|
449
|
+
function getPlayerPosition() {
|
|
450
|
+
if (!Transform.has(engine.PlayerEntity)) return
|
|
451
|
+
if (!Transform.has(engine.CameraEntity)) return
|
|
452
|
+
|
|
453
|
+
// Player position (at chest height, ~0.88m above ground)
|
|
454
|
+
const playerPos = Transform.get(engine.PlayerEntity).position
|
|
455
|
+
|
|
456
|
+
// Player rotation (direction the avatar is facing)
|
|
457
|
+
const playerRot = Transform.get(engine.PlayerEntity).rotation
|
|
458
|
+
|
|
459
|
+
// Camera position (at eye level, ~1.75m above ground in 1st person)
|
|
460
|
+
const cameraPos = Transform.get(engine.CameraEntity).position
|
|
461
|
+
|
|
462
|
+
// Camera rotation
|
|
463
|
+
const cameraRot = Transform.get(engine.CameraEntity).rotation
|
|
464
|
+
|
|
465
|
+
console.log('Player position:', playerPos)
|
|
466
|
+
console.log('Player rotation:', playerRot)
|
|
467
|
+
console.log('Camera position:', cameraPos)
|
|
468
|
+
console.log('Camera rotation:', cameraRot)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Add as a system to continuously track player position
|
|
472
|
+
engine.addSystem(getPlayerPosition)
|
|
473
|
+
|
|
474
|
+
// Get player position once
|
|
475
|
+
executeTask(async () => {
|
|
476
|
+
// Wait for player entity to be available
|
|
477
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
478
|
+
getPlayerPosition()
|
|
479
|
+
})
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Player Identity Data
|
|
483
|
+
```typescript
|
|
484
|
+
import { engine, PlayerIdentityData, AvatarBase, AvatarEquippedData } from '@dcl/sdk/ecs'
|
|
485
|
+
|
|
486
|
+
// Access player identity and avatar data
|
|
487
|
+
function getPlayerData() {
|
|
488
|
+
for (const [entity, identity, base, equipped] of engine.getEntitiesWith(
|
|
489
|
+
PlayerIdentityData,
|
|
490
|
+
AvatarBase,
|
|
491
|
+
AvatarEquippedData
|
|
492
|
+
)) {
|
|
493
|
+
// Player address and guest status
|
|
494
|
+
console.log('Player address:', identity.address)
|
|
495
|
+
|
|
496
|
+
// Avatar base information
|
|
497
|
+
console.log('Player name:', base.name)
|
|
498
|
+
console.log('Body shape:', base.bodyShapeUrn)
|
|
499
|
+
console.log('Skin color:', base.skinColor)
|
|
500
|
+
console.log('Eye color:', base.eyeColor)
|
|
501
|
+
console.log('Hair color:', base.hairColor)
|
|
502
|
+
|
|
503
|
+
// Equipped wearables and emotes
|
|
504
|
+
console.log('Wearables:', equipped.wearableUrns)
|
|
505
|
+
console.log('Emotes:', equipped.emoteUrns)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Add as a system to continuously track player data
|
|
510
|
+
engine.addSystem(getPlayerData)
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Get Player Information
|
|
514
|
+
```typescript
|
|
515
|
+
import { getPlayer } from '@dcl/sdk/network'
|
|
516
|
+
|
|
517
|
+
// Get current player information
|
|
518
|
+
executeTask(async () => {
|
|
519
|
+
const player = getPlayer()
|
|
520
|
+
|
|
521
|
+
console.log('Player ID:', player.userId)
|
|
522
|
+
console.log('Player address:', player.publicKey)
|
|
523
|
+
console.log('Player name:', player.name)
|
|
524
|
+
console.log('Player description:', player.description)
|
|
525
|
+
console.log('Player avatar:', player.avatar)
|
|
526
|
+
|
|
527
|
+
// Access avatar details
|
|
528
|
+
console.log('Body shape:', player.avatar.bodyShape)
|
|
529
|
+
console.log('Wearables:', player.avatar.wearables)
|
|
530
|
+
console.log('Emotes:', player.avatar.emotes)
|
|
531
|
+
})
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### Get Portable Experiences
|
|
535
|
+
```typescript
|
|
536
|
+
import { getPortableExperiencesLoaded } from '~system/PortableExperiences'
|
|
537
|
+
|
|
538
|
+
// Check if player has portable experiences loaded
|
|
539
|
+
executeTask(async () => {
|
|
540
|
+
const { loaded } = await getPortableExperiencesLoaded({})
|
|
541
|
+
|
|
542
|
+
if (loaded.length > 0) {
|
|
543
|
+
console.log('Player has portable experiences loaded:')
|
|
544
|
+
loaded.forEach(px => {
|
|
545
|
+
console.log(`- ID: ${px.id}`)
|
|
546
|
+
})
|
|
547
|
+
} else {
|
|
548
|
+
console.log('Player has no portable experiences loaded')
|
|
549
|
+
}
|
|
550
|
+
})
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Camera Mode
|
|
554
|
+
```typescript
|
|
555
|
+
import { engine, CameraMode, CameraType } from '@dcl/sdk/ecs'
|
|
556
|
+
|
|
557
|
+
// Check player's camera mode (1st or 3rd person)
|
|
558
|
+
function checkCameraMode() {
|
|
559
|
+
if (!CameraMode.has(engine.CameraEntity)) return
|
|
560
|
+
|
|
561
|
+
const cameraMode = CameraMode.get(engine.CameraEntity)
|
|
562
|
+
|
|
563
|
+
if (cameraMode.mode === CameraType.CT_THIRD_PERSON) {
|
|
564
|
+
console.log('Player is using 3rd person camera')
|
|
565
|
+
} else {
|
|
566
|
+
console.log('Player is using 1st person camera')
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Add as a system to continuously check camera mode
|
|
571
|
+
engine.addSystem(checkCameraMode)
|
|
572
|
+
|
|
573
|
+
// Create a camera mode area to force first-person view
|
|
574
|
+
import { CameraModeArea } from '@dcl/sdk/ecs'
|
|
575
|
+
|
|
576
|
+
function createFirstPersonArea() {
|
|
577
|
+
const area = engine.addEntity()
|
|
578
|
+
|
|
579
|
+
CameraModeArea.create(area, {
|
|
580
|
+
area: Vector3.create(5, 5, 5), // Box size
|
|
581
|
+
mode: CameraType.CT_FIRST_PERSON
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
Transform.create(area, {
|
|
585
|
+
position: Vector3.create(8, 1, 8)
|
|
586
|
+
})
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### Pointer Lock
|
|
591
|
+
```typescript
|
|
592
|
+
import { engine, PointerLock } from '@dcl/sdk/ecs'
|
|
593
|
+
|
|
594
|
+
// Check if the player's cursor is locked
|
|
595
|
+
function checkPointerLock() {
|
|
596
|
+
if (!PointerLock.has(engine.CameraEntity)) return
|
|
597
|
+
|
|
598
|
+
const isLocked = PointerLock.get(engine.CameraEntity).isPointerLocked
|
|
599
|
+
|
|
600
|
+
if (isLocked) {
|
|
601
|
+
console.log('Cursor is locked (camera control mode)')
|
|
602
|
+
} else {
|
|
603
|
+
console.log('Cursor is unlocked (UI interaction mode)')
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Add as a system to continuously check pointer lock
|
|
608
|
+
engine.addSystem(checkPointerLock)
|
|
609
|
+
|
|
610
|
+
// Listen for pointer lock changes
|
|
611
|
+
import { inputSystem, InputAction, PointerEventType } from '@dcl/sdk/ecs'
|
|
612
|
+
|
|
613
|
+
function setupPointerLockListener() {
|
|
614
|
+
// Check for right mouse button press (unlocks cursor)
|
|
615
|
+
if (inputSystem.isTriggered(InputAction.IA_SECONDARY, PointerEventType.PET_DOWN)) {
|
|
616
|
+
console.log('Right mouse button pressed - cursor unlocked')
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Check for left mouse button press (locks cursor)
|
|
620
|
+
if (inputSystem.isTriggered(InputAction.IA_PRIMARY, PointerEventType.PET_DOWN)) {
|
|
621
|
+
console.log('Left mouse button pressed - cursor locked')
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
engine.addSystem(setupPointerLockListener)
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
## Input Handling
|
|
629
|
+
|
|
630
|
+
### Pointer Events
|
|
631
|
+
```typescript
|
|
632
|
+
import { PointerEvents, PointerEventType, InputAction, pointerEventsSystem } from '@dcl/sdk/ecs'
|
|
633
|
+
|
|
634
|
+
// Add clickable behavior to an entity
|
|
635
|
+
PointerEvents.create(entity, {
|
|
636
|
+
pointerEvents: [
|
|
637
|
+
{
|
|
638
|
+
eventType: PointerEventType.PET_DOWN,
|
|
639
|
+
eventInfo: {
|
|
640
|
+
button: InputAction.IA_POINTER,
|
|
641
|
+
hoverText: 'Click me',
|
|
642
|
+
showFeedback: true, // Show interaction feedback
|
|
643
|
+
maxDistance: 10 // Max interaction distance
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
]
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
// Response to click events
|
|
650
|
+
pointerEventsSystem.onPointerDown(
|
|
651
|
+
{ entity, opts: { button: InputAction.IA_POINTER } },
|
|
652
|
+
(event) => {
|
|
653
|
+
console.log('Entity clicked!')
|
|
654
|
+
// Handle the click
|
|
655
|
+
}
|
|
656
|
+
)
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### Input System
|
|
660
|
+
```typescript
|
|
661
|
+
import { inputSystem, InputAction, PointerEventType } from '@dcl/sdk/ecs'
|
|
662
|
+
|
|
663
|
+
// Check if a key/button is pressed
|
|
664
|
+
if (inputSystem.isPressed(InputAction.IA_FORWARD)) {
|
|
665
|
+
// W key or forward movement is active
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Check for a single press/trigger
|
|
669
|
+
if (inputSystem.isTriggered(InputAction.IA_JUMP, PointerEventType.PET_DOWN)) {
|
|
670
|
+
// Space bar was just pressed
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Check for key release
|
|
674
|
+
if (inputSystem.isTriggered(InputAction.IA_PRIMARY, PointerEventType.PET_UP)) {
|
|
675
|
+
// Primary button was just released
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### Input Modifiers
|
|
680
|
+
```typescript
|
|
681
|
+
import { InputModifier } from '@dcl/sdk/ecs'
|
|
682
|
+
|
|
683
|
+
// Disable player movement controls
|
|
684
|
+
InputModifier.create(engine.PlayerEntity, {
|
|
685
|
+
mode: {
|
|
686
|
+
$case: 'standard',
|
|
687
|
+
standard: {
|
|
688
|
+
disableWalk: true, // Disable walking
|
|
689
|
+
disableRun: true, // Disable running
|
|
690
|
+
disableJump: true // Disable jumping
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
// Re-enable movement
|
|
696
|
+
InputModifier.getMutable(engine.PlayerEntity).mode = {
|
|
697
|
+
$case: 'standard',
|
|
698
|
+
standard: {
|
|
699
|
+
disableWalk: false,
|
|
700
|
+
disableRun: false,
|
|
701
|
+
disableJump: false
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
## Event Listeners
|
|
707
|
+
|
|
708
|
+
### Scene Entry/Exit Events
|
|
709
|
+
```typescript
|
|
710
|
+
import { onEnterScene, onLeaveScene } from '@dcl/sdk/src/players'
|
|
711
|
+
|
|
712
|
+
// Listen for players entering the scene
|
|
713
|
+
onEnterScene((player) => {
|
|
714
|
+
if (!player) return
|
|
715
|
+
console.log('Player entered:', player.userId)
|
|
716
|
+
console.log('Player data:', player)
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
// Listen for players leaving the scene
|
|
720
|
+
onLeaveScene((userId) => {
|
|
721
|
+
if (!userId) return
|
|
722
|
+
console.log('Player left:', userId)
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
// Filter for current player only
|
|
726
|
+
import { getPlayer } from '@dcl/sdk/network'
|
|
727
|
+
|
|
728
|
+
export function main() {
|
|
729
|
+
let myPlayer = getPlayer()
|
|
730
|
+
|
|
731
|
+
onEnterScene((player) => {
|
|
732
|
+
if (!player) return
|
|
733
|
+
if (myPlayer && player.userId == myPlayer.userId) {
|
|
734
|
+
console.log('I entered the scene')
|
|
735
|
+
}
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
onLeaveScene((userId) => {
|
|
739
|
+
if (!userId) return
|
|
740
|
+
if (myPlayer && userId == myPlayer.userId) {
|
|
741
|
+
console.log('I left the scene')
|
|
742
|
+
}
|
|
743
|
+
})
|
|
744
|
+
}
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### Avatar Emote Events
|
|
748
|
+
```typescript
|
|
749
|
+
import { AvatarEmoteCommand } from '@dcl/sdk/ecs'
|
|
750
|
+
|
|
751
|
+
// Listen for emote events from the current player
|
|
752
|
+
AvatarEmoteCommand.onChange(engine.PlayerEntity, (emote) => {
|
|
753
|
+
if (!emote) return
|
|
754
|
+
console.log('Emote played:', emote.emoteUrn)
|
|
755
|
+
console.log('Is looping:', emote.loop)
|
|
756
|
+
console.log('Timestamp:', emote.timestamp)
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
// Listen for emote events from other players
|
|
760
|
+
function setupEmoteListener(playerEntity: Entity) {
|
|
761
|
+
AvatarEmoteCommand.onChange(playerEntity, (emote) => {
|
|
762
|
+
if (!emote) return
|
|
763
|
+
console.log('Player emote:', emote.emoteUrn)
|
|
764
|
+
})
|
|
765
|
+
}
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
### Camera Mode Events
|
|
769
|
+
```typescript
|
|
770
|
+
import { CameraMode } from '@dcl/sdk/ecs'
|
|
771
|
+
|
|
772
|
+
// Listen for camera mode changes
|
|
773
|
+
CameraMode.onChange(engine.CameraEntity, (cameraComponent) => {
|
|
774
|
+
if (!cameraComponent) return
|
|
775
|
+
console.log('Camera mode:', cameraComponent.mode)
|
|
776
|
+
// 0 = first person
|
|
777
|
+
// 1 = third person
|
|
778
|
+
})
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
### Pointer Lock Events
|
|
782
|
+
```typescript
|
|
783
|
+
import { PointerLock } from '@dcl/sdk/ecs'
|
|
784
|
+
|
|
785
|
+
// Listen for cursor lock state changes
|
|
786
|
+
PointerLock.onChange(engine.CameraEntity, (pointerLock) => {
|
|
787
|
+
if (!pointerLock) return
|
|
788
|
+
console.log('Cursor locked:', pointerLock.isPointerLocked)
|
|
789
|
+
})
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
### Avatar Profile Events
|
|
793
|
+
```typescript
|
|
794
|
+
import { AvatarEquippedData, AvatarBase } from '@dcl/sdk/ecs'
|
|
795
|
+
|
|
796
|
+
// Listen for wearable/emote changes
|
|
797
|
+
AvatarEquippedData.onChange(engine.PlayerEntity, (equipped) => {
|
|
798
|
+
if (!equipped) return
|
|
799
|
+
console.log('Wearables:', equipped.wearableUrns)
|
|
800
|
+
console.log('Emotes:', equipped.emoteUrns)
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
// Listen for avatar base changes
|
|
804
|
+
AvatarBase.onChange(engine.PlayerEntity, (body) => {
|
|
805
|
+
if (!body) return
|
|
806
|
+
console.log('Name:', body.name)
|
|
807
|
+
console.log('Body shape:', body.bodyShapeUrn)
|
|
808
|
+
console.log('Skin color:', body.skinColor)
|
|
809
|
+
console.log('Eye color:', body.eyeColor)
|
|
810
|
+
console.log('Hair color:', body.hairColor)
|
|
811
|
+
})
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
## Movement & Animation
|
|
815
|
+
|
|
816
|
+
### Tweens
|
|
817
|
+
```typescript
|
|
818
|
+
import { Tween, EasingFunction, TweenSequence, TweenLoop } from '@dcl/sdk/ecs'
|
|
819
|
+
import { Vector3, Quaternion } from '@dcl/sdk/math'
|
|
820
|
+
|
|
821
|
+
// Move an entity
|
|
822
|
+
Tween.setMove(entity,
|
|
823
|
+
Vector3.create(1, 0, 1),
|
|
824
|
+
Vector3.create(5, 0, 5),
|
|
825
|
+
2000,
|
|
826
|
+
EasingFunction.EF_LINEAR
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
// Rotate an entity
|
|
830
|
+
Tween.setRotate(entity,
|
|
831
|
+
Quaternion.fromEulerDegrees(0, 0, 0),
|
|
832
|
+
Quaternion.fromEulerDegrees(0, 180, 0),
|
|
833
|
+
2000,
|
|
834
|
+
EasingFunction.EF_EASEINQUAD
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
// Scale an entity
|
|
838
|
+
Tween.setScale(entity,
|
|
839
|
+
Vector3.create(1, 1, 1),
|
|
840
|
+
Vector3.create(2, 2, 2),
|
|
841
|
+
2000,
|
|
842
|
+
EasingFunction.EF_EASEOUTQUAD
|
|
843
|
+
})
|
|
844
|
+
|
|
845
|
+
// Move continuously
|
|
846
|
+
Tween.setMoveContinuous(entity,
|
|
847
|
+
Vector3.create(0, 0, 1),
|
|
848
|
+
2000
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
// Rotate continuously
|
|
852
|
+
Tween.setRotateContinuous(entity,
|
|
853
|
+
Quaternion.fromEulerDegrees(0, 0, 90),
|
|
854
|
+
2000
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
// Tween sequences (chained animations)
|
|
858
|
+
TweenSequence.create(entity, {
|
|
859
|
+
sequence: [
|
|
860
|
+
{
|
|
861
|
+
mode: Tween.Mode.Move({
|
|
862
|
+
start: Vector3.create(5, 0, 5),
|
|
863
|
+
end: Vector3.create(10, 0, 5)
|
|
864
|
+
}),
|
|
865
|
+
duration: 2000,
|
|
866
|
+
easingFunction: EasingFunction.EF_LINEAR
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
mode: Tween.Mode.Move({
|
|
870
|
+
start: Vector3.create(10, 0, 5),
|
|
871
|
+
end: Vector3.create(10, 0, 10)
|
|
872
|
+
}),
|
|
873
|
+
duration: 2000,
|
|
874
|
+
easingFunction: EasingFunction.EF_LINEAR
|
|
875
|
+
}
|
|
876
|
+
],
|
|
877
|
+
loop: TweenLoop.TL_RESTART // Can be TL_RESTART, TL_YOYO, or undefined (no loop)
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
// Texture move
|
|
881
|
+
Tween.setTextureMove(entity,
|
|
882
|
+
Vector2.create(0, 0),
|
|
883
|
+
Vector2.create(1, 0),
|
|
884
|
+
2000
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
// Texture move continuously
|
|
888
|
+
Tween.setTextureMoveContinuous(entity,
|
|
889
|
+
Vector2.create(0, 1),
|
|
890
|
+
2000
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
// Control tween playback
|
|
895
|
+
const tween = Tween.getMutable(entity)
|
|
896
|
+
tween.playing = false // Pause the tween
|
|
897
|
+
tween.currentTime = 0 // Reset to beginning
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
### Animator Component
|
|
901
|
+
```typescript
|
|
902
|
+
import { Animator, engine } from '@dcl/sdk/ecs'
|
|
903
|
+
|
|
904
|
+
// Create an entity with a 3D model
|
|
905
|
+
const shark = engine.addEntity()
|
|
906
|
+
GltfContainer.create(shark, {
|
|
907
|
+
src: 'models/shark.glb'
|
|
908
|
+
})
|
|
909
|
+
|
|
910
|
+
// Add the Animator component with animation states
|
|
911
|
+
Animator.create(shark, {
|
|
912
|
+
states: [
|
|
913
|
+
{
|
|
914
|
+
clip: 'swim', // Name of the animation clip in the model
|
|
915
|
+
playing: true,
|
|
916
|
+
loop: true,
|
|
917
|
+
speed: 1.0,
|
|
918
|
+
weight: 1.0
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
clip: 'bite',
|
|
922
|
+
playing: false,
|
|
923
|
+
loop: false,
|
|
924
|
+
speed: 1.0,
|
|
925
|
+
weight: 0.0
|
|
926
|
+
}
|
|
927
|
+
]
|
|
928
|
+
})
|
|
929
|
+
|
|
930
|
+
// Play a specific animation
|
|
931
|
+
Animator.playSingleAnimation(shark, 'swim')
|
|
932
|
+
|
|
933
|
+
// Stop all animations
|
|
934
|
+
Animator.stopAllAnimations(shark)
|
|
935
|
+
|
|
936
|
+
// Get a specific animation clip to modify its properties
|
|
937
|
+
const swimAnim = Animator.getClip(shark, 'swim')
|
|
938
|
+
if (swimAnim) {
|
|
939
|
+
swimAnim.speed = 0.5 // Play at half speed
|
|
940
|
+
swimAnim.weight = 0.8 // Set animation weight
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Play multiple animations with different weights
|
|
944
|
+
Animator.create(shark, {
|
|
945
|
+
states: [
|
|
946
|
+
{
|
|
947
|
+
clip: 'swim',
|
|
948
|
+
playing: true,
|
|
949
|
+
loop: true,
|
|
950
|
+
weight: 0.7
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
clip: 'bite',
|
|
954
|
+
playing: true,
|
|
955
|
+
loop: false,
|
|
956
|
+
weight: 0.3
|
|
957
|
+
}
|
|
958
|
+
]
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
// Create an animation that resets to the first frame when finished
|
|
962
|
+
Animator.create(shark, {
|
|
963
|
+
states: [
|
|
964
|
+
{
|
|
965
|
+
clip: 'bite',
|
|
966
|
+
playing: true,
|
|
967
|
+
loop: false,
|
|
968
|
+
shouldReset: true // Return to first frame when animation ends
|
|
969
|
+
}
|
|
970
|
+
]
|
|
971
|
+
})
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
### Moving the Player
|
|
975
|
+
```typescript
|
|
976
|
+
import { movePlayerTo } from '~system/RestrictedActions'
|
|
977
|
+
|
|
978
|
+
// Move the player to a position in the scene
|
|
979
|
+
movePlayerTo({
|
|
980
|
+
newRelativePosition: { x: 8, y: 0, z: 8 },
|
|
981
|
+
cameraTarget: { x: 10, y: 1, z: 8 } // Optional: where to look at
|
|
982
|
+
})
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
### Avatar Shapes
|
|
986
|
+
```typescript
|
|
987
|
+
import { AvatarShape } from '@dcl/sdk/ecs'
|
|
988
|
+
|
|
989
|
+
// Create an NPC avatar
|
|
990
|
+
AvatarShape.create(entity, {
|
|
991
|
+
id: 'npc-id',
|
|
992
|
+
name: 'NPC Name',
|
|
993
|
+
bodyShape: 'urn:decentraland:off-chain:base-avatars:BaseMale', // or BaseFemale
|
|
994
|
+
wearables: [
|
|
995
|
+
'urn:decentraland:off-chain:base-avatars:eyebrows_00',
|
|
996
|
+
'urn:decentraland:off-chain:base-avatars:mouth_00',
|
|
997
|
+
'urn:decentraland:off-chain:base-avatars:eyes_00',
|
|
998
|
+
'urn:decentraland:off-chain:base-avatars:blue_tshirt',
|
|
999
|
+
'urn:decentraland:off-chain:base-avatars:brown_pants',
|
|
1000
|
+
'urn:decentraland:off-chain:base-avatars:classic_shoes',
|
|
1001
|
+
'urn:decentraland:off-chain:base-avatars:short_hair'
|
|
1002
|
+
],
|
|
1003
|
+
hairColor: { r: 0.92, g: 0.76, b: 0.62 }, // RGB values 0-1
|
|
1004
|
+
skinColor: { r: 0.94, g: 0.85, b: 0.6 }, // RGB values 0-1
|
|
1005
|
+
emotes: []
|
|
1006
|
+
})
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
### Camera Control
|
|
1010
|
+
```typescript
|
|
1011
|
+
import { MainCamera, VirtualCamera, CameraModeArea, CameraType } from '@dcl/sdk/ecs'
|
|
1012
|
+
|
|
1013
|
+
// Create a virtual camera
|
|
1014
|
+
VirtualCamera.create(entity, {
|
|
1015
|
+
lookAtEntity: targetEntity, // Optional: entity to focus on
|
|
1016
|
+
defaultTransition: {
|
|
1017
|
+
transitionMode: VirtualCamera.Transition.Time(2) // 2 second transition
|
|
1018
|
+
// Or VirtualCamera.Transition.Speed(10) // Speed-based transition
|
|
1019
|
+
}
|
|
1020
|
+
})
|
|
1021
|
+
|
|
1022
|
+
// Activate a virtual camera
|
|
1023
|
+
MainCamera.getMutable(engine.CameraEntity).virtualCameraEntity = cameraEntity
|
|
1024
|
+
|
|
1025
|
+
// Return to normal camera
|
|
1026
|
+
MainCamera.getMutable(engine.CameraEntity).virtualCameraEntity = undefined
|
|
1027
|
+
|
|
1028
|
+
// Create a camera mode area to force first-person view
|
|
1029
|
+
CameraModeArea.create(entity, {
|
|
1030
|
+
area: Vector3.create(5, 5, 5), // Box size
|
|
1031
|
+
mode: CameraType.CT_FIRST_PERSON // Or CT_THIRD_PERSON
|
|
1032
|
+
})
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
### Emotes
|
|
1036
|
+
```typescript
|
|
1037
|
+
import { triggerEmote, triggerSceneEmote } from '~system/RestrictedActions'
|
|
1038
|
+
|
|
1039
|
+
// Play a predefined avatar emote
|
|
1040
|
+
triggerEmote({ predefinedEmote: 'robot' }) // 'wave', 'dance', etc.
|
|
1041
|
+
|
|
1042
|
+
// Play a custom animation
|
|
1043
|
+
triggerSceneEmote({
|
|
1044
|
+
src: 'animations/myAnimation.glb',
|
|
1045
|
+
loop: false
|
|
1046
|
+
})
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
## Lights & Visual Effects
|
|
1050
|
+
|
|
1051
|
+
### Lights
|
|
1052
|
+
```typescript
|
|
1053
|
+
import { LightSource, PBLightSource_ShadowType } from '@dcl/sdk/ecs'
|
|
1054
|
+
import { Color3 } from '@dcl/sdk/math'
|
|
1055
|
+
|
|
1056
|
+
// Create a point light
|
|
1057
|
+
LightSource.create(entity, {
|
|
1058
|
+
color: Color3.White(),
|
|
1059
|
+
intensity: 1.0,
|
|
1060
|
+
range: 10,
|
|
1061
|
+
active: true,
|
|
1062
|
+
type: LightSource.Type.Point({
|
|
1063
|
+
shadow: PBLightSource_ShadowType.ST_HARD // ST_HARD, ST_SOFT, or ST_NONE
|
|
1064
|
+
})
|
|
1065
|
+
})
|
|
1066
|
+
|
|
1067
|
+
// Create a spotlight
|
|
1068
|
+
LightSource.create(entity, {
|
|
1069
|
+
color: Color3.Yellow(),
|
|
1070
|
+
intensity: 1.5,
|
|
1071
|
+
range: 15,
|
|
1072
|
+
active: true,
|
|
1073
|
+
type: LightSource.Type.Spot({
|
|
1074
|
+
innerAngle: 30, // Inner cone angle in degrees
|
|
1075
|
+
outerAngle: 60, // Outer cone angle in degrees
|
|
1076
|
+
shadow: PBLightSource_ShadowType.ST_HARD,
|
|
1077
|
+
shadowMaskTexture: Material.Texture.Common({ src: 'textures/mask.png' }) // Optional light mask
|
|
1078
|
+
})
|
|
1079
|
+
})
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
### Visibility Control
|
|
1083
|
+
```typescript
|
|
1084
|
+
import { VisibilityComponent } from '@dcl/sdk/ecs'
|
|
1085
|
+
|
|
1086
|
+
// Hide an entity
|
|
1087
|
+
VisibilityComponent.create(entity, { visible: false })
|
|
1088
|
+
|
|
1089
|
+
// Show it again
|
|
1090
|
+
VisibilityComponent.getMutable(entity).visible = true
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
## Triggers & Interactions
|
|
1094
|
+
|
|
1095
|
+
### Raycasting
|
|
1096
|
+
```typescript
|
|
1097
|
+
import { Raycast, RaycastQueryType, raycastSystem, RaycastResult } from '@dcl/sdk/ecs'
|
|
1098
|
+
import { Vector3 } from '@dcl/sdk/math'
|
|
1099
|
+
import { ColliderLayer } from '@dcl/sdk/ecs'
|
|
1100
|
+
|
|
1101
|
+
// Basic raycast from entity
|
|
1102
|
+
Raycast.create(entity, {
|
|
1103
|
+
originOffset: Vector3.Zero(), // Offset from entity position
|
|
1104
|
+
direction: { $case: 'globalDirection', globalDirection: Vector3.Down() },
|
|
1105
|
+
maxDistance: 10,
|
|
1106
|
+
queryType: RaycastQueryType.RQT_HIT_FIRST, // First hit
|
|
1107
|
+
timestamp: Date.now() // Used to identify this raycast
|
|
1108
|
+
})
|
|
1109
|
+
|
|
1110
|
+
// Direction types for raycasts
|
|
1111
|
+
// 1. Global direction (ignores entity rotation)
|
|
1112
|
+
direction: { $case: 'globalDirection', globalDirection: Vector3.Down() }
|
|
1113
|
+
|
|
1114
|
+
// 2. Local direction (relative to entity's forward direction)
|
|
1115
|
+
direction: { $case: 'localDirection', localDirection: Vector3.Forward() }
|
|
1116
|
+
|
|
1117
|
+
// 3. Global target (points to a specific world position)
|
|
1118
|
+
direction: { $case: 'globalTarget', globalTarget: Vector3.create(10, 0, 10) }
|
|
1119
|
+
|
|
1120
|
+
// 4. Target entity (points to another entity)
|
|
1121
|
+
direction: { $case: 'targetEntity', targetEntity: targetEntityId }
|
|
1122
|
+
|
|
1123
|
+
// Query types
|
|
1124
|
+
queryType: RaycastQueryType.RQT_HIT_FIRST // Returns only the first hit
|
|
1125
|
+
queryType: RaycastQueryType.RQT_QUERY_ALL // Returns all hits along the ray
|
|
1126
|
+
|
|
1127
|
+
// Global raycast with callback
|
|
1128
|
+
raycastSystem.registerGlobalDirectionRaycast(
|
|
1129
|
+
{
|
|
1130
|
+
entity: engine.PlayerEntity,
|
|
1131
|
+
opts: {
|
|
1132
|
+
direction: Vector3.Down(),
|
|
1133
|
+
maxDistance: 10,
|
|
1134
|
+
collisionMask: ColliderLayer.CL_PHYSICS // Only hit specific layers
|
|
1135
|
+
}
|
|
1136
|
+
},
|
|
1137
|
+
(raycastResult) => {
|
|
1138
|
+
if (raycastResult.hits.length > 0) {
|
|
1139
|
+
console.log('Hit at', raycastResult.hits[0].position)
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1144
|
+
// Access raycast results in a system
|
|
1145
|
+
engine.addSystem(() => {
|
|
1146
|
+
for (const [entity, result] of engine.getEntitiesWith(RaycastResult)) {
|
|
1147
|
+
if (result.hits.length > 0) {
|
|
1148
|
+
// Process hits
|
|
1149
|
+
for (const hit of result.hits) {
|
|
1150
|
+
console.log(`Hit entity: ${hit.entityId}`)
|
|
1151
|
+
console.log(`Hit position: ${hit.position}`)
|
|
1152
|
+
console.log(`Hit normal: ${hit.normalHit}`)
|
|
1153
|
+
console.log(`Hit distance: ${hit.length}`)
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
})
|
|
1158
|
+
|
|
1159
|
+
// Raycast from camera
|
|
1160
|
+
raycastSystem.registerGlobalDirectionRaycast(
|
|
1161
|
+
{
|
|
1162
|
+
entity: engine.CameraEntity,
|
|
1163
|
+
opts: {
|
|
1164
|
+
queryType: RaycastQueryType.RQT_HIT_FIRST,
|
|
1165
|
+
direction: Vector3.rotate(
|
|
1166
|
+
Vector3.Forward(),
|
|
1167
|
+
Transform.get(engine.CameraEntity).rotation
|
|
1168
|
+
),
|
|
1169
|
+
},
|
|
1170
|
+
},
|
|
1171
|
+
function (raycastResult) {
|
|
1172
|
+
console.log(raycastResult)
|
|
1173
|
+
}
|
|
1174
|
+
)
|
|
1175
|
+
|
|
1176
|
+
// Continuous raycast (runs every frame)
|
|
1177
|
+
Raycast.create(entity, {
|
|
1178
|
+
direction: { $case: 'localDirection', localDirection: Vector3.Forward() },
|
|
1179
|
+
maxDistance: 16,
|
|
1180
|
+
queryType: RaycastQueryType.RQT_HIT_FIRST,
|
|
1181
|
+
originOffset: Vector3.create(0.5, 0, 0), // Prevent self-collision
|
|
1182
|
+
continuous: true // Run every frame
|
|
1183
|
+
})
|
|
1184
|
+
|
|
1185
|
+
// Raycast between two entities
|
|
1186
|
+
Raycast.create(entity1, {
|
|
1187
|
+
direction: {
|
|
1188
|
+
$case: "targetEntity",
|
|
1189
|
+
targetEntity: entity2
|
|
1190
|
+
},
|
|
1191
|
+
maxDistance: 16,
|
|
1192
|
+
queryType: RaycastQueryType.RQT_QUERY_ALL
|
|
1193
|
+
})
|
|
1194
|
+
```
|
|
1195
|
+
|
|
1196
|
+
### Avatar Modifier Areas
|
|
1197
|
+
```typescript
|
|
1198
|
+
import { AvatarModifierArea, AvatarModifierType } from '@dcl/sdk/ecs'
|
|
1199
|
+
|
|
1200
|
+
// Create an area that hides other avatars
|
|
1201
|
+
AvatarModifierArea.create(entity, {
|
|
1202
|
+
area: Vector3.create(5, 5, 5), // Box size
|
|
1203
|
+
modifiers: [AvatarModifierType.AMT_HIDE_AVATARS],
|
|
1204
|
+
// Or AMT_DISABLE_PASSPORTS
|
|
1205
|
+
excludeIds: ['user-address-1', 'user-address-2'] // Optional: players not affected
|
|
1206
|
+
})
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
### Portable Experiences
|
|
1210
|
+
```typescript
|
|
1211
|
+
import { spawn, kill, SpawnResponse } from '~system/PortableExperiences'
|
|
1212
|
+
|
|
1213
|
+
// Launch a portable experience
|
|
1214
|
+
let pxId: SpawnResponse
|
|
1215
|
+
spawn({ ens: 'experience.dcl.eth' }).then((response) => {
|
|
1216
|
+
pxId = response
|
|
1217
|
+
})
|
|
1218
|
+
|
|
1219
|
+
// Close a portable experience
|
|
1220
|
+
if (pxId?.pid) {
|
|
1221
|
+
kill({ pid: pxId.pid })
|
|
1222
|
+
}
|
|
1223
|
+
```
|
|
1224
|
+
|
|
1225
|
+
## Restricted Actions
|
|
1226
|
+
|
|
1227
|
+
### External Links
|
|
1228
|
+
```typescript
|
|
1229
|
+
import { openExternalUrl, openNftDialog } from '~system/RestrictedActions'
|
|
1230
|
+
|
|
1231
|
+
// Open a webpage
|
|
1232
|
+
openExternalUrl({ url: 'https://decentraland.org' })
|
|
1233
|
+
|
|
1234
|
+
// Open NFT info dialog
|
|
1235
|
+
openNftDialog({
|
|
1236
|
+
urn: 'urn:decentraland:ethereum:erc721:0x06012c8cf97bead5deae237070f9587f8e7a266d:1540722'
|
|
1237
|
+
})
|
|
1238
|
+
```
|
|
1239
|
+
|
|
1240
|
+
### Teleportation
|
|
1241
|
+
```typescript
|
|
1242
|
+
import { teleportTo, changeRealm } from '~system/RestrictedActions'
|
|
1243
|
+
|
|
1244
|
+
// Teleport to another scene
|
|
1245
|
+
teleportTo({ worldCoordinates: { x: 10, y: 20 } })
|
|
1246
|
+
|
|
1247
|
+
// Change Decentraland realm
|
|
1248
|
+
changeRealm({
|
|
1249
|
+
realm: 'https://peer.decentraland.org',
|
|
1250
|
+
message: 'Do you want to change realms?' // Optional confirmation message
|
|
1251
|
+
})
|
|
1252
|
+
```
|
|
1253
|
+
|
|
1254
|
+
## Testing Framework
|
|
1255
|
+
|
|
1256
|
+
### Writing Tests
|
|
1257
|
+
```typescript
|
|
1258
|
+
import { test } from '@dcl/sdk/testing'
|
|
1259
|
+
import { assertComponentValue } from '@dcl/sdk/testing/assert'
|
|
1260
|
+
|
|
1261
|
+
test('my test case', function* (context) {
|
|
1262
|
+
// Create test setup
|
|
1263
|
+
const entity = engine.addEntity()
|
|
1264
|
+
Transform.create(entity, { position: Vector3.One() })
|
|
1265
|
+
|
|
1266
|
+
// Let the engine run for a frame
|
|
1267
|
+
yield
|
|
1268
|
+
|
|
1269
|
+
// Check component values
|
|
1270
|
+
assertComponentValue(entity, Transform, {
|
|
1271
|
+
position: Vector3.One(),
|
|
1272
|
+
scale: Vector3.One(),
|
|
1273
|
+
rotation: Quaternion.Identity()
|
|
1274
|
+
})
|
|
1275
|
+
})
|
|
1276
|
+
```
|
|
1277
|
+
|
|
1278
|
+
### Test Assertions
|
|
1279
|
+
```typescript
|
|
1280
|
+
import { assertEquals, assertEntitiesCount } from '@dcl/sdk/testing/assert'
|
|
1281
|
+
|
|
1282
|
+
// Basic assertions
|
|
1283
|
+
assertEquals(actual, expected, 'Optional message')
|
|
1284
|
+
|
|
1285
|
+
// Entity assertions
|
|
1286
|
+
assertEntitiesCount(engine.getEntitiesWith(MeshRenderer), 5, 'Should have 5 entities with MeshRenderer')
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
## Scene Optimization
|
|
1290
|
+
|
|
1291
|
+
### Entity Pooling
|
|
1292
|
+
```typescript
|
|
1293
|
+
// Create an entity pool for reuse
|
|
1294
|
+
const entityPool: Entity[] = []
|
|
1295
|
+
|
|
1296
|
+
function getEntityFromPool(): Entity {
|
|
1297
|
+
if (entityPool.length > 0) {
|
|
1298
|
+
return entityPool.pop()!
|
|
1299
|
+
} else {
|
|
1300
|
+
return createNewEntity()
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function returnEntityToPool(entity: Entity) {
|
|
1305
|
+
// Reset the entity to a clean state
|
|
1306
|
+
VisibilityComponent.getMutable(entity).visible = false
|
|
1307
|
+
entityPool.push(entity)
|
|
1308
|
+
}
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1311
|
+
### Visibility Culling
|
|
1312
|
+
```typescript
|
|
1313
|
+
// Create a system that hides distant entities
|
|
1314
|
+
engine.addSystem(() => {
|
|
1315
|
+
const playerPos = Transform.get(engine.PlayerEntity).position
|
|
1316
|
+
|
|
1317
|
+
for (const [entity, transform] of engine.getEntitiesWith(Transform, VisibilityComponent)) {
|
|
1318
|
+
const distance = Vector3.distance(playerPos, transform.position)
|
|
1319
|
+
|
|
1320
|
+
if (distance > 20) {
|
|
1321
|
+
VisibilityComponent.getMutable(entity).visible = false
|
|
1322
|
+
} else {
|
|
1323
|
+
VisibilityComponent.getMutable(entity).visible = true
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
})
|
|
1327
|
+
```
|
|
1328
|
+
|
|
1329
|
+
## Network Connections
|
|
1330
|
+
|
|
1331
|
+
### Fetch API
|
|
1332
|
+
```typescript
|
|
1333
|
+
import { executeTask } from '@dcl/sdk/ecs'
|
|
1334
|
+
|
|
1335
|
+
// Basic GET request
|
|
1336
|
+
executeTask(async () => {
|
|
1337
|
+
try {
|
|
1338
|
+
const response = await fetch('https://api.example.com/data')
|
|
1339
|
+
const json = await response.json()
|
|
1340
|
+
console.log('Response:', json)
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
console.error('Failed to fetch:', error)
|
|
1343
|
+
}
|
|
1344
|
+
})
|
|
1345
|
+
|
|
1346
|
+
// POST request with headers and body
|
|
1347
|
+
executeTask(async () => {
|
|
1348
|
+
try {
|
|
1349
|
+
const response = await fetch('https://api.example.com/data', {
|
|
1350
|
+
method: 'POST',
|
|
1351
|
+
headers: {
|
|
1352
|
+
'Content-Type': 'application/json'
|
|
1353
|
+
},
|
|
1354
|
+
body: JSON.stringify({
|
|
1355
|
+
key: 'value'
|
|
1356
|
+
})
|
|
1357
|
+
})
|
|
1358
|
+
const json = await response.json()
|
|
1359
|
+
console.log('Response:', json)
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
console.error('Failed to fetch:', error)
|
|
1362
|
+
}
|
|
1363
|
+
})
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
### Signed Fetch
|
|
1367
|
+
```typescript
|
|
1368
|
+
import { executeTask } from '@dcl/sdk/ecs'
|
|
1369
|
+
import { signedFetch } from '@dcl/sdk/network'
|
|
1370
|
+
|
|
1371
|
+
// Basic signed GET request
|
|
1372
|
+
executeTask(async () => {
|
|
1373
|
+
try {
|
|
1374
|
+
const response = await signedFetch('https://api.example.com/data')
|
|
1375
|
+
const json = await response.json()
|
|
1376
|
+
console.log('Response:', json)
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
console.error('Failed to fetch:', error)
|
|
1379
|
+
}
|
|
1380
|
+
})
|
|
1381
|
+
|
|
1382
|
+
// Signed POST request with headers and body
|
|
1383
|
+
executeTask(async () => {
|
|
1384
|
+
try {
|
|
1385
|
+
const response = await signedFetch('https://api.example.com/data', {
|
|
1386
|
+
method: 'POST',
|
|
1387
|
+
headers: {
|
|
1388
|
+
'Content-Type': 'application/json'
|
|
1389
|
+
},
|
|
1390
|
+
body: JSON.stringify({
|
|
1391
|
+
key: 'value'
|
|
1392
|
+
})
|
|
1393
|
+
})
|
|
1394
|
+
const json = await response.json()
|
|
1395
|
+
console.log('Response:', json)
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
console.error('Failed to fetch:', error)
|
|
1398
|
+
}
|
|
1399
|
+
})
|
|
1400
|
+
```
|
|
1401
|
+
|
|
1402
|
+
### WebSocket Connections
|
|
1403
|
+
```typescript
|
|
1404
|
+
import { executeTask } from '@dcl/sdk/ecs'
|
|
1405
|
+
|
|
1406
|
+
// Basic WebSocket connection
|
|
1407
|
+
executeTask(async () => {
|
|
1408
|
+
const ws = new WebSocket('wss://example.com/ws')
|
|
1409
|
+
|
|
1410
|
+
ws.onopen = () => {
|
|
1411
|
+
console.log('Connected to WebSocket')
|
|
1412
|
+
ws.send('Hello Server!')
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
ws.onmessage = (event) => {
|
|
1416
|
+
console.log('Received:', event.data)
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
ws.onerror = (error) => {
|
|
1420
|
+
console.error('WebSocket error:', error)
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
ws.onclose = () => {
|
|
1424
|
+
console.log('Disconnected from WebSocket')
|
|
1425
|
+
}
|
|
1426
|
+
})
|
|
1427
|
+
|
|
1428
|
+
// WebSocket with reconnection logic
|
|
1429
|
+
executeTask(async () => {
|
|
1430
|
+
let ws: WebSocket | null = null
|
|
1431
|
+
let reconnectAttempts = 0
|
|
1432
|
+
const maxReconnectAttempts = 5
|
|
1433
|
+
|
|
1434
|
+
function connect() {
|
|
1435
|
+
ws = new WebSocket('wss://example.com/ws')
|
|
1436
|
+
|
|
1437
|
+
ws.onopen = () => {
|
|
1438
|
+
console.log('Connected to WebSocket')
|
|
1439
|
+
reconnectAttempts = 0
|
|
1440
|
+
ws?.send('Hello Server!')
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
ws.onmessage = (event) => {
|
|
1444
|
+
console.log('Received:', event.data)
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
ws.onerror = (error) => {
|
|
1448
|
+
console.error('WebSocket error:', error)
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
ws.onclose = () => {
|
|
1452
|
+
console.log('Disconnected from WebSocket')
|
|
1453
|
+
if (reconnectAttempts < maxReconnectAttempts) {
|
|
1454
|
+
reconnectAttempts++
|
|
1455
|
+
setTimeout(connect, 1000 * reconnectAttempts)
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
connect()
|
|
1461
|
+
})
|
|
1462
|
+
```
|
|
1463
|
+
|
|
1464
|
+
### Entity Synchronization
|
|
1465
|
+
```typescript
|
|
1466
|
+
import { syncEntity } from '@dcl/sdk/network'
|
|
1467
|
+
import { engine, Transform, MeshRenderer } from '@dcl/sdk/ecs'
|
|
1468
|
+
import { Vector3 } from '@dcl/sdk/math'
|
|
1469
|
+
|
|
1470
|
+
// Create a synced entity with a specific entityEnumId
|
|
1471
|
+
const syncedEntity = engine.addEntity()
|
|
1472
|
+
Transform.create(syncedEntity, {
|
|
1473
|
+
position: Vector3.create(8, 1, 8),
|
|
1474
|
+
scale: Vector3.create(1, 1, 1)
|
|
1475
|
+
})
|
|
1476
|
+
MeshRenderer.setBox(syncedEntity)
|
|
1477
|
+
|
|
1478
|
+
// Sync the entity with other players
|
|
1479
|
+
syncEntity(syncedEntity, [Transform.componentId], 1) // entityEnumId: 1
|
|
1480
|
+
|
|
1481
|
+
// Create a synced entity with multiple components
|
|
1482
|
+
const complexEntity = engine.addEntity()
|
|
1483
|
+
Transform.create(complexEntity, {
|
|
1484
|
+
position: Vector3.create(5, 1, 5)
|
|
1485
|
+
})
|
|
1486
|
+
MeshRenderer.setBox(complexEntity)
|
|
1487
|
+
|
|
1488
|
+
// Sync multiple components
|
|
1489
|
+
syncEntity(complexEntity, [
|
|
1490
|
+
Transform.componentId,
|
|
1491
|
+
MeshRenderer.componentId
|
|
1492
|
+
], 2) // entityEnumId: 2
|
|
1493
|
+
|
|
1494
|
+
// Create a synced entity that responds to player interaction
|
|
1495
|
+
const interactiveEntity = engine.addEntity()
|
|
1496
|
+
Transform.create(interactiveEntity, {
|
|
1497
|
+
position: Vector3.create(3, 1, 3)
|
|
1498
|
+
})
|
|
1499
|
+
MeshRenderer.setBox(interactiveEntity)
|
|
1500
|
+
|
|
1501
|
+
// Sync the entity and handle updates
|
|
1502
|
+
syncEntity(interactiveEntity, [Transform.componentId], 3) // entityEnumId: 3
|
|
1503
|
+
|
|
1504
|
+
// Update synced entity position
|
|
1505
|
+
Transform.getMutable(interactiveEntity).position = Vector3.create(4, 1, 4)
|
|
1506
|
+
```
|
|
1507
|
+
|
|
1508
|
+
### Message Bus
|
|
1509
|
+
```typescript
|
|
1510
|
+
import { MessageBus } from '@dcl/sdk/message-bus'
|
|
1511
|
+
|
|
1512
|
+
// Create a message bus instance
|
|
1513
|
+
const messageBus = new MessageBus()
|
|
1514
|
+
|
|
1515
|
+
// Define message types
|
|
1516
|
+
type SpawnMessage = {
|
|
1517
|
+
position: { x: number; y: number; z: number }
|
|
1518
|
+
entityEnumId: number
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
type UpdateMessage = {
|
|
1522
|
+
entityId: number
|
|
1523
|
+
position: { x: number; y: number; z: number }
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Send a spawn message
|
|
1527
|
+
messageBus.emit('spawn', {
|
|
1528
|
+
position: { x: 8, y: 1, z: 8 },
|
|
1529
|
+
entityEnumId: 1
|
|
1530
|
+
} as SpawnMessage)
|
|
1531
|
+
|
|
1532
|
+
// Listen for spawn messages
|
|
1533
|
+
messageBus.on('spawn', (message: SpawnMessage) => {
|
|
1534
|
+
const entity = engine.addEntity()
|
|
1535
|
+
Transform.create(entity, {
|
|
1536
|
+
position: Vector3.create(
|
|
1537
|
+
message.position.x,
|
|
1538
|
+
message.position.y,
|
|
1539
|
+
message.position.z
|
|
1540
|
+
)
|
|
1541
|
+
})
|
|
1542
|
+
MeshRenderer.setBox(entity)
|
|
1543
|
+
|
|
1544
|
+
// Sync the newly created entity
|
|
1545
|
+
syncEntity(entity, [Transform.componentId], message.entityEnumId)
|
|
1546
|
+
})
|
|
1547
|
+
|
|
1548
|
+
// Send an update message
|
|
1549
|
+
messageBus.emit('update', {
|
|
1550
|
+
entityId: 1,
|
|
1551
|
+
position: { x: 10, y: 1, z: 10 }
|
|
1552
|
+
} as UpdateMessage)
|
|
1553
|
+
|
|
1554
|
+
// Listen for update messages
|
|
1555
|
+
messageBus.on('update', (message: UpdateMessage) => {
|
|
1556
|
+
// Find the entity by its synced ID
|
|
1557
|
+
const entity = engine.getEntityById(message.entityId)
|
|
1558
|
+
if (entity) {
|
|
1559
|
+
Transform.getMutable(entity).position = Vector3.create(
|
|
1560
|
+
message.position.x,
|
|
1561
|
+
message.position.y,
|
|
1562
|
+
message.position.z
|
|
1563
|
+
)
|
|
1564
|
+
}
|
|
1565
|
+
})
|
|
1566
|
+
|
|
1567
|
+
// Example of a complete multiplayer interaction
|
|
1568
|
+
function handlePlayerInteraction(entity: Entity) {
|
|
1569
|
+
// When a player interacts with an entity
|
|
1570
|
+
messageBus.emit('interaction', {
|
|
1571
|
+
entityId: entity,
|
|
1572
|
+
action: 'click',
|
|
1573
|
+
timestamp: Date.now()
|
|
1574
|
+
})
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// Listen for player interactions
|
|
1578
|
+
messageBus.on('interaction', (message) => {
|
|
1579
|
+
console.log(`Entity ${message.entityId} was ${message.action}ed at ${message.timestamp}`)
|
|
1580
|
+
// Handle the interaction for all players
|
|
1581
|
+
})
|
|
1582
|
+
```
|
|
1583
|
+
|
|
1584
|
+
## Blockchain Operations
|
|
1585
|
+
|
|
1586
|
+
### Get Player's Ethereum Account
|
|
1587
|
+
```typescript
|
|
1588
|
+
import { getPlayer } from '@dcl/sdk/src/players'
|
|
1589
|
+
|
|
1590
|
+
export function main() {
|
|
1591
|
+
let userData = getPlayer()
|
|
1592
|
+
if (!userData.isGuest) {
|
|
1593
|
+
console.log(userData.userId)
|
|
1594
|
+
} else {
|
|
1595
|
+
log('Player is not connected with Web3')
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
```
|
|
1599
|
+
|
|
1600
|
+
### Check Gas Price
|
|
1601
|
+
```typescript
|
|
1602
|
+
import { RequestManager } from 'eth-connect'
|
|
1603
|
+
import { createEthereumProvider } from '@dcl/sdk/ethereum-provider'
|
|
1604
|
+
|
|
1605
|
+
executeTask(async function () {
|
|
1606
|
+
// Create an instance of the web3 provider to interface with Metamask
|
|
1607
|
+
const provider = createEthereumProvider()
|
|
1608
|
+
// Create the object that will handle the sending and receiving of RPC messages
|
|
1609
|
+
const requestManager = new RequestManager(provider)
|
|
1610
|
+
// Check the current gas price on the Ethereum network
|
|
1611
|
+
const gasPrice = await requestManager.eth_gasPrice()
|
|
1612
|
+
// log response
|
|
1613
|
+
console.log({ gasPrice })
|
|
1614
|
+
})
|
|
1615
|
+
```
|
|
1616
|
+
|
|
1617
|
+
### Import Contract ABI
|
|
1618
|
+
```typescript
|
|
1619
|
+
// Example of one function in the MANA ABI
|
|
1620
|
+
{
|
|
1621
|
+
anonymous: false,
|
|
1622
|
+
inputs: [
|
|
1623
|
+
{
|
|
1624
|
+
indexed: true,
|
|
1625
|
+
name: 'burner',
|
|
1626
|
+
type: 'address'
|
|
1627
|
+
},
|
|
1628
|
+
{
|
|
1629
|
+
indexed: false,
|
|
1630
|
+
name: 'value',
|
|
1631
|
+
type: 'uint256'
|
|
1632
|
+
}
|
|
1633
|
+
],
|
|
1634
|
+
name: 'Burn',
|
|
1635
|
+
type: 'event'
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// Import the ABI
|
|
1639
|
+
import { abi } from '../contracts/mana'
|
|
1640
|
+
```
|
|
1641
|
+
|
|
1642
|
+
### Instance a Contract
|
|
1643
|
+
```typescript
|
|
1644
|
+
import { RequestManager, ContractFactory } from 'eth-connect'
|
|
1645
|
+
import { createEthereumProvider } from '@dcl/sdk/ethereum-provider'
|
|
1646
|
+
import { abi } from '../contracts/mana'
|
|
1647
|
+
|
|
1648
|
+
executeTask(async () => {
|
|
1649
|
+
// Create an instance of the web3 provider to interface with Metamask
|
|
1650
|
+
const provider = createEthereumProvider()
|
|
1651
|
+
// Create the object that will handle the sending and receiving of RPC messages
|
|
1652
|
+
const requestManager = new RequestManager(provider)
|
|
1653
|
+
// Create a factory object based on the abi
|
|
1654
|
+
const factory = new ContractFactory(requestManager, abi)
|
|
1655
|
+
// Use the factory object to instance a `contract` object, referencing a specific contract
|
|
1656
|
+
const contract = (await factory.at(
|
|
1657
|
+
'0x2a8fd99c19271f4f04b1b7b9c4f7cf264b626edb'
|
|
1658
|
+
)) as any
|
|
1659
|
+
})
|
|
1660
|
+
```
|
|
1661
|
+
|
|
1662
|
+
### Call Contract Methods
|
|
1663
|
+
```typescript
|
|
1664
|
+
import { getPlayer } from '@dcl/sdk/src/players'
|
|
1665
|
+
import { createEthereumProvider } from '@dcl/sdk/ethereum-provider'
|
|
1666
|
+
import { RequestManager, ContractFactory } from 'eth-connect'
|
|
1667
|
+
import { abi } from '../contracts/mana'
|
|
1668
|
+
|
|
1669
|
+
executeTask(async () => {
|
|
1670
|
+
try {
|
|
1671
|
+
// Setup steps explained in the section above
|
|
1672
|
+
const provider = createEthereumProvider()
|
|
1673
|
+
const requestManager = new RequestManager(provider)
|
|
1674
|
+
const factory = new ContractFactory(requestManager, abi)
|
|
1675
|
+
const contract = (await factory.at(
|
|
1676
|
+
'0x2a8fd99c19271f4f04b1b7b9c4f7cf264b626edb'
|
|
1677
|
+
)) as any
|
|
1678
|
+
let userData = getPlayer()
|
|
1679
|
+
if (userData.isGuest) {
|
|
1680
|
+
return
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// Perform a function from the contract
|
|
1684
|
+
const res = await contract.setBalance(
|
|
1685
|
+
'0xaFA48Fad27C7cAB28dC6E970E4BFda7F7c8D60Fb',
|
|
1686
|
+
100,
|
|
1687
|
+
{
|
|
1688
|
+
from: userData.userId,
|
|
1689
|
+
}
|
|
1690
|
+
)
|
|
1691
|
+
// Log response
|
|
1692
|
+
console.log(res)
|
|
1693
|
+
} catch (error: any) {
|
|
1694
|
+
console.log(error.toString())
|
|
1695
|
+
}
|
|
1696
|
+
})
|
|
1697
|
+
```
|
|
1698
|
+
|
|
1699
|
+
### Send Custom RPC Messages
|
|
1700
|
+
```typescript
|
|
1701
|
+
import { sendAsync } from '~system/EthereumController'
|
|
1702
|
+
|
|
1703
|
+
// send a message
|
|
1704
|
+
await sendAsync({
|
|
1705
|
+
id: 1,
|
|
1706
|
+
method: 'myMethod',
|
|
1707
|
+
jsonParams: '{ myParam: myValue }',
|
|
1708
|
+
})
|
|
1709
|
+
```
|