@vworlds/vecs 1.0.35 → 1.0.36
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/components.md +10 -8
- package/docs/design-guide.md +82 -69
- package/docs/modules.md +3 -3
- package/package.json +1 -1
package/dist/package.json
CHANGED
package/docs/components.md
CHANGED
|
@@ -212,22 +212,24 @@ world.get(Gravity)?.y; // world.component(Gravity).get(Gravity)
|
|
|
212
212
|
`SingletonModule` enforces the constraint (`lib/vecs/src/modules/singleton.ts`); see
|
|
213
213
|
[Modules](./modules.md#built-in-modules).
|
|
214
214
|
|
|
215
|
-
##
|
|
215
|
+
## Resources as components (non-ECS objects)
|
|
216
216
|
|
|
217
|
-
To
|
|
218
|
-
|
|
217
|
+
To bind a non-ECS resource (a renderer object, a WASM handle, a DOM node) to an entity, **store
|
|
218
|
+
the live object itself as a component** with `entity.attach(obj)` instead of keeping a side
|
|
219
|
+
`Map<eid, resource>`:
|
|
219
220
|
|
|
220
221
|
```ts
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
222
|
+
const obj = scene.add.arc(0, 0); // a Phaser GameObject
|
|
223
|
+
entity.attach(obj); // the GameObject is now a component, keyed by its class
|
|
224
224
|
|
|
225
|
-
world.component(
|
|
225
|
+
world.component(Phaser.GameObjects.Arc).onRemove((_entity, obj) => obj.destroy());
|
|
226
226
|
```
|
|
227
227
|
|
|
228
228
|
The entity's component set then _is_ the resource lifecycle: queries drive create/destroy, and
|
|
229
229
|
`onRemove` releases the resource whether the component is removed explicitly or the entity is
|
|
230
|
-
destroyed.
|
|
230
|
+
destroyed. A plain wrapper component (`class Handle { obj }`) still works when you need extra fields
|
|
231
|
+
beside the resource. The full pattern — create/update, the remove-hook teardown, and `Implements`
|
|
232
|
+
interface markers for polymorphic dispatch — is in the
|
|
231
233
|
[Design guide](./design-guide.md#7-patterns-reach-for-these).
|
|
232
234
|
|
|
233
235
|
## Reference
|
package/docs/design-guide.md
CHANGED
|
@@ -13,6 +13,12 @@ subsystem guides — start with [Concepts](./concepts.md) and the
|
|
|
13
13
|
> by the presence and change of components — not by imperative bookkeeping.** Almost every
|
|
14
14
|
> good pattern below is a consequence of taking that seriously.
|
|
15
15
|
|
|
16
|
+
> **How to use this guide.** You don't need to read all of it every time. Skim §1–§2 (the model
|
|
17
|
+
> and the execution facts everything else follows from) and the §8 anti-patterns + §9 pitfalls
|
|
18
|
+
> tables — together a fast TL;DR — then deep-read only the section your task touches: §3
|
|
19
|
+
> Components, §4 Systems & queries, §5 Phases, §6 Modules, §7 Patterns. The §10 case study shows
|
|
20
|
+
> all of it working together.
|
|
21
|
+
|
|
16
22
|
---
|
|
17
23
|
|
|
18
24
|
## 1. The model in one minute
|
|
@@ -114,22 +120,25 @@ new ServerWorld({ networkComponents: [Position, Rotation, /* … */ Arc] });
|
|
|
114
120
|
> their fields silently fail to register, that's the standard-vs-legacy mismatch. vecs-wire
|
|
115
121
|
> supports both; if you author new decorators, preserve that.
|
|
116
122
|
|
|
117
|
-
###
|
|
123
|
+
### Resources as components (hold non-ECS objects)
|
|
118
124
|
|
|
119
|
-
To
|
|
120
|
-
**
|
|
121
|
-
|
|
125
|
+
To bind a non-ECS resource (a Phaser `GameObject`, a WASM handle, a DOM node) to an entity,
|
|
126
|
+
**store the live object itself as a component** with `entity.attach(obj)` instead of keeping a
|
|
127
|
+
side `Map<eid, resource>`. `attach` registers the instance under its own class, so the entity's
|
|
128
|
+
component set _is_ the lifecycle; queries drive create/destroy; reset is free.
|
|
122
129
|
|
|
123
130
|
```ts
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
131
|
+
const obj = scene.add.arc(0, 0); // a Phaser GameObject
|
|
132
|
+
entity.attach(obj); // the GameObject is now a component, keyed by its class
|
|
127
133
|
|
|
128
|
-
world.component(
|
|
134
|
+
world.component(Phaser.GameObjects.Arc).onRemove((_entity, obj) => obj.destroy());
|
|
129
135
|
```
|
|
130
136
|
|
|
131
|
-
`onRemove` (below) releases the resource whenever
|
|
132
|
-
|
|
137
|
+
`onRemove` (below) releases the resource whenever it leaves — whether removed explicitly or
|
|
138
|
+
because the entity was destroyed. When several resource types should share one cleanup hook or be
|
|
139
|
+
queried together, give them a common **interface** with `Implements` (see §7 and the §10 case
|
|
140
|
+
study). A plain wrapper component (`class Handle { obj }`) is still fine when you need extra fields
|
|
141
|
+
beside the resource.
|
|
133
142
|
|
|
134
143
|
### Component lifecycle hooks: `onAdd` / `onSet` / `onRemove`
|
|
135
144
|
|
|
@@ -139,7 +148,7 @@ explicitly or because the entity was destroyed.
|
|
|
139
148
|
world.component(Impulse).onSet((entity, impulse) => {
|
|
140
149
|
/* accumulate */
|
|
141
150
|
});
|
|
142
|
-
world.component(
|
|
151
|
+
world.component(Phaser.GameObjects.Arc).onRemove((_entity, obj) => obj.destroy());
|
|
143
152
|
```
|
|
144
153
|
|
|
145
154
|
Use them for resource management and cross-cutting reactions that aren't worth a system. (Note
|
|
@@ -224,12 +233,12 @@ Choose `onEnter: true` by asking who owns the initial value:
|
|
|
224
233
|
`{ onEnter: true }` update callback for "initialize from current value, then keep synced."
|
|
225
234
|
|
|
226
235
|
```ts
|
|
227
|
-
// ❌ redundant when update already owns initialization
|
|
228
|
-
.enter([Rotation,
|
|
229
|
-
.update({ watch: Rotation, onEnter: true, inject: [
|
|
236
|
+
// ❌ redundant when update already owns initialization (Transform = Implements marker → the attached GameObject)
|
|
237
|
+
.enter([Rotation, Transform], (_e, [r, obj]) => obj.setRotation(coords.rot(r.angle)))
|
|
238
|
+
.update({ watch: Rotation, onEnter: true, inject: [Transform] }, (_e, r, [obj]) => obj.setRotation(coords.rot(r.angle)));
|
|
230
239
|
|
|
231
240
|
// ✅ update opts into entry-time initialization
|
|
232
|
-
.update({ watch: Rotation, onEnter: true, inject: [
|
|
241
|
+
.update({ watch: Rotation, onEnter: true, inject: [Transform] }, (_e, r, [obj]) => obj.setRotation(coords.rot(r.angle)));
|
|
233
242
|
```
|
|
234
243
|
|
|
235
244
|
### Injection
|
|
@@ -238,8 +247,8 @@ Pull components into the callback instead of `entity.get()`-ing them by hand:
|
|
|
238
247
|
|
|
239
248
|
```ts
|
|
240
249
|
.enter([Arc], (entity, [arc]) => { /* … */ })
|
|
241
|
-
.update({ watch: Arc, onEnter: true, inject: [
|
|
242
|
-
.exit([Arc,
|
|
250
|
+
.update({ watch: Arc, onEnter: true, inject: [Transform] }, (entity, arc, [obj]) => { /* … */ })
|
|
251
|
+
.exit([Arc, Transform], (entity, [arc, obj]) => { /* … */ })
|
|
243
252
|
.each([Position, Velocity], (entity, [pos, vel]) => { /* … */ })
|
|
244
253
|
```
|
|
245
254
|
|
|
@@ -287,8 +296,9 @@ ballsByCell.group(k)?.count;
|
|
|
287
296
|
### One system = one phase
|
|
288
297
|
|
|
289
298
|
A system lives in exactly **one** phase. If a concern needs part of its work earlier and part
|
|
290
|
-
later, **split it into multiple systems** in different phases (e.g.
|
|
291
|
-
|
|
299
|
+
later, **split it into multiple systems** in different phases (e.g. validation in `ON_VALIDATE`,
|
|
300
|
+
corrections in `POST_UPDATE`). You cannot put a system's `.exit` in one phase and its `.update` in
|
|
301
|
+
another.
|
|
292
302
|
|
|
293
303
|
---
|
|
294
304
|
|
|
@@ -305,8 +315,8 @@ build in `ON_STORE`). You cannot put a system's `.exit` in one phase and its `.u
|
|
|
305
315
|
- **Order within a phase = registration order**, which an umbrella module controls by the order
|
|
306
316
|
it loads sub-modules. **Cross-module ordering should be expressed with phases, not registration
|
|
307
317
|
order** — never rely on "module A happened to load before module B" for correctness when the two
|
|
308
|
-
are independent. (Example:
|
|
309
|
-
in `
|
|
318
|
+
are independent. (Example: the server's physics-to-render pose-sync must run after the physics step → put it
|
|
319
|
+
in `POST_UPDATE`, which is after the physics phases, rather than hoping the physics module loaded
|
|
310
320
|
first.)
|
|
311
321
|
|
|
312
322
|
---
|
|
@@ -323,14 +333,14 @@ export class PhaserServerModule extends Module<undefined> {
|
|
|
323
333
|
world
|
|
324
334
|
.system("phaser.sync.position")
|
|
325
335
|
.with(PhysicsPosition, RenderPosition)
|
|
326
|
-
.phase(
|
|
336
|
+
.phase(POST_UPDATE)
|
|
327
337
|
.update({ watch: PhysicsPosition, onEnter: true }, (e, p) =>
|
|
328
338
|
e.set(RenderPosition, { x: p.x, y: p.y })
|
|
329
339
|
);
|
|
330
340
|
world
|
|
331
341
|
.system("phaser.sync.rotation")
|
|
332
342
|
.with(PhysicsRotation, RenderRotation)
|
|
333
|
-
.phase(
|
|
343
|
+
.phase(POST_UPDATE)
|
|
334
344
|
.update({ watch: PhysicsRotation, onEnter: true }, (e, r) =>
|
|
335
345
|
e.set(RenderRotation, { angle: r.angle })
|
|
336
346
|
);
|
|
@@ -353,7 +363,7 @@ Guidelines:
|
|
|
353
363
|
export class PhaserRenderModule extends Module<{ scene; pixelsPerMeter? }> {
|
|
354
364
|
public init(cfg): void {
|
|
355
365
|
const coords = new CoordSpace(cfg.scene, cfg.pixelsPerMeter);
|
|
356
|
-
this.world.component(
|
|
366
|
+
this.world.component(GameObject).onRemove((_e, obj) => obj.destroy()); // GameObject: Implements marker over every Phaser object
|
|
357
367
|
this.world.module(ArcModule, { scene: cfg.scene, coords });
|
|
358
368
|
this.world.module(RectangleModule, { scene: cfg.scene, coords });
|
|
359
369
|
// … shape modules first …
|
|
@@ -374,57 +384,54 @@ Guidelines:
|
|
|
374
384
|
|
|
375
385
|
### Resource-as-component lifecycle (kills the `eid → object` map)
|
|
376
386
|
|
|
377
|
-
The canonical pattern for binding an external object to entities, fully query-driven
|
|
387
|
+
The canonical pattern for binding an external object to entities, fully query-driven. **Attach the
|
|
388
|
+
live object as its own component** and let a remove hook release it — no wrapper, no side map:
|
|
378
389
|
|
|
379
390
|
```ts
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
world.component(VGameObject).onRemove((_e, vgo) => vgo.obj.destroy()); // release on remove/destroy
|
|
391
|
+
// Release the resource whenever its component leaves (explicit remove or entity destroy):
|
|
392
|
+
world.component(Phaser.GameObjects.Arc).onRemove((_e, obj) => obj.destroy());
|
|
384
393
|
|
|
385
394
|
// CREATE: a renderable that has no object yet
|
|
386
395
|
world
|
|
387
396
|
.system("new.arc")
|
|
388
397
|
.with(Arc)
|
|
389
|
-
.without(
|
|
398
|
+
.without(Phaser.GameObjects.Arc)
|
|
390
399
|
.phase(ON_STORE)
|
|
391
|
-
.enter([Arc], (e) => {
|
|
392
|
-
const obj = scene.add.arc(0, 0);
|
|
393
|
-
e.
|
|
400
|
+
.enter([Arc], (e, [arc]) => {
|
|
401
|
+
const obj = scene.add.arc(0, 0, coords.len(arc.radius));
|
|
402
|
+
e.attach(obj); // the GameObject is now a component, keyed by its class
|
|
394
403
|
});
|
|
395
|
-
// ↑
|
|
404
|
+
// ↑ attach is flushed after this system → the entity now matches the systems below, this same tick
|
|
396
405
|
|
|
397
406
|
// UPDATE geometry on the bound object (opts into entry + change)
|
|
398
407
|
world
|
|
399
408
|
.system("render.arc")
|
|
400
|
-
.with(Arc,
|
|
409
|
+
.with(Arc, Phaser.GameObjects.Arc)
|
|
401
410
|
.phase(ON_STORE)
|
|
402
|
-
.update({ watch: Arc,
|
|
403
|
-
|
|
411
|
+
.update({ watch: Arc, inject: [Phaser.GameObjects.Arc] }, (e, arc, [obj]) =>
|
|
412
|
+
obj.setRadius(coords.len(arc.radius))
|
|
404
413
|
);
|
|
405
414
|
|
|
406
|
-
// TEARDOWN: when the shape leaves (type swap) or the entity dies, drop the
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
.
|
|
410
|
-
|
|
411
|
-
.exit([Arc, VGameObject], (e) => {
|
|
412
|
-
if (e.has(VGameObject)) e.remove(VGameObject);
|
|
413
|
-
});
|
|
415
|
+
// TEARDOWN: when the shape data leaves (type swap) or the entity dies, drop the object.
|
|
416
|
+
// A component remove hook keeps this declarative — no exit system, no phase to coordinate:
|
|
417
|
+
world.component(Arc).onRemove((e) => {
|
|
418
|
+
if (e.has(Phaser.GameObjects.Arc)) e.remove(Phaser.GameObjects.Arc);
|
|
419
|
+
});
|
|
414
420
|
```
|
|
415
421
|
|
|
416
|
-
Why it's good: no manual map; **destroy/reset is automatic** (destroying the entity removes
|
|
417
|
-
|
|
418
|
-
(see below).
|
|
419
|
-
|
|
420
|
-
|
|
422
|
+
Why it's good: no manual map; **destroy/reset is automatic** (destroying the entity removes the
|
|
423
|
+
attached object → its `onRemove` releases the resource); **type changes work for free** via
|
|
424
|
+
exclusivity (see below). Teardown is a remove **hook**, not a system, so it fires synchronously at
|
|
425
|
+
the mutation site regardless of phase — no teardown-before-create ordering to get right. When many
|
|
426
|
+
resource types share update/teardown logic, give them a common interface with `Implements` (§10)
|
|
427
|
+
and query the interface instead of each concrete class.
|
|
421
428
|
|
|
422
429
|
### Exclusivity-driven type swap
|
|
423
430
|
|
|
424
431
|
With `setExclusiveComponents(...renderables)`, replacing the renderable removes the old one. The
|
|
425
|
-
old shape's
|
|
426
|
-
|
|
427
|
-
No special-casing anywhere.
|
|
432
|
+
old shape data component's remove hook drops its attached object (→ `onRemove` destroys it); the
|
|
433
|
+
new shape's creation system (`.with(NewShape).without(Phaser.GameObjects.NewKind).enter`) fires and
|
|
434
|
+
rebinds. No teardown system, no special-casing anywhere.
|
|
428
435
|
|
|
429
436
|
### Reactive sync between two component spaces
|
|
430
437
|
|
|
@@ -499,26 +506,32 @@ small **render components**; the client turns them into Phaser `GameObject`s wit
|
|
|
499
506
|
render code**.
|
|
500
507
|
|
|
501
508
|
- **Data (`@vworlds/vecs-phaser`):** small, single-purpose, networked components — `Position`,
|
|
502
|
-
`Rotation`, `Scale`, `Alpha`, `Depth`, `FillStyle`, `StrokeStyle`, and one-of-N
|
|
503
|
-
(`Arc`, `Rectangle`, `Text`, …). `networkComponents` order is the protocol.
|
|
509
|
+
`Rotation`, `Scale`, `Alpha`, `Depth`, `FillStyle`, `StrokeStyle`, `Size`, and one-of-N
|
|
510
|
+
renderables (`Arc`, `Rectangle`, `Text`, …). `networkComponents` order is the protocol.
|
|
504
511
|
- **Server (`PhaserServerModule`, zero-config):** owns exclusivity
|
|
505
|
-
(`setExclusiveComponents(...renderables)`); two **reactive** pose-sync systems in `
|
|
512
|
+
(`setExclusiveComponents(...renderables)`); two **reactive** pose-sync systems in `POST_UPDATE`
|
|
506
513
|
copy physics `Position`/`Rotation` → render pose only when physics moved (`.update`, not
|
|
507
514
|
`.each`), and only for entities that already have a render pose (no creation — pure ECS). No
|
|
508
515
|
builders: game code creates entities and adds the components it wants.
|
|
509
|
-
- **Client (`PhaserRenderModule` umbrella → per-shape modules):**
|
|
510
|
-
to
|
|
511
|
-
|
|
512
|
-
`
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
`
|
|
521
|
-
|
|
516
|
+
- **Client (`PhaserRenderModule` umbrella → per-shape modules):** the live Phaser object is
|
|
517
|
+
attached to its entity **as the component** (`entity.attach(obj)`) — no wrapper. Each concrete
|
|
518
|
+
Phaser class declares the **interface markers** it satisfies with `Implements` (`GameObject`,
|
|
519
|
+
`Transform`, `Fill`, … plus a geometry marker like `Arc`/`Vertices`/`Text`), so one set of
|
|
520
|
+
systems drives every renderable polymorphically. Per shape: a **creation** system
|
|
521
|
+
(`.with(Shape).without(Phaser.GameObjects.Kind).enter` → `scene.add.*` + `entity.attach`) in
|
|
522
|
+
`ON_STORE`; a **geometry** system (`.with(Shape, m.Geometry).update`) in `ON_STORE`; and a
|
|
523
|
+
shape-data **remove hook** (`component(Shape).onRemove` → remove the Phaser object) instead of a
|
|
524
|
+
teardown system. Shared transform/style **sync** systems
|
|
525
|
+
(`.with(Component, m.Marker).update({ watch: Component, onEnter: true })`) apply
|
|
526
|
+
position/rotation/scale/alpha/depth/size/fill/stroke through the markers — initialization is
|
|
527
|
+
explicit via `onEnter: true`, and the umbrella loads shape modules _before_ the sync module so
|
|
528
|
+
creation precedes sync within `ON_STORE`.
|
|
529
|
+
- **Lifecycle that falls out for free:** destruction is a single hook —
|
|
530
|
+
`component(GameObject).onRemove((_e, obj) => obj.destroy())`. A renderable **type swap**
|
|
531
|
+
(exclusivity removes the old shape data → its remove hook drops the attached object → it's
|
|
532
|
+
destroyed → the new shape's creation system rebinds) and a **baseline reset / disconnect**
|
|
533
|
+
(`clearAllEntities()` → entities destroyed → attached objects removed → destroyed) both work with
|
|
534
|
+
no maps, no `start/stop`, no reset handler.
|
|
522
535
|
|
|
523
536
|
The whole thing is "server says _what exists, where, and what it looks like_; the client owns the
|
|
524
537
|
Phaser lifecycle" — expressed entirely as small components + phased reactive systems + modules.
|
package/docs/modules.md
CHANGED
|
@@ -10,7 +10,7 @@ the module is registered. It is the unit of packaging for a feature: registering
|
|
|
10
10
|
declaring systems, setting exclusivity and cleanup policy, and (rarely) inserting phases.
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
|
-
import { Module,
|
|
13
|
+
import { Module, POST_UPDATE } from "@vworlds/vecs";
|
|
14
14
|
|
|
15
15
|
export class PoseSyncModule extends Module {
|
|
16
16
|
public init(): void {
|
|
@@ -20,7 +20,7 @@ export class PoseSyncModule extends Module {
|
|
|
20
20
|
world
|
|
21
21
|
.system("sync.position")
|
|
22
22
|
.with(PhysicsPosition, RenderPosition)
|
|
23
|
-
.phase(
|
|
23
|
+
.phase(POST_UPDATE)
|
|
24
24
|
.update({ watch: PhysicsPosition, onEnter: true }, (e, p) =>
|
|
25
25
|
e.set(RenderPosition, { x: p.x, y: p.y })
|
|
26
26
|
);
|
|
@@ -62,7 +62,7 @@ is often one module per item plus a thin umbrella that composes them:
|
|
|
62
62
|
```ts
|
|
63
63
|
export class PhaserRenderModule extends Module<{ scene: Scene }> {
|
|
64
64
|
public init(cfg: { scene: Scene }): void {
|
|
65
|
-
this.world.component(
|
|
65
|
+
this.world.component(GameObject).onRemove((_e, obj) => obj.destroy()); // GameObject: Implements marker over every Phaser object
|
|
66
66
|
this.world.module(ArcModule, { scene: cfg.scene });
|
|
67
67
|
this.world.module(RectangleModule, { scene: cfg.scene });
|
|
68
68
|
// … shape modules first …
|