@vworlds/vecs 1.0.50 → 1.0.51
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 +2 -0
- package/dist/package.json +1 -8
- package/docs/README.md +4 -0
- package/docs/components.md +15 -1
- package/docs/design-guide.md +74 -4
- package/docs/queries-and-filters.md +25 -0
- package/docs/relationships.md +3 -2
- package/docs/systems.md +23 -1
- package/package.json +1 -8
- package/skills/vecs/SKILL.md +13 -0
package/README.md
CHANGED
|
@@ -59,6 +59,8 @@ world.progress(performance.now(), 16); // call once per frame
|
|
|
59
59
|
|
|
60
60
|
## Documentation
|
|
61
61
|
|
|
62
|
+
For Node clients that need replicated Phaser poses, see [Headless clients](../vecs-phaser-client/README.md#headless-clients).
|
|
63
|
+
|
|
62
64
|
The full documentation lives in [`docs/`](./docs/README.md). Recommended reading order:
|
|
63
65
|
|
|
64
66
|
1. [Getting started](./docs/getting-started.md) — install to first ticking world.
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vworlds/vecs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.51",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -27,12 +27,5 @@
|
|
|
27
27
|
"type": "git",
|
|
28
28
|
"url": "https://github.com/vworlds/vecs",
|
|
29
29
|
"directory": "lib/vecs"
|
|
30
|
-
},
|
|
31
|
-
"lint-staged": {
|
|
32
|
-
"*.ts": [
|
|
33
|
-
"prettier --write",
|
|
34
|
-
"eslint --fix"
|
|
35
|
-
],
|
|
36
|
-
"*.md": "prettier --write"
|
|
37
30
|
}
|
|
38
31
|
}
|
package/docs/README.md
CHANGED
|
@@ -36,6 +36,8 @@ Keep the [Glossary](./glossary.md) at hand for terminology.
|
|
|
36
36
|
|
|
37
37
|
## Map
|
|
38
38
|
|
|
39
|
+
For Node clients that need replicated Phaser poses, see [Headless clients](../../vecs-phaser-client/README.md#headless-clients).
|
|
40
|
+
|
|
39
41
|
| Guide | Covers |
|
|
40
42
|
| -------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
41
43
|
| [getting-started.md](./getting-started.md) | A runnable tutorial from `npm install` to a ticking world. |
|
|
@@ -57,6 +59,8 @@ sessions, and the client world) lives in the `@vworlds/vecs-wire`, `@vworlds/vec
|
|
|
57
59
|
`@vworlds/vecs-server`, and `@vworlds/vecs-client` packages:
|
|
58
60
|
|
|
59
61
|
- [Remote procedure calls](../../vecs-protocol/docs/rpc.md) — the call channel and typed errors.
|
|
62
|
+
- [State diffs](../../vecs-protocol/docs/state-diffs.md) — what a diff carries, the order a client
|
|
63
|
+
applies it in, and how a reused entity id stays unambiguous.
|
|
60
64
|
- [Sessions and caller context](../../vecs-server/docs/sessions.md) — per-connection RPC context,
|
|
61
65
|
`closeSession`, and exactly-once teardown.
|
|
62
66
|
- [Client world](../../vecs-client/docs/client-world.md) — replication, render time,
|
package/docs/components.md
CHANGED
|
@@ -73,7 +73,15 @@ released this frame is not immediately recycled. A frame is one `beginFrame()`,
|
|
|
73
73
|
`progress()`; a wait of `0` frees the id at once. Once the wait is over, allocation hands out free
|
|
74
74
|
ids before never-used ones, lowest first, so a released id is recycled by one of the next entities
|
|
75
75
|
created. A `ClientWorld` sets the wait to `0` for the ids of replicated entities and keeps the
|
|
76
|
-
default for its local ones. A
|
|
76
|
+
default for its local ones. A `ServerWorld` allocates its replicated entities from the `entity` pool,
|
|
77
|
+
and holds a freed id for at least `historyFrames + 1` frames (65 by default) whatever the wait
|
|
78
|
+
asks for, so the frame that removed an entity has left every session's history before its id comes
|
|
79
|
+
back (see [Reusing entity ids](../../vecs-server/docs/sessions.md#reusing-entity-ids)). The
|
|
80
|
+
`networkEntityReuseWaitFrames` option of the networked worlds sets the wait of the pool that holds
|
|
81
|
+
the replicated entities: `entity` for a server, `network_entity` for a client. A diff that removes
|
|
82
|
+
an entity and snapshots a new one under the same id is read in a fixed order, which is how a client
|
|
83
|
+
tells the two apart (see [State diffs](../../vecs-protocol/docs/state-diffs.md)). A recycled id is
|
|
84
|
+
why a stored id is not a safe link between entities
|
|
77
85
|
(see [Link with edges, not stored ids](./relationships.md#link-with-edges-not-stored-ids)).
|
|
78
86
|
`world.peekNextEid(poolName)` returns the next id a pool would allocate without reserving it. Id 0
|
|
79
87
|
is reserved.
|
|
@@ -155,6 +163,7 @@ world.component(Orange).set(Implements, { interfaces: [Fruit] });
|
|
|
155
163
|
| `Traversable` | Exclusive relationships | Maintains hierarchy depth for cascade ordering |
|
|
156
164
|
| `OwnedTarget` | Exclusive relationships other than `ChildOf` | Owns each explicit target through `ChildOf` and destructive edge replacement |
|
|
157
165
|
| `OneOf` | Relationships | Constrains each target to be a direct `ChildOf` child of a declared parent |
|
|
166
|
+
| `With` | A component and a target of the same kind | Adding the component also adds the target (the same targets, for edges) |
|
|
158
167
|
|
|
159
168
|
Traits declare policy; their built-in modules install the hooks and maintain the metadata that
|
|
160
169
|
enforce it. Configure traits in feature-module setup rather than opportunistically inside systems.
|
|
@@ -168,6 +177,11 @@ Do not assume all traits have the same lifecycle:
|
|
|
168
177
|
`ChildOf` child of the declared parent.
|
|
169
178
|
- `Traversable` backfills existing relationship instances.
|
|
170
179
|
- Setting or removing `Implements` recomputes interface aliases.
|
|
180
|
+
- Add `With` before the component is in use, as `world.component(X).add(With, world.component(Y))`.
|
|
181
|
+
It is irrevocable, both must be plain components or both relationships, and it only brings `Y`
|
|
182
|
+
along: an existing `Y` is left as it is, and removing `X` does not remove `Y`. On a `ClientWorld`,
|
|
183
|
+
a network removal of `Y` is skipped while `X` is on the entity (see
|
|
184
|
+
[Applying a frame](../../vecs-client/docs/client-world.md#applying-a-frame)).
|
|
171
185
|
|
|
172
186
|
Treat traits as setup policy unless their documentation explicitly supports runtime changes. Before
|
|
173
187
|
writing custom component-type bookkeeping, check whether an existing trait already expresses the
|
package/docs/design-guide.md
CHANGED
|
@@ -177,6 +177,7 @@ world.component(Orange).set(Implements, { interfaces: [Fruit] });
|
|
|
177
177
|
| `Traversable` | An exclusive relationship hierarchy needs depth or cascade ordering |
|
|
178
178
|
| `OwnedTarget` | An exclusive relationship owns its explicitly added target |
|
|
179
179
|
| `OneOf` | A relationship's targets must be direct children of a declared parent (enum-style) |
|
|
180
|
+
| `With` | Adding one component should bring another along, as a companion it does not own |
|
|
180
181
|
|
|
181
182
|
Traits declare policy; modules implement it through hooks and metadata. Configure them in module
|
|
182
183
|
setup, not inside systems. Lifecycle differs by trait: `OwnedTarget` and `Singleton` are setup-time
|
|
@@ -184,6 +185,18 @@ policy, `Traversable` backfills, and `Implements` recomputes declarations. Do no
|
|
|
184
185
|
dynamically removable. See the [Components guide](./components.md#component-traits-policy-on-component-entities)
|
|
185
186
|
for the full model and custom-trait guidance.
|
|
186
187
|
|
|
188
|
+
`With` is a pair-form trait: `world.component(Motion).add(With, world.component(Position))` adds a
|
|
189
|
+
`Position` whenever a `Motion` is added. It only brings the companion along: removing `Motion` does
|
|
190
|
+
not remove `Position`, and a `Position` the entity already holds is left as it is. A networked
|
|
191
|
+
client has one more rule, because the server decides what a client replicates but the client decides
|
|
192
|
+
what it derives. **`ClientWorld.apply` skips a network removal of a component that another
|
|
193
|
+
component present on the entity brings through `With`**, so a server can swap the source of a
|
|
194
|
+
client-local component in one diff, say a streamed `Position` for a dead-reckoned `Motion`, and the
|
|
195
|
+
entity never goes without it: no `remove` hook fires, and the queries and `DrawBy` children that
|
|
196
|
+
match on it never see it leave. A bringer that the same diff sets counts as present. The entity-destroy
|
|
197
|
+
path is unaffected, and a relationship is removed as sent (see
|
|
198
|
+
[Applying a frame](../../vecs-client/docs/client-world.md#applying-a-frame)).
|
|
199
|
+
|
|
187
200
|
### Exclusive component groups
|
|
188
201
|
|
|
189
202
|
`world.setExclusiveComponents(A, B, C)` declares a mutually exclusive set: setting one
|
|
@@ -269,6 +282,23 @@ classes or DSL objects (`{ all: [...] }`, `{ any: [...] }`, `{ parent: X }`, `{
|
|
|
269
282
|
> `.with(...)`, `.without(...)`, and `.update(C)` own membership. `.update(C)` watches change events
|
|
270
283
|
> and also requires `C`, so `.with(A).update(C)` matches entities carrying both `A` and `C`.
|
|
271
284
|
|
|
285
|
+
**`.without` is not free.** `.without(X)` is `.with({ not: X })`: a `not` term holds every live
|
|
286
|
+
entity that lacks `X`, a world-sized set, and the query intersects it with its positive terms. The
|
|
287
|
+
exclusion does not run over the positive terms' candidates. Vecs seeds that set with one scan of
|
|
288
|
+
the world when the term is first built, then adds each new entity to it, removes each destroyed
|
|
289
|
+
entity from it, and flips an entity in or out when `X` is added to it or removed from it. Changes
|
|
290
|
+
to `X`'s data cost nothing. The term is shared by every query that excludes the same `X`, so the
|
|
291
|
+
upkeep is paid once per distinct exclusion, plus one membership check per query that uses it.
|
|
292
|
+
|
|
293
|
+
- Add `.without(X)` only when the exclusion is needed, and put positive terms next to it: they keep
|
|
294
|
+
the query's members and callbacks small. A query made only of `.without` terms tracks every
|
|
295
|
+
entity in the world.
|
|
296
|
+
- The creation pattern `.with(NewShape).without(Phaser.GameObjects.NewKind).enter(...)` relies on
|
|
297
|
+
that narrowing. The positive term limits the members to the few entities that just got
|
|
298
|
+
`NewShape`, while the exclusion keeps those that already have a display object from entering.
|
|
299
|
+
|
|
300
|
+
Details: [Queries and filters](./queries-and-filters.md#the-cost-of-not-and-without).
|
|
301
|
+
|
|
272
302
|
### Reactive callbacks: `enter` / `update` / `exit`
|
|
273
303
|
|
|
274
304
|
- **`enter`** — fires once when an entity _becomes_ a member. Use it for one-time setup
|
|
@@ -282,6 +312,14 @@ classes or DSL objects (`{ all: [...] }`, `{ any: [...] }`, `{ parent: X }`, `{
|
|
|
282
312
|
Its injected components are read from a **snapshot captured at exit time**, so they're still
|
|
283
313
|
resolvable even though they're being removed.
|
|
284
314
|
|
|
315
|
+
**Repeated writes in one tick.** Vecs never merges `update` events, so a callback fires once per
|
|
316
|
+
routed write. `entity.modified(C)` and `entity.getMut(C)` coalesce through the entity's dirty bit:
|
|
317
|
+
repeated calls before the queued command drains give one `update`. Each `entity.set(C, …)` queues
|
|
318
|
+
its own command and routes its own `update`, so three `set` calls give three callbacks. Write in
|
|
319
|
+
place plus `modified` (or `getMut`) to get one callback per tick, or compare before `set`. When
|
|
320
|
+
producers already use `modified`/`getMut`, do not add a defensive per-tick dedupe or batching layer
|
|
321
|
+
on the consumer side. See [Systems](./systems.md#repeated-writes-in-one-tick).
|
|
322
|
+
|
|
285
323
|
Choose `onEnter: true` by asking who owns the initial value:
|
|
286
324
|
|
|
287
325
|
- Use **`onEnter: true`** for projection / sync callbacks: copying ECS data into a runtime object,
|
|
@@ -339,7 +377,8 @@ Components listed in the inject tuple but **not** guaranteed by `.with(...)` res
|
|
|
339
377
|
### `each` vs reactive — don't scan when you can react
|
|
340
378
|
|
|
341
379
|
- **`.each([...], cb)`** iterates **every matching entity every tick**. Use it only when you
|
|
342
|
-
genuinely need a per-frame sweep over all members.
|
|
380
|
+
genuinely need a per-frame sweep over all members. An `.each` must justify running every tick: a
|
|
381
|
+
body that returns early for most members is a reactive system or a narrower query in disguise.
|
|
343
382
|
- **Reactive `enter`/`update`/`exit`** fire only on change. For "copy A→B when A changes,"
|
|
344
383
|
reactive `.update(A)` touches _nothing_ on idle entities, where `.each` would scan them all.
|
|
345
384
|
|
|
@@ -351,6 +390,28 @@ Components listed in the inject tuple but **not** guaranteed by `.with(...)` res
|
|
|
351
390
|
.with(PhysicsPosition, RenderPosition).update(PhysicsPosition, (e, p) => e.set(RenderPosition, { x: p.x, y: p.y }));
|
|
352
391
|
```
|
|
353
392
|
|
|
393
|
+
When the work really is per-tick (a held input, a cooldown, a charge), narrow the query with a tag
|
|
394
|
+
or component that exists **only while the work is live**, instead of sweeping every candidate and
|
|
395
|
+
returning early. A reactive `update` on the intent adds and removes the marker:
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
// ❌ visits every shooter every tick; most return at once
|
|
399
|
+
.with(Weapon).each([Weapon], (e, [w]) => { if (!e.has(TriggerHeld)) return; /* fire */ });
|
|
400
|
+
|
|
401
|
+
// ✅ the cadence system matches only entities that are firing right now
|
|
402
|
+
world.system("TriggerIntent").with(Weapon).update(FireIntent, (e, intent) =>
|
|
403
|
+
intent.held ? e.add(Firing) : e.remove(Firing));
|
|
404
|
+
world.system("Fire").with(Weapon, Firing).interval(0.1).each([Weapon], (e, [w]) => { /* fire */ });
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
For a countdown, do not write a bespoke "tick every timer" system over all entities. Vecs has no
|
|
408
|
+
per-entity timer component: `interval`, `rate` and `tickSource` (see below) throttle _systems_.
|
|
409
|
+
Model the window as an owned helper entity, an `Exclusive` + `OwnedTarget` relationship target that
|
|
410
|
+
carries the deadline (see [Owned target entities](#owned-target-entities-entity-valued-properties)).
|
|
411
|
+
Its presence is the window: an `.each` over the helper entities visits only open windows, and
|
|
412
|
+
destroying a helper ends its window and removes the edge through the relationship's cleanup policy,
|
|
413
|
+
so no remove hook or flag is needed.
|
|
414
|
+
|
|
354
415
|
### `run` and `interval`
|
|
355
416
|
|
|
356
417
|
- **`.run(cb)`** — a system with no per-entity query; the body runs once per tick. For
|
|
@@ -853,10 +914,19 @@ system after whatever produces the entity/companion.
|
|
|
853
914
|
[Pose Updates Fire Only When The Pose Changes](../../vecs-physics/docs/systems-and-phases.md#pose-updates-fire-only-when-the-pose-changes)).
|
|
854
915
|
- **vecs-phaser-server: game code owns the physics pose; the render pose is protocol-private.** On
|
|
855
916
|
the server, read and write the physics `Position` and `Rotation` of every entity, including one
|
|
856
|
-
without a `Body`. The render `Position` and `Rotation` and the dead-reckoning `Motion`
|
|
857
|
-
encodings of that pose that only the sync systems write: a `DeadReckoned`
|
|
858
|
-
at all, and adding one throws (see
|
|
917
|
+
without a `Body`. The render `Position` and `Rotation` and the dead-reckoning `Motion` and
|
|
918
|
+
`MotionParams` are wire encodings of that pose that only the sync systems write: a `DeadReckoned`
|
|
919
|
+
body has no render pose at all, and adding one throws (see
|
|
859
920
|
[Dead reckoning](../../vecs-phaser-server/README.md#dead-reckoning)).
|
|
921
|
+
- **vecs-phaser-server: switch `DeadReckoned` on a change of state, and keep a constantly driven body
|
|
922
|
+
streamed.** The tag may be added or removed at any time, so a body can be dead reckoned while it
|
|
923
|
+
drifts and streamed while joints, thrust or a tow drive it (a driven body diverges from its
|
|
924
|
+
prediction every step and would cost a baseline of about 27 bytes per step against about 22 for a
|
|
925
|
+
streamed pose update). Toggle it when the state changes, not every frame: each switch costs a
|
|
926
|
+
baseline one way and a pose update the other, and clients blend the hand-off over `blendMs`. A
|
|
927
|
+
render pose or `Motion` created or switched late in a frame, in `PRE_STORE`, `ON_STORE` or an RPC
|
|
928
|
+
handler, is still synced from the physics pose before the frame is recorded (see
|
|
929
|
+
[Switching at runtime](../../vecs-phaser-server/README.md#switching-at-runtime)).
|
|
860
930
|
- **vecs-client: attach a `ClientWorld` socket only after `socket.connect()` resolves,** so no ack
|
|
861
931
|
precedes the data channel (`connectDgram` does this). Pending `invoke` calls reject with
|
|
862
932
|
`RPC_CLOSED_CODE` on disconnect or `dispose` (see
|
|
@@ -44,6 +44,31 @@ evaluates it against each entity pointing at the candidate and also matches when
|
|
|
44
44
|
queries and systems. `target` / `parent` are fully reactive to edge changes and target-side changes
|
|
45
45
|
— see [Relationships](./relationships.md#querying-through-relationships).
|
|
46
46
|
|
|
47
|
+
### The cost of `not` and `.without`
|
|
48
|
+
|
|
49
|
+
`.without(X)` is `.with({ not: X })` (`lib/vecs/src/query/query.ts`, `without`), and `.with` merges
|
|
50
|
+
into `{ all: [...] }`. A `not` term is built from the world term plus `X`'s term and holds every live
|
|
51
|
+
entity that lacks `X` (`lib/vecs/src/terms/not_term.ts`). It is a world-sized set, independent of
|
|
52
|
+
the query's positive terms, and the `all` term intersects it with them.
|
|
53
|
+
|
|
54
|
+
| Event | Effect on a `not X` term |
|
|
55
|
+
| ----------------------------------- | ----------------------------------------------------------------------------- |
|
|
56
|
+
| Term built (first use) | One scan of the whole world seeds the set (`term.ts`, `_initializeFromWorld`) |
|
|
57
|
+
| Entity created or destroyed | Added to or removed from the set (`world_term.ts`, `entityCreated`) |
|
|
58
|
+
| `X` added to or removed from entity | The entity leaves or re-enters the set |
|
|
59
|
+
| `X` data modified | Nothing (`not_term.ts`, `onTermRefresh` is a no-op) |
|
|
60
|
+
|
|
61
|
+
Terms are cached by canonical key, so the upkeep is paid once per distinct exclusion, not once per
|
|
62
|
+
query. Each query that uses it adds one membership check per change of the set, and the positive
|
|
63
|
+
term comes first, so most checks stop there.
|
|
64
|
+
|
|
65
|
+
- Use `.without` only when the exclusion is needed, and pair it with positive terms that keep the
|
|
66
|
+
query's members small. A query with only `.without` terms tracks every entity in the world, so
|
|
67
|
+
its `enter` fires for each created entity.
|
|
68
|
+
- The creation pattern `.with(NewShape).without(Phaser.GameObjects.NewKind).enter(...)` depends on
|
|
69
|
+
this: the positive term limits the members, and the exclusion drops the few that already hold the
|
|
70
|
+
object.
|
|
71
|
+
|
|
47
72
|
### `QuerySpec`: adding `cascade` and `hint`
|
|
48
73
|
|
|
49
74
|
Anywhere a DSL is accepted, you can pass the object form
|
package/docs/relationships.md
CHANGED
|
@@ -40,8 +40,9 @@ component field (`ownerEid = 0`, `target: Entity | undefined`). The world mainta
|
|
|
40
40
|
cannot maintain a field:
|
|
41
41
|
|
|
42
42
|
- **A stored eid can come to name another entity.** A destroyed entity's id is reused once its
|
|
43
|
-
pool's `reuseWaitFrames` have passed: 16 frames by default,
|
|
44
|
-
`ClientWorld
|
|
43
|
+
pool's `reuseWaitFrames` have passed: 16 frames by default, `0` for replicated entities on a
|
|
44
|
+
`ClientWorld`, and at least `historyFrames + 1` on a `ServerWorld` (see
|
|
45
|
+
[Id pools](./components.md#id-pools)). Allocation hands out free ids before
|
|
45
46
|
never-used ones, so one of the next entities created takes it. Until an entity does,
|
|
46
47
|
`world.getEntity(id)` returns `undefined`; from then on it returns that live, unrelated entity and
|
|
47
48
|
the stale id looks valid.
|
package/docs/systems.md
CHANGED
|
@@ -44,6 +44,7 @@ the full query DSL (operator semantics in
|
|
|
44
44
|
.with({ parent: Body }) // ChildOf target has Body
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
`.without` is not free: see [its cost](./queries-and-filters.md#the-cost-of-not-and-without).
|
|
47
48
|
Multiple `with` calls merge with `all`. The `source` / `children` operators are non-reactive and
|
|
48
49
|
rejected by systems — they are filter-only (see
|
|
49
50
|
[Queries and filters](./queries-and-filters.md#filters-one-shot-scans)).
|
|
@@ -131,6 +132,25 @@ Use `.update(C, ...)` by itself when `C` is the membership shape and watched val
|
|
|
131
132
|
A query or system can register only one `update(C, ...)` callback for each component. A duplicate
|
|
132
133
|
registration throws instead of replacing the existing callback.
|
|
133
134
|
|
|
135
|
+
#### Repeated writes in one tick
|
|
136
|
+
|
|
137
|
+
Each routed write is one `update` event, and a system's inbox replays its events in arrival order
|
|
138
|
+
without merging any (`lib/vecs/src/system.ts`, `_pushUpdate` and the drain loop in
|
|
139
|
+
`_runDeferred`). What differs is how many events a write routes:
|
|
140
|
+
|
|
141
|
+
| Write | Events routed for `C` in one drain window |
|
|
142
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
143
|
+
| `entity.set(C, …)` × n | n: every call queues its own command and fires its own `onSet` and `update` |
|
|
144
|
+
| `entity.modified(C)` × n | 1: the first call sets the dirty bit, the command clears it when it runs, later calls are no-ops |
|
|
145
|
+
| `entity.getMut(C, cb)` × n | 1: it marks modified through the same dirty bit |
|
|
146
|
+
|
|
147
|
+
Coalescing needs a queued command, so it applies to calls made while commands are deferred (inside
|
|
148
|
+
system callbacks); outside them `modified` applies immediately. To get one callback per tick, write
|
|
149
|
+
in place and call `modified` (or use `getMut`), or compare before `set` and skip equal values. Do
|
|
150
|
+
not add a per-tick dedupe or batching step in the consumer when producers already use
|
|
151
|
+
`modified`/`getMut`. Source: `lib/vecs/src/entity/entity.components.ts` (`_markModified`, `set`,
|
|
152
|
+
`_notifyRaw`).
|
|
153
|
+
|
|
134
154
|
### `.exit(callback)` / `.exit(inject, callback)` / `.exit(options, callback)`
|
|
135
155
|
|
|
136
156
|
Fires when an entity stops matching (component removed, or entity destroyed). Directly injected
|
|
@@ -149,7 +169,9 @@ object remain visible:
|
|
|
149
169
|
### `.each(components, callback)` / `.each(options, callback)`
|
|
150
170
|
|
|
151
171
|
Fires every tick the system runs, once per tracked entity, regardless of change. Use it only for
|
|
152
|
-
genuinely per-frame sweeps — when reacting to change suffices, prefer `update`
|
|
172
|
+
genuinely per-frame sweeps — when reacting to change suffices, prefer `update`. An `.each` must
|
|
173
|
+
justify running every tick, and when it does, narrow `.with(...)` to a tag that exists only while
|
|
174
|
+
the work is live (see
|
|
153
175
|
[Design guide](./design-guide.md#each-vs-reactive--dont-scan-when-you-can-react)):
|
|
154
176
|
|
|
155
177
|
```ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vworlds/vecs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.51",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -27,12 +27,5 @@
|
|
|
27
27
|
"type": "git",
|
|
28
28
|
"url": "https://github.com/vworlds/vecs",
|
|
29
29
|
"directory": "lib/vecs"
|
|
30
|
-
},
|
|
31
|
-
"lint-staged": {
|
|
32
|
-
"*.ts": [
|
|
33
|
-
"prettier --write",
|
|
34
|
-
"eslint --fix"
|
|
35
|
-
],
|
|
36
|
-
"*.md": "prettier --write"
|
|
37
30
|
}
|
|
38
31
|
}
|
package/skills/vecs/SKILL.md
CHANGED
|
@@ -40,6 +40,10 @@ Before implementing, identify:
|
|
|
40
40
|
- The state represented by each component and whether absence has meaning.
|
|
41
41
|
- Which component transition creates, updates, or removes related state or resources.
|
|
42
42
|
- Whether the work should react through `enter`, `update`, or `exit` rather than scan every frame.
|
|
43
|
+
An `.each` must justify running every tick; when it does, narrow its query with a tag that exists
|
|
44
|
+
only while the work is live (for example a `Firing` tag that a reactive `update` on the intent
|
|
45
|
+
adds and removes) instead of looping over every candidate and returning early.
|
|
46
|
+
([`each` vs reactive](../../docs/design-guide.md#each-vs-reactive--dont-scan-when-you-can-react))
|
|
43
47
|
- Which phase owns the work and what earlier or later phase it must order against.
|
|
44
48
|
- Whether a multi-target relationship, an `Exclusive` relationship, an explicitly created owned
|
|
45
49
|
target, a grouped query, or an attached resource expresses the lifecycle better than imperative
|
|
@@ -73,6 +77,15 @@ other anti-patterns and pitfalls; each rule below links to the detail.
|
|
|
73
77
|
- Do not call `getMut` per tick unless a change is intended. It marks the component modified even
|
|
74
78
|
without a callback and even if nothing is written, which fires `update` watchers and replication.
|
|
75
79
|
Read with `get`. ([`getMut`](../../docs/entities.md#getmut-and-ensure-mutating-in-place))
|
|
80
|
+
- `.without(X)` does not filter the positive terms' candidates: it keeps a world-sized "not X" set,
|
|
81
|
+
updated on every entity create and destroy and every `X` add or remove, and intersects it with
|
|
82
|
+
them. Use it only when needed, after positive terms that keep the members small; a query with only
|
|
83
|
+
`.without` terms tracks the whole world.
|
|
84
|
+
([Cost of `.without`](../../docs/queries-and-filters.md#the-cost-of-not-and-without))
|
|
85
|
+
- `entity.modified(C)` and `getMut(C)` coalesce into one `update` per drain; every `entity.set(C, …)`
|
|
86
|
+
routes its own, and the system inbox never merges events. Write in place plus `modified`, or
|
|
87
|
+
compare before `set`, and do not add per-tick dedupe batches when producers use `modified`/`getMut`.
|
|
88
|
+
([Repeated writes](../../docs/systems.md#repeated-writes-in-one-tick))
|
|
76
89
|
- A system's `enter` reaches only entities still in its query when the system drains. An entity
|
|
77
90
|
that left the query or died first gets neither `enter` nor the paired `exit`, and a re-entry
|
|
78
91
|
within one drain gives `enter`, `exit`, `enter`, so keep setup and teardown symmetric.
|