@vworlds/vecs 1.0.25 → 1.0.26
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 +29 -778
- package/dist/component.d.ts +1 -1
- package/dist/component.js +1 -1
- package/dist/component.js.map +1 -1
- package/dist/component_meta.d.ts +5 -1
- package/dist/component_meta.js +10 -0
- package/dist/component_meta.js.map +1 -1
- package/dist/dsl.d.ts +2 -2
- package/dist/entity/entity.base.d.ts +2 -18
- package/dist/entity/entity.base.js +2 -20
- package/dist/entity/entity.base.js.map +1 -1
- package/dist/entity/entity.components.d.ts +1 -0
- package/dist/entity/entity.components.js +50 -0
- package/dist/entity/entity.components.js.map +1 -1
- package/dist/entity/entity.lifecycle.d.ts +3 -3
- package/dist/entity/entity.lifecycle.js +6 -9
- package/dist/entity/entity.lifecycle.js.map +1 -1
- package/dist/entity/entity.relationships.js +2 -2
- package/dist/entity/entity.relationships.js.map +1 -1
- package/dist/filter.d.ts +1 -0
- package/dist/filter.js +3 -2
- package/dist/filter.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/inject.d.ts +3 -2
- package/dist/inject.js.map +1 -1
- package/dist/modules/implements.d.ts +14 -0
- package/dist/modules/implements.js +98 -0
- package/dist/modules/implements.js.map +1 -0
- package/dist/package.json +1 -4
- package/dist/query/callbacks.d.ts +6 -2
- package/dist/query/callbacks.js +5 -2
- package/dist/query/callbacks.js.map +1 -1
- package/dist/query/query.d.ts +14 -1
- package/dist/query/query.js +26 -15
- package/dist/query/query.js.map +1 -1
- package/dist/system.d.ts +5 -4
- package/dist/system.js +17 -6
- package/dist/system.js.map +1 -1
- package/dist/util/array_map.d.ts +70 -12
- package/dist/util/array_map.js +113 -26
- package/dist/util/array_map.js.map +1 -1
- package/dist/util/bitset.js +0 -17
- package/dist/util/bitset.js.map +1 -1
- package/dist/util/events.d.ts +42 -12
- package/dist/util/events.js +94 -43
- package/dist/util/events.js.map +1 -1
- package/dist/util/ordered_set.js +43 -19
- package/dist/util/ordered_set.js.map +1 -1
- package/dist/world/world.deferred.js +2 -0
- package/dist/world/world.deferred.js.map +1 -1
- package/dist/world/world.entities.d.ts +8 -1
- package/dist/world/world.entities.js +25 -6
- package/dist/world/world.entities.js.map +1 -1
- package/dist/world/world.js +8 -1
- package/dist/world/world.js.map +1 -1
- package/dist/world/world.queries.js +6 -1
- package/dist/world/world.queries.js.map +1 -1
- package/dist/world/world.storage.d.ts +2 -2
- package/dist/world/world.storage.js +6 -3
- package/dist/world/world.storage.js.map +1 -1
- package/docs/README.md +50 -0
- package/docs/components.md +267 -0
- package/docs/concepts.md +86 -0
- package/docs/design-guide.md +506 -0
- package/docs/entities.md +175 -0
- package/docs/execution-model.md +173 -0
- package/docs/getting-started.md +215 -0
- package/docs/glossary.md +113 -0
- package/docs/modules.md +108 -0
- package/docs/queries-and-filters.md +187 -0
- package/docs/relationships.md +148 -0
- package/docs/systems.md +311 -0
- package/docs/utilities.md +139 -0
- package/package.json +1 -4
package/docs/systems.md
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# Systems
|
|
2
|
+
|
|
3
|
+
Build systems: declare membership, react to entities entering, changing, and leaving, run per-tick
|
|
4
|
+
logic, control cadence, and inject components into callbacks.
|
|
5
|
+
|
|
6
|
+
## What a system is
|
|
7
|
+
|
|
8
|
+
A `System` (`lib/vecs/src/system.ts`) is a reactive processor over a filtered subset of world
|
|
9
|
+
entities. It extends [`Query`](./queries-and-filters.md), so membership, `enter`/`exit`/`update`
|
|
10
|
+
callbacks, sorting, and tracking are shared — and adds pipeline integration: a phase slot, an
|
|
11
|
+
event **inbox** replayed once per tick, plus `run` and `each` callbacks.
|
|
12
|
+
|
|
13
|
+
Create systems through the world and configure them with a fluent builder (every method returns
|
|
14
|
+
`this`):
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
world
|
|
18
|
+
.system("Render")
|
|
19
|
+
.with(Sprite, Position)
|
|
20
|
+
.enter([Sprite, Position], (e, [sprite, pos]) => sprite.show(pos))
|
|
21
|
+
.update(Position, [Sprite], (e, pos, [sprite]) => sprite.moveTo(pos.x, pos.y))
|
|
22
|
+
.exit([Sprite], (e, [sprite]) => sprite.hide());
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Newly created systems enter the pipeline the next time `world.progress` runs. Configuration must
|
|
26
|
+
happen before the system is built (the first `progress`/`flush` after creation); builder calls
|
|
27
|
+
after that throw.
|
|
28
|
+
|
|
29
|
+
## Membership: `with` / `without`
|
|
30
|
+
|
|
31
|
+
`.with(...)` declares which entities the system tracks. It accepts component classes, arrays, and
|
|
32
|
+
the full query DSL (operator semantics in
|
|
33
|
+
[Queries and filters](./queries-and-filters.md#the-query-dsl)):
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
.with(Position, Velocity) // all listed components
|
|
37
|
+
.with([Position, Velocity]) // array shorthand
|
|
38
|
+
.with({ all: [Position, { any: [Sprite, Container] }] }) // compound DSL
|
|
39
|
+
.with({ not: Invisible })
|
|
40
|
+
.without(Invisible) // shorthand for .with({ not: Invisible })
|
|
41
|
+
.with({ target: [FriendOf, Position] }) // relationship target has Position
|
|
42
|
+
.with({ parent: Body }) // ChildOf target has Body
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Multiple `with` calls merge with `all`. The `source` / `children` operators are non-reactive and
|
|
46
|
+
rejected by systems — they are filter-only (see
|
|
47
|
+
[Queries and filters](./queries-and-filters.md#filters-one-shot-scans)).
|
|
48
|
+
|
|
49
|
+
**Type inference.** Components that `with()` can see statically (plain classes, arrays, `all`,
|
|
50
|
+
`only`) become non-nullable in `orderBy`, `each`, `forEach`, and `update` injection tuples. For
|
|
51
|
+
DSL shapes the type system can't introspect (`any`, `not`, `test`, …), supply a `hint`:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
.with({ with: { any: [Position, Velocity] }, hint: [Position] })
|
|
55
|
+
.each([Position], (e, [pos]) => pos.x);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`hint` is type-level only — it is not validated at runtime.
|
|
59
|
+
|
|
60
|
+
## Phase placement
|
|
61
|
+
|
|
62
|
+
Systems default to the `vecs::OnUpdate` phase. `.phase(anchor)` moves the system after another
|
|
63
|
+
pipeline anchor, by name, constant, or entity:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
.phase(ON_LOAD) // a built-in phase constant
|
|
67
|
+
.phase("physics-step") // a custom anchor, created and tagged Phase if missing
|
|
68
|
+
.phase(world.entity("anchor")) // an existing entity
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A system lives in exactly one phase; calling `.phase` again replaces the previous placement. Phase
|
|
72
|
+
taxonomy, ordering rules, and custom phases are covered in
|
|
73
|
+
[Execution model](./execution-model.md#phases-the-pipeline-spine).
|
|
74
|
+
|
|
75
|
+
## Reactive callbacks: `enter`, `update`, `exit`
|
|
76
|
+
|
|
77
|
+
Mutation events are routed into the system's inbox as the world applies commands; the inbox is
|
|
78
|
+
replayed in arrival order at the top of the system's next run, inside a deferred scope.
|
|
79
|
+
|
|
80
|
+
### `.enter(callback)` / `.enter(inject, callback)`
|
|
81
|
+
|
|
82
|
+
Fires once when an entity starts matching the system:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
.enter((e) => { ... })
|
|
86
|
+
.enter([Position, Sprite], (e, [pos, sprite]) => sprite.setPosition(pos.x, pos.y))
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `.update(ComponentClass, callback)` / `.update(ComponentClass, inject, callback)`
|
|
90
|
+
|
|
91
|
+
Fires when the watched component is set or marked modified on a tracked entity — and **on entry**
|
|
92
|
+
for each watched component the entity already has. `update(ComponentClass, ...)` also adds
|
|
93
|
+
`ComponentClass` to the membership predicate, so the system only matches entities that carry the
|
|
94
|
+
watched component. The callback receives the entity first because component instances carry no owner
|
|
95
|
+
reference:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
.update(Position, (entity, pos) => renderer.setPosition(entity.eid, pos.x, pos.y))
|
|
99
|
+
.update(Position, [Sprite], (entity, pos, [sprite]) => sprite.setPosition(pos.x, pos.y))
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Because `update` fires on entry, a separate `enter` that applies the same value is redundant —
|
|
103
|
+
lean on `update` alone for "apply current value when the entity appears, and again on change".
|
|
104
|
+
|
|
105
|
+
Use `.update(C, ...)` by itself when `C` is the membership shape and watched value. Use
|
|
106
|
+
`.with(A).update(C, ...)` when the callback should run only for entities carrying both `A` and `C`.
|
|
107
|
+
|
|
108
|
+
A query or system can register only one `update(C, ...)` callback for each component. A duplicate
|
|
109
|
+
registration throws instead of replacing the existing callback.
|
|
110
|
+
|
|
111
|
+
### `.exit(callback)` / `.exit(inject, callback)`
|
|
112
|
+
|
|
113
|
+
Fires when an entity stops matching (component removed, or entity destroyed). Directly injected
|
|
114
|
+
components are read from a **snapshot captured at exit time**, so they are still resolvable even
|
|
115
|
+
though they are being removed; `target`-injected components resolve live at callback time and may
|
|
116
|
+
be `undefined`:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
.exit([Sprite], (e, [sprite]) => sprite.destroy());
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Per-tick callbacks: `each` and `run`
|
|
123
|
+
|
|
124
|
+
### `.each(components, callback)`
|
|
125
|
+
|
|
126
|
+
Fires every tick the system runs, once per tracked entity, regardless of change. Use it only for
|
|
127
|
+
genuinely per-frame sweeps — when reacting to change suffices, prefer `update` (see
|
|
128
|
+
[Design guide](./design-guide.md#each-vs-reactive--dont-scan-when-you-can-react)):
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
.with(Position, Velocity)
|
|
132
|
+
.each([Position, Velocity], (e, [pos, vel]) => {
|
|
133
|
+
pos.x += vel.vx;
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`each` implies `.track()`. Only one `each` per system — a second call throws. The resolved tuple
|
|
138
|
+
array is reused across entities; read it inside the callback, don't retain it.
|
|
139
|
+
|
|
140
|
+
### `.run(callback)`
|
|
141
|
+
|
|
142
|
+
Fires once per tick when the system's phase runs, regardless of entity membership. Use it for
|
|
143
|
+
polling, network I/O, stepping an embedded simulation:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
.run((now, delta) => {
|
|
147
|
+
sendNetworkPacket(now);
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`now` is the absolute timestamp passed to `progress`; `delta` is milliseconds since the previous
|
|
152
|
+
tick — or, when the system has a cadence source, the milliseconds accumulated since this system
|
|
153
|
+
last fired.
|
|
154
|
+
|
|
155
|
+
Within one system run the order is: inbox replay (`enter`/`exit`/`update` events), then `run`,
|
|
156
|
+
then `each`.
|
|
157
|
+
|
|
158
|
+
## Component injection
|
|
159
|
+
|
|
160
|
+
`enter`, `exit`, `update`, `each`, `orderBy`, and `forEach` accept an injection list of component
|
|
161
|
+
classes, resolved per entity and passed as a typed tuple:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
.each([Position, Sprite], (e, [pos, sprite]) => { ... })
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Nullability follows membership: components guaranteed by `with` (or `hint`) are non-nullable;
|
|
168
|
+
anything else resolves as `T | undefined` — guard it. (`enter`/`exit` injection requires the
|
|
169
|
+
direct components to be present and throws otherwise; list only components your predicate
|
|
170
|
+
guarantees.)
|
|
171
|
+
|
|
172
|
+
### Relationship injection: `target` and `down`
|
|
173
|
+
|
|
174
|
+
`{ target: [Rel, [C1, C2]] }` follows the entity's relationship to its target and flattens the
|
|
175
|
+
target's components into the tuple. Target-side slots are `undefined` when the relationship or the
|
|
176
|
+
component is absent:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
.forEach([Body, { target: [FriendOf, [Position, Velocity]] }], (e, [body, pos, vel]) => {
|
|
180
|
+
// body is from e; pos and vel are from e.parent(FriendOf).
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
With two or more components in the inner tuple, TypeScript widens the slot types to a union;
|
|
185
|
+
mark the inner tuple `as const` (`{ target: [FriendOf, [Position, Velocity] as const] }`) to get
|
|
186
|
+
precise per-slot types. Single-component tuples infer precisely as-is.
|
|
187
|
+
|
|
188
|
+
`{ down: [Rel, [C1]] }` fans out: the callback fires once per child targeting the visited entity
|
|
189
|
+
through `Rel`, with that child's components filling the slots (nullable). `down` is allowed only
|
|
190
|
+
in `each` and `forEach`, at most once per tuple, and entities with zero children produce no calls.
|
|
191
|
+
`down` iterates **all** children through the relationship, independent of any membership filter:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
.with(Body)
|
|
195
|
+
.each([Body, { down: [ChildOf, [Position]] }], (parent, [body, childPos]) => {
|
|
196
|
+
// One call per ChildOf child; childPos is undefined when that child lacks Position.
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### The `Iter` cursor
|
|
201
|
+
|
|
202
|
+
Pass `Iter` (from `@vworlds/vecs`) as the first argument to `each`, `forEach`, `enter`, `exit`,
|
|
203
|
+
`update`, or `orderBy` to receive a reusable cursor instead of the bare entity:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { Iter } from "@vworlds/vecs";
|
|
207
|
+
|
|
208
|
+
system.each(Iter, [Body, { target: [ChildOf, [Position]] }], (it, [body, pos]) => {
|
|
209
|
+
it.entity; // the visited entity
|
|
210
|
+
it.src[0]; // entity body was read from (the visited entity)
|
|
211
|
+
it.src[1]; // entity pos was read from (the ChildOf target), or undefined
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`it.src` holds the source entity per tuple slot: the visited entity for direct components, the
|
|
216
|
+
relationship target for `target` slots, the specific child for `down` slots. When `Iter` is not
|
|
217
|
+
requested, the non-cursor path runs with zero per-entity overhead — dispatch happens once at setup
|
|
218
|
+
time. `each`/`forEach`/`update`/`orderBy` reuse cursor and tuple instances across the pass, so
|
|
219
|
+
snapshot what you need before triggering further mutations from inside a callback. Passing
|
|
220
|
+
`Entity` as the first argument is also accepted and means the default entity-first signature.
|
|
221
|
+
|
|
222
|
+
## Ordering and tracking
|
|
223
|
+
|
|
224
|
+
### `.orderBy(components, compare)`
|
|
225
|
+
|
|
226
|
+
Keep matched entities in a custom order. Iteration, `forEach`, and `each` then walk entities in
|
|
227
|
+
sorted order. Implies tracking:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
world
|
|
231
|
+
.system("Render")
|
|
232
|
+
.with(Position, Sprite)
|
|
233
|
+
.orderBy([Position], (_ea, [a], _eb, [b]) => a.y - b.y)
|
|
234
|
+
.each([Position, Sprite], (e, [pos, sprite]) => sprite.draw(pos.x, pos.y));
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Ties break by entity id, so the order is deterministic.
|
|
238
|
+
|
|
239
|
+
The order is maintained reactively. When data for a component in the injection tuple changes via
|
|
240
|
+
`set` or `modified`, the system refreshes the sort before the next ordered read. Keep comparators
|
|
241
|
+
dependent only on those injected components; unrelated component data is not watched for ordering.
|
|
242
|
+
|
|
243
|
+
### `.track()`
|
|
244
|
+
|
|
245
|
+
Enable membership tracking without an `each`: exposes `count`, `has(e)`, iteration, and `forEach`.
|
|
246
|
+
`each` and `orderBy` imply it. Tracked systems backfill already-matching entities when built.
|
|
247
|
+
|
|
248
|
+
## Cadence: `interval`, `rate`, and tick sources
|
|
249
|
+
|
|
250
|
+
By default a system fires on every frame of its phase. Three builders change that
|
|
251
|
+
(`lib/vecs/src/timer.ts`):
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
world.system("AI").interval(0.5).run(tickAI); // at most every 0.5 s (seconds!)
|
|
255
|
+
world.system("Send").rate(2).run(flush); // every 2nd tick of the current source
|
|
256
|
+
world.system("Log").tickSource(clock).run(log); // driven by a shared ITickSource
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
- `.interval(seconds)` replaces the cadence with an `IntervalTickSource`. Note the unit: seconds,
|
|
260
|
+
unlike the millisecond deltas of `progress`.
|
|
261
|
+
- `.rate(n)` divides the current cadence source by `n` (for an unconfigured system: every `n`
|
|
262
|
+
frames). `.rate(n, source)` divides an explicit upstream source.
|
|
263
|
+
- `.tickSource(source)` mirrors another source directly — an `IntervalTickSource`,
|
|
264
|
+
`RateTickSource`, or **another system** (systems implement `ITickSource`).
|
|
265
|
+
|
|
266
|
+
When a cadence source is active, the `delta` passed to `run` is the accumulated milliseconds since
|
|
267
|
+
this system last fired. Standalone sources support `stop()`/`start()`. Chains compose:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
const second = new IntervalTickSource(1);
|
|
271
|
+
const minute = new RateTickSource(60, second);
|
|
272
|
+
|
|
273
|
+
world.system("Hourly").tickSource(minute).rate(60).run((now, delta) => { ... });
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Disabling a system suppresses its callbacks, but a disabled system used as a tick source still
|
|
277
|
+
drives downstream consumers — stop the clock itself if you need silence.
|
|
278
|
+
|
|
279
|
+
## `disable` / `enable` / `destroy`
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
const ai = world.system("AI").with(Enemy).run(tickAI);
|
|
283
|
+
ai.disable(); // pause: inbox cleared, new events dropped, run/each silent
|
|
284
|
+
ai.enable(); // resume; events from the disabled period are NOT replayed
|
|
285
|
+
ai.destroy(); // permanent: disables, then unregisters the backing entity
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
While disabled, query membership is still maintained, so the tracked set is correct on resume.
|
|
289
|
+
`destroy` fires no `exit` callbacks. Both `disable` and `enable` are idempotent and chainable.
|
|
290
|
+
|
|
291
|
+
## Reference
|
|
292
|
+
|
|
293
|
+
| Method | Description |
|
|
294
|
+
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
295
|
+
| `with(...specs)` / `without(dsl)` | Declare membership; see [the DSL](./queries-and-filters.md#the-query-dsl). |
|
|
296
|
+
| `phase(anchor)` | Place after a phase anchor (name or entity). Default `ON_UPDATE`. |
|
|
297
|
+
| `enter(cb)` / `enter(inject, cb)` | Fires once when an entity starts matching. |
|
|
298
|
+
| `update(C, cb)` / `update(C, inject, cb)` | Fires on `set`/`modified` of `C` on a member, and on entry for present watched components. |
|
|
299
|
+
| `exit(cb)` / `exit(inject, cb)` | Fires when an entity stops matching; direct injection reads an exit-time snapshot. |
|
|
300
|
+
| `each([C...], cb)` | Fires per tracked entity every tick. Implies `track`. One per system. |
|
|
301
|
+
| `run(cb)` | Fires once per tick, `(now, delta)`. |
|
|
302
|
+
| `interval(seconds)` / `rate(n[, source])` / `tickSource(source)` | Cadence control. |
|
|
303
|
+
| `orderBy([C...], compare)` | Sorted membership refreshed when injected comparator data changes. Implies `track`. |
|
|
304
|
+
| `track()` | Expose `count`, `has`, iteration, `forEach` without `each`. |
|
|
305
|
+
| `ignoreSource(query \| dsl)` | Drop `update` events originating from matching systems; see [Queries](./queries-and-filters.md#silencing-feedback-ignoresource). |
|
|
306
|
+
| `disable()` / `enable()` | Pause / resume processing. Idempotent. |
|
|
307
|
+
| `destroy()` | Permanently remove the system (no `exit` callbacks). |
|
|
308
|
+
| `didTick` / `lastFireDelta` | This frame's cadence result; ms accumulated into the last fire. |
|
|
309
|
+
| `count` / `has(e)` / `belongs(e)` / iteration / `forEach` | Shared query surface; see [Queries](./queries-and-filters.md#reading-a-query). |
|
|
310
|
+
|
|
311
|
+
`groupBy` is not available on systems — use `world.query(...).groupBy(...)`.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Utilities
|
|
2
|
+
|
|
3
|
+
Public helper classes exported by `@vworlds/vecs` for code that lives around the ECS core.
|
|
4
|
+
|
|
5
|
+
## `Events`
|
|
6
|
+
|
|
7
|
+
Use `Events` when a vecs package or integration needs typed package-level events without adding an
|
|
8
|
+
external runtime dependency. It is not an entity lifecycle API; entity teardown is modeled through
|
|
9
|
+
component removal, hooks, query exits, and relationship cleanup.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Events } from "@vworlds/vecs";
|
|
13
|
+
|
|
14
|
+
type ClientEvents = {
|
|
15
|
+
reset(event: { initial: boolean }): void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const events = new Events<ClientEvents>();
|
|
19
|
+
events.on("reset", (event) => console.log(event.initial));
|
|
20
|
+
events.emit("reset", { initial: true });
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Reference
|
|
24
|
+
|
|
25
|
+
| Method | Description |
|
|
26
|
+
| ---------------------------------- | -------------------------------------------------------------------------- |
|
|
27
|
+
| `on(event, fn, context?)` | Register a listener. |
|
|
28
|
+
| `addListener(event, fn, ctx?)` | Alias of `on`. |
|
|
29
|
+
| `once(event, fn, context?)` | Register a listener that removes itself before its first call. |
|
|
30
|
+
| `emit(event, ...args)` | Invoke listeners and return `true` when at least one listener ran. |
|
|
31
|
+
| `off(event, fn?, context?)` | Remove one listener, or every listener for the event when `fn` is omitted. |
|
|
32
|
+
| `removeListener(event, fn?, ctx?)` | Alias of `off`. |
|
|
33
|
+
| `removeAllListeners(event?)` | Remove listeners for one event, or all listeners when no event is given. |
|
|
34
|
+
| `eventNames()` | Return event names with at least one listener. |
|
|
35
|
+
| `listeners(event)` | Return registered listener functions for an event. |
|
|
36
|
+
| `listenerCount(event)` | Return the number of listeners registered for an event. |
|
|
37
|
+
|
|
38
|
+
## `ArrayMap`
|
|
39
|
+
|
|
40
|
+
`ArrayMap` (`lib/vecs/src/util/array_map.ts`) is a cheap `Map<number, T>` substitute backed by a
|
|
41
|
+
sparse JavaScript array. Lookups are a single array read and memory usage is one sparse backing
|
|
42
|
+
array. Use it when you only need random access (`get`, `set`, `has`, `delete`) by non-negative
|
|
43
|
+
integer keys and don't need iteration.
|
|
44
|
+
|
|
45
|
+
The world and entity APIs return read-only `ArrayMap` views (e.g. `entity.components`).
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { ArrayMap } from "@vworlds/vecs";
|
|
49
|
+
|
|
50
|
+
const map = new ArrayMap<string>();
|
|
51
|
+
map.set(42, "hello");
|
|
52
|
+
map.get(42); // "hello"
|
|
53
|
+
map.has(42); // true
|
|
54
|
+
map.delete(42);
|
|
55
|
+
map.size; // 0
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Reference
|
|
59
|
+
|
|
60
|
+
| Method | Description |
|
|
61
|
+
| --------------- | ----------------------------------------------------------------- |
|
|
62
|
+
| `set(key, val)` | Insert or replace the value at `key`. Increments `size` when new. |
|
|
63
|
+
| `get(key)` | Retrieve the value at `key`, or `undefined` if no entry exists. |
|
|
64
|
+
| `has(key)` | `true` when an entry exists at `key`. |
|
|
65
|
+
| `delete(key)` | Remove the entry at `key`. Does nothing if no entry exists there. |
|
|
66
|
+
| `clear()` | Remove all entries and reset `size` to zero. |
|
|
67
|
+
| `size` | The number of entries currently in the map. |
|
|
68
|
+
|
|
69
|
+
## `IterableArrayMap`
|
|
70
|
+
|
|
71
|
+
`IterableArrayMap` (`lib/vecs/src/util/array_map.ts`) is an iterable `Map<number, T>` substitute
|
|
72
|
+
backed by dense entries plus sparse indexes. Use it when callers need fast `forEach` or `values`
|
|
73
|
+
over present entries. Random access resolves `key → dense index` through an internal `ArrayMap`,
|
|
74
|
+
then indexes into a dense items array; iteration walks the dense items array directly.
|
|
75
|
+
|
|
76
|
+
Deletes keep the items array dense by moving the last value into the removed slot, so iteration
|
|
77
|
+
is always a tight loop without sparse holes. Compared with `ArrayMap`, this costs extra memory
|
|
78
|
+
for tuple storage and an index map.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { IterableArrayMap } from "@vworlds/vecs";
|
|
82
|
+
|
|
83
|
+
const map = new IterableArrayMap<{ x: number }>();
|
|
84
|
+
map.set(1, { x: 10 });
|
|
85
|
+
map.set(3, { x: 30 });
|
|
86
|
+
|
|
87
|
+
for (const [key, value] of map) {
|
|
88
|
+
console.log(key, value.x); // 1 10, 3 30 (or swapped after a delete)
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Reference
|
|
93
|
+
|
|
94
|
+
| Method | Description |
|
|
95
|
+
| ------------------- | ---------------------------------------------------------------------- |
|
|
96
|
+
| `set(key, val)` | Insert or replace the value at `key`. |
|
|
97
|
+
| `get(key)` | Retrieve the value at `key`, or `undefined` if no entry exists. |
|
|
98
|
+
| `has(key)` | `true` when an entry exists at `key`. |
|
|
99
|
+
| `delete(key)` | Remove the entry at `key`. Swaps last item into vacated slot (O(1)). |
|
|
100
|
+
| `forEach(cb)` | Visit every entry in dense item order, invoking `cb(value, key, map)`. |
|
|
101
|
+
| `entries()` | Iterator over `[key, value]` entries in dense item order. |
|
|
102
|
+
| `values()` | Every present value in dense item order as `T[]`. |
|
|
103
|
+
| `[Symbol.iterator]` | Same as `entries()`; supports `for..of`. |
|
|
104
|
+
| `clear()` | Remove all entries and reset `size` to zero. |
|
|
105
|
+
| `size` | The number of entries currently in the map. |
|
|
106
|
+
|
|
107
|
+
## `Bitset`
|
|
108
|
+
|
|
109
|
+
`Bitset` (`lib/vecs/src/util/bitset.ts`) is the compact integer-set type the ECS uses for entity
|
|
110
|
+
archetypes and watchlists. It is exported so component data can use it for bit-flag fields:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { Bitset } from "@vworlds/vecs";
|
|
114
|
+
|
|
115
|
+
class Tags {
|
|
116
|
+
tags = new Bitset();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
tags.tags.add(TAG_VISIBLE);
|
|
120
|
+
if (tags.tags.has(TAG_VISIBLE)) { ... }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Reference
|
|
124
|
+
|
|
125
|
+
| Method | Description |
|
|
126
|
+
| --------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
127
|
+
| `add(n)` | Set bit `n`. |
|
|
128
|
+
| `delete(n)` | Clear bit `n`. Storage is not compacted automatically; call `compact()` when needed. |
|
|
129
|
+
| `addBit(bptr)` / `deleteBit(bptr)` / `hasBit(bptr)` | Fast paths using a pre-computed `BitPtr`. |
|
|
130
|
+
| `has(n)` | `true` when bit `n` is set. |
|
|
131
|
+
| `equal(other)` | `true` when both bitsets have exactly the same bits set. |
|
|
132
|
+
| `hasBitset(other)` | `true` when every bit in `other` is also set here (subset check). |
|
|
133
|
+
| `forEach(cb)` | Visit each set bit index in ascending order. |
|
|
134
|
+
| `indices()` | All set bit indices as `number[]`. |
|
|
135
|
+
| `clear()` | Remove every set bit. |
|
|
136
|
+
| `compact()` | Trim trailing zero words from backing storage. |
|
|
137
|
+
|
|
138
|
+
Related: `getDSLKey(dsl, world)` returns a deterministic 32-bit hash of a DSL expression
|
|
139
|
+
(equivalent expressions hash identically), useful for caching keyed on predicates.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vworlds/vecs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.26",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -22,9 +22,6 @@
|
|
|
22
22
|
"typecheck": "tsc --noEmit",
|
|
23
23
|
"format:check": "prettier --check \"**/*.{ts,md}\" --ignore-path ../../.prettierignore"
|
|
24
24
|
},
|
|
25
|
-
"dependencies": {
|
|
26
|
-
"eventemitter3": "^4.0.7"
|
|
27
|
-
},
|
|
28
25
|
"license": "UNLICENSED",
|
|
29
26
|
"repository": {
|
|
30
27
|
"type": "git",
|