@daneren2005/shared-memory-ecs 1.4.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 +138 -30
- 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 +5 -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 +168 -149
- package/dist/index.js.map +1 -1
- package/dist/memory-component.d.ts +6 -3
- package/dist/systems/component-system.d.ts +23 -2
- 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 +18 -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,11 +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
|
-
|
|
17
|
-
|
|
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)).
|
|
18
21
|
- **`ComponentRegistry<C>`** – the map of all component definitions for a game.
|
|
19
22
|
- **`EntityFactory<C>`** – maps an entity `type` name to a base (template) config. Loading an entity layers
|
|
20
23
|
the caller's config over its type's template, so shared static data lives in one place and a save only
|
|
@@ -33,6 +36,7 @@ fog of war, sub-classed entities, required components, etc).
|
|
|
33
36
|
## Defining components
|
|
34
37
|
|
|
35
38
|
```ts
|
|
39
|
+
import { Component } from '@daneren2005/shared-memory-ecs';
|
|
36
40
|
import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';
|
|
37
41
|
|
|
38
42
|
interface HealthComponent {
|
|
@@ -44,7 +48,16 @@ interface HealthComponent {
|
|
|
44
48
|
const HEALTH_INDEX = 0;
|
|
45
49
|
const HEALTH_MAX_INDEX = 1;
|
|
46
50
|
|
|
47
|
-
//
|
|
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`
|
|
48
61
|
// and `save` both see the combined `{ maxHealth: number, health?: number }`.
|
|
49
62
|
const healthDefinition: ComponentDefinition<HealthComponent, Int32Array, { maxHealth: number }, { health?: number }> = {
|
|
50
63
|
type: Int32Array,
|
|
@@ -52,16 +65,15 @@ const healthDefinition: ComponentDefinition<HealthComponent, Int32Array, { maxHe
|
|
|
52
65
|
// Configs are flat and shared: this component loads whenever `maxHealth` is present, then reads what it
|
|
53
66
|
// needs off the same config object. Only Config props may appear here.
|
|
54
67
|
loadProperties: ['maxHealth'],
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
};
|
|
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);
|
|
65
77
|
},
|
|
66
78
|
save(component) {
|
|
67
79
|
// save returns only Serialization; maxHealth is Config and comes back from the template on reload.
|
|
@@ -74,7 +86,7 @@ const registry = {
|
|
|
74
86
|
health: healthDefinition
|
|
75
87
|
// ...other component definitions
|
|
76
88
|
};
|
|
77
|
-
type Components = { [K in keyof typeof registry]: ReturnType<(typeof registry)[K]['
|
|
89
|
+
type Components = { [K in keyof typeof registry]: ReturnType<(typeof registry)[K]['attach']> };
|
|
78
90
|
```
|
|
79
91
|
|
|
80
92
|
## Using the world
|
|
@@ -111,6 +123,12 @@ console.log(goblin.save()); // { type: 'goblin', health: 10 } - no templated max
|
|
|
111
123
|
world.loadEntity(goblin.save());
|
|
112
124
|
```
|
|
113
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
|
+
|
|
114
132
|
## Iterating entities
|
|
115
133
|
|
|
116
134
|
`world.entities` is a `Map` keyed by `eid`, not an array, and so is `entities` on `EntitySystem` and
|
|
@@ -141,10 +159,10 @@ one: it is typed, it is readable, and one read costs nothing worth measuring.
|
|
|
141
159
|
|
|
142
160
|
It is not free, though, and the cost shows up in exactly one situation — reading the same component off
|
|
143
161
|
*thousands* of entities, *every frame*. The accessor walks several objects to get there and ends in a getter
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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.
|
|
148
166
|
|
|
149
167
|
Where that matters, hold the block instead. It is the same memory the accessors read, so nothing changes about
|
|
150
168
|
what you get, and it is what the update functions already work on:
|
|
@@ -152,15 +170,15 @@ what you get, and it is what the update functions already work on:
|
|
|
152
170
|
```ts
|
|
153
171
|
import { TRANSFORM_X_INDEX } from '@daneren2005/shared-memory-physics';
|
|
154
172
|
|
|
155
|
-
// Resolve once, when whatever is doing the reading is set up.
|
|
156
|
-
|
|
157
|
-
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;
|
|
158
176
|
|
|
159
177
|
// Then per frame, per entity:
|
|
160
178
|
block[HEALTH_INDEX];
|
|
161
179
|
```
|
|
162
180
|
|
|
163
|
-
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
|
|
164
182
|
can be removed and re-added, or hang it off something that dies with the entity. And it is indexed rather than
|
|
165
183
|
named, so the offsets have to be exported alongside the definition — which is why every component in the
|
|
166
184
|
physics library exports its `*_INDEX` constants.
|
|
@@ -259,8 +277,8 @@ barrel: importing `createComponentWorker` from `@daneren2005/shared-memory-ecs`
|
|
|
259
277
|
`BaseWorld`, every system, their `@daneren2005/shared-memory-objects` dependencies - into the worker, even
|
|
260
278
|
though a worker never runs any of it (easily ~20kb of dead code per worker). Import worker-side helpers from
|
|
261
279
|
the `@daneren2005/shared-memory-ecs/worker` subpath instead. It exposes only what runs in a worker -
|
|
262
|
-
`createComponentWorker`, `createEntityWorker`, `killEntityWorker`, `DEAD_INDEX` (plus the
|
|
263
|
-
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:
|
|
264
282
|
|
|
265
283
|
```ts
|
|
266
284
|
// damage.worker.ts - the worker entry file
|
|
@@ -275,12 +293,46 @@ or `killEntityWorker` should import them from `/worker` too. Type-only imports (
|
|
|
275
293
|
`ComponentSystemWorld`, ...) can come from either path since types are erased, and main-thread code
|
|
276
294
|
(`ComponentSystem`, `BaseWorld`, `EntityFactory`, ...) keeps importing from the package root.
|
|
277
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
|
+
|
|
278
329
|
### Reporting back to the main thread
|
|
279
330
|
|
|
280
331
|
An update function runs on shared memory, so anything it writes is already visible on the main thread. What
|
|
281
332
|
it cannot do from there is touch the world, so the things that have to happen back on it go through
|
|
282
333
|
`callbacks`: `entityComponentChanged` (emitted on the entity as `component-property-updated`), `entityDied`
|
|
283
|
-
(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.
|
|
284
336
|
|
|
285
337
|
`emitEntityEvent` is the escape hatch for an event of your own: name it whatever you like and give it
|
|
286
338
|
whatever args suit it, and it is emitted on the entity under that name. It exists so a system does not have
|
|
@@ -335,6 +387,59 @@ thread already holds the values - sending them along would only pay to copy what
|
|
|
335
387
|
System events are dispatched before the per-entity events of the same run, so an entity a run both moved and
|
|
336
388
|
killed is still in the world when its move is reported.
|
|
337
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
|
+
|
|
338
443
|
### Freeing component memory safely
|
|
339
444
|
|
|
340
445
|
Component blocks live in a shared pool, so a freed block gets handed straight back out to the next entity that
|
|
@@ -353,7 +458,7 @@ a stuck system there is a bug worth chasing down.
|
|
|
353
458
|
|
|
354
459
|
### Freeing extra resources
|
|
355
460
|
|
|
356
|
-
Everything above frees a component's own block. A component that allocates something *else* in `
|
|
461
|
+
Everything above frees a component's own block. A component that allocates something *else* in `attach` —
|
|
357
462
|
another heap structure (a `SharedList`, a `SharedString`) or child entities it owns — needs to release that
|
|
358
463
|
too, and the block-level deferred free won't do it. Give the definition an optional `free(component)`:
|
|
359
464
|
|
|
@@ -362,7 +467,10 @@ const cargoDefinition: ComponentDefinition<Cargo, Uint32Array, CargoConfig> = {
|
|
|
362
467
|
type: Uint32Array,
|
|
363
468
|
size: 3,
|
|
364
469
|
loadProperties: ['cargoSpace'],
|
|
365
|
-
|
|
470
|
+
toBlock(config) {
|
|
471
|
+
/* the block's own values */
|
|
472
|
+
},
|
|
473
|
+
attach(entity, memory, index) {
|
|
366
474
|
/* allocate a SharedList in the heap, stash its pointer in the block, return accessors */
|
|
367
475
|
},
|
|
368
476
|
free(component) {
|
|
@@ -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,7 +13,8 @@ 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;
|
|
17
19
|
free?(component: Component): void;
|
|
18
20
|
}
|
|
@@ -29,9 +31,9 @@ export type ComponentDefinitionMap = Record<string, ComponentDefinition<BaseComp
|
|
|
29
31
|
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
|
30
32
|
type DefinitionConfig<D> = D extends ComponentDefinition<BaseComponent, ComponentTypedArray, infer Config, infer Serialization> ? Config & Serialization : never;
|
|
31
33
|
export type ComponentsOf<R extends ComponentDefinitionMap> = {
|
|
32
|
-
[K in keyof R]: ReturnType<R[K]['
|
|
34
|
+
[K in keyof R]: ReturnType<R[K]['attach']> & BaseComponent;
|
|
33
35
|
} & {
|
|
34
|
-
entity: EntityComponent;
|
|
36
|
+
entity: EntityComponent & BaseComponent;
|
|
35
37
|
};
|
|
36
38
|
export type EntityConfigOf<R extends ComponentDefinitionMap> = Partial<UnionToIntersection<{
|
|
37
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
|