@dcl/ecs 7.7.3-13090257149.commit-df175f2 → 7.7.3-13092675740.commit-ba2dd71
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
CHANGED
|
@@ -1,10 +1,118 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dcl/ecs
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Run `make install`, this will run the `npm install` and other dependencies
|
|
3
|
+
Core Entity Component System (ECS) package for Decentraland scenes. Implements a CRDT-based ECS architecture for networked scene state.
|
|
5
4
|
|
|
6
|
-
##
|
|
7
|
-
Run `make build`
|
|
5
|
+
## Installation
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm install @dcl/ecs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { engine } from '@dcl/ecs'
|
|
15
|
+
|
|
16
|
+
// Create entity
|
|
17
|
+
const entity = engine.addEntity()
|
|
18
|
+
|
|
19
|
+
// Define and add component
|
|
20
|
+
const Health = engine.defineComponent(1, {
|
|
21
|
+
current: Number,
|
|
22
|
+
max: Number,
|
|
23
|
+
regeneration: Number
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
Health.create(entity, {
|
|
27
|
+
current: 100,
|
|
28
|
+
max: 100,
|
|
29
|
+
regeneration: 1
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Create system
|
|
33
|
+
engine.addSystem((dt: number) => {
|
|
34
|
+
for (const [entity, health] of engine.mutableGroupOf(Health)) {
|
|
35
|
+
if (health.current < health.max) {
|
|
36
|
+
health.current = Math.min(health.max, health.current + health.regeneration * dt)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Technical Overview
|
|
43
|
+
|
|
44
|
+
### Component Definition
|
|
45
|
+
|
|
46
|
+
Components are defined with a unique ID and a schema. The schema is used to:
|
|
47
|
+
|
|
48
|
+
- Generate TypeScript types
|
|
49
|
+
- Create binary serializers/deserializers
|
|
50
|
+
- Set up CRDT operations
|
|
51
|
+
|
|
52
|
+
### CRDT Implementation
|
|
53
|
+
|
|
54
|
+
The ECS uses CRDTs (Conflict-free Replicated Data Types) to enable deterministic state updates across multiple engine instances:
|
|
55
|
+
|
|
56
|
+
- Component updates are CRDT operations with logical timestamps
|
|
57
|
+
- Multiple engine instances can be synced by exchanging CRDT operations
|
|
58
|
+
- Conflict resolution uses timestamps and entity IDs to ensure consistency
|
|
59
|
+
- Binary transport format minimizes network overhead
|
|
60
|
+
|
|
61
|
+
### Network Entities
|
|
62
|
+
|
|
63
|
+
For multiplayer scenes, the `syncEntity` method marks entities that should be synchronized across peers.
|
|
64
|
+
In the background it creates a NetworkEntity and a SyncComponents components with all the info necessary to synchronise the entity through the network.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { engine, NetworkEntity } from '@dcl/ecs'
|
|
68
|
+
|
|
69
|
+
// Create a networked entity
|
|
70
|
+
const foe = engine.addEntity()
|
|
71
|
+
NetworkEntity.create(foe)
|
|
72
|
+
|
|
73
|
+
// Components on this entity will be synced across peers
|
|
74
|
+
Health.create(foe, { current: 100, max: 100, regeneration: 1 })
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Each peer maintains its own engine instance. When using NetworkEntity:
|
|
78
|
+
|
|
79
|
+
- The owner peer can modify the entity's components
|
|
80
|
+
- Other peers receive read-only replicas
|
|
81
|
+
- Updates are propagated through the network transport layer using CRDT operations
|
|
82
|
+
|
|
83
|
+
Example transport message:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
{
|
|
87
|
+
entityId: number
|
|
88
|
+
componentId: number
|
|
89
|
+
timestamp: number
|
|
90
|
+
data: Uint8Array // Serialized component data
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Performance Features
|
|
95
|
+
|
|
96
|
+
- Zero-allocation component iteration
|
|
97
|
+
- Dirty state tracking for efficient updates
|
|
98
|
+
- Binary serialization for network transport
|
|
99
|
+
- Batched component operations
|
|
100
|
+
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Build
|
|
105
|
+
make build
|
|
106
|
+
|
|
107
|
+
# Test
|
|
108
|
+
make test
|
|
109
|
+
|
|
110
|
+
# Clean and reinstall
|
|
111
|
+
make clean && make install
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Documentation
|
|
115
|
+
|
|
116
|
+
- [ECS Guide](https://docs.decentraland.org/creator/development-guide/sdk7/entities-components/)
|
|
117
|
+
- [Component Reference](https://docs.decentraland.org/creator/development-guide/sdk7/components/)
|
|
118
|
+
- [ADR-117: CRDT Protocol](https://adr.decentraland.org/adr/ADR-117)
|
|
@@ -206,14 +206,22 @@ export function crdtSceneSystem(engine, onProcessEntityComponentChange) {
|
|
|
206
206
|
// Send CRDT messages to transports
|
|
207
207
|
const transportBuffer = new ReadWriteByteBuffer();
|
|
208
208
|
for (const index in transports) {
|
|
209
|
+
// NetworkMessages can only have a MAX_SIZE of 13kb. So we need to send it in chunks.
|
|
210
|
+
const LIVEKIT_MAX_SIZE = 13;
|
|
211
|
+
const __NetworkMessagesBuffer = [];
|
|
209
212
|
const transportIndex = Number(index);
|
|
210
213
|
const transport = transports[transportIndex];
|
|
211
214
|
const isRendererTransport = transport.type === 'renderer';
|
|
212
215
|
const isNetworkTransport = transport.type === 'network';
|
|
216
|
+
// Reset Buffer for each Transport
|
|
213
217
|
transportBuffer.resetBuffer();
|
|
214
218
|
const buffer = new ReadWriteByteBuffer();
|
|
215
219
|
// Then we send all the new crdtMessages that the transport needs to process
|
|
216
220
|
for (const message of crdtMessages) {
|
|
221
|
+
if (isNetworkTransport && transportBuffer.toBinary().byteLength / 1024 > LIVEKIT_MAX_SIZE) {
|
|
222
|
+
__NetworkMessagesBuffer.push(transportBuffer.toBinary());
|
|
223
|
+
transportBuffer.resetBuffer();
|
|
224
|
+
}
|
|
217
225
|
// Avoid echo messages
|
|
218
226
|
if (message.transportId === transportIndex)
|
|
219
227
|
continue;
|
|
@@ -261,7 +269,10 @@ export function crdtSceneSystem(engine, onProcessEntityComponentChange) {
|
|
|
261
269
|
// Common message
|
|
262
270
|
transportBuffer.writeBuffer(message.messageBuffer, false);
|
|
263
271
|
}
|
|
264
|
-
|
|
272
|
+
if (isNetworkTransport && transportBuffer.currentWriteOffset()) {
|
|
273
|
+
__NetworkMessagesBuffer.push(transportBuffer.toBinary());
|
|
274
|
+
}
|
|
275
|
+
const message = isNetworkTransport ? __NetworkMessagesBuffer : transportBuffer.toBinary();
|
|
265
276
|
await transport.send(message);
|
|
266
277
|
}
|
|
267
278
|
}
|
|
@@ -21,7 +21,11 @@ export type TransportMessage = Omit<ReceiveMessage, 'data'>;
|
|
|
21
21
|
* @public
|
|
22
22
|
*/
|
|
23
23
|
export type Transport = {
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* For Network messages its an Uint8Array[]. Due too the LiveKit MAX_SIZE = 13kb
|
|
26
|
+
* For Renderer & Other transports we send a single Uint8Array
|
|
27
|
+
*/
|
|
28
|
+
send(message: Uint8Array | Uint8Array[]): Promise<void>;
|
|
25
29
|
onmessage?(message: Uint8Array): void;
|
|
26
30
|
filter(message: Omit<TransportMessage, 'messageBuffer'>): boolean;
|
|
27
31
|
type?: string;
|
|
@@ -232,14 +232,22 @@ function crdtSceneSystem(engine, onProcessEntityComponentChange) {
|
|
|
232
232
|
// Send CRDT messages to transports
|
|
233
233
|
const transportBuffer = new ByteBuffer_1.ReadWriteByteBuffer();
|
|
234
234
|
for (const index in transports) {
|
|
235
|
+
// NetworkMessages can only have a MAX_SIZE of 13kb. So we need to send it in chunks.
|
|
236
|
+
const LIVEKIT_MAX_SIZE = 13;
|
|
237
|
+
const __NetworkMessagesBuffer = [];
|
|
235
238
|
const transportIndex = Number(index);
|
|
236
239
|
const transport = transports[transportIndex];
|
|
237
240
|
const isRendererTransport = transport.type === 'renderer';
|
|
238
241
|
const isNetworkTransport = transport.type === 'network';
|
|
242
|
+
// Reset Buffer for each Transport
|
|
239
243
|
transportBuffer.resetBuffer();
|
|
240
244
|
const buffer = new ByteBuffer_1.ReadWriteByteBuffer();
|
|
241
245
|
// Then we send all the new crdtMessages that the transport needs to process
|
|
242
246
|
for (const message of crdtMessages) {
|
|
247
|
+
if (isNetworkTransport && transportBuffer.toBinary().byteLength / 1024 > LIVEKIT_MAX_SIZE) {
|
|
248
|
+
__NetworkMessagesBuffer.push(transportBuffer.toBinary());
|
|
249
|
+
transportBuffer.resetBuffer();
|
|
250
|
+
}
|
|
243
251
|
// Avoid echo messages
|
|
244
252
|
if (message.transportId === transportIndex)
|
|
245
253
|
continue;
|
|
@@ -287,7 +295,10 @@ function crdtSceneSystem(engine, onProcessEntityComponentChange) {
|
|
|
287
295
|
// Common message
|
|
288
296
|
transportBuffer.writeBuffer(message.messageBuffer, false);
|
|
289
297
|
}
|
|
290
|
-
|
|
298
|
+
if (isNetworkTransport && transportBuffer.currentWriteOffset()) {
|
|
299
|
+
__NetworkMessagesBuffer.push(transportBuffer.toBinary());
|
|
300
|
+
}
|
|
301
|
+
const message = isNetworkTransport ? __NetworkMessagesBuffer : transportBuffer.toBinary();
|
|
291
302
|
await transport.send(message);
|
|
292
303
|
}
|
|
293
304
|
}
|
|
@@ -21,7 +21,11 @@ export type TransportMessage = Omit<ReceiveMessage, 'data'>;
|
|
|
21
21
|
* @public
|
|
22
22
|
*/
|
|
23
23
|
export type Transport = {
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* For Network messages its an Uint8Array[]. Due too the LiveKit MAX_SIZE = 13kb
|
|
26
|
+
* For Renderer & Other transports we send a single Uint8Array
|
|
27
|
+
*/
|
|
28
|
+
send(message: Uint8Array | Uint8Array[]): Promise<void>;
|
|
25
29
|
onmessage?(message: Uint8Array): void;
|
|
26
30
|
filter(message: Omit<TransportMessage, 'messageBuffer'>): boolean;
|
|
27
31
|
type?: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcl/ecs",
|
|
3
3
|
"description": "Decentraland ECS",
|
|
4
|
-
"version": "7.7.3-
|
|
4
|
+
"version": "7.7.3-13092675740.commit-ba2dd71",
|
|
5
5
|
"author": "DCL",
|
|
6
6
|
"bugs": "https://github.com/decentraland/ecs/issues",
|
|
7
7
|
"files": [
|
|
@@ -33,5 +33,5 @@
|
|
|
33
33
|
},
|
|
34
34
|
"types": "./dist/index.d.ts",
|
|
35
35
|
"typings": "./dist/index.d.ts",
|
|
36
|
-
"commit": "
|
|
36
|
+
"commit": "ba2dd71f5c68f8e432074a9fdca6fed2b1b1b863"
|
|
37
37
|
}
|