@vworlds/vecs 1.0.49 → 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/modules/snapshot_types.d.ts +7 -5
- package/dist/modules/snapshot_types.js +7 -5
- package/dist/modules/snapshot_types.js.map +1 -1
- package/dist/package.json +1 -8
- package/dist/world/world.d.ts +2 -0
- package/dist/world/world.disposal.d.ts +69 -0
- package/dist/world/world.disposal.js +146 -0
- package/dist/world/world.disposal.js.map +1 -0
- package/dist/world/world.js +2 -0
- package/dist/world/world.js.map +1 -1
- package/dist/world/world.pipeline.d.ts +2 -2
- package/dist/world/world.pipeline.js +2 -2
- package/dist/world/world.pipeline.js.map +1 -1
- package/docs/README.md +9 -4
- package/docs/components.md +42 -2
- package/docs/design-guide.md +132 -6
- package/docs/entities.md +70 -1
- package/docs/execution-model.md +4 -3
- package/docs/glossary.md +4 -0
- package/docs/queries-and-filters.md +25 -0
- package/docs/relationships.md +55 -0
- package/docs/snapshots.md +5 -2
- package/docs/systems.md +32 -5
- package/package.json +1 -8
- package/skills/vecs/SKILL.md +67 -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.
|
|
@@ -52,12 +52,14 @@ export declare class OnSnapshot extends Relationship {
|
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
54
|
* Default snapshot policy: construct a fresh instance of the component class
|
|
55
|
-
* and
|
|
55
|
+
* and deep-copy the source instance's own enumerable fields onto it.
|
|
56
56
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
57
|
+
* The copy covers primitives, arrays, plain objects, `Map`, `Set`, `Date`,
|
|
58
|
+
* `ArrayBuffer` and typed arrays, so the destination never aliases a nested
|
|
59
|
+
* value of the source. Anything else (a class instance, a function, a symbol)
|
|
60
|
+
* throws at capture with the component and field path. A type that holds such
|
|
61
|
+
* a value, or that must not carry runtime handles across worlds, needs its own
|
|
62
|
+
* policy — see {@link CustomSnapshot} and {@link DontSnapshot}.
|
|
61
63
|
*/
|
|
62
64
|
export declare class ClonePublic {
|
|
63
65
|
}
|
|
@@ -48,12 +48,14 @@ export class OnSnapshot extends Relationship {
|
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* Default snapshot policy: construct a fresh instance of the component class
|
|
51
|
-
* and
|
|
51
|
+
* and deep-copy the source instance's own enumerable fields onto it.
|
|
52
52
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
53
|
+
* The copy covers primitives, arrays, plain objects, `Map`, `Set`, `Date`,
|
|
54
|
+
* `ArrayBuffer` and typed arrays, so the destination never aliases a nested
|
|
55
|
+
* value of the source. Anything else (a class instance, a function, a symbol)
|
|
56
|
+
* throws at capture with the component and field path. A type that holds such
|
|
57
|
+
* a value, or that must not carry runtime handles across worlds, needs its own
|
|
58
|
+
* policy — see {@link CustomSnapshot} and {@link DontSnapshot}.
|
|
57
59
|
*/
|
|
58
60
|
export class ClonePublic {
|
|
59
61
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snapshot_types.js","sourceRoot":"","sources":["../../src/modules/snapshot_types.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,uBAAuB;AACvB,EAAE;AACF,iFAAiF;AACjF,4EAA4E;AAC5E,iFAAiF;AACjF,0EAA0E;AAC1E,4BAA4B;AAG5B,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAoBpD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,OAAO,UAAW,SAAQ,YAAY;IAA5C;;QACE;;;WAGG;QACI,YAAO,GAAgC,SAAS,CAAC;QAExD;;;WAGG;QACI,gBAAW,GAAoC,SAAS,CAAC;IAClE,CAAC;CAAA;AAED
|
|
1
|
+
{"version":3,"file":"snapshot_types.js","sourceRoot":"","sources":["../../src/modules/snapshot_types.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,uBAAuB;AACvB,EAAE;AACF,iFAAiF;AACjF,4EAA4E;AAC5E,iFAAiF;AACjF,0EAA0E;AAC1E,4BAA4B;AAG5B,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAoBpD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,OAAO,UAAW,SAAQ,YAAY;IAA5C;;QACE;;;WAGG;QACI,YAAO,GAAgC,SAAS,CAAC;QAExD;;;WAGG;QACI,gBAAW,GAAoC,SAAS,CAAC;IAClE,CAAC;CAAA;AAED;;;;;;;;;;GAUG;AACH,MAAM,OAAO,WAAW;CAAG;AAE3B;;;;;;;;GAQG;AACH,MAAM,OAAO,YAAY;CAAG;AAE5B;;;;;;GAMG;AACH,MAAM,OAAO,cAAc;CAAG;AAkB9B,+EAA+E;AAC/E,MAAM,CAAC,MAAM,YAAY,GAA2B,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAEtE,6EAA6E;AAC7E,MAAM,CAAC,MAAM,WAAW,GAA2B,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAEpE;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CACpC,SAAiB,EACjB,MAA0B,EAC1B,OAAyB,EACzB,WAAiC;IAEjC,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC;IAC9B,MAAM,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACvC,SAAS,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAClC,SAAS,CAAC,GAAG,CAAC,UAAU,EAAE;QACxB,OAAO,EAAE,MAAM,KAAK,cAAc,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;QACxD,WAAW,EAAE,MAAM,KAAK,cAAc,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;KACjE,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAY,EAAE,IAAmB;IACtE,MAAM,UAAU,GAAG,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;IAC/C,MAAM,WAAW,GAAG,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC,GAAG,CAA2B,CAAC;IAC7F,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC;IACrC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,GAAG,KAAK,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,GAAG,EAAE,CAAC;QAC5E,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,IAAI,MAAM,CAAC,GAAG,KAAK,KAAK,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC,GAAG,EAAE,CAAC;QACvD,OAAO,WAAW,CAAC;IACrB,CAAC;IACD,IAAI,WAAW,CAAC,OAAO,KAAK,SAAS,IAAI,WAAW,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QAC/E,MAAM,IAAI,KAAK,CACb,yBAAyB,IAAI,CAAC,KAAK,CAAC,IAAI,mEAAmE,CAC5G,CAAC;IACJ,CAAC;IACD,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,WAAW,CAAC,OAAO;QAC5B,WAAW,EAAE,WAAW,CAAC,WAAW;KACrC,CAAC;AACJ,CAAC"}
|
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/dist/world/world.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { type WorldOptions } from "./world.base.js";
|
|
|
11
11
|
* 2. **Build the pipeline** — attach systems to phase entities with
|
|
12
12
|
* `DependsOn`, or call {@link setPipeline} with a custom query spec.
|
|
13
13
|
* 3. **Run loop** — call {@link progress} once per frame.
|
|
14
|
+
* 4. **Dispose** — call {@link dispose} when the world is finished, so the
|
|
15
|
+
* `remove` hooks of the components its entities carry release what they own.
|
|
14
16
|
*
|
|
15
17
|
* ## Deferred mode
|
|
16
18
|
*
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Transfers } from "./world.transfers.js";
|
|
2
|
+
import type { World } from "./world.js";
|
|
3
|
+
/**
|
|
4
|
+
* World teardown. It is a layer of its own rather than a method of `World`, because the layers
|
|
5
|
+
* below pass `this` to parameters typed `World`, which only holds while `World` adds no members
|
|
6
|
+
* to `Pipeline`.
|
|
7
|
+
*/
|
|
8
|
+
export declare class Disposal extends Transfers {
|
|
9
|
+
/** `true` once {@link dispose} has started: what makes it idempotent and safe to re-enter. */
|
|
10
|
+
private _disposeStarted;
|
|
11
|
+
/**
|
|
12
|
+
* End the world's life: destroy every entity the application and its modules created, so the
|
|
13
|
+
* `remove` hooks of the components they carry release what those components own (render
|
|
14
|
+
* objects, handles, timers). Call it when the world is finished — a scene or route change, a
|
|
15
|
+
* hot reload — and drop the reference afterwards.
|
|
16
|
+
*
|
|
17
|
+
* Each entity goes through the ordinary destroy path, as if it had received
|
|
18
|
+
* {@link Entity.destroy}: every component's `remove` hook fires exactly once, cleanup policies
|
|
19
|
+
* and relationship cascades (`ChildOf`, `OwnedTarget`, `CleanupPolicy.Delete`) run as they
|
|
20
|
+
* always do, and an entity a cascade or a hook has already destroyed is skipped, never
|
|
21
|
+
* destroyed twice.
|
|
22
|
+
*
|
|
23
|
+
* ## What stays
|
|
24
|
+
*
|
|
25
|
+
* The infrastructure the world needs to stay coherent is not destroyed:
|
|
26
|
+
*
|
|
27
|
+
* - component and module entities (every {@link Component}),
|
|
28
|
+
* - the entities behind queries and systems (they carry the {@link Query} component), and
|
|
29
|
+
* - phase entities (they carry the {@link Phase} component).
|
|
30
|
+
*
|
|
31
|
+
* What those entities hold stays too: data stored on a component entity (a singleton set with
|
|
32
|
+
* {@link set}) and any entity they still carry as a component key, which cannot be destroyed
|
|
33
|
+
* while it is in use. Release such data yourself. {@link clearAllEntities} is the full wipe,
|
|
34
|
+
* infrastructure included.
|
|
35
|
+
*
|
|
36
|
+
* ## Order
|
|
37
|
+
*
|
|
38
|
+
* No order is specified among unrelated entities, so do not make a hook depend on one. The one
|
|
39
|
+
* guarantee is that an entity used as a component key is destroyed after every entity that
|
|
40
|
+
* carries it, because destroying it earlier would throw under the default cleanup policy.
|
|
41
|
+
*
|
|
42
|
+
* ## Pending commands
|
|
43
|
+
*
|
|
44
|
+
* Commands queued by an enclosing deferred scope are neither flushed early nor discarded: they
|
|
45
|
+
* run first, in order, and the destruction follows them in the same queue, so a hook still fires
|
|
46
|
+
* once per component instance however it was removed. An entity whose creation is still queued
|
|
47
|
+
* is created, then destroyed. At top level nothing is queued and `dispose()` returns with the
|
|
48
|
+
* application's entities gone. Called from a system, a hook or a {@link defer} block, it queues
|
|
49
|
+
* the destruction like any `destroy()` in that situation, and the destruction completes when
|
|
50
|
+
* the queue next drains: right after the running system, or at the end of the `defer` block.
|
|
51
|
+
*
|
|
52
|
+
* ## After disposal
|
|
53
|
+
*
|
|
54
|
+
* A disposed world must not be used again. Nothing enforces it — no other method checks, so
|
|
55
|
+
* `entity()` and `progress()` stay as cheap as ever — but the world has no application state
|
|
56
|
+
* left to run on. Calling `dispose()` again, from a hook included, does nothing. A hook that
|
|
57
|
+
* creates entities while the world is being disposed leaves them behind. A system's `exit`
|
|
58
|
+
* callback only queues its event, and a disposed world never runs the system again, so release
|
|
59
|
+
* resources in `remove` hooks, which fire during the call. A hook that throws propagates out of
|
|
60
|
+
* the call and leaves the world partial, as it does from any destroy: treat it as a programming
|
|
61
|
+
* error.
|
|
62
|
+
*/
|
|
63
|
+
dispose(this: World): void;
|
|
64
|
+
/**
|
|
65
|
+
* The entities {@link dispose} destroys, in an order that lets every destroy succeed: those the
|
|
66
|
+
* application created, with each one used as a component key after its carriers.
|
|
67
|
+
*/
|
|
68
|
+
private _destroyOrder;
|
|
69
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Component } from "../component.js";
|
|
2
|
+
import { Phase } from "../modules/pipeline.js";
|
|
3
|
+
import { Query } from "../query/index.js";
|
|
4
|
+
import { Transfers } from "./world.transfers.js";
|
|
5
|
+
/**
|
|
6
|
+
* World teardown. It is a layer of its own rather than a method of `World`, because the layers
|
|
7
|
+
* below pass `this` to parameters typed `World`, which only holds while `World` adds no members
|
|
8
|
+
* to `Pipeline`.
|
|
9
|
+
*/
|
|
10
|
+
export class Disposal extends Transfers {
|
|
11
|
+
constructor() {
|
|
12
|
+
super(...arguments);
|
|
13
|
+
/** `true` once {@link dispose} has started: what makes it idempotent and safe to re-enter. */
|
|
14
|
+
this._disposeStarted = false;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* End the world's life: destroy every entity the application and its modules created, so the
|
|
18
|
+
* `remove` hooks of the components they carry release what those components own (render
|
|
19
|
+
* objects, handles, timers). Call it when the world is finished — a scene or route change, a
|
|
20
|
+
* hot reload — and drop the reference afterwards.
|
|
21
|
+
*
|
|
22
|
+
* Each entity goes through the ordinary destroy path, as if it had received
|
|
23
|
+
* {@link Entity.destroy}: every component's `remove` hook fires exactly once, cleanup policies
|
|
24
|
+
* and relationship cascades (`ChildOf`, `OwnedTarget`, `CleanupPolicy.Delete`) run as they
|
|
25
|
+
* always do, and an entity a cascade or a hook has already destroyed is skipped, never
|
|
26
|
+
* destroyed twice.
|
|
27
|
+
*
|
|
28
|
+
* ## What stays
|
|
29
|
+
*
|
|
30
|
+
* The infrastructure the world needs to stay coherent is not destroyed:
|
|
31
|
+
*
|
|
32
|
+
* - component and module entities (every {@link Component}),
|
|
33
|
+
* - the entities behind queries and systems (they carry the {@link Query} component), and
|
|
34
|
+
* - phase entities (they carry the {@link Phase} component).
|
|
35
|
+
*
|
|
36
|
+
* What those entities hold stays too: data stored on a component entity (a singleton set with
|
|
37
|
+
* {@link set}) and any entity they still carry as a component key, which cannot be destroyed
|
|
38
|
+
* while it is in use. Release such data yourself. {@link clearAllEntities} is the full wipe,
|
|
39
|
+
* infrastructure included.
|
|
40
|
+
*
|
|
41
|
+
* ## Order
|
|
42
|
+
*
|
|
43
|
+
* No order is specified among unrelated entities, so do not make a hook depend on one. The one
|
|
44
|
+
* guarantee is that an entity used as a component key is destroyed after every entity that
|
|
45
|
+
* carries it, because destroying it earlier would throw under the default cleanup policy.
|
|
46
|
+
*
|
|
47
|
+
* ## Pending commands
|
|
48
|
+
*
|
|
49
|
+
* Commands queued by an enclosing deferred scope are neither flushed early nor discarded: they
|
|
50
|
+
* run first, in order, and the destruction follows them in the same queue, so a hook still fires
|
|
51
|
+
* once per component instance however it was removed. An entity whose creation is still queued
|
|
52
|
+
* is created, then destroyed. At top level nothing is queued and `dispose()` returns with the
|
|
53
|
+
* application's entities gone. Called from a system, a hook or a {@link defer} block, it queues
|
|
54
|
+
* the destruction like any `destroy()` in that situation, and the destruction completes when
|
|
55
|
+
* the queue next drains: right after the running system, or at the end of the `defer` block.
|
|
56
|
+
*
|
|
57
|
+
* ## After disposal
|
|
58
|
+
*
|
|
59
|
+
* A disposed world must not be used again. Nothing enforces it — no other method checks, so
|
|
60
|
+
* `entity()` and `progress()` stay as cheap as ever — but the world has no application state
|
|
61
|
+
* left to run on. Calling `dispose()` again, from a hook included, does nothing. A hook that
|
|
62
|
+
* creates entities while the world is being disposed leaves them behind. A system's `exit`
|
|
63
|
+
* callback only queues its event, and a disposed world never runs the system again, so release
|
|
64
|
+
* resources in `remove` hooks, which fire during the call. A hook that throws propagates out of
|
|
65
|
+
* the call and leaves the world partial, as it does from any destroy: treat it as a programming
|
|
66
|
+
* error.
|
|
67
|
+
*/
|
|
68
|
+
dispose() {
|
|
69
|
+
if (this._disposeStarted) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this._disposeStarted = true;
|
|
73
|
+
// The destroys are queued commands, as `Entity.destroy()` makes them in a deferred scope, so a
|
|
74
|
+
// queue that is already filling runs ahead of them. At top level this scope drains them now.
|
|
75
|
+
this.defer(() => {
|
|
76
|
+
for (const entity of this._destroyOrder()) {
|
|
77
|
+
this._enqueue({ kind: 4 /* CommandKind.Destroy */, entity });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The entities {@link dispose} destroys, in an order that lets every destroy succeed: those the
|
|
83
|
+
* application created, with each one used as a component key after its carriers.
|
|
84
|
+
*/
|
|
85
|
+
_destroyOrder() {
|
|
86
|
+
// `getEntity` rather than `entity`: `clearAllEntities()` unregisters these components.
|
|
87
|
+
const queryEid = this.getEntity(Query)?.eid;
|
|
88
|
+
const phaseEid = this.getEntity(Phase)?.eid;
|
|
89
|
+
const isInfrastructure = (entity) => entity instanceof Component ||
|
|
90
|
+
(queryEid !== undefined && entity.has(queryEid)) ||
|
|
91
|
+
(phaseEid !== undefined && entity.has(phaseEid));
|
|
92
|
+
const entities = this._entities.values();
|
|
93
|
+
// In a deferred scope an entity created earlier is only queued, not registered yet. Its
|
|
94
|
+
// creation runs ahead of the destroy that is queued after it.
|
|
95
|
+
for (const command of this._commandQueue) {
|
|
96
|
+
if (command.kind === 0 /* CommandKind.CreateEntity */ &&
|
|
97
|
+
!command.entity.destroyed &&
|
|
98
|
+
this._entities.get(command.entity.eid) !== command.entity) {
|
|
99
|
+
entities.push(command.entity);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return carriersFirst(entities.filter((entity) => !isInfrastructure(entity)), isInfrastructure);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Order `entities` so that an entity used as a component key follows every entity that carries it.
|
|
107
|
+
*
|
|
108
|
+
* A key destroyed while still carried throws under the default cleanup policy, so carriers go
|
|
109
|
+
* first. A key that something outside `entities` carries, which is infrastructure in practice, or
|
|
110
|
+
* that is caught in a cycle, cannot be ordered and is left out: destroying it would throw or strip
|
|
111
|
+
* the infrastructure.
|
|
112
|
+
*/
|
|
113
|
+
function carriersFirst(entities, isInfrastructure) {
|
|
114
|
+
const ordered = [];
|
|
115
|
+
const keys = new Set();
|
|
116
|
+
for (const entity of entities) {
|
|
117
|
+
if (entity.instanceCount() === 0) {
|
|
118
|
+
ordered.push(entity);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
keys.add(entity);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
let progressed = keys.size > 0;
|
|
125
|
+
while (progressed) {
|
|
126
|
+
progressed = false;
|
|
127
|
+
for (const key of keys) {
|
|
128
|
+
if (!isCarried(key, keys, isInfrastructure)) {
|
|
129
|
+
ordered.push(key);
|
|
130
|
+
keys.delete(key);
|
|
131
|
+
progressed = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return ordered;
|
|
136
|
+
}
|
|
137
|
+
/** Whether `key` is still carried by another key that waits to be ordered, or by infrastructure. */
|
|
138
|
+
function isCarried(key, waiting, isInfrastructure) {
|
|
139
|
+
for (const carrier of key.meta._carriers ?? []) {
|
|
140
|
+
if (carrier !== key && (waiting.has(carrier) || isInfrastructure(carrier))) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=world.disposal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"world.disposal.js","sourceRoot":"","sources":["../../src/world/world.disposal.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAC/C,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAGjD;;;;GAIG;AACH,MAAM,OAAO,QAAS,SAAQ,SAAS;IAAvC;;QACE,8FAA8F;QACtF,oBAAe,GAAG,KAAK,CAAC;IAkGlC,CAAC;IAhGC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmDG;IACI,OAAO;QACZ,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACzB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC5B,+FAA+F;QAC/F,6FAA6F;QAC7F,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE;YACd,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;gBAC1C,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,6BAAqB,EAAE,MAAM,EAAE,CAAC,CAAC;YACvD,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACK,aAAa;QACnB,uFAAuF;QACvF,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC;QAC5C,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC;QAC5C,MAAM,gBAAgB,GAAG,CAAC,MAAc,EAAW,EAAE,CACnD,MAAM,YAAY,SAAS;YAC3B,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAChD,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC;QAEnD,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;QACzC,wFAAwF;QACxF,8DAA8D;QAC9D,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACzC,IACE,OAAO,CAAC,IAAI,qCAA6B;gBACzC,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS;gBACzB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,MAAM,EACzD,CAAC;gBACD,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAChC,CAAC;QACH,CAAC;QACD,OAAO,aAAa,CAClB,QAAQ,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,EACtD,gBAAgB,CACjB,CAAC;IACJ,CAAC;CACF;AAED;;;;;;;GAOG;AACH,SAAS,aAAa,CACpB,QAAkB,EAClB,gBAA6C;IAE7C,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,MAAM,IAAI,QAAQ,EAAE,CAAC;QAC9B,IAAI,MAAM,CAAC,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC;YACjC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACvB,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;IACD,IAAI,UAAU,GAAG,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;IAC/B,OAAO,UAAU,EAAE,CAAC;QAClB,UAAU,GAAG,KAAK,CAAC;QACnB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,gBAAgB,CAAC,EAAE,CAAC;gBAC5C,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBAClB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACjB,UAAU,GAAG,IAAI,CAAC;YACpB,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,oGAAoG;AACpG,SAAS,SAAS,CAChB,GAAW,EACX,OAA4B,EAC5B,gBAA6C;IAE7C,KAAK,MAAM,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,EAAE,CAAC;QAC/C,IAAI,OAAO,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,gBAAgB,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;YAC3E,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/dist/world/world.js
CHANGED
|
@@ -19,6 +19,8 @@ import { Pipeline } from "./world.pipeline.js";
|
|
|
19
19
|
* 2. **Build the pipeline** — attach systems to phase entities with
|
|
20
20
|
* `DependsOn`, or call {@link setPipeline} with a custom query spec.
|
|
21
21
|
* 3. **Run loop** — call {@link progress} once per frame.
|
|
22
|
+
* 4. **Dispose** — call {@link dispose} when the world is finished, so the
|
|
23
|
+
* `remove` hooks of the components its entities carry release what they own.
|
|
22
24
|
*
|
|
23
25
|
* ## Deferred mode
|
|
24
26
|
*
|
package/dist/world/world.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"world.js","sourceRoot":"","sources":["../../src/world/world.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAG/C
|
|
1
|
+
{"version":3,"file":"world.js","sourceRoot":"","sources":["../../src/world/world.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAG/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,OAAO,KAAM,SAAQ,QAAQ;IACjC,YAAmB,UAAwB,EAAE;QAC3C,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,aAAa,EAAE,CAAC,CAAC;QAC3F,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;QAC3E;YACE,cAAc;YACd,gBAAgB;YAChB,eAAe;YACf,mBAAmB;YACnB,2EAA2E;YAC3E,sEAAsE;YACtE,qDAAqD;YACrD,cAAc;YACd,cAAc;SACf,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACvC,CAAC;CACF"}
|
|
@@ -2,8 +2,8 @@ import { type QuerySpec } from "../dsl.js";
|
|
|
2
2
|
import { Query } from "../query/index.js";
|
|
3
3
|
import { System } from "../system.js";
|
|
4
4
|
import type { WorldOptions } from "./world.base.js";
|
|
5
|
-
import {
|
|
6
|
-
export declare class Pipeline extends
|
|
5
|
+
import { Disposal } from "./world.disposal.js";
|
|
6
|
+
export declare class Pipeline extends Disposal {
|
|
7
7
|
constructor(options?: WorldOptions);
|
|
8
8
|
/** Open a new frame and evaluate every registered tick source once. */
|
|
9
9
|
beginFrame(delta: number): void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { System } from "../system.js";
|
|
2
2
|
import { ALWAYS_TICK_SOURCE } from "../timer.js";
|
|
3
|
-
import {
|
|
4
|
-
export class Pipeline extends
|
|
3
|
+
import { Disposal } from "./world.disposal.js";
|
|
4
|
+
export class Pipeline extends Disposal {
|
|
5
5
|
constructor(options = {}) {
|
|
6
6
|
super(options);
|
|
7
7
|
/** @internal Flat system execution pipeline, rebuilt from `_pipelineQuery`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"world.pipeline.js","sourceRoot":"","sources":["../../src/world/world.pipeline.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,kBAAkB,EAAoB,MAAM,aAAa,CAAC;AAEnE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"world.pipeline.js","sourceRoot":"","sources":["../../src/world/world.pipeline.ts"],"names":[],"mappings":"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"}
|
package/docs/README.md
CHANGED
|
@@ -16,7 +16,8 @@ Read in this order the first time through:
|
|
|
16
16
|
4. Subsystem guides, in any order:
|
|
17
17
|
- [Components](./components.md) — registration, hooks, traits, exclusivity, cleanup policies,
|
|
18
18
|
interfaces, singletons.
|
|
19
|
-
- [Entities](./entities.md) — creation, lookup, the component API, relationship targets, lifecycle
|
|
19
|
+
- [Entities](./entities.md) — creation, lookup, the component API, relationship targets, lifecycle,
|
|
20
|
+
disposing a world.
|
|
20
21
|
- [Systems](./systems.md) — membership, reactive callbacks, callback options, `each`/`run`, cadence,
|
|
21
22
|
component and relationship injection.
|
|
22
23
|
- [Queries and filters](./queries-and-filters.md) — the query DSL, live queries, one-shot
|
|
@@ -35,13 +36,15 @@ Keep the [Glossary](./glossary.md) at hand for terminology.
|
|
|
35
36
|
|
|
36
37
|
## Map
|
|
37
38
|
|
|
39
|
+
For Node clients that need replicated Phaser poses, see [Headless clients](../../vecs-phaser-client/README.md#headless-clients).
|
|
40
|
+
|
|
38
41
|
| Guide | Covers |
|
|
39
42
|
| -------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
40
43
|
| [getting-started.md](./getting-started.md) | A runnable tutorial from `npm install` to a ticking world. |
|
|
41
44
|
| [concepts.md](./concepts.md) | The core vocabulary and how the pieces relate. |
|
|
42
45
|
| [execution-model.md](./execution-model.md) | `world.progress`, the 8 built-in phases, deferred commands, flush boundaries, event routing. |
|
|
43
46
|
| [components.md](./components.md) | Component registration, hooks, traits, exclusivity, cleanup, aliases, and singletons. |
|
|
44
|
-
| [entities.md](./entities.md) | Entity creation,
|
|
47
|
+
| [entities.md](./entities.md) | Entity creation, mutation, relationship targets, lifecycle, disposal, deferred reads. |
|
|
45
48
|
| [systems.md](./systems.md) | `world.system`, callbacks, injection, ordering, cadence, and tick sources. |
|
|
46
49
|
| [queries-and-filters.md](./queries-and-filters.md) | `QueryDSL`, `Query`, `Filter`, `groupBy`, `ignoreSource`. |
|
|
47
50
|
| [relationships.md](./relationships.md) | Relationships, target creation, projection and ownership, hierarchy, traversal, and cleanup. |
|
|
@@ -56,7 +59,9 @@ sessions, and the client world) lives in the `@vworlds/vecs-wire`, `@vworlds/vec
|
|
|
56
59
|
`@vworlds/vecs-server`, and `@vworlds/vecs-client` packages:
|
|
57
60
|
|
|
58
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.
|
|
59
64
|
- [Sessions and caller context](../../vecs-server/docs/sessions.md) — per-connection RPC context,
|
|
60
65
|
`closeSession`, and exactly-once teardown.
|
|
61
|
-
- [Client world](../../vecs-client/docs/client-world.md) — replication,
|
|
62
|
-
disposal.
|
|
66
|
+
- [Client world](../../vecs-client/docs/client-world.md) — replication, render time,
|
|
67
|
+
`invoke`/`listen`, and disposal.
|
package/docs/components.md
CHANGED
|
@@ -69,8 +69,22 @@ Entity ids are drawn from configurable pools. The defaults
|
|
|
69
69
|
Pass `idPools` to the `World` constructor to change ranges (each entry is an `IdPoolConfig` with
|
|
70
70
|
`name`, `min`, optional `max`, and optional `reuseWaitFrames`). Freed ids wait
|
|
71
71
|
`reuseWaitFrames` frames (default `DEFAULT_ID_REUSE_WAIT_FRAMES = 16`) before reuse, so an id
|
|
72
|
-
released this frame is not immediately recycled. `
|
|
73
|
-
a
|
|
72
|
+
released this frame is not immediately recycled. A frame is one `beginFrame()`, so one
|
|
73
|
+
`progress()`; a wait of `0` frees the id at once. Once the wait is over, allocation hands out free
|
|
74
|
+
ids before never-used ones, lowest first, so a released id is recycled by one of the next entities
|
|
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 `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
|
|
85
|
+
(see [Link with edges, not stored ids](./relationships.md#link-with-edges-not-stored-ids)).
|
|
86
|
+
`world.peekNextEid(poolName)` returns the next id a pool would allocate without reserving it. Id 0
|
|
87
|
+
is reserved.
|
|
74
88
|
|
|
75
89
|
## Hooks: component and relationship edges
|
|
76
90
|
|
|
@@ -103,6 +117,26 @@ command is applied (see [Execution model](./execution-model.md#reactive-routing)
|
|
|
103
117
|
resource management and cross-cutting reactions that aren't worth a system; use systems when the
|
|
104
118
|
reaction belongs to a phase.
|
|
105
119
|
|
|
120
|
+
`remove` also fires when the owning entity is destroyed, and `entity.destroyed` is already `true`
|
|
121
|
+
inside the hook. A dying entity takes no writes: `add`, `set`, `remove`, `modified` and `attach`
|
|
122
|
+
are dropped without an error, and `ensure` throws. Releasing a resource needs no check. A hook that
|
|
123
|
+
also writes to its own entity must skip the write when `entity.destroyed`, and that guard is the
|
|
124
|
+
right one here (relationship hooks follow the same rule, see
|
|
125
|
+
[Relationships — edge hooks](./relationships.md#edge-hooks)):
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
class Shield {}
|
|
129
|
+
class Stats {
|
|
130
|
+
armor = 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
world.component(Shield).on("remove", (entity) => {
|
|
134
|
+
if (!entity.destroyed) {
|
|
135
|
+
entity.ensure(Stats, (stats) => (stats.armor -= 10)); // throws on a dying entity
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
106
140
|
Multiple handlers may be registered for the same event: they fire in registration
|
|
107
141
|
order. Call `on(event, handler)` to append, `on(event, handler, true)` to
|
|
108
142
|
prepend, and `off(event, handler)` to remove a handler by reference. All three
|
|
@@ -129,6 +163,7 @@ world.component(Orange).set(Implements, { interfaces: [Fruit] });
|
|
|
129
163
|
| `Traversable` | Exclusive relationships | Maintains hierarchy depth for cascade ordering |
|
|
130
164
|
| `OwnedTarget` | Exclusive relationships other than `ChildOf` | Owns each explicit target through `ChildOf` and destructive edge replacement |
|
|
131
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) |
|
|
132
167
|
|
|
133
168
|
Traits declare policy; their built-in modules install the hooks and maintain the metadata that
|
|
134
169
|
enforce it. Configure traits in feature-module setup rather than opportunistically inside systems.
|
|
@@ -142,6 +177,11 @@ Do not assume all traits have the same lifecycle:
|
|
|
142
177
|
`ChildOf` child of the declared parent.
|
|
143
178
|
- `Traversable` backfills existing relationship instances.
|
|
144
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)).
|
|
145
185
|
|
|
146
186
|
Treat traits as setup policy unless their documentation explicitly supports runtime changes. Before
|
|
147
187
|
writing custom component-type bookkeeping, check whether an existing trait already expresses the
|
package/docs/design-guide.md
CHANGED
|
@@ -133,11 +133,12 @@ entity.attach(obj); // the GameObject is now a component, keyed by its class
|
|
|
133
133
|
world.component(Phaser.GameObjects.Arc).on("remove", (_entity, obj) => obj.destroy());
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
`onRemove` (below) releases the resource whenever it leaves — whether removed explicitly
|
|
137
|
-
because the entity was destroyed
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
`onRemove` (below) releases the resource whenever it leaves — whether removed explicitly,
|
|
137
|
+
because the entity was destroyed, or because the world was disposed (`world.dispose()` destroys
|
|
138
|
+
every entity the application created; see [Disposing a world](./entities.md#disposing-a-world)).
|
|
139
|
+
When several resource types should share one cleanup hook or be queried together, give them a
|
|
140
|
+
common **interface** with `Implements` (see §7 and the §10 case study). A plain wrapper component
|
|
141
|
+
(`class Handle { obj }`) is still fine when you need extra fields beside the resource.
|
|
141
142
|
|
|
142
143
|
### Component lifecycle hooks: `onAdd` / `onSet` / `onRemove`
|
|
143
144
|
|
|
@@ -176,6 +177,7 @@ world.component(Orange).set(Implements, { interfaces: [Fruit] });
|
|
|
176
177
|
| `Traversable` | An exclusive relationship hierarchy needs depth or cascade ordering |
|
|
177
178
|
| `OwnedTarget` | An exclusive relationship owns its explicitly added target |
|
|
178
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 |
|
|
179
181
|
|
|
180
182
|
Traits declare policy; modules implement it through hooks and metadata. Configure them in module
|
|
181
183
|
setup, not inside systems. Lifecycle differs by trait: `OwnedTarget` and `Singleton` are setup-time
|
|
@@ -183,6 +185,18 @@ policy, `Traversable` backfills, and `Implements` recomputes declarations. Do no
|
|
|
183
185
|
dynamically removable. See the [Components guide](./components.md#component-traits-policy-on-component-entities)
|
|
184
186
|
for the full model and custom-trait guidance.
|
|
185
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
|
+
|
|
186
200
|
### Exclusive component groups
|
|
187
201
|
|
|
188
202
|
`world.setExclusiveComponents(A, B, C)` declares a mutually exclusive set: setting one
|
|
@@ -268,6 +282,23 @@ classes or DSL objects (`{ all: [...] }`, `{ any: [...] }`, `{ parent: X }`, `{
|
|
|
268
282
|
> `.with(...)`, `.without(...)`, and `.update(C)` own membership. `.update(C)` watches change events
|
|
269
283
|
> and also requires `C`, so `.with(A).update(C)` matches entities carrying both `A` and `C`.
|
|
270
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
|
+
|
|
271
302
|
### Reactive callbacks: `enter` / `update` / `exit`
|
|
272
303
|
|
|
273
304
|
- **`enter`** — fires once when an entity _becomes_ a member. Use it for one-time setup
|
|
@@ -281,6 +312,14 @@ classes or DSL objects (`{ all: [...] }`, `{ any: [...] }`, `{ parent: X }`, `{
|
|
|
281
312
|
Its injected components are read from a **snapshot captured at exit time**, so they're still
|
|
282
313
|
resolvable even though they're being removed.
|
|
283
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
|
+
|
|
284
323
|
Choose `onEnter: true` by asking who owns the initial value:
|
|
285
324
|
|
|
286
325
|
- Use **`onEnter: true`** for projection / sync callbacks: copying ECS data into a runtime object,
|
|
@@ -338,7 +377,8 @@ Components listed in the inject tuple but **not** guaranteed by `.with(...)` res
|
|
|
338
377
|
### `each` vs reactive — don't scan when you can react
|
|
339
378
|
|
|
340
379
|
- **`.each([...], cb)`** iterates **every matching entity every tick**. Use it only when you
|
|
341
|
-
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.
|
|
342
382
|
- **Reactive `enter`/`update`/`exit`** fire only on change. For "copy A→B when A changes,"
|
|
343
383
|
reactive `.update(A)` touches _nothing_ on idle entities, where `.each` would scan them all.
|
|
344
384
|
|
|
@@ -350,6 +390,28 @@ Components listed in the inject tuple but **not** guaranteed by `.with(...)` res
|
|
|
350
390
|
.with(PhysicsPosition, RenderPosition).update(PhysicsPosition, (e, p) => e.set(RenderPosition, { x: p.x, y: p.y }));
|
|
351
391
|
```
|
|
352
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
|
+
|
|
353
415
|
### `run` and `interval`
|
|
354
416
|
|
|
355
417
|
- **`.run(cb)`** — a system with no per-entity query; the body runs once per tick. For
|
|
@@ -702,6 +764,11 @@ The lifecycle follows from the relationship and trait:
|
|
|
702
764
|
| `player.remove(Inventory)` | Destroys every current target |
|
|
703
765
|
| Destroy `player` | `ChildOf` destroys the inventory |
|
|
704
766
|
|
|
767
|
+
Prefer this to a component field that holds the child entity, with an `onRemove` hook that destroys
|
|
768
|
+
it. Every operation that drops the edge destroys the child, and destroying the child directly
|
|
769
|
+
removes the edge, so the owner needs no hook and holds nothing that can go stale (see
|
|
770
|
+
[Link with edges, not stored ids](./relationships.md#link-with-edges-not-stored-ids)).
|
|
771
|
+
|
|
705
772
|
Inside deferred execution, keep and mutate the target you created; the queued edge is not visible
|
|
706
773
|
through `target` until flush:
|
|
707
774
|
|
|
@@ -749,6 +816,9 @@ system after whatever produces the entity/companion.
|
|
|
749
816
|
| A god component with many optional fields | Split into small single-purpose components; absence encodes "not applicable." |
|
|
750
817
|
| A large nested `inventory`, `brain`, or progression object | Use an owned target entity when the subobject needs its own components, systems, and lifecycle. |
|
|
751
818
|
| A side `Map<ownerEid, SubsystemState>` | Create a target entity and add an explicit edge; use `Exclusive` + `OwnedTarget` only for destructive ownership. |
|
|
819
|
+
| An `eid` or `Entity` stored in a component field as a link | A relationship edge: vecs cleans it up; a stored eid can name another entity once reused ([why](./relationships.md#link-with-edges-not-stored-ids)). |
|
|
820
|
+
| A component field holding a child entity, destroyed in `onRemove` | `Exclusive` + `OwnedTarget`: dropping the edge destroys the child; no hook, no stale field ([§7](#owned-target-entities-entity-valued-properties)). |
|
|
821
|
+
| `entity.destroyed` guards on an entity looked up from a stored link | A missing edge: replace the link. Keep guards for event buffers and dying-entity hooks ([why](./relationships.md#link-with-edges-not-stored-ids)). |
|
|
752
822
|
| `OwnedTarget` on `CurrentEnemy`, `InCell`, `Team`, or shared state | Use a normal relationship; replacing or removing an owned edge destroys its target. |
|
|
753
823
|
| One owned target used as a collection | Use multiple child/item entities; owned targets model zero-or-one aggregates. |
|
|
754
824
|
| Duplicated `enter` + `update` doing the same thing | One `update({ watch: C, onEnter: true })` when update owns initialization. |
|
|
@@ -774,11 +844,28 @@ system after whatever produces the entity/companion.
|
|
|
774
844
|
replicate a networked component. A writer that runs every tick and copies a value into a networked
|
|
775
845
|
or watched component (simulation output, a derived pose) should compare first and write only when
|
|
776
846
|
the value changed (see §7). An in-place assignment without `modified` notifies nobody.
|
|
847
|
+
- **`getMut` marks the component modified even without a callback and even if you write nothing.**
|
|
848
|
+
Calling it every tick fires `onSet`, `update` watchers and replication every tick. Read with `get`;
|
|
849
|
+
call `getMut` only when a change is intended (see
|
|
850
|
+
[`getMut` and `ensure`](./entities.md#getmut-and-ensure-mutating-in-place)).
|
|
777
851
|
- **A callback that always uses a component should inject it, not `get` it.** `entity.get(C)` is for
|
|
778
852
|
a component that is conditionally needed and rarely required (see §4).
|
|
779
853
|
- **Membership is fixed by `.with(...)` / `.without(...)` / `.update(C)`;** `update(C)` watches `C`
|
|
780
854
|
and requires it.
|
|
855
|
+
- **A system's `enter` reaches only entities still in its query when the system drains.** One that
|
|
856
|
+
left or died first gets neither `enter` nor the paired `exit`, and one that leaves and re-enters
|
|
857
|
+
within a drain gets `enter`, `exit`, `enter`, so keep `enter` setup and `exit` teardown symmetric
|
|
858
|
+
(see [`.enter`](./systems.md#entercallback--enterinject-callback--enteroptions-callback)).
|
|
781
859
|
- **`exit` retains injected references captured at exit** (components are not deep-cloned).
|
|
860
|
+
- **A `remove` hook also fires while the entity is being destroyed,** with `entity.destroyed`
|
|
861
|
+
already `true`. A dying entity takes no writes (`add`, `set`, `remove`, `modified` and `attach` are
|
|
862
|
+
dropped, `ensure` throws), so a hook that writes to its own entity guards on `entity.destroyed`.
|
|
863
|
+
That guard is legitimate; elsewhere a `destroyed` check usually signals a missing edge (see §8 and
|
|
864
|
+
[Hooks](./components.md#hooks-component-and-relationship-edges)).
|
|
865
|
+
- **Release resources in `remove` hooks, not in a system's `exit` callback.** `world.dispose()`
|
|
866
|
+
runs the hooks during the call, but a system's `exit` callback only queues its event, and a
|
|
867
|
+
disposed world never runs the system again (see
|
|
868
|
+
[Disposing a world](./entities.md#disposing-a-world)).
|
|
782
869
|
- **Within a phase, order = registration order;** across phases, the spine guarantees order. Don't
|
|
783
870
|
use registration order for cross-module correctness — use phases.
|
|
784
871
|
- **Networked component order = wire protocol.** Reordering breaks the protocol.
|
|
@@ -789,6 +876,9 @@ system after whatever produces the entity/companion.
|
|
|
789
876
|
ordering; `hasTarget(R, target)` tests one edge.
|
|
790
877
|
- **Relationship edges are explicit.** Add with `add(R, target)`, remove one with
|
|
791
878
|
`remove(R, target)`, and remove all with `remove(R)`. Do not use `set` for target storage.
|
|
879
|
+
- **Relationship removal hooks differ in scope.** `on("targetRemove")` fires once per removed edge;
|
|
880
|
+
`on("remove")` fires once, when the relationship component itself leaves, with no targets left on
|
|
881
|
+
the instance (see [Edge hooks](./relationships.md#edge-hooks)).
|
|
792
882
|
- **Relationship cardinality and component exclusivity are different.** Add the `Exclusive` trait
|
|
793
883
|
to a relationship for at-most-one target; use `setExclusiveComponents` for mutually exclusive
|
|
794
884
|
component types.
|
|
@@ -810,6 +900,42 @@ system after whatever produces the entity/companion.
|
|
|
810
900
|
explicitly, and during deferred execution keep that entity instead of re-reading `target(R)`
|
|
811
901
|
before flush.
|
|
812
902
|
- **`world.module(M)` runs `init` once.** Re-registering with a different config throws.
|
|
903
|
+
- **vecs-phaser: a `DrawBy` child follows only its parent's `Position`.** It turns with the parent
|
|
904
|
+
only with `InheritRotation`, which makes the resolver derive its `Rotation` every frame, so
|
|
905
|
+
writing `Rotation` on a tagged child has no effect (see
|
|
906
|
+
[Turning with the parent](../../vecs-phaser-client/README.md#turning-with-the-parent)).
|
|
907
|
+
- **vecs-phaser: `DrawBy` and `DrawIn` use `CleanupPolicy.Remove`,** so destroying a display parent
|
|
908
|
+
detaches its children instead of destroying them, unlike `ChildOf`, which deletes them. A display
|
|
909
|
+
child that must die with its parent also needs `ChildOf` to it, or must be its owned target (see
|
|
910
|
+
[Destroying the display parent](../../vecs-phaser-client/README.md#destroying-the-display-parent)).
|
|
911
|
+
- **vecs-physics, vecs-phaser-server: pose writes are change-only.** `set` hooks and `update` systems
|
|
912
|
+
on the physics and render pose fire when the pose changed, not on every step; to visit every body
|
|
913
|
+
every step, iterate with `each` (see
|
|
914
|
+
[Pose Updates Fire Only When The Pose Changes](../../vecs-physics/docs/systems-and-phases.md#pose-updates-fire-only-when-the-pose-changes)).
|
|
915
|
+
- **vecs-phaser-server: game code owns the physics pose; the render pose is protocol-private.** On
|
|
916
|
+
the server, read and write the physics `Position` and `Rotation` of every entity, including one
|
|
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
|
|
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)).
|
|
930
|
+
- **vecs-client: attach a `ClientWorld` socket only after `socket.connect()` resolves,** so no ack
|
|
931
|
+
precedes the data channel (`connectDgram` does this). Pending `invoke` calls reject with
|
|
932
|
+
`RPC_CLOSED_CODE` on disconnect or `dispose` (see
|
|
933
|
+
[Connecting](../../vecs-client/docs/client-world.md#connecting) and
|
|
934
|
+
[Disconnection and disposal](../../vecs-client/docs/client-world.md#disconnection-and-disposal)).
|
|
935
|
+
- **vecs-physics: component fields hold plain data,** never Box2D wrappers or other class
|
|
936
|
+
instances. `set` accepts them, but snapshot capture (`snapshotSubtree`, `moveSubtree`) rejects
|
|
937
|
+
non-plain values (see
|
|
938
|
+
[Keep Component Fields Plain Data](../../vecs-physics/docs/components.md#keep-component-fields-plain-data)).
|
|
813
939
|
|
|
814
940
|
---
|
|
815
941
|
|
package/docs/entities.md
CHANGED
|
@@ -39,7 +39,8 @@ setting `entity.name` writes the `Identity` component, and duplicate names are l
|
|
|
39
39
|
`world.entities` is a read-only view of every live entity keyed by id, and `world.clearAllEntities()`
|
|
40
40
|
destroys **every** entity in the world — ordinary entities first, then component entities once
|
|
41
41
|
their carriers are gone. It is a full wipe (systems and phases are backed by entities too); use it
|
|
42
|
-
for end-of-session teardown, not selective resets.
|
|
42
|
+
for end-of-session teardown, not selective resets. To destroy only what the application created
|
|
43
|
+
and keep the world's own entities, see [Disposing a world](#disposing-a-world).
|
|
43
44
|
|
|
44
45
|
## The component API
|
|
45
46
|
|
|
@@ -95,6 +96,14 @@ e.getMut(Velocity, (vel) => {
|
|
|
95
96
|
});
|
|
96
97
|
```
|
|
97
98
|
|
|
99
|
+
`getMut` marks the component modified whenever it is present, with or without a callback and whether
|
|
100
|
+
or not you assign anything: the callback form marks it after the callback returns, and the
|
|
101
|
+
callback-free form marks it before it returns the instance, so even a read-only use notifies. The
|
|
102
|
+
mark fires `onSet`, routes an `update` event to every system watching the component and replicates a
|
|
103
|
+
networked component (repeated marks before the queue drains coalesce into one). A `getMut` that runs
|
|
104
|
+
every tick therefore does all of that every tick. Read with `get`, and call `getMut` only when a
|
|
105
|
+
change is intended. When the component is absent, `getMut` returns `undefined` and marks nothing.
|
|
106
|
+
|
|
98
107
|
Use `ensure` when absence should mean "create with defaults, then mutate":
|
|
99
108
|
|
|
100
109
|
```ts
|
|
@@ -174,6 +183,66 @@ and policy surface as a registered class, and `keyEntity.on("add" / "set" / "rem
|
|
|
174
183
|
"targetRemove", handler)` registers its lifecycle handlers directly. This is how tag-like,
|
|
175
184
|
runtime-created component types work without declaring a class.
|
|
176
185
|
|
|
186
|
+
## Disposing a world
|
|
187
|
+
|
|
188
|
+
When you are finished with a world — a scene or route change, a hot reload — call
|
|
189
|
+
`world.dispose()` and drop the reference. It destroys every entity the application and its modules
|
|
190
|
+
created, so the `remove` hook of each component they carry runs and releases what the component
|
|
191
|
+
owns: a render object, a handle, a timer.
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
class Handle {
|
|
195
|
+
public released = false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
world.component(Handle).on("remove", (_entity, handle) => {
|
|
199
|
+
handle.released = true; // release the resource here
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const handle = new Handle();
|
|
203
|
+
world.entity().attach(handle);
|
|
204
|
+
|
|
205
|
+
world.dispose();
|
|
206
|
+
handle.released; // true
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Each entity goes through the ordinary destroy path. Every `remove` hook fires once,
|
|
210
|
+
[cleanup policies and cascades](./relationships.md#cleanup-when-a-target-dies) run as for any
|
|
211
|
+
`destroy()`, and an entity that a `ChildOf` or `OwnedTarget` cascade, or a hook, has already
|
|
212
|
+
destroyed is skipped instead of destroyed twice.
|
|
213
|
+
|
|
214
|
+
`dispose()` leaves the world's infrastructure in place, because the world needs it to stay
|
|
215
|
+
coherent. It recognizes it as follows (`lib/vecs/src/world/world.disposal.ts`):
|
|
216
|
+
|
|
217
|
+
| Infrastructure | Recognized by |
|
|
218
|
+
| ----------------------------- | ------------------------------------------------------------- |
|
|
219
|
+
| Component and module entities | They are `Component` instances; `Module` extends `Component`. |
|
|
220
|
+
| Query and system entities | They carry the `Query` component. |
|
|
221
|
+
| Phase entities | They carry the `Phase` component. |
|
|
222
|
+
|
|
223
|
+
What those entities hold stays with them: data stored on a component entity (a singleton set with
|
|
224
|
+
`world.set`) and any entity they still carry as a [component key](#entities-as-component-keys),
|
|
225
|
+
which cannot be destroyed while it is in use. Release those yourself; their `remove` hooks do not
|
|
226
|
+
run. `clearAllEntities()` is the full wipe that takes the infrastructure too.
|
|
227
|
+
|
|
228
|
+
- **Order.** Among unrelated entities the order is unspecified, so do not make a hook depend on it.
|
|
229
|
+
An entity used as a component key is destroyed after every entity that carries it, because
|
|
230
|
+
destroying it first would throw under the default cleanup policy.
|
|
231
|
+
- **Pending commands.** `dispose()` neither flushes the command queue early nor discards it. Commands
|
|
232
|
+
queued ahead of it run first, in order, and the destruction follows in the same queue, so each
|
|
233
|
+
hook fires once however the component left. At top level nothing is queued, and `dispose()`
|
|
234
|
+
returns with the entities gone. Inside a system, a hook or a `defer` block the world is
|
|
235
|
+
[deferred](./execution-model.md#deferred-mutation-and-flush-boundaries), so `dispose()` queues the
|
|
236
|
+
destruction as `destroy()` does, and it completes when the queue next drains. An entity whose
|
|
237
|
+
creation is still queued is created, then destroyed.
|
|
238
|
+
- **After disposal.** A disposed world must not be used again. Nothing enforces it — no other method
|
|
239
|
+
checks, so `entity()` and `progress()` keep their cost. Calling `dispose()` again, or from a hook
|
|
240
|
+
during the teardown, does nothing. An entity that a `remove` hook creates during the teardown is
|
|
241
|
+
left behind, so do not spawn from teardown hooks. A system's `exit` callback only queues its event,
|
|
242
|
+
and a disposed world never runs the system again; release resources in `remove` hooks, as
|
|
243
|
+
[Resources as components](./design-guide.md#resources-as-components-hold-non-ecs-objects)
|
|
244
|
+
recommends.
|
|
245
|
+
|
|
177
246
|
## Relationships, in brief
|
|
178
247
|
|
|
179
248
|
`e.target(Rel)` returns one current target, `e.targets(Rel)` a readonly snapshot of every target,
|
package/docs/execution-model.md
CHANGED
|
@@ -162,7 +162,7 @@ component:
|
|
|
162
162
|
their next run (`lib/vecs/src/system.ts`). So systems react to change without scanning, and they
|
|
163
163
|
react _in their own phase slot_, in event arrival order.
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
Four routing rules worth memorizing:
|
|
166
166
|
|
|
167
167
|
- **`update(C)` is change-only unless you opt into entry.** When an entity enters a system,
|
|
168
168
|
`{ onEnter: true }` update watchers push an `update` event for already-present watched components.
|
|
@@ -173,8 +173,9 @@ Three routing rules worth memorizing:
|
|
|
173
173
|
objects are not deep-cloned.
|
|
174
174
|
- **`enter` is drain-time truthful.** A system only receives `enter` when the entity is still a
|
|
175
175
|
member when that system runs. If it left before the inbox drains, vecs drops both that queued
|
|
176
|
-
`enter` and its paired `exit`; an entity that re-enters before draining
|
|
177
|
-
|
|
176
|
+
`enter` and its paired `exit`; an entity that re-enters before draining is a member again, so the
|
|
177
|
+
queued events replay in order as `enter`, `exit`, `enter`. The details are in
|
|
178
|
+
[Systems — `.enter`](./systems.md#entercallback--enterinject-callback--enteroptions-callback).
|
|
178
179
|
- **In-place mutation needs `modified`.** Assigning to component fields directly notifies no one.
|
|
179
180
|
Call `entity.modified(C)` (or use `set` / `getMut` / `ensure`) to route the change.
|
|
180
181
|
|
package/docs/glossary.md
CHANGED
|
@@ -38,6 +38,10 @@ behavior. [Components](./components.md).
|
|
|
38
38
|
queued as commands; `flush` drains the queue at the scope boundary (after each system in the
|
|
39
39
|
pipeline). [Execution model](./execution-model.md#deferred-mutation-and-flush-boundaries).
|
|
40
40
|
|
|
41
|
+
**Dispose** — `world.dispose()`: end a world's life by destroying every entity the application and
|
|
42
|
+
its modules created, so their components' `remove` hooks run. The infrastructure (component,
|
|
43
|
+
module, query, system, and phase entities) stays. [Entities](./entities.md#disposing-a-world).
|
|
44
|
+
|
|
41
45
|
**Entity** — a unique numeric id (`eid`) owned by a world, with a set of components attached.
|
|
42
46
|
[Entities](./entities.md).
|
|
43
47
|
|
|
@@ -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
|
@@ -33,6 +33,36 @@ again is idempotent. Target ordering is not a public contract.
|
|
|
33
33
|
storage. For a non-exclusive relationship, do not use `target(ref)` as a stable selection policy.
|
|
34
34
|
Use `hasTarget(ref, target)` to test one edge without allocating a snapshot.
|
|
35
35
|
|
|
36
|
+
## Link with edges, not stored ids
|
|
37
|
+
|
|
38
|
+
Link one entity to another with a relationship edge, not with an `eid` or an `Entity` kept in a
|
|
39
|
+
component field (`ownerEid = 0`, `target: Entity | undefined`). The world maintains an edge. It
|
|
40
|
+
cannot maintain a field:
|
|
41
|
+
|
|
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, `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
|
|
46
|
+
never-used ones, so one of the next entities created takes it. Until an entity does,
|
|
47
|
+
`world.getEntity(id)` returns `undefined`; from then on it returns that live, unrelated entity and
|
|
48
|
+
the stale id looks valid.
|
|
49
|
+
- **A stored `Entity` dangles.** Reuse never makes it name another entity, because a destroyed
|
|
50
|
+
`Entity` stays `destroyed`, but nothing clears the field when the entity dies. Writes through it
|
|
51
|
+
are dropped and `ensure` throws.
|
|
52
|
+
- **An edge is cleaned up for you.** When the target is destroyed, `onDeleteTarget` removes the edge
|
|
53
|
+
or destroys the source (see [Cleanup when a target dies](#cleanup-when-a-target-dies)). The
|
|
54
|
+
reverse index (`children`), `target` and `parent` queries, `groupBy` and network replication work
|
|
55
|
+
on edges only.
|
|
56
|
+
- **A field does not cross worlds.** `snapshotSubtree` and `moveSubtree` remap relationship targets
|
|
57
|
+
onto the destination's new ids. An eid field is copied as is and names the wrong entity there; an
|
|
58
|
+
`Entity` field makes capture throw (see [Snapshot policies](./snapshots.md#snapshot-policies)).
|
|
59
|
+
|
|
60
|
+
A `destroyed` check on an entity you looked up from a stored link is therefore a symptom of a
|
|
61
|
+
missing edge: replace the link, not only the check. The guard stays right where a reference is
|
|
62
|
+
deliberately short-lived: payloads in per-step event buffers (the `other` entity in `vecs-physics`
|
|
63
|
+
contact events), and hooks that run while an entity or an edge target is being destroyed (see
|
|
64
|
+
[Edge hooks](#edge-hooks) and [component hooks](./components.md#hooks-component-and-relationship-edges)).
|
|
65
|
+
|
|
36
66
|
## Built-in relationships
|
|
37
67
|
|
|
38
68
|
| Class | Purpose |
|
|
@@ -192,6 +222,26 @@ calls `onRemove`. An exclusive replacement calls `onTargetRemove` for the old ta
|
|
|
192
222
|
relationship-dependent query updates, but do not call `onSet`; `onSet` is reserved for ordinary
|
|
193
223
|
relationship data changed through `set`.
|
|
194
224
|
|
|
225
|
+
Pick the removal hook by scope:
|
|
226
|
+
|
|
227
|
+
- `on("targetRemove", handler)` runs once for **each** removed edge: `remove(R, target)`, the
|
|
228
|
+
displaced edge of an `Exclusive` replacement, and every edge when the whole relationship goes away
|
|
229
|
+
(the final edge, `remove(R)`, or destroying the source).
|
|
230
|
+
- `on("remove", handler)` runs **once**, when the relationship component itself leaves the entity.
|
|
231
|
+
It does not run when one of several edges is removed or an `Exclusive` edge is replaced. The
|
|
232
|
+
relationship instance it receives holds no targets any more, so use `targetRemove` to learn which
|
|
233
|
+
targets went.
|
|
234
|
+
|
|
235
|
+
When the whole relationship goes away, `source.has(R)` is already `false` inside both hooks. When
|
|
236
|
+
only one of several edges goes, `source.has(R)` stays `true` and `source.hasTarget(R, removed)` is
|
|
237
|
+
`false`.
|
|
238
|
+
|
|
239
|
+
Destroying either end of an edge runs these hooks too, so `source.destroyed` or `target.destroyed`
|
|
240
|
+
can already be `true` inside them; both are under `Delete` cleanup. The target of an `OwnedTarget`
|
|
241
|
+
edge is destroyed before the public hook runs on every path. A dying end takes no writes (see
|
|
242
|
+
[Hooks](./components.md#hooks-component-and-relationship-edges)), so a handler that writes to
|
|
243
|
+
`source` or `target` guards on `destroyed`.
|
|
244
|
+
|
|
195
245
|
Register handlers with `on("targetAdd", handler)` and `on("targetRemove", handler)`; remove them
|
|
196
246
|
with `off(event, handler)`. Multiple handlers may be registered for the same event and fire in
|
|
197
247
|
registration order. Framework traits install separate internal `targetPolicyAdd` /
|
|
@@ -227,6 +277,11 @@ attached: removing it throws, and only world teardown (`clearAllEntities`) bypas
|
|
|
227
277
|
the same contract as `Traversable` and `OneOf`. Do not share an owned target between
|
|
228
278
|
sources because either owner can destroy it when its edge is removed.
|
|
229
279
|
|
|
280
|
+
An owned target replaces the field-plus-hook pattern, where a component field holds the child
|
|
281
|
+
entity and an `onRemove` hook destroys it. The edge is the field: every path that drops it destroys
|
|
282
|
+
the child, and destroying the child directly removes the edge from its owner. There is no hook to
|
|
283
|
+
write and no stale reference to guard.
|
|
284
|
+
|
|
230
285
|
## Cleanup when a target dies
|
|
231
286
|
|
|
232
287
|
What happens to each incoming edge when its target entity is destroyed is governed by the
|
package/docs/snapshots.md
CHANGED
|
@@ -83,8 +83,11 @@ class Position {
|
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
`Position` has no policy, so it uses **`ClonePublic`**: a fresh instance with the source instance's
|
|
86
|
-
own enumerable fields copied over.
|
|
87
|
-
|
|
86
|
+
own enumerable fields copied over, deeply. The copy covers primitives, arrays, plain objects, `Map`,
|
|
87
|
+
`Set`, `Date`, `ArrayBuffer` and typed arrays, so the destination never aliases a nested value of
|
|
88
|
+
the source. Anything else, such as a class instance, a function or a symbol, is not plain data:
|
|
89
|
+
capture throws with the component and field path instead of sharing it across worlds. A component
|
|
90
|
+
that holds one declares `CustomSnapshot` or `DontSnapshot` (below).
|
|
88
91
|
|
|
89
92
|
### `DontSnapshot`
|
|
90
93
|
|
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)).
|
|
@@ -89,10 +90,15 @@ Fires once when an entity starts matching the system:
|
|
|
89
90
|
.enter({ inject: [Position, Sprite] }, (e, [pos, sprite]) => sprite.setPosition(pos.x, pos.y))
|
|
90
91
|
```
|
|
91
92
|
|
|
92
|
-
The callback runs only if the entity is still a member when this system drains its inbox
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
The callback runs only if the entity is still a member when this system drains its inbox, at the top
|
|
94
|
+
of its next run. The inbox replays in arrival order, so:
|
|
95
|
+
|
|
96
|
+
- An entity that left the query or was destroyed first gets no `enter`, and in a system that
|
|
97
|
+
registers both callbacks no paired `exit` either. Do not add a `destroyed` guard to normal `enter`
|
|
98
|
+
setup code. A system with only `exit` has no `enter` to pair, so it still gets that `exit`.
|
|
99
|
+
- An entity that leaves and re-enters before the drain is a member again, so the system gets
|
|
100
|
+
`enter`, `exit`, `enter` (just `exit`, `enter` when it had already entered in an earlier drain).
|
|
101
|
+
Keep `enter` setup and `exit` teardown symmetric so the second `enter` starts from a clean state.
|
|
96
102
|
|
|
97
103
|
For systems, injected values and `Iter.src` are captured when entry is routed and replayed from that
|
|
98
104
|
snapshot when the system next drains its inbox.
|
|
@@ -126,6 +132,25 @@ Use `.update(C, ...)` by itself when `C` is the membership shape and watched val
|
|
|
126
132
|
A query or system can register only one `update(C, ...)` callback for each component. A duplicate
|
|
127
133
|
registration throws instead of replacing the existing callback.
|
|
128
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
|
+
|
|
129
154
|
### `.exit(callback)` / `.exit(inject, callback)` / `.exit(options, callback)`
|
|
130
155
|
|
|
131
156
|
Fires when an entity stops matching (component removed, or entity destroyed). Directly injected
|
|
@@ -144,7 +169,9 @@ object remain visible:
|
|
|
144
169
|
### `.each(components, callback)` / `.each(options, callback)`
|
|
145
170
|
|
|
146
171
|
Fires every tick the system runs, once per tracked entity, regardless of change. Use it only for
|
|
147
|
-
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
|
|
148
175
|
[Design guide](./design-guide.md#each-vs-reactive--dont-scan-when-you-can-react)):
|
|
149
176
|
|
|
150
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
|
|
@@ -65,6 +69,69 @@ Check the design guide before introducing:
|
|
|
65
69
|
If the requested design conflicts with a documented vecs invariant, explain the conflict and use
|
|
66
70
|
the documented pattern unless the user explicitly chooses otherwise.
|
|
67
71
|
|
|
72
|
+
## Known Gotchas
|
|
73
|
+
|
|
74
|
+
Agents trip on these vecs behaviors repeatedly. Design guide sections 8 and 9 list them with the
|
|
75
|
+
other anti-patterns and pitfalls; each rule below links to the detail.
|
|
76
|
+
|
|
77
|
+
- Do not call `getMut` per tick unless a change is intended. It marks the component modified even
|
|
78
|
+
without a callback and even if nothing is written, which fires `update` watchers and replication.
|
|
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))
|
|
89
|
+
- A system's `enter` reaches only entities still in its query when the system drains. An entity
|
|
90
|
+
that left the query or died first gets neither `enter` nor the paired `exit`, and a re-entry
|
|
91
|
+
within one drain gives `enter`, `exit`, `enter`, so keep setup and teardown symmetric.
|
|
92
|
+
([`.enter`](../../docs/systems.md#entercallback--enterinject-callback--enteroptions-callback))
|
|
93
|
+
- Create relationship edges with `add(Rel, target)` and read them with `target()` / `targets()`.
|
|
94
|
+
React to the relationship component being removed with `on("remove")` and to each removed edge
|
|
95
|
+
with `on("targetRemove")`. `OwnedTarget` needs `Exclusive` and an explicitly created target, and
|
|
96
|
+
destroys that target when its edge is removed.
|
|
97
|
+
([Edge hooks](../../docs/relationships.md#edge-hooks),
|
|
98
|
+
[Owned targets](../../docs/relationships.md#owned-targets))
|
|
99
|
+
- Link entities with relationship edges, never with an `eid` or `Entity` kept in a component field.
|
|
100
|
+
A destroyed entity's eid is reused after `reuseWaitFrames` (16 frames by default, 0 for replicated
|
|
101
|
+
entities on a `ClientWorld`), so a stale eid can name an unrelated entity. A stored `Entity`
|
|
102
|
+
dangles, and snapshots copy an eid unchanged and reject an `Entity`.
|
|
103
|
+
([Link with edges, not stored ids](../../docs/relationships.md#link-with-edges-not-stored-ids))
|
|
104
|
+
- Prefer `Exclusive` + `OwnedTarget` to a component field that holds a child entity. Removing or
|
|
105
|
+
replacing the edge destroys the child and destroying the child removes the edge, so no remove hook
|
|
106
|
+
is needed. ([Owned targets](../../docs/relationships.md#owned-targets))
|
|
107
|
+
- A `remove` hook also fires while its entity is being destroyed, with `entity.destroyed` already
|
|
108
|
+
`true`. A dying entity drops `add`, `set`, `remove`, `modified` and `attach` writes and `ensure`
|
|
109
|
+
throws, so guard a hook's writes to its own entity with `entity.destroyed`; that guard is
|
|
110
|
+
legitimate. ([Hooks](../../docs/components.md#hooks-component-and-relationship-edges))
|
|
111
|
+
- A `destroyed` check outside per-step event buffers and hooks on a dying entity or edge target
|
|
112
|
+
usually means a link that should be a relationship edge. Replace the link, not only add the check.
|
|
113
|
+
([Link with edges, not stored ids](../../docs/relationships.md#link-with-edges-not-stored-ids))
|
|
114
|
+
- `@vworlds/vecs-phaser`: a `DrawBy` child turns with its parent only with `InheritRotation`. The
|
|
115
|
+
resolver derives a tagged child's `Rotation` every frame, so writing `Rotation` on it has no
|
|
116
|
+
effect; set `InheritRotation.angle` instead.
|
|
117
|
+
([Turning with the parent](../../../vecs-phaser-client/README.md#turning-with-the-parent))
|
|
118
|
+
- `@vworlds/vecs-phaser`: `DrawBy` and `DrawIn` use `CleanupPolicy.Remove`, so destroying a display
|
|
119
|
+
parent detaches its children and leaves them alive, unlike `ChildOf`. Add `ChildOf` to the parent,
|
|
120
|
+
or make the child an owned target, when it must die with the parent.
|
|
121
|
+
([Destroying the display parent](../../../vecs-phaser-client/README.md#destroying-the-display-parent))
|
|
122
|
+
- `@vworlds/vecs-physics` and `@vworlds/vecs-phaser-server`: pose `set` hooks and `update` systems
|
|
123
|
+
on the physics and render pose fire only when the pose changes. To visit every body every step,
|
|
124
|
+
iterate with `each`.
|
|
125
|
+
([Pose updates](../../../vecs-physics/docs/systems-and-phases.md#pose-updates-fire-only-when-the-pose-changes))
|
|
126
|
+
- `@vworlds/vecs-client`: attach a `ClientWorld` socket only after `socket.connect()`, so no ack
|
|
127
|
+
precedes the data channel (`connectDgram` does this). Pending `invoke` calls reject with
|
|
128
|
+
`RPC_CLOSED_CODE` on disconnect or dispose.
|
|
129
|
+
([Connecting](../../../vecs-client/docs/client-world.md#connecting),
|
|
130
|
+
[Disconnection and disposal](../../../vecs-client/docs/client-world.md#disconnection-and-disposal))
|
|
131
|
+
- `@vworlds/vecs-physics` components hold plain data only, never Box2D wrappers or other class
|
|
132
|
+
instances: snapshot capture rejects non-plain values.
|
|
133
|
+
([Plain data](../../../vecs-physics/docs/components.md#keep-component-fields-plain-data))
|
|
134
|
+
|
|
68
135
|
## Implement And Verify
|
|
69
136
|
|
|
70
137
|
1. Reuse existing components and modules where their semantics match; do not reuse them merely
|