@daneren2005/shared-memory-ecs 1.8.1 → 1.9.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 +257 -16
- package/dist/{create-system-worker-C4aS3-3R.js → create-system-worker-CNpRs2FO.js} +121 -71
- package/dist/create-system-worker-CNpRs2FO.js.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +675 -215
- package/dist/index.js.map +1 -1
- package/dist/performance-timing.d.ts +3 -1
- package/dist/scheduling/conflict-scheduler.d.ts +53 -0
- package/dist/scheduling/default-scheduler.d.ts +5 -0
- package/dist/scheduling/scheduler.d.ts +28 -0
- package/dist/systems/entity-system.d.ts +4 -1
- package/dist/systems/entity-worker-system.d.ts +19 -4
- package/dist/systems/iterable-system.d.ts +2 -2
- package/dist/systems/system.d.ts +17 -3
- package/dist/systems/worker-system.d.ts +4 -4
- package/dist/systems/workers/create-entity-system-worker.d.ts +1 -0
- package/dist/systems/workers/create-system-worker.d.ts +3 -2
- package/dist/systems/workers/entity-system-web-worker.d.ts +2 -0
- package/dist/systems/workers/entity-system-worker-message.d.ts +21 -3
- package/dist/systems/workers/entity-work-batch.d.ts +10 -0
- package/dist/worker.d.ts +2 -2
- package/dist/worker.js +1 -1
- package/dist/world.d.ts +11 -5
- package/package.json +6 -6
- package/dist/create-system-worker-C4aS3-3R.js.map +0 -1
package/README.md
CHANGED
|
@@ -193,6 +193,195 @@ template.
|
|
|
193
193
|
|
|
194
194
|
Classless factories keep the original behavior: they create `BaseEntity` and scan the complete registry.
|
|
195
195
|
|
|
196
|
+
## System scheduling
|
|
197
|
+
|
|
198
|
+
`world.update(elapsedTime)` uses `DefaultScheduler` to dispatch systems in registration order.
|
|
199
|
+
Synchronous systems run immediately, workers can overlap after dispatch, and iterable systems can
|
|
200
|
+
continue across updates. Systems keep their existing `shouldRun()`, `deltaBetweenRuns`, and `firstRun`
|
|
201
|
+
behavior. The world advances clocks, applies `timeScale`, skips new updates while paused, prepares shared
|
|
202
|
+
memory, and manages deferred component frees.
|
|
203
|
+
|
|
204
|
+
Select a scheduler at construction; `world.scheduler` is readonly:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { BaseWorld, DefaultScheduler } from '@daneren2005/shared-memory-ecs';
|
|
208
|
+
|
|
209
|
+
const world = new BaseWorld(registry, {
|
|
210
|
+
scheduler: new DefaultScheduler<Components>()
|
|
211
|
+
});
|
|
212
|
+
world.update(16); // Synchronous result: { lastSystemError: null } when no dispatch error occurred.
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Custom policies implement `Scheduler<C>`. Its synchronous `update(context, elapsedTime)` receives the
|
|
216
|
+
scaled delta and a `SchedulerContext<C>` exposing the live `systems` array, the world's `emit` method,
|
|
217
|
+
and optional `gameTime` (supplied by `BaseWorld`).
|
|
218
|
+
It returns `SchedulerUpdateResult` (`{ lastSystemError?: Error | null }`). `DefaultScheduler` preserves
|
|
219
|
+
the existing dispatch timing events, error reporting, and continuation after a system throws.
|
|
220
|
+
|
|
221
|
+
Optional `systemAdded(context, system)` and `systemRemoved(context, system)` hooks receive notifications
|
|
222
|
+
after the array changes and before the corresponding world event. Duplicate names remain supported;
|
|
223
|
+
`addSystemIfNotExists` checks names and `removeSystem` removes the first matching name. Direct assignment
|
|
224
|
+
or mutation of `world.systems` bypasses membership hooks, so policies must read the live array when needed.
|
|
225
|
+
The default policy preserves native `forEach` mutation behavior during dispatch.
|
|
226
|
+
|
|
227
|
+
Optional `reset(context)` runs after systems are cleared during reload or non-pristine `clear()`;
|
|
228
|
+
pristine worlds skip that teardown. `destroy(context)` runs once after system destruction.
|
|
229
|
+
`runCompleted(context, system)` receives the existing completion boundary: a synchronous run returned,
|
|
230
|
+
an iterable queue drained, or a current-generation worker reply's effects were applied. Fallback
|
|
231
|
+
completion can arrive inside scheduler dispatch. The world processes eligible deferred frees before
|
|
232
|
+
calling this hook. Dispatch return values and `system-<name>-finished` events do not imply completion.
|
|
233
|
+
These hooks expose lifecycle notifications; they do not establish deterministic ticks or safe edit barriers.
|
|
234
|
+
|
|
235
|
+
Use `ConflictScheduler` to prevent systems from overlapping when one writes a component the other reads
|
|
236
|
+
or writes. Conflicts follow registration order; read-only systems and systems accessing independent
|
|
237
|
+
components can overlap:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { BaseWorld, ConflictScheduler, EntityWorkerSystem } from '@daneren2005/shared-memory-ecs';
|
|
241
|
+
|
|
242
|
+
const world = new BaseWorld(registry, {
|
|
243
|
+
scheduler: new ConflictScheduler<Components>()
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
class DamageSystem extends EntityWorkerSystem<Components, { health: Int32Array }> {
|
|
247
|
+
constructor() {
|
|
248
|
+
super(world, {
|
|
249
|
+
name: 'Damage',
|
|
250
|
+
required: ['health'],
|
|
251
|
+
writes: ['health'],
|
|
252
|
+
updateFunction: damageUpdate,
|
|
253
|
+
getWorker: () => new Worker(new URL('./damage-worker.ts', import.meta.url), { type: 'module' })
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
world.addSystem(new DamageSystem());
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The scheduler compiles its schedule when systems are added or removed. Inspect it before the first
|
|
261
|
+
update using the scheduler instance:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
const scheduler = new ConflictScheduler<Components>();
|
|
265
|
+
const world = new BaseWorld(registry, { scheduler });
|
|
266
|
+
// Register your systems here.
|
|
267
|
+
console.log(scheduler.schedule.order.map(system => system.name));
|
|
268
|
+
console.table(scheduler.schedule.batches.map((batch, index) => ({
|
|
269
|
+
batch: index,
|
|
270
|
+
systems: batch.map(system => system.name).join(', ')
|
|
271
|
+
})));
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`schedule` is a frozen `ConflictSchedule<C>` containing system references. `order` flattens the
|
|
275
|
+
compiled dependency levels, and `batches` group systems that can run concurrently within one update.
|
|
276
|
+
Every conflict points from the earlier registered system to the later one. Each system goes into the
|
|
277
|
+
earliest level after its conflicting predecessors. These groups describe dependencies, not runtime
|
|
278
|
+
barriers: a system starts when its own predecessors finish, even if unrelated systems in an earlier
|
|
279
|
+
group are still running. Actual dispatch can also overlap independent work from later host updates.
|
|
280
|
+
|
|
281
|
+
`writes` lists every component a system changes, including components reached outside its main query.
|
|
282
|
+
Use `writes: []` for a read-only system. Reads are inferred from `EntitySystem.components` and from worker
|
|
283
|
+
`required`, `optional`, and all named `queries`. The built-in `entity` component is also a read. Add
|
|
284
|
+
`reads: ['otherComponent']` for access outside those queries, including filters and hooks. A plain
|
|
285
|
+
`System` or `IterableSystem` supplies both `reads` and `writes`; an `EntitySystem` without a component
|
|
286
|
+
query supplies `reads` explicitly. Component names are checked against the system's component type.
|
|
287
|
+
|
|
288
|
+
Omitting `writes`, or leaving reads unknown, makes a system exclusive. `exclusive: true` also reserves
|
|
289
|
+
exclusive execution for a system whose callbacks, structural changes, or shared resources cannot be
|
|
290
|
+
described through component access. Workers with `createsEntities: true` or `addsComponents: true` remain
|
|
291
|
+
exclusive. Declare writes to `entity` when changing its flags; arbitrary callback effects still need
|
|
292
|
+
explicit declarations or exclusive execution.
|
|
293
|
+
|
|
294
|
+
For parallel worker creation, `EntityWorkerSystem` and `WorkerSystem` accept a component list in the
|
|
295
|
+
existing capability option, for example `createsEntities: ['health', 'movement']`. List every game
|
|
296
|
+
component the system can create, including components supplied by factory templates. The built-in
|
|
297
|
+
`entity` component is included automatically. `writes` still declares changes to existing components;
|
|
298
|
+
creation declarations do not authorize undeclared callback or attachment effects on existing data.
|
|
299
|
+
Both boolean and list forms enable the same worker allocator, factory templates, and stable atomic eids.
|
|
300
|
+
The default scheduler treats both forms identically and adopts results immediately on completion.
|
|
301
|
+
|
|
302
|
+
Under `ConflictScheduler`, compatible creators can allocate and initialize blocks concurrently, even
|
|
303
|
+
in the same component pool. Publication stays in scheduling order and waits for earlier readers and
|
|
304
|
+
writers of the affected components; later readers and writers wait until publication is applied.
|
|
305
|
+
A creator can therefore compute beside an earlier reader without changing that reader's query snapshot.
|
|
306
|
+
A later query waits for publication when the creator's component list can supply every required
|
|
307
|
+
component of that query. For example, creating `['transform', 'item']` does not block a query requiring
|
|
308
|
+
`['transform', 'tree']`, even when it optionally reads `item` or excludes it with `not`. Main and named
|
|
309
|
+
queries are checked separately; a match with any one retains the dependency. An empty or entity-only
|
|
310
|
+
query still observes every creation. Explicit `reads` outside queries and all `writes` retain their
|
|
311
|
+
component conflicts, so accessing `world.entities` directly must still be declared.
|
|
312
|
+
|
|
313
|
+
Creation lists describe all components a system can create, not one exact entity shape. Optional
|
|
314
|
+
components and exclusions therefore cannot prove a query unrelated when its required components could
|
|
315
|
+
all be created. Custom query filters need explicit `reads` for accesses outside their declared
|
|
316
|
+
components. Custom membership overrides retain conservative component-level creation access unless
|
|
317
|
+
they provide accurate `creationReadQueries` metadata. This optional system getter exposes separate
|
|
318
|
+
required-component lists to the conflict scheduler; `undefined` uses `creationReadComponents` as before.
|
|
319
|
+
Queued updates keep their original query and creation snapshots across schedule rebuilds.
|
|
320
|
+
|
|
321
|
+
Worker completion effects are kept together, preserving component-change, event, death, and adoption
|
|
322
|
+
order within each run. Attachment hooks and callbacks affecting other shared data still need truthful
|
|
323
|
+
`writes` or `exclusive: true`. Completion promises and memory-free notifications settle only after
|
|
324
|
+
application; cancellation discards unpublished blocks. These rules apply only to the conflict scheduler.
|
|
325
|
+
|
|
326
|
+
Each host update queues one update per registered system with its scaled delta and logical game time.
|
|
327
|
+
Updates of the same system stay sequential. Read/write and write/write conflicts preserve host-update
|
|
328
|
+
order, then registration order within an update, including dependencies on work that has not started
|
|
329
|
+
yet. Read/read access across different systems can overlap. Independent systems can advance into later
|
|
330
|
+
updates while a slow worker is unfinished; there is no whole-update or whole-batch completion barrier.
|
|
331
|
+
Exclusive work remains a barrier to all earlier and later work, including workers using the boolean
|
|
332
|
+
creation capability.
|
|
333
|
+
|
|
334
|
+
When asynchronous systems finish applying effects, a microtask dispatches newly ready work after
|
|
335
|
+
their completion promises settle, without waiting for another animation frame. Deltas stay in each
|
|
336
|
+
system's order without merging or dropping them; `deltaBetweenRuns`, remainders and `firstRun` retain
|
|
337
|
+
their existing behavior. While unpaused, sliced systems continue at most once per host update with
|
|
338
|
+
zero extra delta, and release their dependents when the slice queue drains. Conflicting work can still
|
|
339
|
+
build a backlog.
|
|
340
|
+
|
|
341
|
+
During scheduled `shouldRun()` and `update()` calls, `System.gameTime` reports the logical time of the
|
|
342
|
+
queued update. Worker `world.gameTime` uses that captured value, on real workers and the synchronous
|
|
343
|
+
fallback alike. Delayed dispatch therefore does not borrow a later host update's clock. Outside that
|
|
344
|
+
scope, `System.gameTime` returns the live world clock. Custom schedulers can use
|
|
345
|
+
`system.withGameTime(time, () => system.update(delta))` to establish the same scope. Plain systems and
|
|
346
|
+
`addDataToWorld()` implementations should use `this.gameTime` when they need scheduled time; direct
|
|
347
|
+
reads of `this.world.gameTime` still see the host clock. The default scheduler continues using its
|
|
348
|
+
existing `update(delta)` path and live clock.
|
|
349
|
+
|
|
350
|
+
The compiled schedule is reused until membership changes or `scheduler.rebuild(world)` is called.
|
|
351
|
+
Call `rebuild` after changing access declarations, or to inspect direct edits to `world.systems` before
|
|
352
|
+
the next update; direct membership edits are otherwise checked before each dispatch. Queued updates
|
|
353
|
+
retain their original access reservations, even if metadata is rebuilt while a worker is running.
|
|
354
|
+
Additions join future host updates; removed pending updates are skipped when their predecessors finish,
|
|
355
|
+
and removed running systems retain their reservations until completion. With `ConflictScheduler`,
|
|
356
|
+
`world.pause()` or `world.paused = true` stops enqueueing new updates but finishes draining existing
|
|
357
|
+
work without further `world.update()` calls. Queued systems retain their captured deltas and logical
|
|
358
|
+
times; sliced runs continue through timer tasks with zero extra delta, yielding between slices.
|
|
359
|
+
Worker effects publish as their dependencies finish. Draining does not advance either world clock;
|
|
360
|
+
calling `world.update()` while paused still advances only `playerTime`. `world.resume()` allows new
|
|
361
|
+
work to be queued on the next host update. Reload/clear and destroy discard queued updates and
|
|
362
|
+
cancel scheduled microtasks and slice timers. The world prepares shared memory before deferred dispatch and prevents
|
|
363
|
+
new dispatch while clearing or destroyed. Errors from automatic dispatch emit `system-error`; they
|
|
364
|
+
cannot change a result already returned by `world.update()`.
|
|
365
|
+
|
|
366
|
+
Custom asynchronous systems must report `isCurrentlyRunning()` and notify completion through
|
|
367
|
+
`onRunFinished()` when their work and effects finish. Schedulers may implement the optional
|
|
368
|
+
`prepareEffects(context, system)` hook: it captures the dispatched run and returns a handler receiving
|
|
369
|
+
`SchedulerEffects` with `apply()` and `discard()`. The handler must eventually call one of them.
|
|
370
|
+
`beforeClear(context)` lets such schedulers discard pending effects before the world awaits workers.
|
|
371
|
+
The optional `pause(context)` hook is called when the world's paused flag becomes true, including
|
|
372
|
+
direct assignment. `context.paused` indicates whether new host updates are suspended;
|
|
373
|
+
`context.dispatchAllowed` and `prepareForSystemDispatch()` gate deferred dispatch during teardown.
|
|
374
|
+
Schedulers without these hooks retain immediate application and the existing clear path.
|
|
375
|
+
|
|
376
|
+
This scheduler provides component conflict ordering with the existing synchronous update API. It does
|
|
377
|
+
not implement fixed simulation ticks or impose a stable order on unrelated workers' callbacks.
|
|
378
|
+
World clocks continue advancing with host updates. Iterable slicing uses wall-clock budgets;
|
|
379
|
+
entity workers can opt into deterministic work batches instead.
|
|
380
|
+
For a fixed input-update sequence, declared conflicting operations and each system's deltas/logical
|
|
381
|
+
times remain ordered independently of worker completion order. Independent callbacks and timing events
|
|
382
|
+
can arrive in different orders; shared effects must be declared as conflicts or marked exclusive.
|
|
383
|
+
Repeatable simulations also need deterministic inputs and system code, and safe external world edits.
|
|
384
|
+
|
|
196
385
|
## Iterating entities
|
|
197
386
|
|
|
198
387
|
`world.entities` is a `Map` keyed by `eid`, not an array, and so is `entities` on `EntitySystem` and
|
|
@@ -273,7 +462,37 @@ class DamageSystem extends EntityWorkerSystem<Components, { health: Int32Array }
|
|
|
273
462
|
}
|
|
274
463
|
```
|
|
275
464
|
|
|
276
|
-
###
|
|
465
|
+
### Incremental entity work
|
|
466
|
+
|
|
467
|
+
Set `workBatchSize: 1024` in an `EntityWorkerSystem` configuration to process at most
|
|
468
|
+
1,024 snapshot positions per scheduled update. The size must be a positive safe integer.
|
|
469
|
+
Batch boundaries depend on entity count and insertion order, never elapsed wall time.
|
|
470
|
+
The last batch can be smaller, and removed entities consume their original positions without
|
|
471
|
+
being updated. New members wait for the next pass; current component bundles and named queries
|
|
472
|
+
are refreshed before every batch.
|
|
473
|
+
|
|
474
|
+
`deltaBetweenRuns` starts each full pass. All its batches share the original `elapsedTime`,
|
|
475
|
+
logical `gameTime`, and data from `addDataToWorld()`. Delta accumulated during the pass is
|
|
476
|
+
carried toward the next pass. `preRun` receives the full current entity list once at the
|
|
477
|
+
start, while optional `preBatch` receives only the selected entities before each batch.
|
|
478
|
+
Worker-local fields assigned to `world` survive until the pass ends. `queryChanged` and
|
|
479
|
+
`entityRemoved` still receive each batch's membership changes.
|
|
480
|
+
|
|
481
|
+
Each batch publishes callbacks, creations and component changes independently. Under
|
|
482
|
+
`ConflictScheduler`, its applied completion releases conflicting systems, allowing physics
|
|
483
|
+
or other frequent work between batches. Access declarations must cover both hooks and
|
|
484
|
+
entity updates. This opt-in permits other systems to change shared state between batches;
|
|
485
|
+
use `preBatch` to refresh live indexes and avoid retaining component block references across
|
|
486
|
+
batch boundaries. A failed preparation hook abandons the remaining pass. Clear/reload drops
|
|
487
|
+
the pending snapshot and worker-local pass state.
|
|
488
|
+
|
|
489
|
+
This also works with `DefaultScheduler` and the main-thread fallback. It does not apply to
|
|
490
|
+
`WorkerSystem`, whose function runs once over named queries. Full-query preparation and
|
|
491
|
+
query-delta handling remain unbounded, so a count limit alone cannot guarantee a frame deadline.
|
|
492
|
+
Paused conflict scheduling drains queued batches; unqueued pass work resumes with future
|
|
493
|
+
host updates. `waitForRunToComplete()` waits for the active batch and its publication.
|
|
494
|
+
|
|
495
|
+
### Update-function hooks: `init`, `queryChanged`, `preRun` and `preBatch`
|
|
277
496
|
|
|
278
497
|
`addDataToWorld` runs on the main thread and re-sends its data every run. When the data instead needs to
|
|
279
498
|
*live in the worker* — computed once, or too big to ship each frame — attach hooks to the update function
|
|
@@ -282,7 +501,11 @@ itself. Both are optional properties on the `EntityUpdateFunction`.
|
|
|
282
501
|
**`init`** runs on `system.finishLoading()` — once at startup (`world.init()` calls it), and again on each
|
|
283
502
|
`world.load()` so a reused world re-seeds its workers. It receives whatever the system's `getInitData()` returned
|
|
284
503
|
(structured-cloned across the boundary) and returns a `Partial<W>` that is merged onto `world` on every
|
|
285
|
-
subsequent run.
|
|
504
|
+
subsequent run. The same `load` message also carries the system's current query membership, so the worker has
|
|
505
|
+
every loaded entity cached before `finishLoading()` resolves. `init(world, data, entities, queries)` receives those
|
|
506
|
+
populated lists plus a base `world` with `gameTime`, `getString` and `heap` (no `elapsedTime` or callbacks: init
|
|
507
|
+
seeds worker state and must not modify entities); that initial membership never reaches `queryChanged`, so build any index over the loaded entities
|
|
508
|
+
in `init`. Use it for state that must be seeded from the main thread but then persist inside the worker
|
|
286
509
|
— a seeded RNG, a lookup table, a config object — without paying to re-send it each frame. Type the init
|
|
287
510
|
data with the `D` type parameter (the fourth on `EntityUpdateFunction` / `EntityWorkerSystem`) so `getInitData`
|
|
288
511
|
and the `init` hook agree on its shape; it defaults to `unknown` when a system has no init data:
|
|
@@ -300,7 +523,7 @@ const damageUpdate: EntityUpdateFunction<Components, { health: Int32Array }, Dam
|
|
|
300
523
|
|
|
301
524
|
// Runs once in the worker; its return is merged onto `world` before every run. `data` is typed as
|
|
302
525
|
// `DamageInitData | undefined` (undefined when the system supplies no getInitData).
|
|
303
|
-
damageUpdate.init = (data) => ({ damage: data?.baseDamage ?? 1 });
|
|
526
|
+
damageUpdate.init = (_world, data) => ({ damage: data?.baseDamage ?? 1 });
|
|
304
527
|
|
|
305
528
|
class DamageSystem extends EntityWorkerSystem<Components, { health: Int32Array }, DamageWorld, DamageInitData> {
|
|
306
529
|
constructor(world: BaseWorld<Components>) {
|
|
@@ -335,11 +558,13 @@ damageUpdate.preRun = (world, entities, queries, callbacks) => {
|
|
|
335
558
|
with that query's name and its `{ added, removed }` delta. Use it to keep a worker-local structure in sync with a
|
|
336
559
|
set of entities that rarely changes - a navigation grid of static walls, a lookup of static targets - instead of
|
|
337
560
|
rescanning the whole query every run. `added` can repeat an entity already in the query (its components changed
|
|
338
|
-
and its blocks were re-sent), so treat it as an upsert.
|
|
339
|
-
|
|
561
|
+
and its blocks were re-sent), so treat it as an upsert. Seed that structure in `init` from the loaded `queries`,
|
|
562
|
+
since entities present at load are never reported to `queryChanged`; a reload re-runs `init`, rebuilding it:
|
|
340
563
|
|
|
341
564
|
```ts
|
|
342
|
-
wallUpdate.init = () => ({
|
|
565
|
+
wallUpdate.init = (_world, _data, _entities, queries) => ({
|
|
566
|
+
walls: new Map((queries.walls ?? []).map(({ entityId, components }) => [entityId, rectOf(components.transform)])),
|
|
567
|
+
});
|
|
343
568
|
wallUpdate.queryChanged = (world, queryName, delta) => {
|
|
344
569
|
delta.removed.forEach(entityId => world.walls.delete(entityId));
|
|
345
570
|
delta.added.forEach(({ entityId, components }) => world.walls.set(entityId, rectOf(components.transform)));
|
|
@@ -390,9 +615,15 @@ import { createSystemWorker } from '@daneren2005/shared-memory-ecs/worker';
|
|
|
390
615
|
createSystemWorker(self, spawnerUpdate, registry);
|
|
391
616
|
```
|
|
392
617
|
|
|
393
|
-
The run function may carry the same `init` / `queryChanged` / `entityRemoved` hooks as an `updateFunction
|
|
618
|
+
The run function may carry the same `init` / `queryChanged` / `entityRemoved` hooks as an `updateFunction`; its
|
|
619
|
+
`init(world, data, queries)` omits the entity list, since there is no main query. Everything else —
|
|
394
620
|
callbacks, worker fallback, off-thread creation — behaves exactly as an `EntityWorkerSystem`.
|
|
395
621
|
|
|
622
|
+
The worker helpers report unhandled promise rejections on the main thread with the system name,
|
|
623
|
+
preserving the error message and stack. Rejections before the startup handshake completes reject
|
|
624
|
+
`system.init()`; later rejections are logged. Custom worker scopes can provide
|
|
625
|
+
`addEventListener('unhandledrejection', listener)` to enable this reporting.
|
|
626
|
+
|
|
396
627
|
### Importing in workers
|
|
397
628
|
|
|
398
629
|
Each worker entry file is bundled on its own, and a single-entry bundle cannot tree-shake this package's
|
|
@@ -527,15 +758,15 @@ createEntityWorker(world, { type: 'ship', x: 100, y: 150 }, callbacks);
|
|
|
527
758
|
counter (so it never collides with one the main thread or another worker hands out), and for each component
|
|
528
759
|
the merged config triggers, pushes a block into its pool and writes the values - all off-thread. With entity
|
|
529
760
|
classes registered, the worker also resolves the template's class and considers only its allowed components. It reports
|
|
530
|
-
back an id-plus-block-indexes descriptor; when
|
|
761
|
+
back an id-plus-block-indexes descriptor; when completion effects are applied the main thread *adopts* it, building the
|
|
531
762
|
always-present `entity` component there (interning `type` and `class` is a main-thread job), asking the factory
|
|
532
|
-
for the registered wrapper, and attaching each block the worker wrote - no block is copied or re-allocated.
|
|
533
|
-
|
|
534
|
-
exists on the following frame, so the system picks it up next run.
|
|
763
|
+
for the registered wrapper, and attaching each block the worker wrote - no block is copied or re-allocated. The new entity joins queries at adoption and is available to subsequent dispatches. The default scheduler
|
|
764
|
+
adopts immediately on completion; the conflict scheduler can defer adoption until publication is safe.
|
|
535
765
|
|
|
536
766
|
Two things make this work, both opt-in so only the workers that create entities pay for them:
|
|
537
767
|
|
|
538
|
-
- Register the system with `createsEntities: true
|
|
768
|
+
- Register the system with `createsEntities: true`, or a component list for parallel conflict scheduling.
|
|
769
|
+
Both forms ship the factory templates to its worker on load.
|
|
539
770
|
- In that system's worker entry, pass your component registry to `createEntitySystemWorker`, so the worker has
|
|
540
771
|
each component's block builder:
|
|
541
772
|
|
|
@@ -683,7 +914,9 @@ from `entity-removed`, which fires for every removal, reloads included).
|
|
|
683
914
|
|
|
684
915
|
A world is meant to be reused rather than rebuilt. `world.load(config)` swaps in a fresh scenario — it removes
|
|
685
916
|
the old entities, clears every system, and loads the new batch (deferred frees from the old contents drain over
|
|
686
|
-
the following updates, as above).
|
|
917
|
+
the following updates, as above). Loading itself is synchronous, but it returns a promise that resolves once every
|
|
918
|
+
system's `finishLoading()` (e.g. a worker re-seeding its `init` data) completes — `await world.load(config)` before
|
|
919
|
+
starting the game loop. `await world.clear()` instead tears the world all the way back down to an
|
|
687
920
|
empty, reusable state: it waits for every system's in-flight worker run to finish so nothing is still reading
|
|
688
921
|
the memory, frees the held blocks immediately, and resolves once the world is ready to load into again.
|
|
689
922
|
|
|
@@ -714,13 +947,21 @@ Every entry is an `{ avg, min, max, samples }` over the window just closed:
|
|
|
714
947
|
- `stats.update` - one whole `world.update` call on the thread the world lives on.
|
|
715
948
|
- `stats.systems[]` - per system, in run order: `run` is the run itself on its worker, as the worker measured
|
|
716
949
|
it, and `events` is what handling that run's results (the events it reported onto entities, the entities it
|
|
717
|
-
asked to be created) cost back on the calling thread.
|
|
718
|
-
|
|
719
|
-
|
|
950
|
+
asked to be created) cost back on the calling thread. Both worker and main-thread fallback runs report
|
|
951
|
+
these timings. For `workBatchSize`, one sample sums all batches in a complete pass: batches taking 3, 5,
|
|
952
|
+
and 4 ms report a 12 ms run with `samples: 1`. Event handling is summed across the pass too; gaps between
|
|
953
|
+
batches are excluded. Failed preparation closes the attempt with the time spent so far.
|
|
720
954
|
- `stats.events` - every system's event handling added together. Workers finish on their own schedule rather
|
|
721
955
|
than on a frame boundary, so there is no per-frame combined sample to take; these are the per-system figures
|
|
722
956
|
summed at the end of the window, giving what a run of every system costs the main thread between them.
|
|
723
957
|
|
|
958
|
+
To coordinate reporting with an external clock, construct with `{ autoUpdate: false }` and call
|
|
959
|
+
`timing.flush()` at each reporting boundary. It publishes and returns a snapshot of all collected update,
|
|
960
|
+
worker, and event samples, then starts a fresh window. This also works while paused: windows without work
|
|
961
|
+
report zeroes. A batched pass retains its partial totals across reporting boundaries and contributes
|
|
962
|
+
one sample when its final batch arrives (event timing completes when its results are handled). Clear/reload
|
|
963
|
+
drops unfinished passes, and the next pass starts fresh totals. Automatic reporting remains enabled by default.
|
|
964
|
+
|
|
724
965
|
`getSystemStats(name)` pulls one system out of the latest snapshot, `reset()` throws away everything collected
|
|
725
966
|
so far (worth doing after loading a new scene, when the samples either side are not comparable), and
|
|
726
967
|
`destroy()` unhooks it from the world.
|