@daneren2005/shared-memory-ecs 1.3.0 → 1.5.0
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 +186 -27
- package/dist/actions/build-worker-entity.d.ts +13 -0
- package/dist/actions/create-entity-worker.d.ts +2 -2
- package/dist/component-definition.d.ts +6 -3
- package/dist/component.d.ts +7 -0
- package/dist/constant-string-cache.d.ts +11 -0
- package/dist/create-component-worker-CaB32Dgn.js +263 -0
- package/dist/create-component-worker-CaB32Dgn.js.map +1 -0
- package/dist/entity-component.d.ts +1 -0
- package/dist/entity.d.ts +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +276 -160
- package/dist/index.js.map +1 -1
- package/dist/memory-component.d.ts +6 -3
- package/dist/systems/component-system.d.ts +27 -2
- package/dist/systems/iterable-system.d.ts +2 -0
- package/dist/systems/system.d.ts +3 -0
- package/dist/systems/workers/component-web-worker.d.ts +6 -2
- package/dist/systems/workers/component-worker-message.d.ts +18 -3
- package/dist/systems/workers/create-component-worker.d.ts +2 -2
- package/dist/worker.d.ts +2 -2
- package/dist/worker.js +2 -2
- package/dist/world.d.ts +31 -1
- package/package.json +2 -2
- package/dist/create-component-worker-vbX3vEia.js +0 -135
- package/dist/create-component-worker-vbX3vEia.js.map +0 -1
package/README.md
CHANGED
|
@@ -10,10 +10,14 @@ fog of war, sub-classed entities, required components, etc).
|
|
|
10
10
|
- **Component** – a plain object with an `index` (its block inside a shared-memory pool) plus getters/
|
|
11
11
|
setters over that memory. Games decide what components exist.
|
|
12
12
|
- **`ComponentDefinition`** – describes a component: its typed array `type`, block `size`, the config keys
|
|
13
|
-
that trigger loading (`loadProperties`),
|
|
14
|
-
|
|
15
|
-
`
|
|
16
|
-
`
|
|
13
|
+
that trigger loading (`loadProperties`), and the two halves that build it — `toBlock(config)` (maps the
|
|
14
|
+
config to the raw block values) and `attach(entity, memory, index)` (builds the accessor over that block).
|
|
15
|
+
Loading a component is `attach(entity, memory, memory.create(toBlock(config)))`; splitting it this way lets a
|
|
16
|
+
worker build the block off-thread (it calls only `toBlock`) and the main thread wrap it (`attach`) when it
|
|
17
|
+
adopts the entity. Plus an optional `save(component)` and `free(component)`. A component's data splits into
|
|
18
|
+
`Config` (defining props supplied up front, e.g. `maxHealth`) and `Serialization` (runtime-derived state,
|
|
19
|
+
e.g. current `health`); `toBlock` sees `Config & Serialization` while `save` returns only the `Serialization`
|
|
20
|
+
slice. `free` runs when the component is torn down (see [Freeing extra resources](#freeing-extra-resources)).
|
|
17
21
|
- **`ComponentRegistry<C>`** – the map of all component definitions for a game.
|
|
18
22
|
- **`EntityFactory<C>`** – maps an entity `type` name to a base (template) config. Loading an entity layers
|
|
19
23
|
the caller's config over its type's template, so shared static data lives in one place and a save only
|
|
@@ -32,6 +36,7 @@ fog of war, sub-classed entities, required components, etc).
|
|
|
32
36
|
## Defining components
|
|
33
37
|
|
|
34
38
|
```ts
|
|
39
|
+
import { Component } from '@daneren2005/shared-memory-ecs';
|
|
35
40
|
import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';
|
|
36
41
|
|
|
37
42
|
interface HealthComponent {
|
|
@@ -43,7 +48,16 @@ interface HealthComponent {
|
|
|
43
48
|
const HEALTH_INDEX = 0;
|
|
44
49
|
const HEALTH_MAX_INDEX = 1;
|
|
45
50
|
|
|
46
|
-
//
|
|
51
|
+
// The accessor is a subclass of `Component`, so its get/set live on one shared prototype: reading a component off
|
|
52
|
+
// thousands of entities stays monomorphic and inline, and each instance is a single allocation rather than a
|
|
53
|
+
// closure per accessor. Index `this.block` (the typed-array view, set for you) by the exported *_INDEX constants.
|
|
54
|
+
class Health extends Component<Int32Array> implements HealthComponent {
|
|
55
|
+
get health() { return this.block[HEALTH_INDEX]; }
|
|
56
|
+
set health(value: number) { this.block[HEALTH_INDEX] = value; }
|
|
57
|
+
get maxHealth() { return this.block[HEALTH_MAX_INDEX]; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// `maxHealth` is the defining Config prop; `health` is optional, runtime-derived Serialization. `toBlock`
|
|
47
61
|
// and `save` both see the combined `{ maxHealth: number, health?: number }`.
|
|
48
62
|
const healthDefinition: ComponentDefinition<HealthComponent, Int32Array, { maxHealth: number }, { health?: number }> = {
|
|
49
63
|
type: Int32Array,
|
|
@@ -51,16 +65,15 @@ const healthDefinition: ComponentDefinition<HealthComponent, Int32Array, { maxHe
|
|
|
51
65
|
// Configs are flat and shared: this component loads whenever `maxHealth` is present, then reads what it
|
|
52
66
|
// needs off the same config object. Only Config props may appear here.
|
|
53
67
|
loadProperties: ['maxHealth'],
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
};
|
|
68
|
+
// The block values, purely from config — so a worker can build the block off-thread. No entity/world access.
|
|
69
|
+
toBlock(config) {
|
|
70
|
+
return [config.health ?? config.maxHealth, config.maxHealth];
|
|
71
|
+
},
|
|
72
|
+
// The accessor over an already-allocated block. Runs on the main thread both when loading and when adopting a
|
|
73
|
+
// worker-built entity; a component's own extra allocations (a SharedList, a resource block) go here — a
|
|
74
|
+
// subclass that owns extra memory takes more constructor args (the entity, another pool) and stores them.
|
|
75
|
+
attach(entity, memoryComponent, index) {
|
|
76
|
+
return new Health(memoryComponent.getBlock(index), index);
|
|
64
77
|
},
|
|
65
78
|
save(component) {
|
|
66
79
|
// save returns only Serialization; maxHealth is Config and comes back from the template on reload.
|
|
@@ -73,7 +86,7 @@ const registry = {
|
|
|
73
86
|
health: healthDefinition
|
|
74
87
|
// ...other component definitions
|
|
75
88
|
};
|
|
76
|
-
type Components = { [K in keyof typeof registry]: ReturnType<(typeof registry)[K]['
|
|
89
|
+
type Components = { [K in keyof typeof registry]: ReturnType<(typeof registry)[K]['attach']> };
|
|
77
90
|
```
|
|
78
91
|
|
|
79
92
|
## Using the world
|
|
@@ -110,6 +123,12 @@ console.log(goblin.save()); // { type: 'goblin', health: 10 } - no templated max
|
|
|
110
123
|
world.loadEntity(goblin.save());
|
|
111
124
|
```
|
|
112
125
|
|
|
126
|
+
An entity's `type` lives in shared memory (worker-visible), not as a plain field. Each distinct type string is
|
|
127
|
+
interned once as an immutable `ConstantString` in the heap — 15 `"Space Ship"` entities share one allocation —
|
|
128
|
+
and the `entity` block stores only a pointer to it at `TYPE_INDEX`. Reading `entity.components.entity.type`
|
|
129
|
+
resolves that pointer back to the string through the world's cache; a worker can do the same (see
|
|
130
|
+
[Reading an entity's type in a worker](#reading-an-entitys-type-in-a-worker)).
|
|
131
|
+
|
|
113
132
|
## Iterating entities
|
|
114
133
|
|
|
115
134
|
`world.entities` is a `Map` keyed by `eid`, not an array, and so is `entities` on `EntitySystem` and
|
|
@@ -140,10 +159,10 @@ one: it is typed, it is readable, and one read costs nothing worth measuring.
|
|
|
140
159
|
|
|
141
160
|
It is not free, though, and the cost shows up in exactly one situation — reading the same component off
|
|
142
161
|
*thousands* of entities, *every frame*. The accessor walks several objects to get there and ends in a getter
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
162
|
+
call over the shared block. `Component`-subclass accessors are prototype getters (all instances of a type share
|
|
163
|
+
one hidden class, so those reads stay monomorphic and inline — unlike the old per-entity closures, which went
|
|
164
|
+
megamorphic), but a getter call plus the property walk still loses to a raw indexed read in the very tightest
|
|
165
|
+
per-frame loops.
|
|
147
166
|
|
|
148
167
|
Where that matters, hold the block instead. It is the same memory the accessors read, so nothing changes about
|
|
149
168
|
what you get, and it is what the update functions already work on:
|
|
@@ -151,15 +170,15 @@ what you get, and it is what the update functions already work on:
|
|
|
151
170
|
```ts
|
|
152
171
|
import { TRANSFORM_X_INDEX } from '@daneren2005/shared-memory-physics';
|
|
153
172
|
|
|
154
|
-
// Resolve once, when whatever is doing the reading is set up.
|
|
155
|
-
|
|
156
|
-
const block =
|
|
173
|
+
// Resolve once, when whatever is doing the reading is set up. The component caches its view on `.block` when it
|
|
174
|
+
// is attached, so read that rather than paying for another getBlock (each call allocates a fresh subarray).
|
|
175
|
+
const block = entity.components.health!.block as Int32Array;
|
|
157
176
|
|
|
158
177
|
// Then per frame, per entity:
|
|
159
178
|
block[HEALTH_INDEX];
|
|
160
179
|
```
|
|
161
180
|
|
|
162
|
-
Two things come with that. The block is only valid while the component is:
|
|
181
|
+
Two things come with that. The block is only valid while the component is: re-read `.block` if the component
|
|
163
182
|
can be removed and re-added, or hang it off something that dies with the entity. And it is indexed rather than
|
|
164
183
|
named, so the offsets have to be exported alongside the definition — which is why every component in the
|
|
165
184
|
physics library exports its `*_INDEX` constants.
|
|
@@ -258,8 +277,8 @@ barrel: importing `createComponentWorker` from `@daneren2005/shared-memory-ecs`
|
|
|
258
277
|
`BaseWorld`, every system, their `@daneren2005/shared-memory-objects` dependencies - into the worker, even
|
|
259
278
|
though a worker never runs any of it (easily ~20kb of dead code per worker). Import worker-side helpers from
|
|
260
279
|
the `@daneren2005/shared-memory-ecs/worker` subpath instead. It exposes only what runs in a worker -
|
|
261
|
-
`createComponentWorker`, `createEntityWorker`, `killEntityWorker`, `DEAD_INDEX` (plus the
|
|
262
|
-
types) - so the bundle stays tiny:
|
|
280
|
+
`createComponentWorker`, `createEntityWorker`, `killEntityWorker`, `DEAD_INDEX`, `TYPE_INDEX` (plus the
|
|
281
|
+
worker-relevant types) - so the bundle stays tiny:
|
|
263
282
|
|
|
264
283
|
```ts
|
|
265
284
|
// damage.worker.ts - the worker entry file
|
|
@@ -274,12 +293,46 @@ or `killEntityWorker` should import them from `/worker` too. Type-only imports (
|
|
|
274
293
|
`ComponentSystemWorld`, ...) can come from either path since types are erased, and main-thread code
|
|
275
294
|
(`ComponentSystem`, `BaseWorld`, `EntityFactory`, ...) keeps importing from the package root.
|
|
276
295
|
|
|
296
|
+
A worker that creates entities (see [Creating entities from a worker](#creating-entities-from-a-worker)) passes
|
|
297
|
+
your component registry as the third argument - `createComponentWorker(self, shipUpdate, registry)` - so it has
|
|
298
|
+
each component's `toBlock`. Only do this in workers that actually create entities; it pulls the registry (and
|
|
299
|
+
whatever it imports) into that worker's bundle.
|
|
300
|
+
|
|
301
|
+
### Reading an entity's type in a worker
|
|
302
|
+
|
|
303
|
+
The `entity` component carries the entity's `type` in shared memory as a pointer, so a worker can resolve it
|
|
304
|
+
back to a string. Pull `entity` into the system's query (its block only reaches the worker if it is `required`
|
|
305
|
+
or `optional`), read the pointer at `TYPE_INDEX`, and hand it to `world.getString` — the framework injects that
|
|
306
|
+
resolver on `world` before every run:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
import { TYPE_INDEX } from '@daneren2005/shared-memory-ecs/worker';
|
|
310
|
+
import type { EntityUpdateFunction } from '@daneren2005/shared-memory-ecs/worker';
|
|
311
|
+
|
|
312
|
+
const update: EntityUpdateFunction<Components, { entity: Uint32Array }> = (world, entityId, components) => {
|
|
313
|
+
const type = world.getString(components.entity[TYPE_INDEX]); // e.g. "Space Ship"
|
|
314
|
+
// ...branch on type, etc.
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
class TypedSystem extends ComponentSystem<Components, { entity: Uint32Array }> {
|
|
318
|
+
constructor(world: BaseWorld<Components>) {
|
|
319
|
+
super(world, { name: 'TypedSystem', required: ['entity'], updateFunction: update, getWorker: () => new Worker(/* ... */) });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
`world.getString(pointer)` is a Map lookup before it ever rebuilds the string from memory, and returns `''` for
|
|
325
|
+
the empty type or a pointer whose buffer has not synced to the worker yet. This works identically on the
|
|
326
|
+
main-thread fallback. It is not limited to type — any pointer to a `ConstantString` (via `world.constantStrings`
|
|
327
|
+
on the main thread) resolves the same way.
|
|
328
|
+
|
|
277
329
|
### Reporting back to the main thread
|
|
278
330
|
|
|
279
331
|
An update function runs on shared memory, so anything it writes is already visible on the main thread. What
|
|
280
332
|
it cannot do from there is touch the world, so the things that have to happen back on it go through
|
|
281
333
|
`callbacks`: `entityComponentChanged` (emitted on the entity as `component-property-updated`), `entityDied`
|
|
282
|
-
(as `death`), and `createEntity
|
|
334
|
+
(as `death`), and `createEntity` (see [Creating entities from a worker](#creating-entities-from-a-worker)).
|
|
335
|
+
All of them are collected during the run and applied once it completes.
|
|
283
336
|
|
|
284
337
|
`emitEntityEvent` is the escape hatch for an event of your own: name it whatever you like and give it
|
|
285
338
|
whatever args suit it, and it is emitted on the entity under that name. It exists so a system does not have
|
|
@@ -334,6 +387,112 @@ thread already holds the values - sending them along would only pay to copy what
|
|
|
334
387
|
System events are dispatched before the per-entity events of the same run, so an entity a run both moved and
|
|
335
388
|
killed is still in the world when its move is reported.
|
|
336
389
|
|
|
390
|
+
### Creating entities from a worker
|
|
391
|
+
|
|
392
|
+
An update function can spawn a whole entity, off-thread, from a factory config - the same
|
|
393
|
+
`{ type, ...overrides }` you would pass to `world.loadEntity`. Because component pools live in shared memory,
|
|
394
|
+
the worker merges the type's factory template, allocates each component's block and writes it, then reports
|
|
395
|
+
what it made; the main thread only wraps the result:
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
// in the update function - spawn a ship, overriding two fields of its template
|
|
399
|
+
createEntityWorker(world, { type: 'ship', x: 100, y: 150 }, callbacks);
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
`createEntityWorker` layers your overrides over the `ship` template, mints a unique id from a shared atomic
|
|
403
|
+
counter (so it never collides with one the main thread or another worker hands out), and for each component
|
|
404
|
+
the merged config triggers, pushes a block into its pool and writes the values - all off-thread. It reports
|
|
405
|
+
back an id-plus-block-indexes descriptor; when the run completes the main thread *adopts* it, building the
|
|
406
|
+
always-present `entity` component there (interning the `type` is a main-thread job) and wrapping each block the
|
|
407
|
+
worker wrote - no block is copied or re-allocated. Like every other worker report-back, the new entity first
|
|
408
|
+
exists on the following frame, so the system picks it up next run.
|
|
409
|
+
|
|
410
|
+
Two things make this work, both opt-in so only the workers that create entities pay for them:
|
|
411
|
+
|
|
412
|
+
- Register the system with `createsEntities: true`. That ships the factory templates to its worker on load.
|
|
413
|
+
- In that system's worker entry, pass your component registry to `createComponentWorker`, so the worker has
|
|
414
|
+
each component's block builder:
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
createComponentWorker(self, shipUpdate, registry);
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Every component is already defined as two halves for exactly this — `toBlock(config)` (the block values) and
|
|
421
|
+
`attach(entity, memory, index)` (the accessor), see [Defining components](#defining-components). The worker
|
|
422
|
+
runs only `toBlock` (off-thread, no entity/world), and the main thread runs `attach` when it adopts the entity.
|
|
423
|
+
So creating a component in a worker needs nothing extra as long as its `toBlock` is genuinely pure over config —
|
|
424
|
+
no `entity`/`world` access:
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
const shipHealth: ComponentDefinition<HealthComponent, Int32Array, HealthConfig> = {
|
|
428
|
+
// ...
|
|
429
|
+
toBlock(config) {
|
|
430
|
+
return [config.health ?? config.maxHealth, config.maxHealth];
|
|
431
|
+
},
|
|
432
|
+
attach(entity, memory, index) {
|
|
433
|
+
const block = memory.getBlock(index);
|
|
434
|
+
// ...build the accessor over `block`
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
This runs identically on the main-thread fallback. Two current limits: a `loadInFinishLoading` component (one
|
|
440
|
+
that reads other entities) is skipped, since the worker has no world to read; and an adopted entity is always
|
|
441
|
+
the base `BaseEntity` - a factory per-type subclass is not applied to it.
|
|
442
|
+
|
|
443
|
+
### Freeing component memory safely
|
|
444
|
+
|
|
445
|
+
Component blocks live in a shared pool, so a freed block gets handed straight back out to the next entity that
|
|
446
|
+
needs one. That is a problem across workers: if one system kills an entity, a second reuses its freed block for
|
|
447
|
+
a brand-new entity, and a third is still mid-run over what it thinks is the old entity, the third system writes
|
|
448
|
+
into the new entity's memory.
|
|
449
|
+
|
|
450
|
+
So the library never frees a block the instant it is orphaned. When an entity dies or you call
|
|
451
|
+
`removeComponent`, the block is *deferred* — the component is gone from the entity immediately, but the memory is
|
|
452
|
+
held until every system that could be mid-run over it has finished a run. Only then is the block returned to the
|
|
453
|
+
pool for reuse. This is automatic; there is nothing to call. The one visible effect is that
|
|
454
|
+
`memoryComponent.length` can briefly sit one higher than the number of live components, until the next update
|
|
455
|
+
lets the holding systems finish. If a system stays stuck (never completes a run) for more than ten seconds of
|
|
456
|
+
unscaled time, the world logs a warning naming that system and frees the blocks anyway rather than leak them —
|
|
457
|
+
a stuck system there is a bug worth chasing down.
|
|
458
|
+
|
|
459
|
+
### Freeing extra resources
|
|
460
|
+
|
|
461
|
+
Everything above frees a component's own block. A component that allocates something *else* in `attach` —
|
|
462
|
+
another heap structure (a `SharedList`, a `SharedString`) or child entities it owns — needs to release that
|
|
463
|
+
too, and the block-level deferred free won't do it. Give the definition an optional `free(component)`:
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
const cargoDefinition: ComponentDefinition<Cargo, Uint32Array, CargoConfig> = {
|
|
467
|
+
type: Uint32Array,
|
|
468
|
+
size: 3,
|
|
469
|
+
loadProperties: ['cargoSpace'],
|
|
470
|
+
toBlock(config) {
|
|
471
|
+
/* the block's own values */
|
|
472
|
+
},
|
|
473
|
+
attach(entity, memory, index) {
|
|
474
|
+
/* allocate a SharedList in the heap, stash its pointer in the block, return accessors */
|
|
475
|
+
},
|
|
476
|
+
free(component) {
|
|
477
|
+
component.items.free(); // release the SharedList + the item entities it holds
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
`free` runs exactly once per component teardown: on `removeComponent`, and on every path that removes an
|
|
483
|
+
entity — `removeEntity`, a `death` (from `killEntity`), a `load` that drops the old batch, and `clear`.
|
|
484
|
+
It is deferred to the same safe point as the component's own block (see above), not fired the instant the
|
|
485
|
+
entity dies — so a system still mid-run over that memory can't see your resource released early. Reload calls
|
|
486
|
+
it without any `death` event, so it is the reliable place to avoid leaks when a world is reused.
|
|
487
|
+
|
|
488
|
+
### Reusing a world: `load` and `clear`
|
|
489
|
+
|
|
490
|
+
A world is meant to be reused rather than rebuilt. `world.load(config)` swaps in a fresh scenario — it removes
|
|
491
|
+
the old entities, clears every system, and loads the new batch (deferred frees from the old contents drain over
|
|
492
|
+
the following updates, as above). `await world.clear()` instead tears the world all the way back down to an
|
|
493
|
+
empty, reusable state: it waits for every system's in-flight worker run to finish so nothing is still reading
|
|
494
|
+
the memory, frees the held blocks immediately, and resolves once the world is ready to load into again.
|
|
495
|
+
|
|
337
496
|
## Measuring performance
|
|
338
497
|
|
|
339
498
|
`PerformanceTiming` watches a world and reports what running it costs. Hand it the world and it hooks itself
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WorkerAllocator, WorkerCreateEntityConfig, WorkerCreatedEntity } from '../systems/component-system';
|
|
2
|
+
export interface WorkerCreatableComponent {
|
|
3
|
+
loadProperties: Array<string>;
|
|
4
|
+
loadInFinishLoading?: boolean;
|
|
5
|
+
toBlock(config: Record<string, unknown>): Array<number>;
|
|
6
|
+
}
|
|
7
|
+
export type WorkerCreateRegistry = {
|
|
8
|
+
[name: string]: WorkerCreatableComponent;
|
|
9
|
+
};
|
|
10
|
+
export type FactoryConfigs = {
|
|
11
|
+
[type: string]: Record<string, unknown>;
|
|
12
|
+
};
|
|
13
|
+
export declare function buildWorkerEntity(config: WorkerCreateEntityConfig, factoryConfigs: FactoryConfigs, registry: WorkerCreateRegistry, allocator: WorkerAllocator): WorkerCreatedEntity;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { ComponentSystemCallbacks,
|
|
2
|
-
export default function createEntityWorker(config:
|
|
1
|
+
import type { ComponentSystemCallbacks, ComponentSystemWorld, WorkerCreateEntityConfig } from '../systems/component-system';
|
|
2
|
+
export default function createEntityWorker(world: ComponentSystemWorld, config: WorkerCreateEntityConfig, callbacks: ComponentSystemCallbacks): void;
|
|
@@ -5,6 +5,7 @@ import type BaseEntity from './entity';
|
|
|
5
5
|
import type { EntityComponent, EntityComponentConfig, EntityComponentSerialization } from './entity-component';
|
|
6
6
|
export interface BaseComponent {
|
|
7
7
|
index: number;
|
|
8
|
+
block?: ComponentTypedArray;
|
|
8
9
|
}
|
|
9
10
|
export type ComponentMap = Record<string, BaseComponent>;
|
|
10
11
|
export interface ComponentDefinition<Component extends BaseComponent, T extends ComponentTypedArray = ComponentTypedArray, Config = any, Serialization = object> {
|
|
@@ -12,8 +13,10 @@ export interface ComponentDefinition<Component extends BaseComponent, T extends
|
|
|
12
13
|
size: number;
|
|
13
14
|
loadProperties: Array<keyof Config & string>;
|
|
14
15
|
loadInFinishLoading?: boolean;
|
|
15
|
-
|
|
16
|
+
toBlock(config: Config & Serialization, entity?: BaseEntity): Array<number>;
|
|
17
|
+
attach(entity: BaseEntity, memory: MemoryComponent<T>, index: number): Component;
|
|
16
18
|
save?(component: Component): Serialization;
|
|
19
|
+
free?(component: Component): void;
|
|
17
20
|
}
|
|
18
21
|
export type RegisteredComponentDefinition<Component extends BaseComponent, T extends ComponentTypedArray = ComponentTypedArray, Config = any, Serialization = object> = ComponentDefinition<Component, T, Config, Serialization> & {
|
|
19
22
|
memoryComponent: MemoryComponent<T>;
|
|
@@ -28,9 +31,9 @@ export type ComponentDefinitionMap = Record<string, ComponentDefinition<BaseComp
|
|
|
28
31
|
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
|
29
32
|
type DefinitionConfig<D> = D extends ComponentDefinition<BaseComponent, ComponentTypedArray, infer Config, infer Serialization> ? Config & Serialization : never;
|
|
30
33
|
export type ComponentsOf<R extends ComponentDefinitionMap> = {
|
|
31
|
-
[K in keyof R]: ReturnType<R[K]['
|
|
34
|
+
[K in keyof R]: ReturnType<R[K]['attach']> & BaseComponent;
|
|
32
35
|
} & {
|
|
33
|
-
entity: EntityComponent;
|
|
36
|
+
entity: EntityComponent & BaseComponent;
|
|
34
37
|
};
|
|
35
38
|
export type EntityConfigOf<R extends ComponentDefinitionMap> = Partial<UnionToIntersection<{
|
|
36
39
|
[K in keyof R]: DefinitionConfig<R[K]>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ComponentTypedArray } from './memory-component';
|
|
2
|
+
import type { BaseComponent } from './component-definition';
|
|
3
|
+
export default abstract class Component<T extends ComponentTypedArray = ComponentTypedArray> implements BaseComponent {
|
|
4
|
+
readonly index: number;
|
|
5
|
+
readonly block: T;
|
|
6
|
+
constructor(block: T, index: number);
|
|
7
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import ConstantString from '@daneren2005/shared-memory-objects/constant-string';
|
|
2
|
+
import type MemoryHeap from '@daneren2005/shared-memory-objects/memory-heap';
|
|
3
|
+
export default class ConstantStringCache {
|
|
4
|
+
private heap;
|
|
5
|
+
private byValue;
|
|
6
|
+
private byPointer;
|
|
7
|
+
constructor(heap: MemoryHeap);
|
|
8
|
+
getOrCreate(value: string): ConstantString;
|
|
9
|
+
getString(pointer: number): string | undefined;
|
|
10
|
+
clear(): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import e from "@daneren2005/shared-memory-objects/memory-heap";
|
|
2
|
+
import { getPointer as t } from "@daneren2005/shared-memory-objects/utils/pointer";
|
|
3
|
+
import n from "@daneren2005/shared-memory-objects/shared-pool";
|
|
4
|
+
import r from "@daneren2005/shared-memory-objects/constant-string";
|
|
5
|
+
//#region src/memory-component.ts
|
|
6
|
+
var i = class e {
|
|
7
|
+
heap;
|
|
8
|
+
pool;
|
|
9
|
+
constructor(e, t, r, i) {
|
|
10
|
+
this.heap = e, this.pool = i ? new n(e, i) : new n(e, {
|
|
11
|
+
type: t,
|
|
12
|
+
dataLength: r
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
static fromSharedMemory(t, n) {
|
|
16
|
+
return new e(t, Uint32Array, 1, n);
|
|
17
|
+
}
|
|
18
|
+
getSharedMemory() {
|
|
19
|
+
return this.pool.getSharedMemory();
|
|
20
|
+
}
|
|
21
|
+
get length() {
|
|
22
|
+
return this.pool.length;
|
|
23
|
+
}
|
|
24
|
+
get rawLength() {
|
|
25
|
+
return this.pool.bufferLength;
|
|
26
|
+
}
|
|
27
|
+
create(e) {
|
|
28
|
+
return this.pool.push(e);
|
|
29
|
+
}
|
|
30
|
+
getBlock(e) {
|
|
31
|
+
return this.pool.at(e);
|
|
32
|
+
}
|
|
33
|
+
get(e, t) {
|
|
34
|
+
return this.pool.get(e, t);
|
|
35
|
+
}
|
|
36
|
+
set(e, t, n) {
|
|
37
|
+
let r = this.pool.at(e);
|
|
38
|
+
r[t] = n;
|
|
39
|
+
}
|
|
40
|
+
delete(e) {
|
|
41
|
+
this.pool.deleteIndex(e);
|
|
42
|
+
}
|
|
43
|
+
clear() {
|
|
44
|
+
this.pool.clear();
|
|
45
|
+
}
|
|
46
|
+
}, a = class {
|
|
47
|
+
heap;
|
|
48
|
+
byValue = /* @__PURE__ */ new Map();
|
|
49
|
+
byPointer = /* @__PURE__ */ new Map();
|
|
50
|
+
constructor(e) {
|
|
51
|
+
this.heap = e;
|
|
52
|
+
}
|
|
53
|
+
getOrCreate(e) {
|
|
54
|
+
let t = this.byValue.get(e);
|
|
55
|
+
if (t) return t;
|
|
56
|
+
let n = new r(this.heap, e);
|
|
57
|
+
return this.byValue.set(e, n), this.byPointer.set(n.pointer, e), n;
|
|
58
|
+
}
|
|
59
|
+
getString(e) {
|
|
60
|
+
if (e === 0) return "";
|
|
61
|
+
let n = this.byPointer.get(e);
|
|
62
|
+
if (n !== void 0) return n;
|
|
63
|
+
let i = t(e);
|
|
64
|
+
if (this.heap.buffers[i.bufferPosition] === void 0) return;
|
|
65
|
+
let a = new r(this.heap, i).value;
|
|
66
|
+
return this.byPointer.set(e, a), a;
|
|
67
|
+
}
|
|
68
|
+
clear() {
|
|
69
|
+
this.byValue.forEach((e) => e.free()), this.byValue.clear(), this.byPointer.clear();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/systems/workers/apply-query-delta.ts
|
|
74
|
+
function o(e, t) {
|
|
75
|
+
if (t.removed.length) {
|
|
76
|
+
let n = new Set(t.removed);
|
|
77
|
+
e = e.filter((e) => !n.has(e.entityId));
|
|
78
|
+
}
|
|
79
|
+
if (t.added.length) {
|
|
80
|
+
let n = /* @__PURE__ */ new Map();
|
|
81
|
+
e.forEach((e, t) => n.set(e.entityId, t));
|
|
82
|
+
for (let r of t.added) {
|
|
83
|
+
let t = n.get(r.entityId);
|
|
84
|
+
t === void 0 ? (n.set(r.entityId, e.length), e.push(r)) : e[t] = r;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return e;
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/actions/build-worker-entity.ts
|
|
91
|
+
function s(e, t, n, r) {
|
|
92
|
+
let i = {
|
|
93
|
+
...t[e.type] ?? {},
|
|
94
|
+
...e
|
|
95
|
+
}, a = r.allocateEid(), o = {};
|
|
96
|
+
for (let e of Object.keys(n)) {
|
|
97
|
+
if (e === "entity") continue;
|
|
98
|
+
let t = n[e];
|
|
99
|
+
t.loadInFinishLoading || t.loadProperties.some((e) => e in i) && (o[e] = r.allocateComponentBlock(e, t.toBlock(i)));
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
eid: a,
|
|
103
|
+
type: e.type,
|
|
104
|
+
isStatic: i.isStatic,
|
|
105
|
+
components: o
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/component.ts
|
|
110
|
+
var c = class {
|
|
111
|
+
index;
|
|
112
|
+
block;
|
|
113
|
+
constructor(e, t) {
|
|
114
|
+
this.block = e, this.index = t;
|
|
115
|
+
}
|
|
116
|
+
}, l = 0, u = 1, d = 2, f = class extends c {
|
|
117
|
+
cache;
|
|
118
|
+
constructor(e, t, n) {
|
|
119
|
+
super(e, t), this.cache = n;
|
|
120
|
+
}
|
|
121
|
+
get type() {
|
|
122
|
+
return this.cache.getString(this.block[2]) ?? "";
|
|
123
|
+
}
|
|
124
|
+
set type(e) {
|
|
125
|
+
this.block[2] = e ? this.cache.getOrCreate(e).pointer : 0;
|
|
126
|
+
}
|
|
127
|
+
get dead() {
|
|
128
|
+
return this.block[0] === 1;
|
|
129
|
+
}
|
|
130
|
+
set dead(e) {
|
|
131
|
+
this.block[0] = +!!e;
|
|
132
|
+
}
|
|
133
|
+
get isStatic() {
|
|
134
|
+
return this.block[1] === 1;
|
|
135
|
+
}
|
|
136
|
+
set isStatic(e) {
|
|
137
|
+
this.block[1] = +!!e;
|
|
138
|
+
}
|
|
139
|
+
}, p = {
|
|
140
|
+
type: Uint32Array,
|
|
141
|
+
size: 3,
|
|
142
|
+
loadProperties: ["type", "isStatic"],
|
|
143
|
+
toBlock(e, t) {
|
|
144
|
+
let n = t.world.constantStrings, r = e.type ? n.getOrCreate(e.type).pointer : 0;
|
|
145
|
+
return [
|
|
146
|
+
+!!e.dead,
|
|
147
|
+
+!!e.isStatic,
|
|
148
|
+
r
|
|
149
|
+
];
|
|
150
|
+
},
|
|
151
|
+
attach(e, t, n) {
|
|
152
|
+
return new f(t.getBlock(n), n, e.world.constantStrings);
|
|
153
|
+
},
|
|
154
|
+
save(e) {
|
|
155
|
+
let t = {};
|
|
156
|
+
return e.type && (t.type = e.type), e.dead && (t.dead = !0), t;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/actions/kill-entity-worker.ts
|
|
161
|
+
function m(e, t, n) {
|
|
162
|
+
let r = t.entity;
|
|
163
|
+
r && (r[0] = 1), n.entityDied(e);
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/actions/create-entity-worker.ts
|
|
167
|
+
function h(e, t, n) {
|
|
168
|
+
if (!e.buildEntityDescriptor) throw Error("createEntityWorker requires the system to be registered with createsEntities: true and the component registry passed to createComponentWorker");
|
|
169
|
+
n.createEntity(e.buildEntityDescriptor(t));
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/systems/workers/create-component-worker.ts
|
|
173
|
+
function g(t, n, r) {
|
|
174
|
+
let c = [], l = {}, u, d, f, p = {}, m, h, g = (e) => f?.getString(e) ?? "";
|
|
175
|
+
t.onmessage = function(v) {
|
|
176
|
+
let y = v.data;
|
|
177
|
+
if (y.type === "init") _(t, { type: "init-complete" });
|
|
178
|
+
else if (y.type === "load") {
|
|
179
|
+
if (y.heap && (d = new e(y.heap), f = new a(d), d.addOnGrowBufferHandlers((e) => _(t, {
|
|
180
|
+
type: "grow-buffer-from-worker",
|
|
181
|
+
buffer: e
|
|
182
|
+
}))), d && y.sharedMemory) {
|
|
183
|
+
p = {};
|
|
184
|
+
for (let e of Object.keys(y.sharedMemory.components)) p[e] = i.fromSharedMemory(d, y.sharedMemory.components[e]);
|
|
185
|
+
m = d.getSharedAlloc(y.sharedMemory.eidCounter);
|
|
186
|
+
}
|
|
187
|
+
h = y.factoryConfigs, u = n.init?.(y.data) ?? void 0, _(t, { type: "loaded" });
|
|
188
|
+
} else if (y.type === "grow-buffer") d && d.buffers[y.buffer.bufferPosition] === void 0 && d.addSharedBuffer(y.buffer);
|
|
189
|
+
else if (y.type === "reset") {
|
|
190
|
+
c = [];
|
|
191
|
+
for (let e of Object.keys(l)) delete l[e];
|
|
192
|
+
u = void 0;
|
|
193
|
+
} else if (y.type === "run") {
|
|
194
|
+
u && Object.assign(y.world, u), y.world.getString = g;
|
|
195
|
+
let e = {
|
|
196
|
+
allocateEid: () => m ? Atomics.add(m.data, 0, 1) + 1 : 0,
|
|
197
|
+
allocateComponentBlock: (e, t) => p[e].create(t)
|
|
198
|
+
};
|
|
199
|
+
y.world.allocate = e, h && r && (y.world.buildEntityDescriptor = (t) => s(t, h, r, e));
|
|
200
|
+
let i = performance.now(), a = [], d = {}, f = [];
|
|
201
|
+
c = o(c, y.entities);
|
|
202
|
+
let v = {};
|
|
203
|
+
Object.entries(y.queries).forEach(([e, t]) => {
|
|
204
|
+
let n = o(l[e] ?? [], t);
|
|
205
|
+
l[e] = n, v[e] = n;
|
|
206
|
+
});
|
|
207
|
+
let b = {
|
|
208
|
+
entityComponentChanged(e, t, n, r) {
|
|
209
|
+
a.push({
|
|
210
|
+
entityId: e,
|
|
211
|
+
event: "component-property-updated",
|
|
212
|
+
args: [
|
|
213
|
+
t,
|
|
214
|
+
n,
|
|
215
|
+
r
|
|
216
|
+
]
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
emitEntityEvent(e, t, ...n) {
|
|
220
|
+
a.push({
|
|
221
|
+
entityId: e,
|
|
222
|
+
event: t,
|
|
223
|
+
args: n
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
emitSystemEvent(e, t) {
|
|
227
|
+
(d[e] ?? (d[e] = [])).push(t);
|
|
228
|
+
},
|
|
229
|
+
entityDied(e) {
|
|
230
|
+
a.push({
|
|
231
|
+
entityId: e,
|
|
232
|
+
event: "death",
|
|
233
|
+
args: []
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
createEntity(e) {
|
|
237
|
+
f.push(e);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
n.preRun && n.preRun(y.world, c, v, b), c.forEach((e) => {
|
|
241
|
+
n(y.world, e.entityId, e.components, v, b);
|
|
242
|
+
}), n.entityRemoved && y.entities.removed.forEach((e) => {
|
|
243
|
+
n.entityRemoved(y.world, e, b);
|
|
244
|
+
});
|
|
245
|
+
let x = performance.now() - i;
|
|
246
|
+
_(t, {
|
|
247
|
+
type: "run-complete",
|
|
248
|
+
generation: y.generation,
|
|
249
|
+
runTime: x,
|
|
250
|
+
events: a,
|
|
251
|
+
systemEvents: d,
|
|
252
|
+
created: f
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function _(e, t) {
|
|
258
|
+
e.postMessage(t);
|
|
259
|
+
}
|
|
260
|
+
//#endregion
|
|
261
|
+
export { u as a, c, a as d, i as f, l as i, s as l, h as n, d as o, m as r, p as s, g as t, o as u };
|
|
262
|
+
|
|
263
|
+
//# sourceMappingURL=create-component-worker-CaB32Dgn.js.map
|