@vworlds/vecs 1.0.47 → 1.0.49
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/dist/package.json +1 -1
- package/docs/design-guide.md +65 -9
- package/docs/systems.md +3 -0
- package/package.json +1 -1
package/dist/package.json
CHANGED
package/docs/design-guide.md
CHANGED
|
@@ -305,7 +305,8 @@ Choose `onEnter: true` by asking who owns the initial value:
|
|
|
305
305
|
|
|
306
306
|
### Injection
|
|
307
307
|
|
|
308
|
-
Pull
|
|
308
|
+
**Inject every component a system callback always uses.** Pull it into the callback instead of
|
|
309
|
+
`entity.get()`-ing it by hand:
|
|
309
310
|
|
|
310
311
|
```ts
|
|
311
312
|
.enter([Arc], (entity, [arc]) => { /* … */ })
|
|
@@ -314,6 +315,23 @@ Pull components into the callback instead of `entity.get()`-ing them by hand:
|
|
|
314
315
|
.each([Position, Velocity], (entity, [pos, vel]) => { /* … */ })
|
|
315
316
|
```
|
|
316
317
|
|
|
318
|
+
Use `entity.get(C)` only for a component that is conditionally needed and rarely required: a read on
|
|
319
|
+
a branch most calls never take, where resolving the component for every call would be waste.
|
|
320
|
+
|
|
321
|
+
Why:
|
|
322
|
+
|
|
323
|
+
- **The signature states the inputs.** What a callback reads sits next to the `.with(...)` that
|
|
324
|
+
guarantees it, not in the body.
|
|
325
|
+
- **No dead guards.** A component guaranteed by `.with(...)` is injected as non-nullable, so there
|
|
326
|
+
is no `undefined` to check. `get` returns `Readonly<C> | undefined`, which invites an
|
|
327
|
+
`if (c !== undefined)` that tells the next reader the component might be missing when the query
|
|
328
|
+
says it cannot be. An injected instance is also the live, mutable one: a callback that writes in
|
|
329
|
+
place and calls `entity.modified(C)` (see §7) needs no cast.
|
|
330
|
+
- **Resolved once.** Injection resolves a component's id when the callback is registered;
|
|
331
|
+
`entity.get(C)` resolves the class on every call. In an `exit` callback injection matters more
|
|
332
|
+
still: it reads the snapshot captured when the entity left, while a `get` at replay time can find
|
|
333
|
+
the component already removed.
|
|
334
|
+
|
|
317
335
|
Components listed in the inject tuple but **not** guaranteed by `.with(...)` resolve as
|
|
318
336
|
`T | undefined` (the types reflect this) — guard them; the ones in `.with` are always present.
|
|
319
337
|
|
|
@@ -328,7 +346,7 @@ Components listed in the inject tuple but **not** guaranteed by `.with(...)` res
|
|
|
328
346
|
// ❌ scans every body every tick, even if it didn't move
|
|
329
347
|
.with(PhysicsPosition, RenderPosition).each([...], (e, [...]) => { if (changed) e.set(...) });
|
|
330
348
|
|
|
331
|
-
// ✅ fires only when physics actually moved the body
|
|
349
|
+
// ✅ fires only when physics actually moved the body (compare before writing a networked B: see §7)
|
|
332
350
|
.with(PhysicsPosition, RenderPosition).update(PhysicsPosition, (e, p) => e.set(RenderPosition, { x: p.x, y: p.y }));
|
|
333
351
|
```
|
|
334
352
|
|
|
@@ -396,15 +414,28 @@ export class PhaserServerModule extends Module<undefined> {
|
|
|
396
414
|
.system("phaser.sync.position")
|
|
397
415
|
.with(PhysicsPosition, RenderPosition)
|
|
398
416
|
.phase(POST_UPDATE)
|
|
399
|
-
.update(
|
|
400
|
-
|
|
417
|
+
.update(
|
|
418
|
+
{ watch: PhysicsPosition, onEnter: true, inject: [RenderPosition] },
|
|
419
|
+
(e, p, [render]) => {
|
|
420
|
+
if (render.x !== p.x || render.y !== p.y) {
|
|
421
|
+
render.x = p.x;
|
|
422
|
+
render.y = p.y;
|
|
423
|
+
e.modified(RenderPosition);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
401
426
|
);
|
|
402
427
|
world
|
|
403
428
|
.system("phaser.sync.rotation")
|
|
404
429
|
.with(PhysicsRotation, RenderRotation)
|
|
405
430
|
.phase(POST_UPDATE)
|
|
406
|
-
.update(
|
|
407
|
-
|
|
431
|
+
.update(
|
|
432
|
+
{ watch: PhysicsRotation, onEnter: true, inject: [RenderRotation] },
|
|
433
|
+
(e, r, [render]) => {
|
|
434
|
+
if (render.angle !== r.angle) {
|
|
435
|
+
render.angle = r.angle;
|
|
436
|
+
e.modified(RenderRotation);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
408
439
|
);
|
|
409
440
|
}
|
|
410
441
|
}
|
|
@@ -583,14 +614,31 @@ rebinds. No teardown system, no special-casing anywhere.
|
|
|
583
614
|
|
|
584
615
|
Copy `A → B`, gated on both existing (so you never _create_ `B`, you only keep an existing `B` in
|
|
585
616
|
sync — pure-ECS: the owner decides whether `B` exists). Use `{ onEnter: true }` when `B` must reflect
|
|
586
|
-
the current `A` as soon as the entity enters the sync system
|
|
617
|
+
the current `A` as soon as the entity enters the sync system, inject `B` (the `.with(...)` guarantees
|
|
618
|
+
it, so it is non-nullable, see §4), and write only when `B` differs from `A`:
|
|
587
619
|
|
|
588
620
|
```ts
|
|
589
|
-
.with(PhysicsPosition, RenderPosition).update(
|
|
590
|
-
|
|
621
|
+
.with(PhysicsPosition, RenderPosition).update(
|
|
622
|
+
{ watch: PhysicsPosition, onEnter: true, inject: [RenderPosition] },
|
|
623
|
+
(e, p, [render]) => {
|
|
624
|
+
if (render.x !== p.x || render.y !== p.y) {
|
|
625
|
+
render.x = p.x;
|
|
626
|
+
render.y = p.y;
|
|
627
|
+
e.modified(RenderPosition);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
591
630
|
);
|
|
592
631
|
```
|
|
593
632
|
|
|
633
|
+
Every write is a modification, even with an equal value: both `set` and `modified` fire `onSet`,
|
|
634
|
+
route `update` events to every watching system and replicate a networked component. So a sync into a
|
|
635
|
+
networked or watched `B` compares before it writes. The injected `B` is the live instance, so assign
|
|
636
|
+
into it and call `entity.modified(B)`: that has the same effects as `set` without the props literal
|
|
637
|
+
and the deferred copy, and a second event for the same entity in one drain finds the first write
|
|
638
|
+
applied and writes nothing. Always pair an in-place write with `modified`: assignment alone notifies
|
|
639
|
+
nobody, so hooks, watchers and replication never see it. Use `set` when you do not hold the instance,
|
|
640
|
+
or when the write should create the component.
|
|
641
|
+
|
|
594
642
|
Leave `onEnter: true` off (or set `onEnter: false`) only when a separate `.enter(...)` already initializes `B`,
|
|
595
643
|
or when the callback is intentionally "future changes only."
|
|
596
644
|
|
|
@@ -707,6 +755,7 @@ system after whatever produces the entity/companion.
|
|
|
707
755
|
| One system trying to act in two phases | Split into two systems, one per phase. |
|
|
708
756
|
| Custom phases for app/render logic | Use the standard taxonomy; only engine modules insert phases. |
|
|
709
757
|
| `.each` scanning every entity to detect change | Reactive `.update(C)` — fires only on change. |
|
|
758
|
+
| `entity.get(C)` for a component the callback always uses | Inject it (`inject: [C]`); keep `get` for a component that is conditionally needed and rarely required. |
|
|
710
759
|
| Treating target storage as component data | Use `add(Rel, target)` / `remove(Rel, target)` and read with `target` / `targets` / `hasTarget`; target storage is private. |
|
|
711
760
|
| A `registerFooSystem(world)` wrapper per system | Declare the system inline in the module's `init`. |
|
|
712
761
|
|
|
@@ -720,6 +769,13 @@ system after whatever produces the entity/companion.
|
|
|
720
769
|
- **`update(C)` is change-only by default.** Add `{ onEnter: true }` for projection/sync callbacks
|
|
721
770
|
that must apply the current watched value on entry; leave it off for mutation-only callbacks or
|
|
722
771
|
when `.enter(...)` already owns initialization.
|
|
772
|
+
- **A write marks the component modified, even with an equal value.** `set`, and an in-place
|
|
773
|
+
assignment followed by `modified`, fire `onSet`, route `update` events to every watching system and
|
|
774
|
+
replicate a networked component. A writer that runs every tick and copies a value into a networked
|
|
775
|
+
or watched component (simulation output, a derived pose) should compare first and write only when
|
|
776
|
+
the value changed (see §7). An in-place assignment without `modified` notifies nobody.
|
|
777
|
+
- **A callback that always uses a component should inject it, not `get` it.** `entity.get(C)` is for
|
|
778
|
+
a component that is conditionally needed and rarely required (see §4).
|
|
723
779
|
- **Membership is fixed by `.with(...)` / `.without(...)` / `.update(C)`;** `update(C)` watches `C`
|
|
724
780
|
and requires it.
|
|
725
781
|
- **`exit` retains injected references captured at exit** (components are not deep-cloned).
|
package/docs/systems.md
CHANGED
|
@@ -189,6 +189,9 @@ an options object with `inject`, resolved per entity and passed as a typed tuple
|
|
|
189
189
|
.each({ inject: [Position, Sprite] }, (e, [pos, sprite]) => { ... })
|
|
190
190
|
```
|
|
191
191
|
|
|
192
|
+
Inject every component a callback always uses, and keep `entity.get` for one that is only
|
|
193
|
+
conditionally needed (see [Design guide](./design-guide.md#injection)).
|
|
194
|
+
|
|
192
195
|
Nullability follows membership: components guaranteed by `with` (or `hint`) are non-nullable;
|
|
193
196
|
anything else resolves as `T | undefined` — guard it. (`enter`/`exit` injection requires the
|
|
194
197
|
direct components to be present and throws otherwise; list only components your predicate
|