@vworlds/vecs 1.0.53 → 1.0.55
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 +9 -0
- package/dist/entity/entity.components.js +4 -2
- package/dist/entity/entity.components.js.map +1 -1
- package/dist/entity/entity.lifecycle.d.ts +2 -0
- package/dist/entity/entity.lifecycle.js +9 -4
- package/dist/entity/entity.lifecycle.js.map +1 -1
- package/dist/geometry.d.ts +5 -0
- package/dist/geometry.js +2 -0
- package/dist/geometry.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/system.d.ts +10 -0
- package/dist/system.js +19 -2
- package/dist/system.js.map +1 -1
- package/dist/timer.d.ts +15 -1
- package/dist/timer.js +24 -4
- package/dist/timer.js.map +1 -1
- package/dist/world/destroy-order.d.ts +8 -0
- package/dist/world/destroy-order.js +60 -0
- package/dist/world/destroy-order.js.map +1 -0
- package/dist/world/stats.d.ts +51 -0
- package/dist/world/stats.js +88 -0
- package/dist/world/stats.js.map +1 -0
- package/dist/world/world.base.js +2 -0
- package/dist/world/world.base.js.map +1 -1
- package/dist/world/world.components.d.ts +2 -0
- package/dist/world/world.components.js +4 -0
- package/dist/world/world.components.js.map +1 -1
- package/dist/world/world.disposal.d.ts +34 -36
- package/dist/world/world.disposal.js +89 -94
- package/dist/world/world.disposal.js.map +1 -1
- package/dist/world/world.entities.d.ts +21 -1
- package/dist/world/world.entities.js +54 -7
- package/dist/world/world.entities.js.map +1 -1
- package/dist/world/world.js +17 -11
- package/dist/world/world.js.map +1 -1
- package/dist/world/world.modules.d.ts +8 -0
- package/dist/world/world.modules.js +22 -1
- package/dist/world/world.modules.js.map +1 -1
- package/dist/world/world.pipeline.d.ts +15 -1
- package/dist/world/world.pipeline.js +40 -4
- package/dist/world/world.pipeline.js.map +1 -1
- package/docs/README.md +21 -19
- package/docs/components.md +9 -0
- package/docs/concepts.md +4 -0
- package/docs/design-guide.md +7 -3
- package/docs/entities.md +36 -25
- package/docs/glossary.md +3 -3
- package/docs/modules.md +62 -0
- package/docs/systems.md +108 -2
- package/package.json +1 -1
- package/tsconfig.types.json +6 -1
package/docs/modules.md
CHANGED
|
@@ -34,6 +34,10 @@ world.module(PoseSyncModule);
|
|
|
34
34
|
module on first call — drawing its entity id from the `module` pool (900–999 by default, or pass
|
|
35
35
|
an explicit `eid`) — and invokes `init(config)` exactly once. Subsequent calls return the existing
|
|
36
36
|
module instance; passing a config again after the first registration throws.
|
|
37
|
+
Install new modules outside `progress()` and `defer`; installation also throws during a command
|
|
38
|
+
drain, before registering anything. Looking up an already installed module works in any of those
|
|
39
|
+
contexts. Lazy loading from a system or hook is the application's job: schedule it after
|
|
40
|
+
`progress()` returns.
|
|
37
41
|
|
|
38
42
|
## Configuration
|
|
39
43
|
|
|
@@ -84,6 +88,63 @@ Guidelines:
|
|
|
84
88
|
placement, component registration. Keep that out of application code.
|
|
85
89
|
- **One module per cohesive thing.** Don't pile unrelated features into one `init`.
|
|
86
90
|
|
|
91
|
+
## Scopes and module provenance
|
|
92
|
+
|
|
93
|
+
`world.scope(parent, fn)` assigns `ChildOf parent` to entities created **synchronously in its body**:
|
|
94
|
+
plain entities, components, modules, query/system backing entities, and phases.
|
|
95
|
+
Nested scopes restore the caller's parent in `finally`, even when the callback throws. Enter scopes
|
|
96
|
+
outside deferred mode: `world.scope()` throws inside `progress()`, a `defer` block, or a command
|
|
97
|
+
drain, without running the callback. Scoped creations use `.childOf(parent)` before their creation
|
|
98
|
+
notification. Existing name/id/class lookups do not reparent anything. An explicit
|
|
99
|
+
`.childOf(other)` afterwards replaces the scope parent, just as any other exclusive `ChildOf` edge.
|
|
100
|
+
The parent must be live and belong to the same world. Async continuations are outside the scope.
|
|
101
|
+
|
|
102
|
+
Entities created by callbacks that vecs runs deferred inside the body are **not parented**. This
|
|
103
|
+
includes `query.forEach(cb)`, a query's `enter` backfill when built, and an explicit `world.defer`
|
|
104
|
+
inside a module's `init`. Callback-created entities are run-time data: the same callback outside the
|
|
105
|
+
scope would not parent them either. For example, a module's `enter` or `forEach` callback may create
|
|
106
|
+
one health-bar entity per existing ship. Those health bars remain roots; an entity created directly
|
|
107
|
+
in `init` is a child of the module.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { Entity, Module, World } from "@vworlds/vecs";
|
|
111
|
+
|
|
112
|
+
class Position {}
|
|
113
|
+
class Feature extends Module {
|
|
114
|
+
public init(): void {
|
|
115
|
+
this.world.component(Position);
|
|
116
|
+
this.world.entity("camera");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const world = new World();
|
|
120
|
+
const feature = world.module(Feature);
|
|
121
|
+
world.component(Position).parent === feature; // true
|
|
122
|
+
world.entity("camera").parent === feature; // true
|
|
123
|
+
|
|
124
|
+
function owningModule(entity: Entity): Module | undefined {
|
|
125
|
+
const visited = new Set<Entity>();
|
|
126
|
+
for (let parent = entity.parent; parent && !visited.has(parent); parent = parent.parent) {
|
|
127
|
+
if (parent instanceof Module) return parent;
|
|
128
|
+
visited.add(parent);
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
owningModule(world.component(Position)); // feature
|
|
133
|
+
world.scope(feature, () => world.entity("another child"));
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`world.module()` runs a new module's `init` inside `world.scope(moduleEntity, ...)`. Nested modules
|
|
137
|
+
are children of the enclosing scope; their own creations belong to the nested module. Provenance is
|
|
138
|
+
the **nearest module ancestor**, not necessarily the direct parent: a `OneOf` value may be parented
|
|
139
|
+
to its relationship entity, which is parented to the module. This is a real ownership hierarchy:
|
|
140
|
+
ordinary parent destruction cascades through its children, and subtree traversal follows it.
|
|
141
|
+
|
|
142
|
+
Vecs' own built-in modules installed by the `World` constructor are **not scoped**; their entities
|
|
143
|
+
remain roots. Provenance covers modules installed after the world is constructed, including physics,
|
|
144
|
+
network, Phaser and application modules. Registration outside a scope leaves a root. Idempotent
|
|
145
|
+
re-registration keeps the existing parent: there is no list of duplicate registrars and no record of
|
|
146
|
+
which module added a trait edge. Explicit reparenting changes ownership, not a historical audit.
|
|
147
|
+
|
|
87
148
|
## Built-in modules
|
|
88
149
|
|
|
89
150
|
Every `World` constructor installs these (`lib/vecs/src/world/world.ts`); you interact with their
|
|
@@ -107,4 +168,5 @@ All are exported, so a custom world setup can reuse them.
|
|
|
107
168
|
| `init(config)` | One-time setup hook; called by the world on first registration. |
|
|
108
169
|
| `world.module(M)` / `world.module(M, config)` / `world.module(M, config, eid)` | Register (first call) or retrieve (later calls) the module. Config after first registration throws. |
|
|
109
170
|
| `this.world` | Inside `init`: the owning world (a module is an entity). |
|
|
171
|
+
| `world.scope(parent, fn)` | Parent all synchronous creations in `fn`; existing lookups are unchanged. |
|
|
110
172
|
| `ModuleConfig<T>` | Utility type extracting a module's config type. |
|
package/docs/systems.md
CHANGED
|
@@ -26,7 +26,48 @@ world
|
|
|
26
26
|
|
|
27
27
|
Newly created systems enter the pipeline the next time `world.progress` runs. Configuration must
|
|
28
28
|
happen before the system is built (the first `progress`/`flush` after creation); builder calls
|
|
29
|
-
after that throw.
|
|
29
|
+
after that throw. Reading `world.pipeline` between frames also builds pending queries.
|
|
30
|
+
|
|
31
|
+
## Measuring systems
|
|
32
|
+
|
|
33
|
+
Enable opt-in synchronous timing for benchmarks, cadence guards, or live diagnostics:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
world.stats.enableSystems();
|
|
37
|
+
world.progress(1000, 16);
|
|
38
|
+
for (const { system, runs, totalMs, maxMs } of world.stats.systems()) {
|
|
39
|
+
console.log(system.name, runs, totalMs, maxMs);
|
|
40
|
+
}
|
|
41
|
+
world.stats.resetSystems(); // clear system and extension-operation totals
|
|
42
|
+
world.stats.disableSystems(); // stop measuring; retain results until reset
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Timing is off by default. `progress()` chooses the plain or instrumented loop once per tick;
|
|
46
|
+
the plain loop has no per-system timing branch, counter, or clock read. The instrumented loop
|
|
47
|
+
uses one `performance.now()` pair per pipeline entry and reuses first-seen slots.
|
|
48
|
+
|
|
49
|
+
`systems()` returns independent snapshots in current pipeline order, including systems with zero
|
|
50
|
+
runs. Each row holds the original `System`, not copied identity or cadence metadata. Newly
|
|
51
|
+
registered systems appear after the next pipeline refresh in `progress()`. Removed systems are
|
|
52
|
+
absent. Reading does not reset the totals.
|
|
53
|
+
|
|
54
|
+
A run means execution admitted by the system's enabled and tick-source gates, not a callback or
|
|
55
|
+
entity count: an idle reactive system or an empty `each` query still counts when due. Disabled
|
|
56
|
+
and cadence-skipped entries contribute neither runs nor time. For an admitted execution, timing
|
|
57
|
+
includes inbox processing, callbacks, and **the flush after that system**, including command
|
|
58
|
+
routing and hooks. Initial frame preparation and final frame closure are not attributed. An
|
|
59
|
+
execution that throws before completing its flush does not contribute a completed run.
|
|
60
|
+
|
|
61
|
+
Extensions expose breakdown rows through `world.stats.operations()`, under the same switch and
|
|
62
|
+
reset. See [ServerWorld replication timing](../../vecs-server/docs/replication-timing.md).
|
|
63
|
+
Reading either table allocates snapshots; normal measured ticks reuse slots. Reset keeps slots
|
|
64
|
+
and clears totals, including systems temporarily absent from the pipeline.
|
|
65
|
+
|
|
66
|
+
When implementing a world extension, choose the measured path once per frame with
|
|
67
|
+
`stats.systemsEnabled`, then call `stats.recordOperation(name, elapsedMs)` once per completed
|
|
68
|
+
operation. It aggregates a finite, non-negative duration into a reusable named slot. This method
|
|
69
|
+
does not read a clock or recheck the switch; it lets an already-measured frame complete even if
|
|
70
|
+
a callback disables future timing. Invalid durations throw `RangeError`.
|
|
30
71
|
|
|
31
72
|
## Membership: `with` / `without`
|
|
32
73
|
|
|
@@ -389,7 +430,10 @@ world.system("Log").tickSource(clock).run(log); // driven by a shared ITickSourc
|
|
|
389
430
|
```
|
|
390
431
|
|
|
391
432
|
- `.interval(seconds)` replaces the cadence with an `IntervalTickSource`. Note the unit: seconds,
|
|
392
|
-
unlike the millisecond deltas of `progress`.
|
|
433
|
+
unlike the millisecond deltas of `progress`. Interval boundaries tolerate floating-point
|
|
434
|
+
residue so whole-frame intervals do not fire one frame late (see `IntervalTickSource`'s
|
|
435
|
+
TSDoc for the tolerance). Each source fires at most once per frame, even with a large delta;
|
|
436
|
+
positive residual time is preserved, and a tolerated negative remainder is clamped to zero.
|
|
393
437
|
- `.rate(n)` divides the current cadence source by `n` (for an unconfigured system: every `n`
|
|
394
438
|
frames). `.rate(n, source)` divides an explicit upstream source.
|
|
395
439
|
- `.tickSource(source)` mirrors another source directly — an `IntervalTickSource`,
|
|
@@ -420,6 +464,66 @@ ai.destroy(); // permanent: disables, then unregisters the backing entity
|
|
|
420
464
|
While disabled, query membership is still maintained, so the tracked set is correct on resume.
|
|
421
465
|
`destroy` fires no `exit` callbacks. Both `disable` and `enable` are idempotent and chainable.
|
|
422
466
|
|
|
467
|
+
## Introspection
|
|
468
|
+
|
|
469
|
+
Read `world.pipeline` after finishing system configuration to inspect the **existing flat execution
|
|
470
|
+
pipeline**, not a separately maintained inventory. It returns `readonly System[]`, including disabled
|
|
471
|
+
systems, in the order selected by the pipeline query (including a custom `setPipeline`). Between
|
|
472
|
+
frames it builds pending queries and refreshes only when the pipeline query changed. It does not
|
|
473
|
+
execute systems or drain deferred commands. During a frame it returns that frame's pipeline; queued
|
|
474
|
+
changes become visible after their normal flush. Re-read the getter after changes: an old array is
|
|
475
|
+
not a live subscription. Read-only means the TypeScript array API; do not cast away the type to mutate it.
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
import {
|
|
479
|
+
ALWAYS_TICK_SOURCE,
|
|
480
|
+
DependsOn,
|
|
481
|
+
IntervalTickSource,
|
|
482
|
+
RateTickSource,
|
|
483
|
+
System,
|
|
484
|
+
World,
|
|
485
|
+
type ITickSource,
|
|
486
|
+
} from "@vworlds/vecs";
|
|
487
|
+
|
|
488
|
+
function describeCadence(source: ITickSource): string {
|
|
489
|
+
if (source === ALWAYS_TICK_SOURCE) return "every tick";
|
|
490
|
+
if (source instanceof IntervalTickSource) return `every ${source.seconds}s`;
|
|
491
|
+
if (source instanceof RateTickSource) {
|
|
492
|
+
return `every ${source.rate} ticks of ${
|
|
493
|
+
source.source ? describeCadence(source.source) : "world frames"
|
|
494
|
+
}`;
|
|
495
|
+
}
|
|
496
|
+
if (source instanceof System) return `system ${source.name}`;
|
|
497
|
+
return "custom clock";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const world = new World();
|
|
501
|
+
world
|
|
502
|
+
.system("AI")
|
|
503
|
+
.interval(0.5)
|
|
504
|
+
.run(() => {});
|
|
505
|
+
for (const [order, system] of world.pipeline.entries()) {
|
|
506
|
+
console.log({
|
|
507
|
+
order,
|
|
508
|
+
name: system.name,
|
|
509
|
+
phase: system.entity.target(DependsOn)?.name,
|
|
510
|
+
cadence: describeCadence(system.cadence),
|
|
511
|
+
enabled: system.enabled,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
`system.cadence` exposes the current source; the existing `.tickSource(source)` method configures
|
|
517
|
+
it. Compare cadence by identity with the exported `ALWAYS_TICK_SOURCE` for the default every-tick
|
|
518
|
+
case. `IntervalTickSource.seconds`, `RateTickSource.rate`, and `RateTickSource.source` are getters;
|
|
519
|
+
an unconfigured standalone rate source has no upstream source and counts world frames. Replacing
|
|
520
|
+
cadence changes the getter immediately. A source's existing `start`/`stop` controls still work.
|
|
521
|
+
|
|
522
|
+
Name and phase need no new API: `system.name` and `system.entity.target(DependsOn)` already expose
|
|
523
|
+
them. Systems are entities carrying `System`, so a query with `System` can inventory them independently
|
|
524
|
+
of pipeline membership; retrieve the instance with `entity.get(System)`. The existing `toString()`
|
|
525
|
+
display is unchanged; introspection does not add structured query terms or `world.systems()`.
|
|
526
|
+
|
|
423
527
|
## Reference
|
|
424
528
|
|
|
425
529
|
| Method | Description |
|
|
@@ -438,6 +542,8 @@ While disabled, query membership is still maintained, so the tracked set is corr
|
|
|
438
542
|
| `disable()` / `enable()` | Pause / resume processing. Idempotent. |
|
|
439
543
|
| `destroy()` | Permanently remove the system (no `exit` callbacks). |
|
|
440
544
|
| `didTick` / `lastFireDelta` | This frame's cadence result; ms accumulated into the last fire. |
|
|
545
|
+
| `cadence` / `enabled` | Read current cadence source and enabled state; see [Introspection](#introspection). |
|
|
546
|
+
| `world.pipeline` | Read systems in pipeline execution order; see [Introspection](#introspection). |
|
|
441
547
|
| `count` / `has(e)` / `belongs(e)` / iteration / `forEach` | Shared query surface; see [Queries](./queries-and-filters.md#reading-a-query). |
|
|
442
548
|
|
|
443
549
|
`groupBy` is not available on systems — use `world.query(...).groupBy(...)`.
|
package/package.json
CHANGED