@vworlds/vecs 1.0.48 → 1.0.50
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/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 -1
- 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 +5 -4
- package/docs/components.md +28 -2
- package/docs/design-guide.md +126 -14
- package/docs/entities.md +70 -1
- package/docs/execution-model.md +4 -3
- package/docs/glossary.md +4 -0
- package/docs/relationships.md +54 -0
- package/docs/snapshots.md +5 -2
- package/docs/systems.md +12 -4
- package/package.json +1 -1
- package/skills/vecs/SKILL.md +54 -0
|
@@ -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
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":"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"}
|
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
|
|
@@ -41,7 +42,7 @@ Keep the [Glossary](./glossary.md) at hand for terminology.
|
|
|
41
42
|
| [concepts.md](./concepts.md) | The core vocabulary and how the pieces relate. |
|
|
42
43
|
| [execution-model.md](./execution-model.md) | `world.progress`, the 8 built-in phases, deferred commands, flush boundaries, event routing. |
|
|
43
44
|
| [components.md](./components.md) | Component registration, hooks, traits, exclusivity, cleanup, aliases, and singletons. |
|
|
44
|
-
| [entities.md](./entities.md) | Entity creation,
|
|
45
|
+
| [entities.md](./entities.md) | Entity creation, mutation, relationship targets, lifecycle, disposal, deferred reads. |
|
|
45
46
|
| [systems.md](./systems.md) | `world.system`, callbacks, injection, ordering, cadence, and tick sources. |
|
|
46
47
|
| [queries-and-filters.md](./queries-and-filters.md) | `QueryDSL`, `Query`, `Filter`, `groupBy`, `ignoreSource`. |
|
|
47
48
|
| [relationships.md](./relationships.md) | Relationships, target creation, projection and ownership, hierarchy, traversal, and cleanup. |
|
|
@@ -58,5 +59,5 @@ sessions, and the client world) lives in the `@vworlds/vecs-wire`, `@vworlds/vec
|
|
|
58
59
|
- [Remote procedure calls](../../vecs-protocol/docs/rpc.md) — the call channel and typed errors.
|
|
59
60
|
- [Sessions and caller context](../../vecs-server/docs/sessions.md) — per-connection RPC context,
|
|
60
61
|
`closeSession`, and exactly-once teardown.
|
|
61
|
-
- [Client world](../../vecs-client/docs/client-world.md) — replication,
|
|
62
|
-
disposal.
|
|
62
|
+
- [Client world](../../vecs-client/docs/client-world.md) — replication, render time,
|
|
63
|
+
`invoke`/`listen`, and disposal.
|
package/docs/components.md
CHANGED
|
@@ -69,8 +69,14 @@ 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 recycled id is why a stored id is not a safe link between entities
|
|
77
|
+
(see [Link with edges, not stored ids](./relationships.md#link-with-edges-not-stored-ids)).
|
|
78
|
+
`world.peekNextEid(poolName)` returns the next id a pool would allocate without reserving it. Id 0
|
|
79
|
+
is reserved.
|
|
74
80
|
|
|
75
81
|
## Hooks: component and relationship edges
|
|
76
82
|
|
|
@@ -103,6 +109,26 @@ command is applied (see [Execution model](./execution-model.md#reactive-routing)
|
|
|
103
109
|
resource management and cross-cutting reactions that aren't worth a system; use systems when the
|
|
104
110
|
reaction belongs to a phase.
|
|
105
111
|
|
|
112
|
+
`remove` also fires when the owning entity is destroyed, and `entity.destroyed` is already `true`
|
|
113
|
+
inside the hook. A dying entity takes no writes: `add`, `set`, `remove`, `modified` and `attach`
|
|
114
|
+
are dropped without an error, and `ensure` throws. Releasing a resource needs no check. A hook that
|
|
115
|
+
also writes to its own entity must skip the write when `entity.destroyed`, and that guard is the
|
|
116
|
+
right one here (relationship hooks follow the same rule, see
|
|
117
|
+
[Relationships — edge hooks](./relationships.md#edge-hooks)):
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
class Shield {}
|
|
121
|
+
class Stats {
|
|
122
|
+
armor = 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
world.component(Shield).on("remove", (entity) => {
|
|
126
|
+
if (!entity.destroyed) {
|
|
127
|
+
entity.ensure(Stats, (stats) => (stats.armor -= 10)); // throws on a dying entity
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
106
132
|
Multiple handlers may be registered for the same event: they fire in registration
|
|
107
133
|
order. Call `on(event, handler)` to append, `on(event, handler, true)` to
|
|
108
134
|
prepend, and `off(event, handler)` to remove a handler by reference. All three
|
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
|
|
|
@@ -305,7 +306,8 @@ Choose `onEnter: true` by asking who owns the initial value:
|
|
|
305
306
|
|
|
306
307
|
### Injection
|
|
307
308
|
|
|
308
|
-
Pull
|
|
309
|
+
**Inject every component a system callback always uses.** Pull it into the callback instead of
|
|
310
|
+
`entity.get()`-ing it by hand:
|
|
309
311
|
|
|
310
312
|
```ts
|
|
311
313
|
.enter([Arc], (entity, [arc]) => { /* … */ })
|
|
@@ -314,6 +316,23 @@ Pull components into the callback instead of `entity.get()`-ing them by hand:
|
|
|
314
316
|
.each([Position, Velocity], (entity, [pos, vel]) => { /* … */ })
|
|
315
317
|
```
|
|
316
318
|
|
|
319
|
+
Use `entity.get(C)` only for a component that is conditionally needed and rarely required: a read on
|
|
320
|
+
a branch most calls never take, where resolving the component for every call would be waste.
|
|
321
|
+
|
|
322
|
+
Why:
|
|
323
|
+
|
|
324
|
+
- **The signature states the inputs.** What a callback reads sits next to the `.with(...)` that
|
|
325
|
+
guarantees it, not in the body.
|
|
326
|
+
- **No dead guards.** A component guaranteed by `.with(...)` is injected as non-nullable, so there
|
|
327
|
+
is no `undefined` to check. `get` returns `Readonly<C> | undefined`, which invites an
|
|
328
|
+
`if (c !== undefined)` that tells the next reader the component might be missing when the query
|
|
329
|
+
says it cannot be. An injected instance is also the live, mutable one: a callback that writes in
|
|
330
|
+
place and calls `entity.modified(C)` (see §7) needs no cast.
|
|
331
|
+
- **Resolved once.** Injection resolves a component's id when the callback is registered;
|
|
332
|
+
`entity.get(C)` resolves the class on every call. In an `exit` callback injection matters more
|
|
333
|
+
still: it reads the snapshot captured when the entity left, while a `get` at replay time can find
|
|
334
|
+
the component already removed.
|
|
335
|
+
|
|
317
336
|
Components listed in the inject tuple but **not** guaranteed by `.with(...)` resolve as
|
|
318
337
|
`T | undefined` (the types reflect this) — guard them; the ones in `.with` are always present.
|
|
319
338
|
|
|
@@ -328,7 +347,7 @@ Components listed in the inject tuple but **not** guaranteed by `.with(...)` res
|
|
|
328
347
|
// ❌ scans every body every tick, even if it didn't move
|
|
329
348
|
.with(PhysicsPosition, RenderPosition).each([...], (e, [...]) => { if (changed) e.set(...) });
|
|
330
349
|
|
|
331
|
-
// ✅ fires only when physics actually moved the body
|
|
350
|
+
// ✅ fires only when physics actually moved the body (compare before writing a networked B: see §7)
|
|
332
351
|
.with(PhysicsPosition, RenderPosition).update(PhysicsPosition, (e, p) => e.set(RenderPosition, { x: p.x, y: p.y }));
|
|
333
352
|
```
|
|
334
353
|
|
|
@@ -396,15 +415,28 @@ export class PhaserServerModule extends Module<undefined> {
|
|
|
396
415
|
.system("phaser.sync.position")
|
|
397
416
|
.with(PhysicsPosition, RenderPosition)
|
|
398
417
|
.phase(POST_UPDATE)
|
|
399
|
-
.update(
|
|
400
|
-
|
|
418
|
+
.update(
|
|
419
|
+
{ watch: PhysicsPosition, onEnter: true, inject: [RenderPosition] },
|
|
420
|
+
(e, p, [render]) => {
|
|
421
|
+
if (render.x !== p.x || render.y !== p.y) {
|
|
422
|
+
render.x = p.x;
|
|
423
|
+
render.y = p.y;
|
|
424
|
+
e.modified(RenderPosition);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
401
427
|
);
|
|
402
428
|
world
|
|
403
429
|
.system("phaser.sync.rotation")
|
|
404
430
|
.with(PhysicsRotation, RenderRotation)
|
|
405
431
|
.phase(POST_UPDATE)
|
|
406
|
-
.update(
|
|
407
|
-
|
|
432
|
+
.update(
|
|
433
|
+
{ watch: PhysicsRotation, onEnter: true, inject: [RenderRotation] },
|
|
434
|
+
(e, r, [render]) => {
|
|
435
|
+
if (render.angle !== r.angle) {
|
|
436
|
+
render.angle = r.angle;
|
|
437
|
+
e.modified(RenderRotation);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
408
440
|
);
|
|
409
441
|
}
|
|
410
442
|
}
|
|
@@ -583,14 +615,31 @@ rebinds. No teardown system, no special-casing anywhere.
|
|
|
583
615
|
|
|
584
616
|
Copy `A → B`, gated on both existing (so you never _create_ `B`, you only keep an existing `B` in
|
|
585
617
|
sync — pure-ECS: the owner decides whether `B` exists). Use `{ onEnter: true }` when `B` must reflect
|
|
586
|
-
the current `A` as soon as the entity enters the sync system
|
|
618
|
+
the current `A` as soon as the entity enters the sync system, inject `B` (the `.with(...)` guarantees
|
|
619
|
+
it, so it is non-nullable, see §4), and write only when `B` differs from `A`:
|
|
587
620
|
|
|
588
621
|
```ts
|
|
589
|
-
.with(PhysicsPosition, RenderPosition).update(
|
|
590
|
-
|
|
622
|
+
.with(PhysicsPosition, RenderPosition).update(
|
|
623
|
+
{ watch: PhysicsPosition, onEnter: true, inject: [RenderPosition] },
|
|
624
|
+
(e, p, [render]) => {
|
|
625
|
+
if (render.x !== p.x || render.y !== p.y) {
|
|
626
|
+
render.x = p.x;
|
|
627
|
+
render.y = p.y;
|
|
628
|
+
e.modified(RenderPosition);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
591
631
|
);
|
|
592
632
|
```
|
|
593
633
|
|
|
634
|
+
Every write is a modification, even with an equal value: both `set` and `modified` fire `onSet`,
|
|
635
|
+
route `update` events to every watching system and replicate a networked component. So a sync into a
|
|
636
|
+
networked or watched `B` compares before it writes. The injected `B` is the live instance, so assign
|
|
637
|
+
into it and call `entity.modified(B)`: that has the same effects as `set` without the props literal
|
|
638
|
+
and the deferred copy, and a second event for the same entity in one drain finds the first write
|
|
639
|
+
applied and writes nothing. Always pair an in-place write with `modified`: assignment alone notifies
|
|
640
|
+
nobody, so hooks, watchers and replication never see it. Use `set` when you do not hold the instance,
|
|
641
|
+
or when the write should create the component.
|
|
642
|
+
|
|
594
643
|
Leave `onEnter: true` off (or set `onEnter: false`) only when a separate `.enter(...)` already initializes `B`,
|
|
595
644
|
or when the callback is intentionally "future changes only."
|
|
596
645
|
|
|
@@ -654,6 +703,11 @@ The lifecycle follows from the relationship and trait:
|
|
|
654
703
|
| `player.remove(Inventory)` | Destroys every current target |
|
|
655
704
|
| Destroy `player` | `ChildOf` destroys the inventory |
|
|
656
705
|
|
|
706
|
+
Prefer this to a component field that holds the child entity, with an `onRemove` hook that destroys
|
|
707
|
+
it. Every operation that drops the edge destroys the child, and destroying the child directly
|
|
708
|
+
removes the edge, so the owner needs no hook and holds nothing that can go stale (see
|
|
709
|
+
[Link with edges, not stored ids](./relationships.md#link-with-edges-not-stored-ids)).
|
|
710
|
+
|
|
657
711
|
Inside deferred execution, keep and mutate the target you created; the queued edge is not visible
|
|
658
712
|
through `target` until flush:
|
|
659
713
|
|
|
@@ -701,12 +755,16 @@ system after whatever produces the entity/companion.
|
|
|
701
755
|
| A god component with many optional fields | Split into small single-purpose components; absence encodes "not applicable." |
|
|
702
756
|
| A large nested `inventory`, `brain`, or progression object | Use an owned target entity when the subobject needs its own components, systems, and lifecycle. |
|
|
703
757
|
| A side `Map<ownerEid, SubsystemState>` | Create a target entity and add an explicit edge; use `Exclusive` + `OwnedTarget` only for destructive ownership. |
|
|
758
|
+
| 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)). |
|
|
759
|
+
| 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)). |
|
|
760
|
+
| `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)). |
|
|
704
761
|
| `OwnedTarget` on `CurrentEnemy`, `InCell`, `Team`, or shared state | Use a normal relationship; replacing or removing an owned edge destroys its target. |
|
|
705
762
|
| One owned target used as a collection | Use multiple child/item entities; owned targets model zero-or-one aggregates. |
|
|
706
763
|
| Duplicated `enter` + `update` doing the same thing | One `update({ watch: C, onEnter: true })` when update owns initialization. |
|
|
707
764
|
| One system trying to act in two phases | Split into two systems, one per phase. |
|
|
708
765
|
| Custom phases for app/render logic | Use the standard taxonomy; only engine modules insert phases. |
|
|
709
766
|
| `.each` scanning every entity to detect change | Reactive `.update(C)` — fires only on change. |
|
|
767
|
+
| `entity.get(C)` for a component the callback always uses | Inject it (`inject: [C]`); keep `get` for a component that is conditionally needed and rarely required. |
|
|
710
768
|
| Treating target storage as component data | Use `add(Rel, target)` / `remove(Rel, target)` and read with `target` / `targets` / `hasTarget`; target storage is private. |
|
|
711
769
|
| A `registerFooSystem(world)` wrapper per system | Declare the system inline in the module's `init`. |
|
|
712
770
|
|
|
@@ -720,9 +778,33 @@ system after whatever produces the entity/companion.
|
|
|
720
778
|
- **`update(C)` is change-only by default.** Add `{ onEnter: true }` for projection/sync callbacks
|
|
721
779
|
that must apply the current watched value on entry; leave it off for mutation-only callbacks or
|
|
722
780
|
when `.enter(...)` already owns initialization.
|
|
781
|
+
- **A write marks the component modified, even with an equal value.** `set`, and an in-place
|
|
782
|
+
assignment followed by `modified`, fire `onSet`, route `update` events to every watching system and
|
|
783
|
+
replicate a networked component. A writer that runs every tick and copies a value into a networked
|
|
784
|
+
or watched component (simulation output, a derived pose) should compare first and write only when
|
|
785
|
+
the value changed (see §7). An in-place assignment without `modified` notifies nobody.
|
|
786
|
+
- **`getMut` marks the component modified even without a callback and even if you write nothing.**
|
|
787
|
+
Calling it every tick fires `onSet`, `update` watchers and replication every tick. Read with `get`;
|
|
788
|
+
call `getMut` only when a change is intended (see
|
|
789
|
+
[`getMut` and `ensure`](./entities.md#getmut-and-ensure-mutating-in-place)).
|
|
790
|
+
- **A callback that always uses a component should inject it, not `get` it.** `entity.get(C)` is for
|
|
791
|
+
a component that is conditionally needed and rarely required (see §4).
|
|
723
792
|
- **Membership is fixed by `.with(...)` / `.without(...)` / `.update(C)`;** `update(C)` watches `C`
|
|
724
793
|
and requires it.
|
|
794
|
+
- **A system's `enter` reaches only entities still in its query when the system drains.** One that
|
|
795
|
+
left or died first gets neither `enter` nor the paired `exit`, and one that leaves and re-enters
|
|
796
|
+
within a drain gets `enter`, `exit`, `enter`, so keep `enter` setup and `exit` teardown symmetric
|
|
797
|
+
(see [`.enter`](./systems.md#entercallback--enterinject-callback--enteroptions-callback)).
|
|
725
798
|
- **`exit` retains injected references captured at exit** (components are not deep-cloned).
|
|
799
|
+
- **A `remove` hook also fires while the entity is being destroyed,** with `entity.destroyed`
|
|
800
|
+
already `true`. A dying entity takes no writes (`add`, `set`, `remove`, `modified` and `attach` are
|
|
801
|
+
dropped, `ensure` throws), so a hook that writes to its own entity guards on `entity.destroyed`.
|
|
802
|
+
That guard is legitimate; elsewhere a `destroyed` check usually signals a missing edge (see §8 and
|
|
803
|
+
[Hooks](./components.md#hooks-component-and-relationship-edges)).
|
|
804
|
+
- **Release resources in `remove` hooks, not in a system's `exit` callback.** `world.dispose()`
|
|
805
|
+
runs the hooks during the call, but a system's `exit` callback only queues its event, and a
|
|
806
|
+
disposed world never runs the system again (see
|
|
807
|
+
[Disposing a world](./entities.md#disposing-a-world)).
|
|
726
808
|
- **Within a phase, order = registration order;** across phases, the spine guarantees order. Don't
|
|
727
809
|
use registration order for cross-module correctness — use phases.
|
|
728
810
|
- **Networked component order = wire protocol.** Reordering breaks the protocol.
|
|
@@ -733,6 +815,9 @@ system after whatever produces the entity/companion.
|
|
|
733
815
|
ordering; `hasTarget(R, target)` tests one edge.
|
|
734
816
|
- **Relationship edges are explicit.** Add with `add(R, target)`, remove one with
|
|
735
817
|
`remove(R, target)`, and remove all with `remove(R)`. Do not use `set` for target storage.
|
|
818
|
+
- **Relationship removal hooks differ in scope.** `on("targetRemove")` fires once per removed edge;
|
|
819
|
+
`on("remove")` fires once, when the relationship component itself leaves, with no targets left on
|
|
820
|
+
the instance (see [Edge hooks](./relationships.md#edge-hooks)).
|
|
736
821
|
- **Relationship cardinality and component exclusivity are different.** Add the `Exclusive` trait
|
|
737
822
|
to a relationship for at-most-one target; use `setExclusiveComponents` for mutually exclusive
|
|
738
823
|
component types.
|
|
@@ -754,6 +839,33 @@ system after whatever produces the entity/companion.
|
|
|
754
839
|
explicitly, and during deferred execution keep that entity instead of re-reading `target(R)`
|
|
755
840
|
before flush.
|
|
756
841
|
- **`world.module(M)` runs `init` once.** Re-registering with a different config throws.
|
|
842
|
+
- **vecs-phaser: a `DrawBy` child follows only its parent's `Position`.** It turns with the parent
|
|
843
|
+
only with `InheritRotation`, which makes the resolver derive its `Rotation` every frame, so
|
|
844
|
+
writing `Rotation` on a tagged child has no effect (see
|
|
845
|
+
[Turning with the parent](../../vecs-phaser-client/README.md#turning-with-the-parent)).
|
|
846
|
+
- **vecs-phaser: `DrawBy` and `DrawIn` use `CleanupPolicy.Remove`,** so destroying a display parent
|
|
847
|
+
detaches its children instead of destroying them, unlike `ChildOf`, which deletes them. A display
|
|
848
|
+
child that must die with its parent also needs `ChildOf` to it, or must be its owned target (see
|
|
849
|
+
[Destroying the display parent](../../vecs-phaser-client/README.md#destroying-the-display-parent)).
|
|
850
|
+
- **vecs-physics, vecs-phaser-server: pose writes are change-only.** `set` hooks and `update` systems
|
|
851
|
+
on the physics and render pose fire when the pose changed, not on every step; to visit every body
|
|
852
|
+
every step, iterate with `each` (see
|
|
853
|
+
[Pose Updates Fire Only When The Pose Changes](../../vecs-physics/docs/systems-and-phases.md#pose-updates-fire-only-when-the-pose-changes)).
|
|
854
|
+
- **vecs-phaser-server: game code owns the physics pose; the render pose is protocol-private.** On
|
|
855
|
+
the server, read and write the physics `Position` and `Rotation` of every entity, including one
|
|
856
|
+
without a `Body`. The render `Position` and `Rotation` and the dead-reckoning `Motion` are wire
|
|
857
|
+
encodings of that pose that only the sync systems write: a `DeadReckoned` body has no render pose
|
|
858
|
+
at all, and adding one throws (see
|
|
859
|
+
[Dead reckoning](../../vecs-phaser-server/README.md#dead-reckoning)).
|
|
860
|
+
- **vecs-client: attach a `ClientWorld` socket only after `socket.connect()` resolves,** so no ack
|
|
861
|
+
precedes the data channel (`connectDgram` does this). Pending `invoke` calls reject with
|
|
862
|
+
`RPC_CLOSED_CODE` on disconnect or `dispose` (see
|
|
863
|
+
[Connecting](../../vecs-client/docs/client-world.md#connecting) and
|
|
864
|
+
[Disconnection and disposal](../../vecs-client/docs/client-world.md#disconnection-and-disposal)).
|
|
865
|
+
- **vecs-physics: component fields hold plain data,** never Box2D wrappers or other class
|
|
866
|
+
instances. `set` accepts them, but snapshot capture (`snapshotSubtree`, `moveSubtree`) rejects
|
|
867
|
+
non-plain values (see
|
|
868
|
+
[Keep Component Fields Plain Data](../../vecs-physics/docs/components.md#keep-component-fields-plain-data)).
|
|
757
869
|
|
|
758
870
|
---
|
|
759
871
|
|
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
|
|
package/docs/relationships.md
CHANGED
|
@@ -33,6 +33,35 @@ 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, and `0` for replicated entities on a
|
|
44
|
+
`ClientWorld` (see [Id pools](./components.md#id-pools)). Allocation hands out free ids before
|
|
45
|
+
never-used ones, so one of the next entities created takes it. Until an entity does,
|
|
46
|
+
`world.getEntity(id)` returns `undefined`; from then on it returns that live, unrelated entity and
|
|
47
|
+
the stale id looks valid.
|
|
48
|
+
- **A stored `Entity` dangles.** Reuse never makes it name another entity, because a destroyed
|
|
49
|
+
`Entity` stays `destroyed`, but nothing clears the field when the entity dies. Writes through it
|
|
50
|
+
are dropped and `ensure` throws.
|
|
51
|
+
- **An edge is cleaned up for you.** When the target is destroyed, `onDeleteTarget` removes the edge
|
|
52
|
+
or destroys the source (see [Cleanup when a target dies](#cleanup-when-a-target-dies)). The
|
|
53
|
+
reverse index (`children`), `target` and `parent` queries, `groupBy` and network replication work
|
|
54
|
+
on edges only.
|
|
55
|
+
- **A field does not cross worlds.** `snapshotSubtree` and `moveSubtree` remap relationship targets
|
|
56
|
+
onto the destination's new ids. An eid field is copied as is and names the wrong entity there; an
|
|
57
|
+
`Entity` field makes capture throw (see [Snapshot policies](./snapshots.md#snapshot-policies)).
|
|
58
|
+
|
|
59
|
+
A `destroyed` check on an entity you looked up from a stored link is therefore a symptom of a
|
|
60
|
+
missing edge: replace the link, not only the check. The guard stays right where a reference is
|
|
61
|
+
deliberately short-lived: payloads in per-step event buffers (the `other` entity in `vecs-physics`
|
|
62
|
+
contact events), and hooks that run while an entity or an edge target is being destroyed (see
|
|
63
|
+
[Edge hooks](#edge-hooks) and [component hooks](./components.md#hooks-component-and-relationship-edges)).
|
|
64
|
+
|
|
36
65
|
## Built-in relationships
|
|
37
66
|
|
|
38
67
|
| Class | Purpose |
|
|
@@ -192,6 +221,26 @@ calls `onRemove`. An exclusive replacement calls `onTargetRemove` for the old ta
|
|
|
192
221
|
relationship-dependent query updates, but do not call `onSet`; `onSet` is reserved for ordinary
|
|
193
222
|
relationship data changed through `set`.
|
|
194
223
|
|
|
224
|
+
Pick the removal hook by scope:
|
|
225
|
+
|
|
226
|
+
- `on("targetRemove", handler)` runs once for **each** removed edge: `remove(R, target)`, the
|
|
227
|
+
displaced edge of an `Exclusive` replacement, and every edge when the whole relationship goes away
|
|
228
|
+
(the final edge, `remove(R)`, or destroying the source).
|
|
229
|
+
- `on("remove", handler)` runs **once**, when the relationship component itself leaves the entity.
|
|
230
|
+
It does not run when one of several edges is removed or an `Exclusive` edge is replaced. The
|
|
231
|
+
relationship instance it receives holds no targets any more, so use `targetRemove` to learn which
|
|
232
|
+
targets went.
|
|
233
|
+
|
|
234
|
+
When the whole relationship goes away, `source.has(R)` is already `false` inside both hooks. When
|
|
235
|
+
only one of several edges goes, `source.has(R)` stays `true` and `source.hasTarget(R, removed)` is
|
|
236
|
+
`false`.
|
|
237
|
+
|
|
238
|
+
Destroying either end of an edge runs these hooks too, so `source.destroyed` or `target.destroyed`
|
|
239
|
+
can already be `true` inside them; both are under `Delete` cleanup. The target of an `OwnedTarget`
|
|
240
|
+
edge is destroyed before the public hook runs on every path. A dying end takes no writes (see
|
|
241
|
+
[Hooks](./components.md#hooks-component-and-relationship-edges)), so a handler that writes to
|
|
242
|
+
`source` or `target` guards on `destroyed`.
|
|
243
|
+
|
|
195
244
|
Register handlers with `on("targetAdd", handler)` and `on("targetRemove", handler)`; remove them
|
|
196
245
|
with `off(event, handler)`. Multiple handlers may be registered for the same event and fire in
|
|
197
246
|
registration order. Framework traits install separate internal `targetPolicyAdd` /
|
|
@@ -227,6 +276,11 @@ attached: removing it throws, and only world teardown (`clearAllEntities`) bypas
|
|
|
227
276
|
the same contract as `Traversable` and `OneOf`. Do not share an owned target between
|
|
228
277
|
sources because either owner can destroy it when its edge is removed.
|
|
229
278
|
|
|
279
|
+
An owned target replaces the field-plus-hook pattern, where a component field holds the child
|
|
280
|
+
entity and an `onRemove` hook destroys it. The edge is the field: every path that drops it destroys
|
|
281
|
+
the child, and destroying the child directly removes the edge from its owner. There is no hook to
|
|
282
|
+
write and no stale reference to guard.
|
|
283
|
+
|
|
230
284
|
## Cleanup when a target dies
|
|
231
285
|
|
|
232
286
|
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
|
@@ -89,10 +89,15 @@ Fires once when an entity starts matching the system:
|
|
|
89
89
|
.enter({ inject: [Position, Sprite] }, (e, [pos, sprite]) => sprite.setPosition(pos.x, pos.y))
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
The callback runs only if the entity is still a member when this system drains its inbox
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
The callback runs only if the entity is still a member when this system drains its inbox, at the top
|
|
93
|
+
of its next run. The inbox replays in arrival order, so:
|
|
94
|
+
|
|
95
|
+
- An entity that left the query or was destroyed first gets no `enter`, and in a system that
|
|
96
|
+
registers both callbacks no paired `exit` either. Do not add a `destroyed` guard to normal `enter`
|
|
97
|
+
setup code. A system with only `exit` has no `enter` to pair, so it still gets that `exit`.
|
|
98
|
+
- An entity that leaves and re-enters before the drain is a member again, so the system gets
|
|
99
|
+
`enter`, `exit`, `enter` (just `exit`, `enter` when it had already entered in an earlier drain).
|
|
100
|
+
Keep `enter` setup and `exit` teardown symmetric so the second `enter` starts from a clean state.
|
|
96
101
|
|
|
97
102
|
For systems, injected values and `Iter.src` are captured when entry is routed and replayed from that
|
|
98
103
|
snapshot when the system next drains its inbox.
|
|
@@ -189,6 +194,9 @@ an options object with `inject`, resolved per entity and passed as a typed tuple
|
|
|
189
194
|
.each({ inject: [Position, Sprite] }, (e, [pos, sprite]) => { ... })
|
|
190
195
|
```
|
|
191
196
|
|
|
197
|
+
Inject every component a callback always uses, and keep `entity.get` for one that is only
|
|
198
|
+
conditionally needed (see [Design guide](./design-guide.md#injection)).
|
|
199
|
+
|
|
192
200
|
Nullability follows membership: components guaranteed by `with` (or `hint`) are non-nullable;
|
|
193
201
|
anything else resolves as `T | undefined` — guard it. (`enter`/`exit` injection requires the
|
|
194
202
|
direct components to be present and throws otherwise; list only components your predicate
|
package/package.json
CHANGED
package/skills/vecs/SKILL.md
CHANGED
|
@@ -65,6 +65,60 @@ Check the design guide before introducing:
|
|
|
65
65
|
If the requested design conflicts with a documented vecs invariant, explain the conflict and use
|
|
66
66
|
the documented pattern unless the user explicitly chooses otherwise.
|
|
67
67
|
|
|
68
|
+
## Known Gotchas
|
|
69
|
+
|
|
70
|
+
Agents trip on these vecs behaviors repeatedly. Design guide sections 8 and 9 list them with the
|
|
71
|
+
other anti-patterns and pitfalls; each rule below links to the detail.
|
|
72
|
+
|
|
73
|
+
- Do not call `getMut` per tick unless a change is intended. It marks the component modified even
|
|
74
|
+
without a callback and even if nothing is written, which fires `update` watchers and replication.
|
|
75
|
+
Read with `get`. ([`getMut`](../../docs/entities.md#getmut-and-ensure-mutating-in-place))
|
|
76
|
+
- A system's `enter` reaches only entities still in its query when the system drains. An entity
|
|
77
|
+
that left the query or died first gets neither `enter` nor the paired `exit`, and a re-entry
|
|
78
|
+
within one drain gives `enter`, `exit`, `enter`, so keep setup and teardown symmetric.
|
|
79
|
+
([`.enter`](../../docs/systems.md#entercallback--enterinject-callback--enteroptions-callback))
|
|
80
|
+
- Create relationship edges with `add(Rel, target)` and read them with `target()` / `targets()`.
|
|
81
|
+
React to the relationship component being removed with `on("remove")` and to each removed edge
|
|
82
|
+
with `on("targetRemove")`. `OwnedTarget` needs `Exclusive` and an explicitly created target, and
|
|
83
|
+
destroys that target when its edge is removed.
|
|
84
|
+
([Edge hooks](../../docs/relationships.md#edge-hooks),
|
|
85
|
+
[Owned targets](../../docs/relationships.md#owned-targets))
|
|
86
|
+
- Link entities with relationship edges, never with an `eid` or `Entity` kept in a component field.
|
|
87
|
+
A destroyed entity's eid is reused after `reuseWaitFrames` (16 frames by default, 0 for replicated
|
|
88
|
+
entities on a `ClientWorld`), so a stale eid can name an unrelated entity. A stored `Entity`
|
|
89
|
+
dangles, and snapshots copy an eid unchanged and reject an `Entity`.
|
|
90
|
+
([Link with edges, not stored ids](../../docs/relationships.md#link-with-edges-not-stored-ids))
|
|
91
|
+
- Prefer `Exclusive` + `OwnedTarget` to a component field that holds a child entity. Removing or
|
|
92
|
+
replacing the edge destroys the child and destroying the child removes the edge, so no remove hook
|
|
93
|
+
is needed. ([Owned targets](../../docs/relationships.md#owned-targets))
|
|
94
|
+
- A `remove` hook also fires while its entity is being destroyed, with `entity.destroyed` already
|
|
95
|
+
`true`. A dying entity drops `add`, `set`, `remove`, `modified` and `attach` writes and `ensure`
|
|
96
|
+
throws, so guard a hook's writes to its own entity with `entity.destroyed`; that guard is
|
|
97
|
+
legitimate. ([Hooks](../../docs/components.md#hooks-component-and-relationship-edges))
|
|
98
|
+
- A `destroyed` check outside per-step event buffers and hooks on a dying entity or edge target
|
|
99
|
+
usually means a link that should be a relationship edge. Replace the link, not only add the check.
|
|
100
|
+
([Link with edges, not stored ids](../../docs/relationships.md#link-with-edges-not-stored-ids))
|
|
101
|
+
- `@vworlds/vecs-phaser`: a `DrawBy` child turns with its parent only with `InheritRotation`. The
|
|
102
|
+
resolver derives a tagged child's `Rotation` every frame, so writing `Rotation` on it has no
|
|
103
|
+
effect; set `InheritRotation.angle` instead.
|
|
104
|
+
([Turning with the parent](../../../vecs-phaser-client/README.md#turning-with-the-parent))
|
|
105
|
+
- `@vworlds/vecs-phaser`: `DrawBy` and `DrawIn` use `CleanupPolicy.Remove`, so destroying a display
|
|
106
|
+
parent detaches its children and leaves them alive, unlike `ChildOf`. Add `ChildOf` to the parent,
|
|
107
|
+
or make the child an owned target, when it must die with the parent.
|
|
108
|
+
([Destroying the display parent](../../../vecs-phaser-client/README.md#destroying-the-display-parent))
|
|
109
|
+
- `@vworlds/vecs-physics` and `@vworlds/vecs-phaser-server`: pose `set` hooks and `update` systems
|
|
110
|
+
on the physics and render pose fire only when the pose changes. To visit every body every step,
|
|
111
|
+
iterate with `each`.
|
|
112
|
+
([Pose updates](../../../vecs-physics/docs/systems-and-phases.md#pose-updates-fire-only-when-the-pose-changes))
|
|
113
|
+
- `@vworlds/vecs-client`: attach a `ClientWorld` socket only after `socket.connect()`, so no ack
|
|
114
|
+
precedes the data channel (`connectDgram` does this). Pending `invoke` calls reject with
|
|
115
|
+
`RPC_CLOSED_CODE` on disconnect or dispose.
|
|
116
|
+
([Connecting](../../../vecs-client/docs/client-world.md#connecting),
|
|
117
|
+
[Disconnection and disposal](../../../vecs-client/docs/client-world.md#disconnection-and-disposal))
|
|
118
|
+
- `@vworlds/vecs-physics` components hold plain data only, never Box2D wrappers or other class
|
|
119
|
+
instances: snapshot capture rejects non-plain values.
|
|
120
|
+
([Plain data](../../../vecs-physics/docs/components.md#keep-component-fields-plain-data))
|
|
121
|
+
|
|
68
122
|
## Implement And Verify
|
|
69
123
|
|
|
70
124
|
1. Reuse existing components and modules where their semantics match; do not reuse them merely
|