@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,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: multiplayer-sync
|
|
3
|
+
description: Synchronize state between players in Decentraland multiplayer scenes using CRDT-based networking. Use when user wants multiplayer, sync state, network entities, shared world state, or real-time collaboration.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Multiplayer Synchronization in Decentraland
|
|
7
|
+
|
|
8
|
+
Decentraland scenes are inherently multiplayer. All players in the same scene share the same space. SDK7 uses CRDT-based synchronization.
|
|
9
|
+
|
|
10
|
+
## How Sync Works
|
|
11
|
+
|
|
12
|
+
- Components on entities created via `engine.addEntity()` are **automatically synced** between all players in the scene.
|
|
13
|
+
- The Decentraland runtime uses CRDTs (Conflict-free Replicated Data Types) to resolve conflicts.
|
|
14
|
+
- Last-write-wins semantics for most components (Transform, Material, etc.).
|
|
15
|
+
- No server code needed — sync is built into the runtime.
|
|
16
|
+
|
|
17
|
+
## Basic Synced Entity
|
|
18
|
+
|
|
19
|
+
Any entity with standard components syncs automatically:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { engine, Transform, MeshRenderer, Material } from '@dcl/sdk/ecs'
|
|
23
|
+
import { Vector3, Color4 } from '@dcl/sdk/math'
|
|
24
|
+
|
|
25
|
+
// This entity and all its components sync to all players
|
|
26
|
+
const sharedCube = engine.addEntity()
|
|
27
|
+
Transform.create(sharedCube, { position: Vector3.create(8, 1, 8) })
|
|
28
|
+
MeshRenderer.setBox(sharedCube)
|
|
29
|
+
Material.setPbrMaterial(sharedCube, { albedoColor: Color4.Red() })
|
|
30
|
+
|
|
31
|
+
// When any player changes the transform, all players see it
|
|
32
|
+
function moveCube() {
|
|
33
|
+
const transform = Transform.getMutable(sharedCube)
|
|
34
|
+
transform.position.x += 1 // All players see this change
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Custom Synced Components
|
|
39
|
+
|
|
40
|
+
Define custom components that sync between players:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { engine, Schemas } from '@dcl/sdk/ecs'
|
|
44
|
+
|
|
45
|
+
// Define a custom synced component
|
|
46
|
+
const ScoreBoard = engine.defineComponent('scoreBoard', {
|
|
47
|
+
score: Schemas.Int,
|
|
48
|
+
playerName: Schemas.String,
|
|
49
|
+
lastUpdated: Schemas.Int64
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Use it on an entity — automatically syncs
|
|
53
|
+
const board = engine.addEntity()
|
|
54
|
+
ScoreBoard.create(board, { score: 0, playerName: '', lastUpdated: 0 })
|
|
55
|
+
|
|
56
|
+
// Update from any player
|
|
57
|
+
function addScore(points: number) {
|
|
58
|
+
const data = ScoreBoard.getMutable(board)
|
|
59
|
+
data.score += points
|
|
60
|
+
data.lastUpdated = Date.now()
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Player-Specific Data
|
|
65
|
+
|
|
66
|
+
Use `PlayerIdentityData` to distinguish players:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { engine, PlayerIdentityData } from '@dcl/sdk/ecs'
|
|
70
|
+
|
|
71
|
+
engine.addSystem(() => {
|
|
72
|
+
for (const [entity] of engine.getEntitiesWith(PlayerIdentityData)) {
|
|
73
|
+
const data = PlayerIdentityData.get(entity)
|
|
74
|
+
console.log('Player:', data.address, 'Guest:', data.isGuest)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Schema Types
|
|
80
|
+
|
|
81
|
+
Available schema types for custom components:
|
|
82
|
+
|
|
83
|
+
| Type | Usage |
|
|
84
|
+
|------|-------|
|
|
85
|
+
| `Schemas.Boolean` | true/false |
|
|
86
|
+
| `Schemas.Int` | Integer numbers |
|
|
87
|
+
| `Schemas.Float` | Decimal numbers |
|
|
88
|
+
| `Schemas.String` | Text strings |
|
|
89
|
+
| `Schemas.Int64` | Large integers (timestamps) |
|
|
90
|
+
| `Schemas.Vector3` | 3D coordinates |
|
|
91
|
+
| `Schemas.Quaternion` | Rotations |
|
|
92
|
+
| `Schemas.Color3` | RGB colors |
|
|
93
|
+
| `Schemas.Color4` | RGBA colors |
|
|
94
|
+
| `Schemas.Entity` | Entity reference |
|
|
95
|
+
| `Schemas.Array(innerType)` | Array of values |
|
|
96
|
+
| `Schemas.Map(valueType)` | Key-value maps |
|
|
97
|
+
| `Schemas.Optional(innerType)` | Nullable values |
|
|
98
|
+
| `Schemas.Enum(enumType)` | Enum values |
|
|
99
|
+
|
|
100
|
+
## Communication Patterns
|
|
101
|
+
|
|
102
|
+
### Global State (Shared Object)
|
|
103
|
+
```typescript
|
|
104
|
+
// One entity holds shared game state
|
|
105
|
+
const gameState = engine.addEntity()
|
|
106
|
+
const GameState = engine.defineComponent('gameState', {
|
|
107
|
+
phase: Schemas.String,
|
|
108
|
+
timeRemaining: Schemas.Int,
|
|
109
|
+
isActive: Schemas.Boolean
|
|
110
|
+
})
|
|
111
|
+
GameState.create(gameState, { phase: 'waiting', timeRemaining: 60, isActive: false })
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Per-Player State
|
|
115
|
+
```typescript
|
|
116
|
+
// Track each player's state separately using their entity
|
|
117
|
+
engine.addSystem(() => {
|
|
118
|
+
for (const [entity] of engine.getEntitiesWith(PlayerIdentityData)) {
|
|
119
|
+
// Each player's entity is unique to them
|
|
120
|
+
// Attach custom components to player entities for per-player data
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Important Notes
|
|
126
|
+
|
|
127
|
+
- **All component changes sync automatically** — no explicit "send" calls needed
|
|
128
|
+
- **CRDT resolution**: If two players change the same component simultaneously, last-write-wins
|
|
129
|
+
- **No server-side code**: Decentraland scenes run entirely client-side with CRDT sync
|
|
130
|
+
- **Entity limits apply**: Each synced entity counts toward the scene's entity budget
|
|
131
|
+
- **Custom schemas must be deterministic**: Same component name = same schema across all clients
|
|
132
|
+
- For server-authoritative multiplayer with validation and anti-cheat, see the `authoritative-server` skill
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nft-blockchain
|
|
3
|
+
description: Display NFT artwork and interact with blockchain from Decentraland scenes. Show NFTs using NftShape with frame styles, check player wallet with getPlayer, sign messages with signedFetch, interact with smart contracts using eth-connect and createEthereumProvider, and handle MANA transactions. Use when user wants NFTs, blockchain, wallet, smart contracts, Web3, or crypto.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# NFT and Blockchain in Decentraland
|
|
7
|
+
|
|
8
|
+
## Display NFT Artwork
|
|
9
|
+
|
|
10
|
+
Show an NFT from Ethereum in a decorative frame:
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { engine, Transform, NftShape, NftFrameType } from '@dcl/sdk/ecs'
|
|
14
|
+
import { Vector3, Color4 } from '@dcl/sdk/math'
|
|
15
|
+
|
|
16
|
+
const nftFrame = engine.addEntity()
|
|
17
|
+
Transform.create(nftFrame, {
|
|
18
|
+
position: Vector3.create(8, 2, 8),
|
|
19
|
+
rotation: Quaternion.fromEulerDegrees(0, 0, 0)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
NftShape.create(nftFrame, {
|
|
23
|
+
urn: 'urn:decentraland:ethereum:erc721:0x06012c8cf97bead5deae237070f9587f8e7a266d:558536',
|
|
24
|
+
color: Color4.White(),
|
|
25
|
+
style: NftFrameType.NFT_CLASSIC
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### NFT URN Format
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
urn:decentraland:ethereum:erc721:<contractAddress>:<tokenId>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- Works with any ERC-721 NFT on Ethereum mainnet
|
|
36
|
+
- The image is loaded automatically from the NFT's metadata
|
|
37
|
+
|
|
38
|
+
### Available Frame Styles
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
NftFrameType.NFT_CLASSIC // Simple classic frame
|
|
42
|
+
NftFrameType.NFT_BAROQUE_ORNAMENT // Ornate baroque
|
|
43
|
+
NftFrameType.NFT_DIAMOND_ORNAMENT // Diamond pattern
|
|
44
|
+
NftFrameType.NFT_MINIMAL_WIDE // Minimal wide border
|
|
45
|
+
NftFrameType.NFT_MINIMAL_GREY // Minimal grey border
|
|
46
|
+
NftFrameType.NFT_BLOCKY // Pixelated/blocky
|
|
47
|
+
NftFrameType.NFT_GOLD_EDGES // Gold edge trim
|
|
48
|
+
NftFrameType.NFT_GOLD_CARVED // Carved gold
|
|
49
|
+
NftFrameType.NFT_GOLD_WIDE // Wide gold border
|
|
50
|
+
NftFrameType.NFT_GOLD_ROUNDED // Rounded gold
|
|
51
|
+
NftFrameType.NFT_METAL_MEDIUM // Medium metal
|
|
52
|
+
NftFrameType.NFT_METAL_WIDE // Wide metal
|
|
53
|
+
NftFrameType.NFT_METAL_SLIM // Slim metal
|
|
54
|
+
NftFrameType.NFT_METAL_ROUNDED // Rounded metal
|
|
55
|
+
NftFrameType.NFT_PINS // Pinned to wall
|
|
56
|
+
NftFrameType.NFT_MINIMAL_BLACK // Minimal black
|
|
57
|
+
NftFrameType.NFT_MINIMAL_WHITE // Minimal white
|
|
58
|
+
NftFrameType.NFT_TAPE // Taped to wall
|
|
59
|
+
NftFrameType.NFT_WOOD_SLIM // Slim wood
|
|
60
|
+
NftFrameType.NFT_WOOD_WIDE // Wide wood
|
|
61
|
+
NftFrameType.NFT_WOOD_TWIGS // Twig/branch wood
|
|
62
|
+
NftFrameType.NFT_CANVAS // Canvas style
|
|
63
|
+
NftFrameType.NFT_NONE // No frame
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Check Player Wallet
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { getPlayer } from '@dcl/sdk/src/players'
|
|
70
|
+
|
|
71
|
+
function checkWallet() {
|
|
72
|
+
const player = getPlayer()
|
|
73
|
+
if (player && !player.isGuest) {
|
|
74
|
+
console.log('Player wallet address:', player.userId)
|
|
75
|
+
// userId is the Ethereum wallet address
|
|
76
|
+
} else {
|
|
77
|
+
console.log('Player is guest (no wallet)')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Always check `isGuest` before attempting any blockchain interaction — guest players don't have a connected wallet.
|
|
83
|
+
|
|
84
|
+
## Signed Requests
|
|
85
|
+
|
|
86
|
+
Send authenticated requests to a backend, signed with the player's wallet:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { signedFetch } from '@dcl/sdk/signed-fetch'
|
|
90
|
+
|
|
91
|
+
executeTask(async () => {
|
|
92
|
+
try {
|
|
93
|
+
const response = await signedFetch('https://example.com/api/action', {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: { 'Content-Type': 'application/json' },
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
action: 'claimReward',
|
|
98
|
+
amount: 100
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const result = await response.json()
|
|
103
|
+
console.log('Result:', result)
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.log('Request failed:', error)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`signedFetch` automatically includes a cryptographic signature proving the player's identity. Your backend can verify this signature to authenticate requests.
|
|
111
|
+
|
|
112
|
+
## MANA Transactions
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { manaUser } from '@dcl/sdk/ethereum'
|
|
116
|
+
|
|
117
|
+
executeTask(async () => {
|
|
118
|
+
try {
|
|
119
|
+
// Check MANA balance
|
|
120
|
+
const balance = await manaUser.balance()
|
|
121
|
+
console.log('MANA balance:', balance)
|
|
122
|
+
|
|
123
|
+
// Send MANA to another address
|
|
124
|
+
const result = await manaUser.send('0x123...abc', 100) // 100 MANA
|
|
125
|
+
console.log('MANA sent:', result)
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.log('MANA transaction failed:', error)
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Smart Contract Interaction
|
|
133
|
+
|
|
134
|
+
Requires the `eth-connect` package:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npm install eth-connect
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Store ABI in a Separate File
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// contracts/myContract.ts
|
|
144
|
+
export default [
|
|
145
|
+
{
|
|
146
|
+
"constant": true,
|
|
147
|
+
"inputs": [{ "name": "_owner", "type": "address" }],
|
|
148
|
+
"name": "balanceOf",
|
|
149
|
+
"outputs": [{ "name": "balance", "type": "uint256" }],
|
|
150
|
+
"type": "function"
|
|
151
|
+
}
|
|
152
|
+
// ... rest of ABI
|
|
153
|
+
]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Create Contract Instance
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { RequestManager, ContractFactory } from 'eth-connect'
|
|
160
|
+
import { createEthereumProvider } from '@dcl/sdk/ethereum-provider'
|
|
161
|
+
import { abi } from '../contracts/myContract'
|
|
162
|
+
|
|
163
|
+
executeTask(async () => {
|
|
164
|
+
try {
|
|
165
|
+
// Create web3 provider
|
|
166
|
+
const provider = createEthereumProvider()
|
|
167
|
+
const requestManager = new RequestManager(provider)
|
|
168
|
+
|
|
169
|
+
// Create contract at a specific address
|
|
170
|
+
const factory = new ContractFactory(requestManager, abi)
|
|
171
|
+
const contract = await factory.at('0x2a8fd99c19271f4f04b1b7b9c4f7cf264b626edb') as any
|
|
172
|
+
|
|
173
|
+
// Read data (no gas required)
|
|
174
|
+
const balance = await contract.balanceOf('0x123...abc')
|
|
175
|
+
console.log('Balance:', balance)
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.log('Contract interaction failed:', error)
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Write Operations (Require Gas)
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
executeTask(async () => {
|
|
186
|
+
try {
|
|
187
|
+
const userData = getPlayer()
|
|
188
|
+
if (userData.isGuest) return
|
|
189
|
+
|
|
190
|
+
// Write operation — prompts the player to sign the transaction
|
|
191
|
+
const writeResult = await contract.transfer(
|
|
192
|
+
'0xRecipientAddress',
|
|
193
|
+
100,
|
|
194
|
+
{
|
|
195
|
+
from: userData.userId,
|
|
196
|
+
gas: 100000,
|
|
197
|
+
gasPrice: await requestManager.eth_gasPrice()
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
console.log('Transaction hash:', writeResult)
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.log('Transaction failed:', error)
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Gas Price and Balance Checking
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import { RequestManager } from 'eth-connect'
|
|
211
|
+
import { createEthereumProvider } from '@dcl/sdk/ethereum-provider'
|
|
212
|
+
|
|
213
|
+
executeTask(async () => {
|
|
214
|
+
const provider = createEthereumProvider()
|
|
215
|
+
const requestManager = new RequestManager(provider)
|
|
216
|
+
|
|
217
|
+
const gasPrice = await requestManager.eth_gasPrice()
|
|
218
|
+
console.log('Current gas price:', gasPrice)
|
|
219
|
+
|
|
220
|
+
const balance = await requestManager.eth_getBalance('0x123...abc', 'latest')
|
|
221
|
+
console.log('Account balance:', balance)
|
|
222
|
+
})
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Testing with Sepolia
|
|
226
|
+
|
|
227
|
+
For development, use the Sepolia testnet:
|
|
228
|
+
|
|
229
|
+
1. Set MetaMask to Sepolia network
|
|
230
|
+
2. Get test ETH from a Sepolia faucet
|
|
231
|
+
3. Deploy your contracts to Sepolia
|
|
232
|
+
4. Contract addresses differ between mainnet and testnet — use environment checks
|
|
233
|
+
|
|
234
|
+
## Best Practices
|
|
235
|
+
|
|
236
|
+
- **Always check `isGuest`** before any blockchain interaction — guest players can't sign transactions
|
|
237
|
+
- Use `executeTask(async () => { ... })` for all async blockchain calls
|
|
238
|
+
- Store ABI files separately (e.g., `contracts/`) — don't inline large ABIs
|
|
239
|
+
- Handle errors gracefully — blockchain operations can fail (rejected by user, insufficient gas, network issues)
|
|
240
|
+
- `eth-connect` must be installed as a dependency: `npm install eth-connect`
|
|
241
|
+
- Use `signedFetch` for backend authentication instead of raw `fetch` — it proves the player's identity
|
|
242
|
+
- Read operations (view/pure functions) don't require gas; write operations prompt the user to sign
|
|
243
|
+
- Test on Sepolia before deploying to mainnet
|
|
244
|
+
- NFT URNs only work with Ethereum mainnet ERC-721 tokens
|
|
245
|
+
|
|
246
|
+
For component field details, see `context/components-reference.md`.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: optimize-scene
|
|
3
|
+
description: Optimize Decentraland scene performance, reduce entity count, minimize triangle budgets, improve loading times, and stay within scene limits. Use when user wants to optimize, improve performance, fix lag, reduce load time, or check scene limits.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Optimizing Decentraland Scenes
|
|
7
|
+
|
|
8
|
+
## Scene Limits (Per Parcel Count)
|
|
9
|
+
|
|
10
|
+
| Parcels | Max Entities | Max Triangles | Max Textures | Max Materials | Max Height |
|
|
11
|
+
|---------|-------------|---------------|-------------|--------------|-----------|
|
|
12
|
+
| 1 | 512 | 10,000 | 10 MB | 20 | 20m |
|
|
13
|
+
| 2 | 1,024 | 20,000 | 20 MB | 40 | 20m |
|
|
14
|
+
| 4 | 2,048 | 40,000 | 40 MB | 80 | 20m |
|
|
15
|
+
| 8 | 4,096 | 80,000 | 80 MB | 160 | 20m |
|
|
16
|
+
| 16 | 8,192 | 160,000 | 160 MB | 320 | 20m |
|
|
17
|
+
|
|
18
|
+
## Entity Count Optimization
|
|
19
|
+
|
|
20
|
+
### Reuse Entities
|
|
21
|
+
```typescript
|
|
22
|
+
// BAD: Creating new entity each time
|
|
23
|
+
function spawnBullet() {
|
|
24
|
+
const bullet = engine.addEntity() // Creates entity every call
|
|
25
|
+
// ...
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// GOOD: Object pooling
|
|
29
|
+
const bulletPool: Entity[] = []
|
|
30
|
+
function getBullet(): Entity {
|
|
31
|
+
const existing = bulletPool.find(e => !ActiveBullet.has(e))
|
|
32
|
+
if (existing) return existing
|
|
33
|
+
const newBullet = engine.addEntity()
|
|
34
|
+
bulletPool.push(newBullet)
|
|
35
|
+
return newBullet
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Remove Unused Entities
|
|
40
|
+
```typescript
|
|
41
|
+
engine.removeEntity(entity) // Frees the entity slot
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Use Parenting
|
|
45
|
+
Instead of separate transforms for each child, use entity hierarchy:
|
|
46
|
+
```typescript
|
|
47
|
+
const parent = engine.addEntity()
|
|
48
|
+
Transform.create(parent, { position: Vector3.create(8, 0, 8) })
|
|
49
|
+
|
|
50
|
+
// Children inherit parent transform
|
|
51
|
+
const child1 = engine.addEntity()
|
|
52
|
+
Transform.create(child1, { position: Vector3.create(0, 1, 0), parent })
|
|
53
|
+
|
|
54
|
+
const child2 = engine.addEntity()
|
|
55
|
+
Transform.create(child2, { position: Vector3.create(1, 1, 0), parent })
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Triangle Count Optimization
|
|
59
|
+
|
|
60
|
+
### Use Lower-Poly Models
|
|
61
|
+
- Small props: 100-500 triangles
|
|
62
|
+
- Medium objects: 500-1,500 triangles
|
|
63
|
+
- Large buildings: 1,500-5,000 triangles
|
|
64
|
+
- Hero pieces: Up to 10,000 triangles
|
|
65
|
+
|
|
66
|
+
### Use LOD (Level of Detail)
|
|
67
|
+
Show simpler models at distance:
|
|
68
|
+
```typescript
|
|
69
|
+
engine.addSystem(() => {
|
|
70
|
+
// Check distance to player and swap models
|
|
71
|
+
const playerPos = Transform.get(engine.PlayerEntity).position
|
|
72
|
+
const objPos = Transform.get(myEntity).position
|
|
73
|
+
const distance = Vector3.distance(playerPos, objPos)
|
|
74
|
+
|
|
75
|
+
const gltf = GltfContainer.getMutable(myEntity)
|
|
76
|
+
if (distance > 30) {
|
|
77
|
+
gltf.src = 'models/building_lod2.glb' // Low poly
|
|
78
|
+
} else if (distance > 15) {
|
|
79
|
+
gltf.src = 'models/building_lod1.glb' // Medium poly
|
|
80
|
+
} else {
|
|
81
|
+
gltf.src = 'models/building_lod0.glb' // High poly
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Use Primitives Instead of Models
|
|
87
|
+
For simple shapes, `MeshRenderer` is lighter than loading a .glb:
|
|
88
|
+
```typescript
|
|
89
|
+
MeshRenderer.setBox(entity) // Very cheap
|
|
90
|
+
MeshRenderer.setSphere(entity) // Cheap
|
|
91
|
+
MeshRenderer.setPlane(entity) // Very cheap
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Texture Optimization
|
|
95
|
+
|
|
96
|
+
- Use `.png` for UI/sprites with transparency
|
|
97
|
+
- Use `.jpg` for photos and textures without transparency
|
|
98
|
+
- Compress textures: 512x512 or 1024x1024 max for most use cases
|
|
99
|
+
- Use texture atlases (combine multiple textures into one image)
|
|
100
|
+
- Avoid 4096x4096 textures unless absolutely necessary
|
|
101
|
+
- Reuse materials across entities:
|
|
102
|
+
```typescript
|
|
103
|
+
// GOOD: Define material once, apply to many
|
|
104
|
+
Material.setPbrMaterial(entity1, { texture: Material.Texture.Common({ src: 'images/wall.jpg' }) })
|
|
105
|
+
Material.setPbrMaterial(entity2, { texture: Material.Texture.Common({ src: 'images/wall.jpg' }) })
|
|
106
|
+
// Same texture URL = shared in memory
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## System Optimization
|
|
110
|
+
|
|
111
|
+
### Avoid Per-Frame Allocations
|
|
112
|
+
```typescript
|
|
113
|
+
// BAD: Creates new Vector3 every frame
|
|
114
|
+
engine.addSystem(() => {
|
|
115
|
+
const target = Vector3.create(8, 1, 8) // Allocation!
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// GOOD: Reuse constants
|
|
119
|
+
const TARGET = Vector3.create(8, 1, 8)
|
|
120
|
+
engine.addSystem(() => {
|
|
121
|
+
// Use TARGET
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Throttle Expensive Operations
|
|
126
|
+
```typescript
|
|
127
|
+
let lastCheck = 0
|
|
128
|
+
engine.addSystem((dt) => {
|
|
129
|
+
lastCheck += dt
|
|
130
|
+
if (lastCheck < 0.5) return // Only run every 0.5 seconds
|
|
131
|
+
lastCheck = 0
|
|
132
|
+
// Expensive operation here
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Remove Systems When Not Needed
|
|
137
|
+
```typescript
|
|
138
|
+
const systemFn = (dt: number) => { /* ... */ }
|
|
139
|
+
engine.addSystem(systemFn)
|
|
140
|
+
|
|
141
|
+
// When no longer needed:
|
|
142
|
+
engine.removeSystem(systemFn)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Loading Time Optimization
|
|
146
|
+
|
|
147
|
+
- Lazy-load 3D models (load on demand, not all at scene start)
|
|
148
|
+
- Use compressed .glb files (Draco compression)
|
|
149
|
+
- Minimize total asset size
|
|
150
|
+
- Use CDN URLs for large shared assets when possible
|
|
151
|
+
- Preload critical assets, defer non-essential ones
|
|
152
|
+
|
|
153
|
+
## Common Performance Pitfalls
|
|
154
|
+
|
|
155
|
+
1. **Too many systems**: Each system runs every frame. Combine related logic.
|
|
156
|
+
2. **Unnecessary component queries**: Cache `engine.getEntitiesWith()` results when the set doesn't change.
|
|
157
|
+
3. **Large GLTF files**: Optimize in Blender before export (decimate, remove hidden faces).
|
|
158
|
+
4. **Uncompressed audio**: Use .mp3 instead of .wav for music (10x smaller).
|
|
159
|
+
5. **Continuous raycasting**: Set `continuous: false` unless you need per-frame raycasting.
|
|
160
|
+
6. **Text rendering**: `TextShape` is expensive. Use `Label` (UI) for text that doesn't need to be in 3D space.
|