@vworlds/vecs 1.0.25 → 1.0.27
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 +29 -778
- package/dist/component.d.ts +1 -1
- package/dist/component.js +1 -1
- package/dist/component.js.map +1 -1
- package/dist/component_meta.d.ts +5 -1
- package/dist/component_meta.js +10 -0
- package/dist/component_meta.js.map +1 -1
- package/dist/dsl.d.ts +23 -2
- package/dist/dsl.js +21 -18
- package/dist/dsl.js.map +1 -1
- package/dist/entity/entity.base.d.ts +2 -18
- package/dist/entity/entity.base.js +2 -20
- package/dist/entity/entity.base.js.map +1 -1
- package/dist/entity/entity.components.d.ts +1 -0
- package/dist/entity/entity.components.js +50 -0
- package/dist/entity/entity.components.js.map +1 -1
- package/dist/entity/entity.d.ts +8 -0
- package/dist/entity/entity.js +21 -0
- package/dist/entity/entity.js.map +1 -1
- package/dist/entity/entity.lifecycle.d.ts +3 -3
- package/dist/entity/entity.lifecycle.js +6 -9
- package/dist/entity/entity.lifecycle.js.map +1 -1
- package/dist/entity/entity.relationships.d.ts +1 -1
- package/dist/entity/entity.relationships.js +3 -3
- package/dist/entity/entity.relationships.js.map +1 -1
- package/dist/filter.d.ts +1 -0
- package/dist/filter.js +3 -2
- package/dist/filter.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/inject.d.ts +3 -2
- package/dist/inject.js.map +1 -1
- package/dist/modules/implements.d.ts +14 -0
- package/dist/modules/implements.js +98 -0
- package/dist/modules/implements.js.map +1 -0
- package/dist/modules/relationships.d.ts +1 -1
- package/dist/modules/relationships.js +2 -2
- package/dist/package.json +1 -4
- package/dist/query/callbacks.d.ts +6 -2
- package/dist/query/callbacks.js +5 -2
- package/dist/query/callbacks.js.map +1 -1
- package/dist/query/grouped_query.js +1 -1
- package/dist/query/query.d.ts +14 -1
- package/dist/query/query.js +26 -15
- package/dist/query/query.js.map +1 -1
- package/dist/system.d.ts +5 -4
- package/dist/system.js +17 -6
- package/dist/system.js.map +1 -1
- package/dist/terms/build.js +7 -5
- package/dist/terms/build.js.map +1 -1
- package/dist/util/array_map.d.ts +70 -12
- package/dist/util/array_map.js +113 -26
- package/dist/util/array_map.js.map +1 -1
- package/dist/util/bitset.js +0 -17
- package/dist/util/bitset.js.map +1 -1
- package/dist/util/events.d.ts +42 -12
- package/dist/util/events.js +94 -43
- package/dist/util/events.js.map +1 -1
- package/dist/util/ordered_set.js +43 -19
- package/dist/util/ordered_set.js.map +1 -1
- package/dist/world/world.deferred.js +2 -0
- package/dist/world/world.deferred.js.map +1 -1
- package/dist/world/world.entities.d.ts +8 -1
- package/dist/world/world.entities.js +25 -6
- package/dist/world/world.entities.js.map +1 -1
- package/dist/world/world.js +8 -1
- package/dist/world/world.js.map +1 -1
- package/dist/world/world.queries.js +6 -1
- package/dist/world/world.queries.js.map +1 -1
- package/dist/world/world.storage.d.ts +2 -2
- package/dist/world/world.storage.js +6 -3
- package/dist/world/world.storage.js.map +1 -1
- package/docs/README.md +50 -0
- package/docs/components.md +267 -0
- package/docs/concepts.md +86 -0
- package/docs/design-guide.md +506 -0
- package/docs/entities.md +177 -0
- package/docs/execution-model.md +173 -0
- package/docs/getting-started.md +215 -0
- package/docs/glossary.md +113 -0
- package/docs/modules.md +108 -0
- package/docs/queries-and-filters.md +187 -0
- package/docs/relationships.md +149 -0
- package/docs/systems.md +311 -0
- package/docs/utilities.md +139 -0
- package/package.json +1 -4
package/docs/modules.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Modules
|
|
2
|
+
|
|
3
|
+
Package components, systems, and policy into reusable units, compose them, and meet the built-in
|
|
4
|
+
modules every world installs.
|
|
5
|
+
|
|
6
|
+
## What a module is
|
|
7
|
+
|
|
8
|
+
A `Module` (`lib/vecs/src/module.ts`) is a component entity whose `init()` method runs once when
|
|
9
|
+
the module is registered. It is the unit of packaging for a feature: registering components,
|
|
10
|
+
declaring systems, setting exclusivity and cleanup policy, and (rarely) inserting phases.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { Module, PRE_STORE } from "@vworlds/vecs";
|
|
14
|
+
|
|
15
|
+
export class PoseSyncModule extends Module {
|
|
16
|
+
public init(): void {
|
|
17
|
+
const world = this.world;
|
|
18
|
+
world.component(PhysicsPosition);
|
|
19
|
+
world.component(RenderPosition);
|
|
20
|
+
world
|
|
21
|
+
.system("sync.position")
|
|
22
|
+
.with(PhysicsPosition, RenderPosition)
|
|
23
|
+
.phase(PRE_STORE)
|
|
24
|
+
.update(PhysicsPosition, (e, p) => e.set(RenderPosition, { x: p.x, y: p.y }));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
world.module(PoseSyncModule);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`world.module(ModuleClass, config?, eid?)` (`lib/vecs/src/world/world.modules.ts`) registers the
|
|
32
|
+
module on first call — drawing its entity id from the `module` pool (900–999 by default, or pass
|
|
33
|
+
an explicit `eid`) — and invokes `init(config)` exactly once. Subsequent calls return the existing
|
|
34
|
+
module instance; passing a config again after the first registration throws.
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
Type the config through the `Module<Config>` parameter; it flows from `world.module(M, config)`
|
|
39
|
+
into `init(config)`:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
export class RenderModule extends Module<{ scene: Scene; pixelsPerMeter?: number }> {
|
|
43
|
+
public init(cfg: { scene: Scene; pixelsPerMeter?: number }): void {
|
|
44
|
+
const ppm = cfg.pixelsPerMeter ?? 32;
|
|
45
|
+
// ...
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
world.module(RenderModule, { scene });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For `Module<undefined>` (the default), the config argument is omitted. Keep configs minimal;
|
|
53
|
+
prefer zero-config when a sensible default exists.
|
|
54
|
+
|
|
55
|
+
## Composition and umbrella modules
|
|
56
|
+
|
|
57
|
+
A module's `init` can register sub-modules with `this.world.module(Sub, subConfig)`. A big feature
|
|
58
|
+
is often one module per item plus a thin umbrella that composes them:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
export class PhaserRenderModule extends Module<{ scene: Scene }> {
|
|
62
|
+
public init(cfg: { scene: Scene }): void {
|
|
63
|
+
this.world.component(VGameObject).onRemove((_e, vgo) => vgo.obj.destroy());
|
|
64
|
+
this.world.module(ArcModule, { scene: cfg.scene });
|
|
65
|
+
this.world.module(RectangleModule, { scene: cfg.scene });
|
|
66
|
+
// … shape modules first …
|
|
67
|
+
this.world.module(TransformSyncModule, {}); // … then sync, so create precedes sync within a phase
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Load order sets same-phase order.** Systems in the same phase run in creation order, so the
|
|
73
|
+
order an umbrella loads sub-modules is the order their systems run within a shared phase. Use
|
|
74
|
+
that for intra-feature ordering — but express **cross-module** ordering with phases, never with
|
|
75
|
+
load order (see [Execution model](./execution-model.md#ordering-guarantees)).
|
|
76
|
+
|
|
77
|
+
Guidelines:
|
|
78
|
+
|
|
79
|
+
- **Declare systems inline in `init`.** A `registerFooSystem(world)` helper per system is a layer
|
|
80
|
+
for nothing.
|
|
81
|
+
- **Modules own cross-cutting policy** — `setExclusiveComponents`, cleanup policies, phase
|
|
82
|
+
placement, component registration. Keep that out of application code.
|
|
83
|
+
- **One module per cohesive thing.** Don't pile unrelated features into one `init`.
|
|
84
|
+
|
|
85
|
+
## Built-in modules
|
|
86
|
+
|
|
87
|
+
Every `World` constructor installs these (`lib/vecs/src/world/world.ts`); you interact with their
|
|
88
|
+
_components_, not with the module objects:
|
|
89
|
+
|
|
90
|
+
| Module | Provides |
|
|
91
|
+
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
92
|
+
| `IdentityModule` | The `Identity` name component and the name → entity index behind `world.entity("name")` / `entity.name`. |
|
|
93
|
+
| `ImplementsModule` | Maintains [`Implements` interface aliases](./components.md#interface-aliases-with-implements) and their transitive closure. |
|
|
94
|
+
| `SingletonModule` | Enforces the [`Singleton`](./components.md#singletons-and-world-global-data) one-carrier constraint. |
|
|
95
|
+
| `RelationshipsModule` | Registers `ChildOf` (with the `Delete` cascade) and maintains [`Traversable` depth tracking](./relationships.md#traversal-order-traversable-and-cascade). |
|
|
96
|
+
| `PipelineModule` | Creates the eight [built-in phases](./execution-model.md#phases-the-pipeline-spine) and installs the default pipeline query. |
|
|
97
|
+
|
|
98
|
+
All are exported, so a custom world setup can reuse them.
|
|
99
|
+
|
|
100
|
+
## Reference
|
|
101
|
+
|
|
102
|
+
| API | Description |
|
|
103
|
+
| ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
|
|
104
|
+
| `class M extends Module<Config>` | Declare a module; `Config` defaults to `undefined`. |
|
|
105
|
+
| `init(config)` | One-time setup hook; called by the world on first registration. |
|
|
106
|
+
| `world.module(M)` / `world.module(M, config)` / `world.module(M, config, eid)` | Register (first call) or retrieve (later calls) the module. Config after first registration throws. |
|
|
107
|
+
| `this.world` | Inside `init`: the owning world (a module is an entity). |
|
|
108
|
+
| `ModuleConfig<T>` | Utility type extracting a module's config type. |
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Queries and Filters
|
|
2
|
+
|
|
3
|
+
Express entity predicates with the query DSL, track them live with `Query`, scan once with
|
|
4
|
+
`Filter`, index with `groupBy`, and reach for the `Bitset` utility that powers matching.
|
|
5
|
+
|
|
6
|
+
## The query DSL
|
|
7
|
+
|
|
8
|
+
A `QueryDSL` (`lib/vecs/src/dsl.ts`) is a composable expression describing which entities match.
|
|
9
|
+
It is accepted by `query().with`, `system().with`, and `world.filter`:
|
|
10
|
+
|
|
11
|
+
| Operator | Meaning |
|
|
12
|
+
| ---------------------- | ------------------------------------------------------------------- |
|
|
13
|
+
| a component class/id | Entity has that component |
|
|
14
|
+
| an array `[A, B]` | Entity has all of `A` and `B` (empty array matches all) |
|
|
15
|
+
| `true` / `false` | Match all / match no entities |
|
|
16
|
+
| `{ all: [q1, q2] }` | Every sub-expression matches |
|
|
17
|
+
| `{ any: [q1, q2] }` | At least one sub-expression matches |
|
|
18
|
+
| `{ not: q }` | Sub-expression must not match |
|
|
19
|
+
| `{ only: [A, B] }` | Entity has exactly `A` and `B`, nothing else |
|
|
20
|
+
| `{ target: [R, q] }` | Relationship `R`'s target matches `q` |
|
|
21
|
+
| `{ source: [R, q] }` | Filter-only: some entity targeting this one through `R` matches `q` |
|
|
22
|
+
| `{ parent: q }` | Built-in `ChildOf` target matches `q` |
|
|
23
|
+
| `{ children: q }` | Filter-only: some `ChildOf` child matches `q` |
|
|
24
|
+
| `{ test: fn, watch? }` | Custom predicate; rechecked when a `watch`-listed component changes |
|
|
25
|
+
|
|
26
|
+
Operators nest arbitrarily:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
world.system("render").with({
|
|
30
|
+
all: [Position, { any: [Sprite, Container] }],
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Class-valued terms are components — register them first; an unregistered class throws. Use
|
|
35
|
+
`{ test: fn }` for arbitrary logic the operators can't express, with `watch` naming the components
|
|
36
|
+
whose changes should re-evaluate it.
|
|
37
|
+
|
|
38
|
+
`target` and `source` take tuple form only: `{ target: [RelationshipClass, innerDSL] }`. The first
|
|
39
|
+
element must extend `Relationship` (a plain component throws). `target` evaluates the inner DSL
|
|
40
|
+
against the relationship's target; `source` evaluates it against each entity pointing at the
|
|
41
|
+
candidate and matches when at least one does. `parent` / `children` are the same operators
|
|
42
|
+
specialized to `ChildOf`. **`source` and `children` are non-reactive**: exact for each filter
|
|
43
|
+
scan, but rejected by tracked queries and systems. `target` / `parent` are fully reactive — see
|
|
44
|
+
[Relationships](./relationships.md#querying-through-relationships).
|
|
45
|
+
|
|
46
|
+
### `QuerySpec`: adding `cascade` and `hint`
|
|
47
|
+
|
|
48
|
+
Anywhere a DSL is accepted, you can pass the object form
|
|
49
|
+
`{ with: dsl, cascade?: Rel, hint?: [C...] }`:
|
|
50
|
+
|
|
51
|
+
- `cascade` orders the tracked set depth-first along a
|
|
52
|
+
[`Traversable` relationship](./relationships.md#traversal-order-traversable-and-cascade).
|
|
53
|
+
- `hint` declares components as guaranteed-present for **type inference only** (not validated at
|
|
54
|
+
runtime). Use it when the DSL shape (`any`, `not`, `test`, numeric ids) hides a guarantee the
|
|
55
|
+
type extractor can't see:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
world
|
|
59
|
+
.filter({ with: { any: [Position, Velocity] }, hint: [Position] })
|
|
60
|
+
.forEach([Position], (e, [pos]) => pos.x);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Without a hint, only plain classes, arrays, `only`, and `all`-chains of those count as guaranteed,
|
|
64
|
+
and everything else injects as `T | undefined`.
|
|
65
|
+
|
|
66
|
+
## Live queries: `Query`
|
|
67
|
+
|
|
68
|
+
`world.query(name)` returns a standalone reactive entity set (`lib/vecs/src/query/query.ts`),
|
|
69
|
+
configured through the same fluent builder as systems but without pipeline integration:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const projectiles = world
|
|
73
|
+
.query("Projectiles")
|
|
74
|
+
.with(Position, Velocity)
|
|
75
|
+
.enter((e) => console.log("spawned", e.eid))
|
|
76
|
+
.exit((e) => console.log("gone", e.eid));
|
|
77
|
+
|
|
78
|
+
projectiles.count; // kept up to date automatically
|
|
79
|
+
projectiles.has(e);
|
|
80
|
+
for (const e of projectiles) { ... }
|
|
81
|
+
projectiles.forEach([Position], (e, [pos]) => { ... });
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The crucial difference from systems: **standalone query callbacks fire synchronously** while the
|
|
85
|
+
world routes a command, not in a phase slot. Mutations made inside one callback are themselves
|
|
86
|
+
observed immediately by other queries. Systems instead queue events into an inbox replayed on
|
|
87
|
+
their tick (see [Execution model](./execution-model.md#reactive-routing)).
|
|
88
|
+
|
|
89
|
+
### Lifecycle: build, backfill, destroy
|
|
90
|
+
|
|
91
|
+
A query is **built** — its predicate materialized and registered — at the next `world.flush()` /
|
|
92
|
+
`progress()`, or immediately via `query.build()` outside deferred mode. Tracked queries (and any
|
|
93
|
+
query with callbacks) **backfill**: entities that already match are routed through `enter` at
|
|
94
|
+
build time. Builder methods throw once the query is built; `built` reports the state.
|
|
95
|
+
|
|
96
|
+
`query.destroy()` permanently removes the query: entity references are purged silently (no `exit`
|
|
97
|
+
fires), the tracked set is cleared, and further use of the object is undefined behavior. Use a
|
|
98
|
+
standalone query when you need a reactive set you can destroy mid-session.
|
|
99
|
+
|
|
100
|
+
### Reading a query
|
|
101
|
+
|
|
102
|
+
| Member | Description |
|
|
103
|
+
| --------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
104
|
+
| `count` | Number of currently tracked entities (0 unless tracking). |
|
|
105
|
+
| `has(e)` | `true` when `e` is currently tracked. |
|
|
106
|
+
| `belongs(e)` | Evaluate the predicate against `e` right now (works regardless of tracking). |
|
|
107
|
+
| `[Symbol.iterator]` | Iterate tracked entities. |
|
|
108
|
+
| `forEach(...)` | Iterate with optional [component injection](./systems.md#component-injection); runs in a deferred scope. |
|
|
109
|
+
| `changed` | `true` when membership or watched data changed since the last `forEach` reset it. |
|
|
110
|
+
| `name` / `entity` / `world` | Display name; the backing entity; the owning world. |
|
|
111
|
+
|
|
112
|
+
`orderBy`, `track`, `enter`, `exit`, `update`, `with`, and `without` behave exactly as on systems
|
|
113
|
+
— see [Systems](./systems.md) — except that `update` callbacks fire synchronously here. A plain
|
|
114
|
+
`Query.update` callback can therefore re-enter during command routing; snapshot reused `Iter` /
|
|
115
|
+
tuple values before triggering further mutations from inside it.
|
|
116
|
+
|
|
117
|
+
### Silencing feedback: `ignoreSource`
|
|
118
|
+
|
|
119
|
+
`query.ignoreSource(dsl | query)` makes the query drop `update` notifications whose originating
|
|
120
|
+
system matches the given query — the standard cure for write feedback loops (A updates B, B's
|
|
121
|
+
system writes back, A reacts again). The relationship is stored as the built-in `SilenceSource`
|
|
122
|
+
component on the query's backing entity. Entry-time initialization bypasses the silence filter, so
|
|
123
|
+
newly entered entities still initialize.
|
|
124
|
+
|
|
125
|
+
## Grouped queries: `groupBy`
|
|
126
|
+
|
|
127
|
+
`groupBy` partitions a query's members into live buckets you can look up by key — spatial grids,
|
|
128
|
+
interest management, any "find members in bucket K" — without re-scanning:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const ballsByCell = world
|
|
132
|
+
.query("ByCell")
|
|
133
|
+
.with(Ball, Position)
|
|
134
|
+
.groupBy([Position], (_e, [p]) => cellIndex(p)); // re-buckets reactively as Position changes
|
|
135
|
+
|
|
136
|
+
ballsByCell.group(key)?.forEach([Position], (e, [p]) => { ... });
|
|
137
|
+
ballsByCell.group(key)?.count;
|
|
138
|
+
for (const group of ballsByCell.groups()) { ... }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Two forms (`lib/vecs/src/query/grouped_query.ts`):
|
|
142
|
+
|
|
143
|
+
- `groupBy([components], keyFn)` — bucket by a computed key; injected components are watched, so
|
|
144
|
+
changing them re-keys the entity.
|
|
145
|
+
- `groupBy(RelationshipClass)` — bucket by the relationship's target entity. Moving between
|
|
146
|
+
buckets is a retarget, a plain data change (see
|
|
147
|
+
[Relationships](./relationships.md#retargeting)).
|
|
148
|
+
|
|
149
|
+
The result is a `GroupedQuery<K, R>` adding `group(key)`, `groups()`, `groupKeys()`, `groupCount`,
|
|
150
|
+
and `onGroupEnter` / `onGroupExit` callbacks for bucket creation and disposal. Each `Group` exposes
|
|
151
|
+
`key`, `count`, `has`, iteration, and injected `forEach`. Grouping implies tracking. Systems do
|
|
152
|
+
not support `groupBy` — keep the grouped query standalone and read it from a system.
|
|
153
|
+
|
|
154
|
+
## Filters: one-shot scans
|
|
155
|
+
|
|
156
|
+
`world.filter(spec)` returns a `Filter` (`lib/vecs/src/filter.ts`): a non-reactive predicate that
|
|
157
|
+
walks entities on each `forEach` call. It accepts the same DSL (including the filter-only
|
|
158
|
+
`source` / `children` operators):
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// Entity only:
|
|
162
|
+
world.filter([Position]).forEach((e) => console.log(e.eid));
|
|
163
|
+
|
|
164
|
+
// With component injection (auto-deduced non-null types):
|
|
165
|
+
world.filter([Position, Velocity]).forEach([Position, Velocity], (e, [pos, vel]) => {
|
|
166
|
+
pos.x += vel.vx;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Manual hint where the extractor can't see through the DSL:
|
|
170
|
+
world
|
|
171
|
+
.filter({ with: { any: [Position, Velocity] }, hint: [Position] })
|
|
172
|
+
.forEach([Position], (e, [pos]) => pos.x);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A `Filter` holds no tracked set, registers nothing, and needs no `destroy()` — create it anywhere
|
|
176
|
+
and discard freely. `forEach` runs inside a deferred scope, so mutations made by the callback are
|
|
177
|
+
batched and visible after iteration; nested inside an already-deferred block it inherits the outer
|
|
178
|
+
scope. When the world already maintains a live term for the same predicate, the filter iterates
|
|
179
|
+
that term instead of scanning every entity.
|
|
180
|
+
|
|
181
|
+
Use a `Filter` for occasional or one-off scans; use a `Query`/`System` when you need continuous
|
|
182
|
+
reactivity.
|
|
183
|
+
|
|
184
|
+
## `Bitset`
|
|
185
|
+
|
|
186
|
+
`Bitset` is the compact integer-set type that powers entity archetypes and query watchlists.
|
|
187
|
+
It is documented in full in [Utilities](./utilities.md#bitset).
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Relationships
|
|
2
|
+
|
|
3
|
+
Link entities to entities with relationship components: hierarchy, reverse lookup, retargeting,
|
|
4
|
+
cleanup cascades, and depth-ordered traversal.
|
|
5
|
+
|
|
6
|
+
## What a relationship is
|
|
7
|
+
|
|
8
|
+
A relationship is a component whose `target` field points at another entity. Define one by
|
|
9
|
+
extending the abstract `Relationship` base (`lib/vecs/src/component_meta.ts`) and registering it
|
|
10
|
+
like any component:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { Relationship } from "@vworlds/vecs";
|
|
14
|
+
|
|
15
|
+
class EquippedBy extends Relationship {}
|
|
16
|
+
|
|
17
|
+
world.component(EquippedBy);
|
|
18
|
+
|
|
19
|
+
item.set(EquippedBy, { target: player });
|
|
20
|
+
item.target(EquippedBy); // player
|
|
21
|
+
player.children(EquippedBy).has(item); // true
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The **source** entity stores the relationship component; the **target** can enumerate its sources
|
|
25
|
+
with `children(ref)` through a reverse index the world maintains automatically. An entity holds at
|
|
26
|
+
most one instance of a given relationship class (like any component), so multi-target links are
|
|
27
|
+
modeled with one relationship class per meaning, or child entities.
|
|
28
|
+
|
|
29
|
+
Because `target` is an `Entity` reference, **relationships are not wire-serializable — don't try
|
|
30
|
+
to network them**. Replicate a plain id or tag instead, or keep the relationship server-local.
|
|
31
|
+
|
|
32
|
+
## Built-in relationships
|
|
33
|
+
|
|
34
|
+
| Class | Purpose |
|
|
35
|
+
| --------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
+
| `ChildOf` | General parent/child hierarchy. `onDeleteTarget = Delete`: destroying a parent destroys its `ChildOf` children. |
|
|
37
|
+
| `DependsOn` | Pipeline ordering: phases chain to each other and systems attach to phases through it. |
|
|
38
|
+
| `SilenceSource` | Marks a query's silence domain; configured via [`ignoreSource`](./queries-and-filters.md#silencing-feedback-ignoresource). |
|
|
39
|
+
|
|
40
|
+
`entity.childOf(parent)` is shorthand for `entity.set(ChildOf, { target: parent })`.
|
|
41
|
+
|
|
42
|
+
## Retargeting
|
|
43
|
+
|
|
44
|
+
Point a relationship somewhere else with a plain `set`:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
unit.set(InCell, { target: newCell }); // reverse index updates automatically
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Retargeting is a **data change, not an archetype change** — no component is added or removed, so
|
|
51
|
+
no `enter`/`exit` churn. Prefer retargeting a relationship over swapping marker components to
|
|
52
|
+
express "which bucket this entity is in", and pair it with
|
|
53
|
+
[`groupBy(Relationship)`](./queries-and-filters.md#grouped-queries-groupby) for O(1) bucket
|
|
54
|
+
lookups.
|
|
55
|
+
|
|
56
|
+
`getMut` throws for relationship components: in-place target mutation would bypass the reverse
|
|
57
|
+
index used by `children()` and the cleanup cascades. Always retarget with `set`.
|
|
58
|
+
|
|
59
|
+
## Cleanup when a target dies
|
|
60
|
+
|
|
61
|
+
What happens to sources when their target entity is destroyed is governed by the relationship's
|
|
62
|
+
`meta.onDeleteTarget`:
|
|
63
|
+
|
|
64
|
+
| Policy | Meaning |
|
|
65
|
+
| ---------------------- | ----------------------------------------------------------------------------------------- |
|
|
66
|
+
| `CleanupPolicy.Remove` | Default. The relationship component is removed from each source. |
|
|
67
|
+
| `CleanupPolicy.Delete` | Each source entity targeting the destroyed entity through this relationship is destroyed. |
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { CleanupPolicy } from "@vworlds/vecs";
|
|
71
|
+
|
|
72
|
+
world.component(MyRel).meta.onDeleteTarget = CleanupPolicy.Delete; // cascade like ChildOf
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`ChildOf` ships with `Delete`, so destroying a parent recursively destroys its subtree:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
child.set(ChildOf, { target: parent });
|
|
79
|
+
parent.destroy(); // child (and its own ChildOf children) are destroyed too
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`onDeleteTarget` is independent from `onDelete`: `onDeleteTarget` runs when a relationship
|
|
83
|
+
**target** is destroyed; `onDelete` runs when the relationship **component entity itself** is
|
|
84
|
+
destroyed (see
|
|
85
|
+
[Components — cleanup policies](./components.md#cleanup-policies-destroying-a-component-entity)).
|
|
86
|
+
|
|
87
|
+
## Querying through relationships
|
|
88
|
+
|
|
89
|
+
The DSL follows relationships in both directions
|
|
90
|
+
([operator table](./queries-and-filters.md#the-query-dsl)):
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// Reactive: candidates whose FriendOf target has Position and Velocity.
|
|
94
|
+
world.query("body-friends").with({ all: [Body, { target: [FriendOf, [Position, Velocity]] }] });
|
|
95
|
+
|
|
96
|
+
// Reactive: candidates whose ChildOf parent has Body.
|
|
97
|
+
world.query("positioned-children").with({ all: [Position, { parent: Body }] });
|
|
98
|
+
|
|
99
|
+
// Filter-only: parents with at least one ChildOf child that has Position.
|
|
100
|
+
world.filter({ all: [Body, { children: Position }] }).forEach((parent) => { ... });
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`target` / `parent` queries are **live on both sides**: adding or removing the relationship on the
|
|
104
|
+
candidate re-routes it normally, and changing the _target's_ components refreshes every candidate
|
|
105
|
+
pointing at it — across multiple nested `target` hops. `source` / `children` look down the reverse
|
|
106
|
+
index and are **non-reactive**: exact for each `Filter` scan but rejected by tracked queries and
|
|
107
|
+
systems.
|
|
108
|
+
|
|
109
|
+
To pull a target's or children's components into callbacks, use `target` / `down` injection — see
|
|
110
|
+
[Systems — relationship injection](./systems.md#relationship-injection-target-and-down).
|
|
111
|
+
|
|
112
|
+
## Traversal order: `Traversable` and `cascade`
|
|
113
|
+
|
|
114
|
+
Tag a relationship component entity with `Traversable` to enable automatic depth tracking: every
|
|
115
|
+
entity carrying the relationship gets a depth (hops to its root), maintained reactively by
|
|
116
|
+
`RelationshipsModule` (`lib/vecs/src/modules/relationships.ts`):
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { Traversable } from "@vworlds/vecs";
|
|
120
|
+
|
|
121
|
+
world.component(InCell).add(Traversable);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
A query can then order its members depth-first along the relationship with `cascade`:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
world.query("tree").with({ with: Node, cascade: ChildOf });
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Parents sort before children. Retargeting updates relationship depths, and cascade-ordered queries
|
|
131
|
+
refresh their order before the next iteration. `ChildOf` and `DependsOn` are `Traversable` out of
|
|
132
|
+
the box — the world's default pipeline is itself a `cascade: DependsOn` query (see
|
|
133
|
+
[Execution model](./execution-model.md#the-pipeline-is-a-query)).
|
|
134
|
+
|
|
135
|
+
## Reference
|
|
136
|
+
|
|
137
|
+
| API | Description |
|
|
138
|
+
| --------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
139
|
+
| `class R extends Relationship {}` | Declare a relationship component with a `target: Entity` field. |
|
|
140
|
+
| `e.set(R, { target })` | Create or retarget the relationship. |
|
|
141
|
+
| `e.target(R)` | The target entity through relationship `R`, or `undefined`. |
|
|
142
|
+
| `e.children(R)` | `ReadonlySet<Entity>` of sources targeting `e` through `R`. |
|
|
143
|
+
| `e.childOf(parent)` | Shorthand for `e.set(ChildOf, { target: parent })`. |
|
|
144
|
+
| `e.parent` / `e.parent = p` | Getter/setter for the built-in `ChildOf` target; setting `undefined` removes `ChildOf`. |
|
|
145
|
+
| `meta.onDeleteTarget` | `Remove` (default) or `Delete` cascade when a target is destroyed. |
|
|
146
|
+
| `world.component(R).add(Traversable)` | Enable depth tracking for `cascade` ordering. |
|
|
147
|
+
| `{ target: [R, q] }` / `{ parent: q }` | Reactive DSL operators through the relationship. |
|
|
148
|
+
| `{ source: [R, q] }` / `{ children: q }` | Filter-only DSL operators down the reverse index. |
|
|
149
|
+
| `{ target: [R, [C...]] }` / `{ down: [R, [C...]] }` | Injection markers; see [Systems](./systems.md#relationship-injection-target-and-down). |
|