@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,329 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: authoritative-server
|
|
3
|
+
description: Build multiplayer scenes with a headless authoritative server that controls game state, validates changes, and prevents cheating. Install @dcl/sdk@auth-server and run with hammurabi-server. Use isServer() to branch logic, registerMessages() for client-server communication, validateBeforeChange() for server-only components, Storage for persistence, and EnvVar for configuration. Use when user wants authoritative server, anti-cheat, server-side validation, persistent storage, environment variables, or server messages.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Authoritative Server Pattern
|
|
7
|
+
|
|
8
|
+
Build multiplayer Decentraland scenes where a **headless server** controls game state, validates changes, and prevents cheating. The same codebase runs on both server and client, with the server having full authority.
|
|
9
|
+
|
|
10
|
+
Before reading this skill, read `{baseDir}/../../context/sdk7-complete-reference.md` for general SDK7 knowledge. For basic CRDT multiplayer (no server), see the `multiplayer-sync` skill instead.
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
Install the auth-server SDK branch:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @dcl/sdk@auth-server
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Your `scene.json` must include a world name:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"worldConfiguration": {
|
|
25
|
+
"name": "my-world-name"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Run the scene:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# With authoritative server (required for this pattern)
|
|
34
|
+
npx @dcl/hammurabi-server@next
|
|
35
|
+
|
|
36
|
+
# Standard dev server (no auth server, for client-only testing)
|
|
37
|
+
npm run start
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Server/Client Branching
|
|
41
|
+
|
|
42
|
+
Use `isServer()` to branch logic in a single codebase:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { isServer } from '@dcl/sdk/network'
|
|
46
|
+
|
|
47
|
+
export async function main() {
|
|
48
|
+
if (isServer()) {
|
|
49
|
+
// Server-only: game logic, validation, state management
|
|
50
|
+
const { server } = await import('./server/server')
|
|
51
|
+
server()
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Client-only: UI, input, message sending
|
|
56
|
+
setupClient()
|
|
57
|
+
setupUi()
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The server runs your scene code headlessly (no rendering). It has access to all player positions via `PlayerIdentityData` and manages all authoritative game state.
|
|
62
|
+
|
|
63
|
+
## Synced Components with Validation
|
|
64
|
+
|
|
65
|
+
Define custom components that sync from server to all clients. **Always** use `validateBeforeChange()` to prevent clients from modifying server-authoritative state.
|
|
66
|
+
|
|
67
|
+
### Custom Components (Global Validation)
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { engine, Schemas } from '@dcl/sdk/ecs'
|
|
71
|
+
import { AUTH_SERVER_PEER_ID } from '@dcl/sdk/network/message-bus-sync'
|
|
72
|
+
|
|
73
|
+
export const GameState = engine.defineComponent('game:State', {
|
|
74
|
+
phase: Schemas.String,
|
|
75
|
+
score: Schemas.Number,
|
|
76
|
+
timeRemaining: Schemas.Number
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Restrict ALL modifications to server only
|
|
80
|
+
GameState.validateBeforeChange((value) => {
|
|
81
|
+
return value.senderAddress === AUTH_SERVER_PEER_ID
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Built-in Components (Per-Entity Validation)
|
|
86
|
+
|
|
87
|
+
For built-in components like `Transform` and `GltfContainer`, use per-entity validation so you don't block client-side transforms on the player's own entities:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { Entity, Transform, GltfContainer } from '@dcl/sdk/ecs'
|
|
91
|
+
import { AUTH_SERVER_PEER_ID } from '@dcl/sdk/network/message-bus-sync'
|
|
92
|
+
|
|
93
|
+
type ComponentWithValidation = {
|
|
94
|
+
validateBeforeChange: (entity: Entity, cb: (value: { senderAddress: string }) => boolean) => void
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function protectServerEntity(entity: Entity, components: ComponentWithValidation[]) {
|
|
98
|
+
for (const component of components) {
|
|
99
|
+
component.validateBeforeChange(entity, (value) => {
|
|
100
|
+
return value.senderAddress === AUTH_SERVER_PEER_ID
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Usage: after creating a server-managed entity
|
|
106
|
+
const entity = engine.addEntity()
|
|
107
|
+
Transform.create(entity, { position: Vector3.create(10, 5, 10) })
|
|
108
|
+
GltfContainer.create(entity, { src: 'assets/model.glb' })
|
|
109
|
+
protectServerEntity(entity, [Transform, GltfContainer])
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Syncing Entities
|
|
113
|
+
|
|
114
|
+
After creating and protecting an entity, sync it to all clients:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { syncEntity } from '@dcl/sdk/network'
|
|
118
|
+
|
|
119
|
+
syncEntity(entity, [Transform.componentId, GameState.componentId])
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Messages
|
|
123
|
+
|
|
124
|
+
Use `registerMessages()` for client-to-server and server-to-client communication:
|
|
125
|
+
|
|
126
|
+
### Define Messages
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { Schemas } from '@dcl/sdk/ecs'
|
|
130
|
+
import { registerMessages } from '@dcl/sdk/network'
|
|
131
|
+
|
|
132
|
+
export const Messages = {
|
|
133
|
+
// Client -> Server
|
|
134
|
+
playerJoin: Schemas.Map({ displayName: Schemas.String }),
|
|
135
|
+
playerAction: Schemas.Map({ actionType: Schemas.String, data: Schemas.Number }),
|
|
136
|
+
|
|
137
|
+
// Server -> Client
|
|
138
|
+
gameEvent: Schemas.Map({ eventType: Schemas.String, playerName: Schemas.String })
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const room = registerMessages(Messages)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Send Messages
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// Client sends to server
|
|
148
|
+
room.send('playerJoin', { displayName: 'Alice' })
|
|
149
|
+
|
|
150
|
+
// Server sends to ALL clients
|
|
151
|
+
room.send('gameEvent', { eventType: 'ROUND_START', playerName: '' })
|
|
152
|
+
|
|
153
|
+
// Server sends to ONE client
|
|
154
|
+
room.send('gameEvent', { eventType: 'YOU_WIN', playerName: 'Alice' }, { to: [playerAddress] })
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Receive Messages
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// Server receives from client
|
|
161
|
+
room.onMessage('playerJoin', (data, context) => {
|
|
162
|
+
if (!context) return
|
|
163
|
+
const playerAddress = context.from // Wallet address of sender
|
|
164
|
+
console.log(`[Server] Player joined: ${data.displayName} (${playerAddress})`)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// Client receives from server
|
|
168
|
+
room.onMessage('gameEvent', (data) => {
|
|
169
|
+
console.log(`Event: ${data.eventType}`)
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Wait for Room Connection
|
|
174
|
+
|
|
175
|
+
Before sending messages from the client, wait for the connected scene room:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { engine } from '@dcl/sdk/ecs'
|
|
179
|
+
import { RealmInfo } from '@dcl/sdk/ecs'
|
|
180
|
+
|
|
181
|
+
let joined = false
|
|
182
|
+
engine.addSystem(() => {
|
|
183
|
+
if (joined) return
|
|
184
|
+
const realm = RealmInfo.getOrNull(engine.RootEntity)
|
|
185
|
+
if (realm?.isConnectedSceneRoom) {
|
|
186
|
+
joined = true
|
|
187
|
+
room.send('playerJoin', { displayName: 'Player' })
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Server Reading Player Positions
|
|
193
|
+
|
|
194
|
+
The server can read **actual** player positions — critical for anti-cheat:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { engine, PlayerIdentityData, Transform } from '@dcl/sdk/ecs'
|
|
198
|
+
|
|
199
|
+
engine.addSystem(() => {
|
|
200
|
+
for (const [entity, identity] of engine.getEntitiesWith(PlayerIdentityData)) {
|
|
201
|
+
const transform = Transform.getOrNull(entity)
|
|
202
|
+
if (!transform) continue
|
|
203
|
+
|
|
204
|
+
const address = identity.address
|
|
205
|
+
const position = transform.position
|
|
206
|
+
// Use actual server-verified position, not client-reported data
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Never trust client-reported positions. Always read `PlayerIdentityData` + `Transform` on the server.
|
|
212
|
+
|
|
213
|
+
## Storage
|
|
214
|
+
|
|
215
|
+
Persist data across server restarts. **Server-only** — guard with `isServer()`.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { Storage } from '@dcl/sdk/server'
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### World Storage (Global)
|
|
222
|
+
|
|
223
|
+
Shared across all players:
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
// Store
|
|
227
|
+
await Storage.world.set('leaderboard', JSON.stringify(leaderboardData))
|
|
228
|
+
|
|
229
|
+
// Retrieve
|
|
230
|
+
const data = await Storage.world.get<string>('leaderboard')
|
|
231
|
+
if (data) {
|
|
232
|
+
const leaderboard = JSON.parse(data)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Delete
|
|
236
|
+
await Storage.world.delete('oldKey')
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Player Storage (Per-Player)
|
|
240
|
+
|
|
241
|
+
Keyed by player wallet address:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// Store
|
|
245
|
+
await Storage.player.set(playerAddress, 'highScore', String(score))
|
|
246
|
+
|
|
247
|
+
// Retrieve
|
|
248
|
+
const saved = await Storage.player.get<string>(playerAddress, 'highScore')
|
|
249
|
+
const highScore = saved ? parseInt(saved) : 0
|
|
250
|
+
|
|
251
|
+
// Delete
|
|
252
|
+
await Storage.player.delete(playerAddress, 'highScore')
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Storage only accepts strings. Use `JSON.stringify()`/`JSON.parse()` for objects and `String()`/`parseInt()` for numbers.
|
|
256
|
+
|
|
257
|
+
Local development storage is at `node_modules/@dcl/sdk-commands/.runtime-data/server-storage.json`.
|
|
258
|
+
|
|
259
|
+
## Environment Variables
|
|
260
|
+
|
|
261
|
+
Configure your scene without hardcoding values. **Server-only** — guard with `isServer()`.
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { EnvVar } from '@dcl/sdk/server'
|
|
265
|
+
|
|
266
|
+
// Read a variable with default
|
|
267
|
+
const maxPlayers = parseInt((await EnvVar.get('MAX_PLAYERS')) || '4')
|
|
268
|
+
const debugMode = ((await EnvVar.get('DEBUG')) || 'false') === 'true'
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Local Development
|
|
272
|
+
|
|
273
|
+
Create a `.env` file in your project root:
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
MAX_PLAYERS=8
|
|
277
|
+
GAME_DURATION=300
|
|
278
|
+
DEBUG=true
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Add `.env` to your `.gitignore`.
|
|
282
|
+
|
|
283
|
+
### Deploy to Production
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
# Set a variable
|
|
287
|
+
npx sdk-commands deploy-env MAX_PLAYERS --value 8
|
|
288
|
+
|
|
289
|
+
# Delete a variable
|
|
290
|
+
npx sdk-commands deploy-env OLD_VAR --delete
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Deployed env vars take precedence over `.env` file values.
|
|
294
|
+
|
|
295
|
+
## Recommended Project Structure
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
src/
|
|
299
|
+
├── index.ts # Entry point — isServer() branching
|
|
300
|
+
├── client/
|
|
301
|
+
│ ├── setup.ts # Client initialization, message handlers
|
|
302
|
+
│ └── ui.tsx # React ECS UI reading synced state
|
|
303
|
+
├── server/
|
|
304
|
+
│ ├── server.ts # Server init, systems, message handlers
|
|
305
|
+
│ └── gameState.ts # Server state management class
|
|
306
|
+
└── shared/
|
|
307
|
+
├── schemas.ts # Synced component definitions + validateBeforeChange
|
|
308
|
+
└── messages.ts # Message definitions via registerMessages()
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Put synced components and messages in `shared/` so both server and client import the same definitions. Keep server logic (Storage, EnvVar, game systems) in `server/`. Keep UI and client input in `client/`.
|
|
312
|
+
|
|
313
|
+
## Testing & Debugging
|
|
314
|
+
|
|
315
|
+
- **Log prefixes**: Use `[Server]` and `[Client]` prefixes in `console.log()` to distinguish server and client output in the terminal.
|
|
316
|
+
- **Stale CRDT files**: If you see "Outside of the bounds of written data" errors, delete `main.crdt` and `main1.crdt` files and restart.
|
|
317
|
+
- **Storage inspection**: Check `node_modules/@dcl/sdk-commands/.runtime-data/server-storage.json` to inspect persisted data during local development.
|
|
318
|
+
- **No setTimeout/setInterval**: The DCL runtime does not support these. Use `engine.addSystem()` with a timer variable instead.
|
|
319
|
+
- **Entity sync issues**: Verify you call `syncEntity(entity, [componentIds])` with the correct component IDs (`MyComponent.componentId`).
|
|
320
|
+
|
|
321
|
+
## Important Notes
|
|
322
|
+
|
|
323
|
+
- **Use `Schemas.Int64` for timestamps**: `Schemas.Number` corrupts large numbers (13+ digits). Always use `Schemas.Int64` for values like `Date.now()`.
|
|
324
|
+
- **Room readiness**: Clients must wait for `RealmInfo.get(engine.RootEntity).isConnectedSceneRoom` before sending messages.
|
|
325
|
+
- **Custom vs built-in validation**: Custom components use global `validateBeforeChange((value) => ...)`. Built-in components (Transform, GltfContainer) use per-entity `validateBeforeChange(entity, (value) => ...)`.
|
|
326
|
+
- **Single codebase**: Both server and client run the same `index.ts` entry point. Use `isServer()` to branch.
|
|
327
|
+
- **No Node.js APIs**: The DCL runtime uses sandboxed QuickJS — no `fs`, `http`, `setTimeout`, etc. Use SDK-provided APIs (Storage, EnvVar, engine systems) instead.
|
|
328
|
+
- **SDK branch**: The auth-server pattern requires `@dcl/sdk@auth-server`, not the standard `@dcl/sdk` package.
|
|
329
|
+
- For basic CRDT multiplayer without a server, see the `multiplayer-sync` skill.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-ui
|
|
3
|
+
description: Build 2D user interfaces for Decentraland scenes using React-ECS. Create HUDs, menus, health bars, scoreboards, dialogs, buttons, and input forms. Use when user wants to add UI, HUD, buttons, text overlays, menus, or on-screen elements.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Building UI with React-ECS
|
|
7
|
+
|
|
8
|
+
Decentraland SDK7 uses a React-like JSX system for 2D UI overlays.
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
### File: src/ui.tsx
|
|
13
|
+
```tsx
|
|
14
|
+
import ReactEcs, { UiEntity, Label, Button } from '@dcl/sdk/react-ecs'
|
|
15
|
+
|
|
16
|
+
function MyUI() {
|
|
17
|
+
return (
|
|
18
|
+
<UiEntity
|
|
19
|
+
uiTransform={{
|
|
20
|
+
width: '100%',
|
|
21
|
+
height: '100%',
|
|
22
|
+
justifyContent: 'center',
|
|
23
|
+
alignItems: 'center'
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
<Label value="Hello Decentraland!" fontSize={24} />
|
|
27
|
+
</UiEntity>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function setupUi() {
|
|
32
|
+
ReactEcs.setUiRenderer(MyUI)
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### File: src/index.ts
|
|
37
|
+
```typescript
|
|
38
|
+
import { setupUi } from './ui'
|
|
39
|
+
|
|
40
|
+
export function main() {
|
|
41
|
+
setupUi()
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Required tsconfig.json settings
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"compilerOptions": {
|
|
49
|
+
"jsx": "react-jsx",
|
|
50
|
+
"jsxImportSource": "@dcl/sdk/react-ecs-lib"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Core Components
|
|
56
|
+
|
|
57
|
+
### UiEntity (Container)
|
|
58
|
+
```tsx
|
|
59
|
+
<UiEntity
|
|
60
|
+
uiTransform={{
|
|
61
|
+
width: 300, // Pixels or '50%'
|
|
62
|
+
height: 200,
|
|
63
|
+
positionType: 'absolute', // 'absolute' or 'relative' (default)
|
|
64
|
+
position: { top: 10, right: 10 }, // Only with absolute
|
|
65
|
+
flexDirection: 'column', // 'row' | 'column'
|
|
66
|
+
justifyContent: 'center', // 'flex-start' | 'center' | 'flex-end' | 'space-between'
|
|
67
|
+
alignItems: 'center', // 'flex-start' | 'center' | 'flex-end' | 'stretch'
|
|
68
|
+
padding: { top: 10, bottom: 10, left: 10, right: 10 },
|
|
69
|
+
margin: { top: 5 },
|
|
70
|
+
display: 'flex' // 'flex' | 'none' (hide)
|
|
71
|
+
}}
|
|
72
|
+
uiBackground={{
|
|
73
|
+
color: { r: 0, g: 0, b: 0, a: 0.8 } // Semi-transparent black
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Label (Text)
|
|
79
|
+
```tsx
|
|
80
|
+
<Label
|
|
81
|
+
value="Score: 100"
|
|
82
|
+
fontSize={18}
|
|
83
|
+
color={{ r: 1, g: 1, b: 1, a: 1 }}
|
|
84
|
+
textAlign="middle-center"
|
|
85
|
+
font="sans-serif"
|
|
86
|
+
uiTransform={{ width: 200, height: 30 }}
|
|
87
|
+
/>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Button
|
|
91
|
+
```tsx
|
|
92
|
+
<Button
|
|
93
|
+
value="Click Me"
|
|
94
|
+
variant="primary" // 'primary' | 'secondary'
|
|
95
|
+
fontSize={16}
|
|
96
|
+
uiTransform={{ width: 150, height: 40 }}
|
|
97
|
+
onMouseDown={() => {
|
|
98
|
+
console.log('Button clicked!')
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Input
|
|
104
|
+
```tsx
|
|
105
|
+
import { Input } from '@dcl/sdk/react-ecs'
|
|
106
|
+
|
|
107
|
+
<Input
|
|
108
|
+
placeholder="Type here..."
|
|
109
|
+
fontSize={14}
|
|
110
|
+
color={{ r: 1, g: 1, b: 1, a: 1 }}
|
|
111
|
+
uiTransform={{ width: 250, height: 35 }}
|
|
112
|
+
onSubmit={(value) => {
|
|
113
|
+
console.log('Submitted:', value)
|
|
114
|
+
}}
|
|
115
|
+
/>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Dropdown
|
|
119
|
+
```tsx
|
|
120
|
+
import { Dropdown } from '@dcl/sdk/react-ecs'
|
|
121
|
+
|
|
122
|
+
<Dropdown
|
|
123
|
+
options={['Option A', 'Option B', 'Option C']}
|
|
124
|
+
selectedIndex={0}
|
|
125
|
+
onChange={(index) => {
|
|
126
|
+
console.log('Selected:', index)
|
|
127
|
+
}}
|
|
128
|
+
uiTransform={{ width: 200, height: 35 }}
|
|
129
|
+
fontSize={14}
|
|
130
|
+
/>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## State Management
|
|
134
|
+
|
|
135
|
+
Use module-level variables for UI state (React hooks are NOT available):
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
let score = 0
|
|
139
|
+
let showMenu = false
|
|
140
|
+
|
|
141
|
+
function GameUI() {
|
|
142
|
+
return (
|
|
143
|
+
<UiEntity uiTransform={{ width: '100%', height: '100%' }}>
|
|
144
|
+
{/* HUD - always visible */}
|
|
145
|
+
<Label
|
|
146
|
+
value={`Score: ${score}`}
|
|
147
|
+
fontSize={20}
|
|
148
|
+
uiTransform={{
|
|
149
|
+
positionType: 'absolute',
|
|
150
|
+
position: { top: 10, left: 10 }
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
|
|
154
|
+
{/* Menu - conditionally shown */}
|
|
155
|
+
{showMenu && (
|
|
156
|
+
<UiEntity
|
|
157
|
+
uiTransform={{
|
|
158
|
+
width: 300,
|
|
159
|
+
height: 400,
|
|
160
|
+
positionType: 'absolute',
|
|
161
|
+
position: { top: '50%', left: '50%' }
|
|
162
|
+
}}
|
|
163
|
+
uiBackground={{ color: { r: 0.1, g: 0.1, b: 0.1, a: 0.9 } }}
|
|
164
|
+
>
|
|
165
|
+
<Label value="Game Menu" fontSize={24} />
|
|
166
|
+
<Button
|
|
167
|
+
value="Resume"
|
|
168
|
+
variant="primary"
|
|
169
|
+
onMouseDown={() => { showMenu = false }}
|
|
170
|
+
uiTransform={{ width: 200, height: 40 }}
|
|
171
|
+
/>
|
|
172
|
+
</UiEntity>
|
|
173
|
+
)}
|
|
174
|
+
</UiEntity>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Update state from game logic
|
|
179
|
+
export function addScore(points: number) {
|
|
180
|
+
score += points
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function toggleMenu() {
|
|
184
|
+
showMenu = !showMenu
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Common UI Patterns
|
|
189
|
+
|
|
190
|
+
### Health Bar
|
|
191
|
+
```tsx
|
|
192
|
+
let health = 100
|
|
193
|
+
|
|
194
|
+
function HealthBar() {
|
|
195
|
+
return (
|
|
196
|
+
<UiEntity
|
|
197
|
+
uiTransform={{
|
|
198
|
+
width: 200, height: 20,
|
|
199
|
+
positionType: 'absolute',
|
|
200
|
+
position: { bottom: 20, left: '50%' }
|
|
201
|
+
}}
|
|
202
|
+
uiBackground={{ color: { r: 0.3, g: 0.3, b: 0.3, a: 0.8 } }}
|
|
203
|
+
>
|
|
204
|
+
<UiEntity
|
|
205
|
+
uiTransform={{ width: `${health}%`, height: '100%' }}
|
|
206
|
+
uiBackground={{ color: { r: 0.2, g: 0.8, b: 0.2, a: 1 } }}
|
|
207
|
+
/>
|
|
208
|
+
</UiEntity>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Image Background
|
|
214
|
+
```tsx
|
|
215
|
+
<UiEntity
|
|
216
|
+
uiTransform={{ width: 200, height: 200 }}
|
|
217
|
+
uiBackground={{
|
|
218
|
+
textureMode: 'stretch',
|
|
219
|
+
texture: { src: 'images/logo.png' }
|
|
220
|
+
}}
|
|
221
|
+
/>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Important Notes
|
|
225
|
+
|
|
226
|
+
- React hooks (`useState`, `useEffect`, etc.) are **NOT** available — use module-level variables
|
|
227
|
+
- The UI renderer re-renders every frame, so state changes are reflected immediately
|
|
228
|
+
- UI is rendered as a 2D overlay on top of the 3D scene
|
|
229
|
+
- Use `display: 'none'` in `uiTransform` to hide elements without removing them
|
|
230
|
+
- File extension must be `.tsx` for JSX support
|
|
231
|
+
- Only one `ReactEcs.setUiRenderer()` call per scene — combine all UI into one root component
|