@vworlds/vecs 1.0.25 → 1.0.26
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 +2 -2
- 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.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.js +2 -2
- 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/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/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/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 +175 -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 +148 -0
- package/docs/systems.md +311 -0
- package/docs/utilities.md +139 -0
- package/package.json +1 -4
package/README.md
CHANGED
|
@@ -2,833 +2,84 @@
|
|
|
2
2
|
|
|
3
3
|
A TypeScript Entity Component System (ECS) for real-time games and simulations.
|
|
4
4
|
|
|
5
|
-
`vecs` lets you model game state as **entities** (numeric ids) with **components** (typed data
|
|
5
|
+
`vecs` lets you model game state as **entities** (numeric ids) with **components** (typed data
|
|
6
|
+
classes) attached to them. **Systems** declare which component combinations they care about and
|
|
7
|
+
receive automatic callbacks when entities enter or leave their query, when component data changes,
|
|
8
|
+
and on every tick. A **World** ties it all together and drives a phased update pipeline.
|
|
6
9
|
|
|
7
10
|
## Install
|
|
8
11
|
|
|
12
|
+
```sh
|
|
13
|
+
npm install @vworlds/vecs
|
|
9
14
|
```
|
|
10
|
-
yarn add @vworlds/vecs
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Concepts
|
|
14
|
-
|
|
15
|
-
| Concept | What it is |
|
|
16
|
-
| ------------------------ | ------------------------------------------------------------------------------- |
|
|
17
|
-
| **World** | Central container. Owns every entity, query, system, and flat system pipeline. |
|
|
18
|
-
| **Component** | A registered plain data class with a no-argument constructor. |
|
|
19
|
-
| **Entity** | A numeric id with a set of components. Created via the world. |
|
|
20
|
-
| **Query** | A reactive, always-up-to-date set of entities matching a predicate. |
|
|
21
|
-
| **System** | A `Query` with per-tick logic (`update`, `each`, `run`). |
|
|
22
|
-
| **Filter** | A non-reactive, one-shot scan: walks all world entities on each `forEach` call. |
|
|
23
|
-
| **Hook** | Lightweight `onAdd` / `onRemove` / `onSet` callbacks per component class. |
|
|
24
|
-
| **Phase** | Marker component for entities used as pipeline ordering anchors. |
|
|
25
|
-
| **Exclusive components** | A group of components where at most one may exist on any entity at a time. |
|
|
26
15
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
world.component() / system() / query() × N → progress() every frame
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Components must be registered before they are used as component classes. Components, systems, and standalone queries can be created at any time. Newly-created systems are inserted into the pipeline the next time `world.progress()` runs; standalone queries backfill existing matched entities immediately.
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## Example
|
|
16
|
+
## A taste
|
|
38
17
|
|
|
39
18
|
```ts
|
|
40
|
-
import {
|
|
41
|
-
|
|
42
|
-
// ─── Components ────────────────────────────────────────────────────────────
|
|
19
|
+
import { ON_VALIDATE, World } from "@vworlds/vecs";
|
|
43
20
|
|
|
44
21
|
class Position {
|
|
45
22
|
x = 0;
|
|
46
23
|
y = 0;
|
|
47
24
|
}
|
|
48
|
-
|
|
49
25
|
class Velocity {
|
|
50
26
|
vx = 0;
|
|
51
27
|
vy = 0;
|
|
52
28
|
}
|
|
53
|
-
|
|
54
29
|
class Health {
|
|
55
30
|
hp = 100;
|
|
56
31
|
}
|
|
57
32
|
|
|
58
|
-
// ─── World setup ───────────────────────────────────────────────────────────
|
|
59
|
-
|
|
60
33
|
const world = new World();
|
|
61
|
-
|
|
62
34
|
world.component(Position);
|
|
63
35
|
world.component(Velocity);
|
|
64
36
|
world.component(Health);
|
|
65
37
|
|
|
66
|
-
// ─── Pipeline anchors ──────────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
const update = world.entity(ON_UPDATE);
|
|
69
|
-
const cleanup = world.entity("cleanup").add(Phase).set(DependsOn, { target: update });
|
|
70
|
-
|
|
71
|
-
// ─── Systems ───────────────────────────────────────────────────────────────
|
|
72
|
-
|
|
73
|
-
// MoveSystem: integrates Velocity into Position every tick.
|
|
74
38
|
world
|
|
75
39
|
.system("Move")
|
|
76
40
|
.with(Position, Velocity)
|
|
77
41
|
.each([Position, Velocity], (e, [pos, vel]) => {
|
|
78
42
|
pos.x += vel.vx;
|
|
79
43
|
pos.y += vel.vy;
|
|
80
|
-
e.modified(Position);
|
|
44
|
+
e.modified(Position);
|
|
81
45
|
});
|
|
82
46
|
|
|
83
|
-
// HealthSystem: despawns entities whose HP drops to zero.
|
|
84
47
|
world
|
|
85
|
-
.system("
|
|
86
|
-
.phase(
|
|
48
|
+
.system("Reap")
|
|
49
|
+
.phase(ON_VALIDATE)
|
|
87
50
|
.with(Health)
|
|
88
51
|
.update(Health, (entity, health) => {
|
|
89
|
-
if (health.hp <= 0)
|
|
90
|
-
entity.destroy();
|
|
91
|
-
}
|
|
52
|
+
if (health.hp <= 0) entity.destroy();
|
|
92
53
|
});
|
|
93
54
|
|
|
94
|
-
// ─── Hooks ─────────────────────────────────────────────────────────────────
|
|
95
|
-
|
|
96
|
-
world
|
|
97
|
-
.component(Health)
|
|
98
|
-
.onAdd((entity, h) => console.log(`entity ${entity.eid} spawned with hp=${h.hp}`))
|
|
99
|
-
.onRemove((entity) => console.log(`entity ${entity.eid} died`));
|
|
100
|
-
|
|
101
|
-
// ─── Spawn entities ────────────────────────────────────────────────────────
|
|
102
|
-
|
|
103
55
|
world.entity().set(Position, { x: 0, y: 0 }).set(Velocity, { vx: 5, vy: 0 }).set(Health, { hp: 3 });
|
|
104
56
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
let now = 0;
|
|
108
|
-
for (let tick = 0; tick < 5; tick++) {
|
|
109
|
-
now += 16;
|
|
110
|
-
world.progress(now, 16);
|
|
111
|
-
}
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
---
|
|
115
|
-
|
|
116
|
-
## Deferred mode
|
|
117
|
-
|
|
118
|
-
Inside a system body, a `Query.forEach`, or any `world.defer(...)` block, the world is in **deferred mode**: entity mutations (`add` / `attach` / `set` / `remove` / `destroy` / `modified`) are queued instead of applied inline. The queue drains on the boundary that opened the deferred scope.
|
|
119
|
-
|
|
120
|
-
Concretely, while deferred:
|
|
121
|
-
|
|
122
|
-
- `entity.get(C)` / `entity.getMut(C)` returns `undefined` after `entity.add(C)` (no instance has been created yet).
|
|
123
|
-
- `entity.get(C)` / `entity.getMut(C)` returns `undefined` after `entity.attach(instance)` if C was absent.
|
|
124
|
-
- `entity.get(C)` / `entity.getMut(C)` returns the **previous** value after `entity.set(C, props)`.
|
|
125
|
-
- `entity.get(C)` / `entity.getMut(C)` still returns the component after `entity.remove(C)`.
|
|
126
|
-
|
|
127
|
-
Outside any deferred scope (top-level user code) the same calls execute inline and effects are visible immediately. `world.flush()` drains any pending top-level commands; `world.defer(fn)` is sugar for `beginDefer / fn / endDefer`.
|
|
128
|
-
|
|
129
|
-
---
|
|
130
|
-
|
|
131
|
-
## API Reference
|
|
132
|
-
|
|
133
|
-
### `World`
|
|
134
|
-
|
|
135
|
-
Create one per game session.
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
const world = new World();
|
|
57
|
+
world.progress(performance.now(), 16); // call once per frame
|
|
139
58
|
```
|
|
140
59
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
Components are ordinary classes. They do not inherit from a vecs base class, and vecs constructs them with `new ComponentClass()`, so constructors should take no parameters. Register every component class before using it in `add`, `set`, `get`, `with`, `filter`, hook registration, or `setExclusiveComponents`. Registration can happen at any time.
|
|
60
|
+
## Documentation
|
|
144
61
|
|
|
145
|
-
|
|
146
|
-
class Position {
|
|
147
|
-
x = 0;
|
|
148
|
-
y = 0;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Auto-assigned component id (drawn from the "component" id pool, 1–899 by default):
|
|
152
|
-
const positionComponent = world.component(Position);
|
|
153
|
-
|
|
154
|
-
// Explicit numeric id (e.g. a server-assigned stable id):
|
|
155
|
-
world.component(Position, 42);
|
|
156
|
-
|
|
157
|
-
// Access component metadata (numeric id, cleanup policy, etc.):
|
|
158
|
-
const componentMeta = world.component(Position).meta; // ComponentMeta
|
|
159
|
-
|
|
160
|
-
// Give the component a name so it can be looked up by string later:
|
|
161
|
-
world.component(Position).name = "Position";
|
|
162
|
-
world.component("Position"); // resolves the same Component entity
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
`world.component(Class)` returns the `Component` entity for that class, creating and registering it on the first call. Subsequent calls with the same class return the same entity. The same class can be registered independently in multiple worlds.
|
|
166
|
-
|
|
167
|
-
Id ranges are configured by passing `idPools` to the `World` constructor. The built-in defaults are:
|
|
168
|
-
|
|
169
|
-
| Pool | Range |
|
|
170
|
-
| ----------- | --------- |
|
|
171
|
-
| `component` | 1 – 899 |
|
|
172
|
-
| `module` | 900 – 999 |
|
|
173
|
-
| `entity` | 1000 – ∞ |
|
|
174
|
-
|
|
175
|
-
There is no automatic component registration; using an unregistered component class as a component is an error.
|
|
176
|
-
|
|
177
|
-
#### Exclusive component groups
|
|
178
|
-
|
|
179
|
-
```ts
|
|
180
|
-
world.setExclusiveComponents(Walking, Running, Idle);
|
|
181
|
-
|
|
182
|
-
const e = world.entity();
|
|
183
|
-
e.add(Walking);
|
|
184
|
-
e.add(Running); // Walking is automatically removed first
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
Each call defines one independent group. A component may belong to at most one group; calling `setExclusiveComponents` again with the same class overwrites its group. Safe to call at any time.
|
|
188
|
-
|
|
189
|
-
#### Entity management
|
|
190
|
-
|
|
191
|
-
```ts
|
|
192
|
-
// New entity with an auto-incrementing id:
|
|
193
|
-
const e = world.entity();
|
|
194
|
-
|
|
195
|
-
// Look up by id (throws if not found):
|
|
196
|
-
const found = world.entity(42);
|
|
197
|
-
|
|
198
|
-
// Optional lookup by id or component ref:
|
|
199
|
-
const maybeFound = world.getEntity(42);
|
|
200
|
-
|
|
201
|
-
// Server-assigned id; creates the entity if it doesn't exist yet:
|
|
202
|
-
const net = world.getOrCreateEntity(serverId, (newEntity) => {
|
|
203
|
-
tracked.add(newEntity);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// Destroy everything (e.g. on level reset):
|
|
207
|
-
world.clearAllEntities();
|
|
208
|
-
|
|
209
|
-
// Named entity lookup via Identity.name — creates entity with that name if absent:
|
|
210
|
-
world.entity("player").name === "player"; // true
|
|
211
|
-
world.getEntity("player") === entity; // true (returns undefined when absent)
|
|
212
|
-
|
|
213
|
-
// Named component lookup — set the name first, then resolve by name:
|
|
214
|
-
world.component(Position).name = "Position";
|
|
215
|
-
world.component("Position"); // resolves the same component entity
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
#### Component entity deletion cleanup
|
|
219
|
-
|
|
220
|
-
Every component key is backed by a component entity. Destroying that component entity is controlled by `world.component(C).meta.onDelete`:
|
|
221
|
-
|
|
222
|
-
| Policy | Meaning |
|
|
223
|
-
| ---------------------- | -------------------------------------------------------------------------------------------------- |
|
|
224
|
-
| `CleanupPolicy.Throw` | Default. Destroying the component entity while other entities carry it throws and changes nothing. |
|
|
225
|
-
| `CleanupPolicy.Remove` | Remove the component from every carrier, then destroy the component entity. |
|
|
226
|
-
| `CleanupPolicy.Delete` | Destroy every carrier, then destroy the component entity. |
|
|
227
|
-
|
|
228
|
-
```ts
|
|
229
|
-
import { CleanupPolicy } from "@vworlds/vecs";
|
|
230
|
-
|
|
231
|
-
world.component(Temporary).meta.onDelete = CleanupPolicy.Remove;
|
|
232
|
-
world.component(Temporary).destroy(); // strips Temporary from all carriers
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
The `Throw` default also applies to plain entities used as component keys (`entity.add(key)`). It preserves consistency: a failed destroy leaves the component entity, its carriers, and the usage count untouched.
|
|
236
|
-
|
|
237
|
-
#### Hooks
|
|
238
|
-
|
|
239
|
-
```ts
|
|
240
|
-
world
|
|
241
|
-
.component(Sprite)
|
|
242
|
-
.onAdd((entity, sprite) => sprite.initialize(scene, entity))
|
|
243
|
-
.onRemove((entity, sprite) => sprite.destroy(scene, entity))
|
|
244
|
-
.onSet((entity, sprite) => sprite.syncToScene(entity));
|
|
245
|
-
```
|
|
62
|
+
The full documentation lives in [`docs/`](./docs/README.md). Recommended reading order:
|
|
246
63
|
|
|
247
|
-
|
|
64
|
+
1. [Getting started](./docs/getting-started.md) — install to first ticking world.
|
|
65
|
+
2. [Concepts](./docs/concepts.md) — the mental model.
|
|
66
|
+
3. [Execution model](./docs/execution-model.md) — phases, deferred mutation, event routing.
|
|
67
|
+
4. Subsystem guides: [Components](./docs/components.md) · [Entities](./docs/entities.md) ·
|
|
68
|
+
[Systems](./docs/systems.md) · [Queries and filters](./docs/queries-and-filters.md) ·
|
|
69
|
+
[Relationships](./docs/relationships.md) · [Modules](./docs/modules.md)
|
|
70
|
+
5. [Design guide](./docs/design-guide.md) — patterns and anti-patterns from real designs.
|
|
248
71
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const update = world.entity("vecs::OnUpdate");
|
|
253
|
-
const send = world.entity("send").add(Phase).set(DependsOn, { target: update });
|
|
254
|
-
|
|
255
|
-
world.system("Send").phase(send).run(sendPackets);
|
|
256
|
-
|
|
257
|
-
// Drive every system returned by the pipeline query:
|
|
258
|
-
world.progress(now, delta);
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
Systems default to `vecs::OnUpdate`; call `.phase(...)` only when a system needs another pipeline anchor. The default pipeline query matches systems whose backing entity `DependsOn` a `Phase`, ordered by the `DependsOn` cascade. Built-in phase entity names are namespaced: `vecs::OnLoad`, `vecs::PostLoad`, `vecs::PreUpdate`, `vecs::OnUpdate`, `vecs::OnValidate`, `vecs::PostUpdate`, `vecs::PreStore`, and `vecs::OnStore`.
|
|
262
|
-
|
|
263
|
-
Use `world.setPipeline(spec)` to replace the default. `World` appends `System` to the query, so the query result is always flattened to systems.
|
|
264
|
-
|
|
265
|
-
#### Systems
|
|
266
|
-
|
|
267
|
-
```ts
|
|
268
|
-
world
|
|
269
|
-
.system("MySystem")
|
|
270
|
-
.with(A, B)
|
|
271
|
-
.enter(...)
|
|
272
|
-
.update(...)
|
|
273
|
-
.each(...)
|
|
274
|
-
.exit(...);
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
#### Timers and rate filters
|
|
278
|
-
|
|
279
|
-
Systems can opt into a slower cadence instead of running on every phase tick. `interval()` takes seconds; throttled `run()` callbacks receive the accumulated milliseconds since the previous fire as `delta`.
|
|
280
|
-
|
|
281
|
-
```ts
|
|
282
|
-
import { IntervalTickSource, RateTickSource } from "@vworlds/vecs";
|
|
283
|
-
|
|
284
|
-
world
|
|
285
|
-
.system("Move")
|
|
286
|
-
.interval(1.0)
|
|
287
|
-
.each([Position], (e, [pos]) => {
|
|
288
|
-
// 1 Hz
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
world
|
|
292
|
-
.system("Move")
|
|
293
|
-
.rate(2)
|
|
294
|
-
.each([Position], (e, [pos]) => {
|
|
295
|
-
// every 2nd frame
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
const second = new IntervalTickSource(1.0);
|
|
299
|
-
|
|
300
|
-
world
|
|
301
|
-
.system("Move")
|
|
302
|
-
.tickSource(second)
|
|
303
|
-
.each([Position], (e, [pos]) => {
|
|
304
|
-
// driven by a shared timer
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
second.stop();
|
|
308
|
-
second.start();
|
|
309
|
-
|
|
310
|
-
const minute = new RateTickSource(60, second);
|
|
311
|
-
const hour = world
|
|
312
|
-
.system("Hour")
|
|
313
|
-
.tickSource(minute)
|
|
314
|
-
.rate(60)
|
|
315
|
-
.run((now, delta) => {
|
|
316
|
-
console.log("hour tick", now, delta);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
// Systems can also be tick sources for each other.
|
|
320
|
-
const eachSecond = world
|
|
321
|
-
.system("EachSecond")
|
|
322
|
-
.interval(1)
|
|
323
|
-
.run(() => {
|
|
324
|
-
// ...
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
const eachMinute = world
|
|
328
|
-
.system("EachMinute")
|
|
329
|
-
.tickSource(eachSecond)
|
|
330
|
-
.rate(60)
|
|
331
|
-
.run(() => {
|
|
332
|
-
// ...
|
|
333
|
-
});
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
Tick source objects and systems can both be used as sources. Disabling a source system suppresses its callbacks, but its clock still drives downstream consumers.
|
|
337
|
-
|
|
338
|
-
#### Queries
|
|
339
|
-
|
|
340
|
-
```ts
|
|
341
|
-
const enemies = world
|
|
342
|
-
.query("Enemies")
|
|
343
|
-
.with(Enemy, Health)
|
|
344
|
-
.enter((e) => console.log("enemy spawned", e.eid));
|
|
345
|
-
|
|
346
|
-
// enemies.count and query iteration are kept up-to-date automatically.
|
|
347
|
-
|
|
348
|
-
// Standalone queries can also be created later; existing matched entities are
|
|
349
|
-
// backfilled immediately.
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
#### Filters
|
|
353
|
-
|
|
354
|
-
```ts
|
|
355
|
-
// Entity only:
|
|
356
|
-
world.filter([Position]).forEach((e) => console.log(e.eid));
|
|
357
|
-
|
|
358
|
-
// With component injection:
|
|
359
|
-
world.filter([Position, Velocity]).forEach([Position, Velocity], (e, [pos, vel]) => {
|
|
360
|
-
pos.x += vel.vx;
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
// Full DSL, with auto-deduced required components:
|
|
364
|
-
world.filter({ all: [Position, Velocity] }).forEach([Position, Velocity], (e, [pos, vel]) => {
|
|
365
|
-
pos.x += vel.vx;
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
// Manual hint for queries the type extractor can't see through:
|
|
369
|
-
world
|
|
370
|
-
.filter({ with: { any: [Position, Velocity] }, hint: [Position] })
|
|
371
|
-
.forEach([Position], (e, [pos]) => pos.x);
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
A `Filter` requires no name and no `destroy()` — create it anywhere and discard freely.
|
|
375
|
-
|
|
376
|
-
#### Pipeline control
|
|
377
|
-
|
|
378
|
-
```ts
|
|
379
|
-
world.flush(); // drain queued top-level mutations
|
|
380
|
-
world.defer(() => { ... }); // run a block in deferred mode
|
|
381
|
-
world.beginDefer(); // pair with endDefer() for finer scoping
|
|
382
|
-
world.endDefer();
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
---
|
|
386
|
-
|
|
387
|
-
### `ComponentInstance`
|
|
388
|
-
|
|
389
|
-
Components are plain classes. vecs does not provide a runtime base class and does not attach `entity`, `meta`, `type`, `bitPtr`, or `modified()` to component instances.
|
|
390
|
-
|
|
391
|
-
```ts
|
|
392
|
-
class Position {
|
|
393
|
-
x = 0;
|
|
394
|
-
y = 0;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
world.component(Position);
|
|
398
|
-
|
|
399
|
-
entity.add(Position);
|
|
400
|
-
const pos = entity.getMut(Position)!;
|
|
401
|
-
pos.x = 100;
|
|
402
|
-
// getMut marks Position changed before returning the mutable instance.
|
|
403
|
-
|
|
404
|
-
// Equivalent — set assigns props and fires onSet automatically:
|
|
405
|
-
entity.set(Position, { x: 100 });
|
|
406
|
-
|
|
407
|
-
// Store an existing instance directly:
|
|
408
|
-
const shared = new Position();
|
|
409
|
-
entity.attach(shared);
|
|
410
|
-
entity.get(Position) === shared; // true
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
| Rule | Description |
|
|
414
|
-
| ------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
415
|
-
| Plain class | Components should be ordinary classes with field initializers and methods as needed. |
|
|
416
|
-
| No-arg construction | vecs calls `new ComponentClass()`, so constructors should be omitted or take no parameters. |
|
|
417
|
-
| Explicit registration | Call `world.component(C)` before using the class as a component. |
|
|
418
|
-
| Shared instances possible | `entity.attach(instance)` stores the exact passed object; code should use the entity passed by vecs callbacks. |
|
|
419
|
-
| Manual dirty marking | After mutating fields directly, call `entity.modified(C)` to notify hooks, queries, and systems. |
|
|
420
|
-
|
|
421
|
-
Use `world.component(C).meta` when you need metadata such as the numeric type id or component name. Metadata is world-specific.
|
|
422
|
-
|
|
423
|
-
---
|
|
424
|
-
|
|
425
|
-
### `Entity`
|
|
426
|
-
|
|
427
|
-
Created via `world.entity()` (auto-assigned id) or `world.getOrCreateEntity(id, ...)` (caller-supplied id).
|
|
428
|
-
|
|
429
|
-
| Property / Method | Description |
|
|
430
|
-
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
431
|
-
| `eid` | Unique numeric entity id. |
|
|
432
|
-
| `world` | The `World` that owns this entity. |
|
|
433
|
-
| `componentBitmask` | `Bitset` of component type ids attached to this entity. Used by archetype matching. |
|
|
434
|
-
| `properties` | `Map<string, any>` free-form bag for module-level bookkeeping. |
|
|
435
|
-
| `add(Class)` | Attach a component (idempotent). Returns the entity for chaining. |
|
|
436
|
-
| `attach(instance)` | Attach an existing registered component instance directly; replaces any previous instance for that component class and fires `onSet`. |
|
|
437
|
-
| `set(Class, props)` | Attach a component and assign `props`; fires `onSet`. Returns the entity for chaining. |
|
|
438
|
-
| `modified(Class)` | Queue an `onSet` / `update` notification for a component class or numeric type id. Returns the entity for chaining. |
|
|
439
|
-
| `get(Class)` | Return the component as a readonly-typed value, or `undefined`. |
|
|
440
|
-
| `getMut(Class)` | Mark the component modified and return the mutable instance, or `undefined`. |
|
|
441
|
-
| `remove(Class)` | Detach a component (fires `onRemove` and `exit`). |
|
|
442
|
-
| `destroy()` | Remove all components, unregister from the world, then applies incoming relationship cleanup policies. |
|
|
443
|
-
| `components` | `ReadonlyArrayMap<ComponentInstance>` — read-only view of attached components keyed by type id. Supports `forEach`, `get`, `has`, and `size`. |
|
|
444
|
-
| `empty` | `true` when no components are attached. |
|
|
445
|
-
| `parent(ref)` | Target entity for relationship component `ref`, or `undefined`. |
|
|
446
|
-
| `children(ref)` | `ReadonlySet<Entity>` of entities targeting this one through relationship component `ref`. |
|
|
447
|
-
| `events` | Typed event emitter. Currently emits `"destroy"` just before teardown. |
|
|
448
|
-
| `toString()` | Returns `"EntityN"`. |
|
|
449
|
-
|
|
450
|
-
Use `entity.getMut(C)` when you need to mutate a component directly. It calls `entity.modified(C)` before returning the mutable instance. Repeated modified notifications for the same component type are coalesced while the world is deferred:
|
|
451
|
-
|
|
452
|
-
```ts
|
|
453
|
-
const vel = entity.getMut(Velocity)!;
|
|
454
|
-
vel.vx += accel;
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
Use `entity.attach(instance)` when component ownership is intentionally shared with caller code or another object graph. The instance constructor must be registered in the entity's world; unregistered instances throw. If the component belongs to an exclusive component group, conflicting components are removed before the instance is stored.
|
|
458
|
-
|
|
459
|
-
#### Relationships and parent / child hierarchy
|
|
460
|
-
|
|
461
|
-
```ts
|
|
462
|
-
import { ChildOf, CleanupPolicy, Relationship } from "@vworlds/vecs";
|
|
463
|
-
|
|
464
|
-
child.set(ChildOf, { target: parent });
|
|
465
|
-
child.parent(ChildOf) === parent; // true
|
|
466
|
-
parent.children(ChildOf).has(child); // true
|
|
467
|
-
|
|
468
|
-
// ChildOf is configured with CleanupPolicy.Delete, so destroying a parent
|
|
469
|
-
// recursively destroys children that target it through ChildOf:
|
|
470
|
-
parent.destroy();
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
Any component that extends `Relationship` can point at another entity through its `target` field. The source entity stores the relationship component; the target entity can enumerate sources with `children(ref)`.
|
|
474
|
-
|
|
475
|
-
```ts
|
|
476
|
-
class EquippedBy extends Relationship {}
|
|
477
|
-
|
|
478
|
-
item.set(EquippedBy, { target: player });
|
|
479
|
-
item.parent(EquippedBy); // player
|
|
480
|
-
player.children(EquippedBy).has(item); // true
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
Relationship target cleanup is controlled by `meta.onDeleteTarget`:
|
|
484
|
-
|
|
485
|
-
| Policy | Meaning |
|
|
486
|
-
| ---------------------- | -------------------------------------------------------------------------------------------- |
|
|
487
|
-
| `CleanupPolicy.Remove` | Default. When a target is destroyed, remove the relationship component from each source. |
|
|
488
|
-
| `CleanupPolicy.Delete` | When a target is destroyed, destroy each source entity that targets it through the relation. |
|
|
489
|
-
|
|
490
|
-
`ChildOf` is registered by every `World` and uses `CleanupPolicy.Delete`. Custom relationships default to `Remove`; set `world.component(MyRelationship).meta.onDeleteTarget = CleanupPolicy.Delete` if target deletion should cascade.
|
|
491
|
-
|
|
492
|
-
`onDeleteTarget` is independent from `onDelete`: `onDeleteTarget` runs when a relationship target is destroyed, while `onDelete` runs when the relationship component entity itself is destroyed.
|
|
493
|
-
|
|
494
|
-
Retarget relationships with `entity.set(RelationshipClass, { target })`. `entity.getMut(RelationshipClass)` throws for relationship components because in-place target mutation would bypass the reverse index used by `children(ref)` and cleanup cascades.
|
|
495
|
-
|
|
496
|
-
Relationship queries use `target` to follow a relationship from the candidate entity to its target and test that target with another DSL expression. Use `parent` for the built-in `ChildOf` hierarchy:
|
|
497
|
-
|
|
498
|
-
```ts
|
|
499
|
-
world.query("body-friends").with({ all: [Body, { target: [FriendOf, [Position, Velocity]] }] });
|
|
500
|
-
world.query("positioned-children").with({ all: [Position, { parent: Body }] });
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
Tracked queries stay live when either side changes. Adding or removing `FriendOf` on the candidate re-routes the candidate normally; adding or removing `Position` / `Velocity` on the target also refreshes every candidate that points at that target. Nested `target` works the same way across multiple hops.
|
|
504
|
-
|
|
505
|
-
Use `source` with `world.filter(...)` when you need the opposite direction: test whether at least one child points at the candidate through a relationship and matches an inner DSL expression. Use `children` for the built-in `ChildOf` hierarchy. `source` and `children` are exact for each filter scan, but they are non-reactive and rejected by tracked `Query` / `System` membership.
|
|
506
|
-
|
|
507
|
-
```ts
|
|
508
|
-
world.filter({ all: [Body, { children: Position }] }).forEach((parent) => {
|
|
509
|
-
// parent has at least one ChildOf child with Position right now.
|
|
510
|
-
});
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
Component injection supports lowercase `down` in `forEach` and system `each` injection lists. It fans out: the callback fires once for each child pointing at the visited entity through the relationship, and child component slots are nullable.
|
|
514
|
-
|
|
515
|
-
```ts
|
|
516
|
-
world
|
|
517
|
-
.query("parents")
|
|
518
|
-
.with(Body)
|
|
519
|
-
.forEach([Body, { down: [ChildOf, [Position]] }], (parent, [body, childPos]) => {
|
|
520
|
-
// One call per ChildOf child of parent. childPos is undefined when that child lacks Position.
|
|
521
|
-
});
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
Two details matter:
|
|
525
|
-
|
|
526
|
-
- A `down` injection does not call the callback for a matched entity with zero children through that relationship.
|
|
527
|
-
- `down` injection iterates all children through the relationship, independent of any `children` / `source` filter used for membership. For example, `world.filter({ children: Position }).forEach([{ down: [ChildOf, [Position]] }], ...)` also visits children without `Position`, with `childPos === undefined`.
|
|
528
|
-
|
|
529
|
-
---
|
|
530
|
-
|
|
531
|
-
### `System`
|
|
532
|
-
|
|
533
|
-
Systems are created via `world.system(name)` and configured through a fluent builder. Every method returns `this` for chaining. `System` extends `Query`, so the membership / enter / exit / update / sort APIs are shared.
|
|
534
|
-
|
|
535
|
-
#### `.with(...specs)`
|
|
536
|
-
|
|
537
|
-
Declare which entities the system tracks.
|
|
538
|
-
|
|
539
|
-
```ts
|
|
540
|
-
.with(Position, Velocity) // explicit component list
|
|
541
|
-
.with([Position, Velocity]) // array shorthand
|
|
542
|
-
.with({ all: [Position, { any: [Sprite, Container] }] }) // compound
|
|
543
|
-
.with({ not: Invisible })
|
|
544
|
-
.without(Invisible) // shorthand for .with({ not: Invisible })
|
|
545
|
-
.with({ target: [FriendOf, Position] }) // relationship target has Position
|
|
546
|
-
.with({ parent: Body }) // ChildOf target has Body
|
|
547
|
-
```
|
|
548
|
-
|
|
549
|
-
**Query operators:**
|
|
550
|
-
|
|
551
|
-
| Operator | Meaning |
|
|
552
|
-
| -------------------- | ------------------------------------------------------ |
|
|
553
|
-
| A single class / id | Entity has that component |
|
|
554
|
-
| An array `[A, B]` | Entity has all of A and B |
|
|
555
|
-
| `{ only: [A, B] }` | Entity has exactly A and B, nothing else |
|
|
556
|
-
| `{ all: [q1, q2] }` | Both sub-queries must match |
|
|
557
|
-
| `{ any: [q1, q2] }` | Either sub-query matches |
|
|
558
|
-
| `{ not: q }` | Sub-query must not match |
|
|
559
|
-
| `{ target: [R, q] }` | Relationship `R` target matches `q` |
|
|
560
|
-
| `{ source: [R, q] }` | Filter-only: some `R` source/child matches `q` |
|
|
561
|
-
| `{ parent: q }` | Built-in `ChildOf` target matches `q` |
|
|
562
|
-
| `{ children: q }` | Filter-only: some built-in `ChildOf` child matches `q` |
|
|
563
|
-
| `{ test: fn }` | Custom membership logic |
|
|
564
|
-
|
|
565
|
-
`target` and `source` use tuple form only: `{ target: [RelationshipClass, innerDSL] }` / `{ source: [RelationshipClass, innerDSL] }`. The first element must be a component that extends `Relationship`; passing a normal component throws. `target` evaluates the inner DSL against the relationship target; `source` evaluates it against each child/source and matches when at least one child/source matches. Because `source` and `children` are non-reactive, use them with `world.filter(...)`, not tracked queries or systems.
|
|
566
|
-
|
|
567
|
-
Component injection also supports lowercase relationship markers. `target` works in `forEach`, `each`, `enter`, and `exit` injection lists. It follows the relationship target and flattens the requested target components into the callback tuple:
|
|
568
|
-
|
|
569
|
-
```ts
|
|
570
|
-
world
|
|
571
|
-
.query("friends")
|
|
572
|
-
.with({ target: [FriendOf, Position] })
|
|
573
|
-
.forEach([Body, { target: [FriendOf, [Position, Velocity]] }], (e, [body, pos, vel]) => {
|
|
574
|
-
// body is from e; pos and vel are from e.parent(FriendOf)
|
|
575
|
-
// target-side injected components are undefined if the target is absent or lacks them.
|
|
576
|
-
});
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
On `exit`, direct component injection is snapshot-stable when the exiting component was just removed. `target` injection resolves the target live at callback time, so target-side slots can be `undefined` if the exit was caused by the target losing that component.
|
|
580
|
-
|
|
581
|
-
Lowercase `down` is allowed only in `forEach` and system `each`, where fan-out has a clear meaning. Only one `down` marker is allowed per injection tuple.
|
|
582
|
-
|
|
583
|
-
#### Iter cursor
|
|
584
|
-
|
|
585
|
-
Pass `Iter` as the first argument to `each`, `forEach`, `enter`, `exit`, `update`, or `orderBy` to receive a reusable cursor object instead of the bare entity. The cursor exposes:
|
|
586
|
-
|
|
587
|
-
- `it.entity` — the visited entity.
|
|
588
|
-
- `it.src` — a same-length array of source entities: the visited entity for a directly-injected component, the relationship target for a `target`-injected one, or the specific child for a `down`-injected one.
|
|
589
|
-
|
|
590
|
-
```ts
|
|
591
|
-
system.each(Iter, [Body, { target: [ChildOf, [Position]] }], (it, [body, pos]) => {
|
|
592
|
-
it.entity; // the visited entity
|
|
593
|
-
it.src[0]; // entity `body` was read from (the visited entity)
|
|
594
|
-
it.src[1]; // entity `pos` was read from (the ChildOf target)
|
|
595
|
-
});
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
When `Iter` is **not** requested, the original code path runs with zero per-entity overhead. Dispatch to the cursor or non-cursor path happens once at setup time — not per entity. A single `Iter` instance is mutated before each callback; read what you need inside the callback but do not retain the cursor or its `src` array across callbacks.
|
|
599
|
-
|
|
600
|
-
`enter` and `exit` allocate a fresh `Iter` per event (they fire reentrantly during command routing). `each` / `forEach` / `update` reuse a single `Iter` instance for the whole pass; `orderBy` reuses two (one per side of the comparison). Because a plain `Query.update` callback fires synchronously during command routing, a nested mutation triggered from inside it can re-enter `update` and overwrite that reused cursor and tuple mid-callback — snapshot any `it`, `it.src`, or tuple values you need before triggering further mutations.
|
|
601
|
-
|
|
602
|
-
Class-valued query terms are components. Register component classes before using them in a `QueryDSL`; an unregistered class throws. Use `{ test: fn }` for arbitrary predicate functions.
|
|
603
|
-
|
|
604
|
-
**Type inference.** `with()` records inferrable component classes as a type parameter `R` on the system. Callbacks in `.orderBy()`, `.each()`, and `.update()` injection treat those components as non-nullable — no `!` needed. For complex `with()` expressions the type system can't introspect, supply a `hint` field:
|
|
605
|
-
|
|
606
|
-
```ts
|
|
607
|
-
.with({ with: { all: [Position, Velocity] }, hint: [Position, Velocity] })
|
|
608
|
-
.each([Position, Velocity], (e, [pos, vel]) => {
|
|
609
|
-
pos.x += vel.vx; // pos and vel are non-null
|
|
610
|
-
});
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
#### `.phase(anchor)`
|
|
614
|
-
|
|
615
|
-
Place the system after a pipeline anchor by name or entity. Sets `DependsOn` on the system's backing entity (replacing any previous target) and tags the anchor with `Phase`, so it is picked up and ordered by the default pipeline query. Systems default to `vecs::OnUpdate`, so call this only to run elsewhere.
|
|
616
|
-
|
|
617
|
-
```ts
|
|
618
|
-
.phase(ON_LOAD) // a built-in phase constant
|
|
619
|
-
.phase("custom") // a custom anchor, created and tagged if missing
|
|
620
|
-
.phase(world.entity("custom")) // an existing entity
|
|
621
|
-
```
|
|
622
|
-
|
|
623
|
-
#### `.enter(callback)` / `.enter(inject, callback)`
|
|
624
|
-
|
|
625
|
-
Fires once when an entity first matches the system.
|
|
626
|
-
|
|
627
|
-
```ts
|
|
628
|
-
.enter((e) => { ... })
|
|
629
|
-
.enter([Position, Sprite], (e, [pos, sprite]) => {
|
|
630
|
-
sprite.setPosition(pos.x, pos.y);
|
|
631
|
-
})
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
#### `.exit(callback)` / `.exit(inject, callback)`
|
|
635
|
-
|
|
636
|
-
Fires when an entity leaves the system (component removed or entity destroyed). Direct components removed in the same frame are still resolvable in `inject`; `target` injection resolves relationship targets live.
|
|
637
|
-
|
|
638
|
-
```ts
|
|
639
|
-
.exit([Sprite], (e, [sprite]) => sprite.destroy());
|
|
640
|
-
```
|
|
641
|
-
|
|
642
|
-
#### `.update(ComponentClass, callback)` / `.update(ComponentClass, inject, callback)`
|
|
643
|
-
|
|
644
|
-
Fires when `entity.modified(ComponentClass)` is called for the watched component on a tracked entity. It also fires for `entity.set(C, props)` on an already-attached component and for initial watched components when an entity enters the query. The callback receives the entity first because component instances do not carry owner references.
|
|
645
|
-
|
|
646
|
-
```ts
|
|
647
|
-
.update(Position, (entity, pos) => renderer.setPosition(entity.eid, pos.x, pos.y));
|
|
648
|
-
|
|
649
|
-
.update(Position, [Sprite], (entity, pos, [sprite]) => {
|
|
650
|
-
sprite.sprite.setPosition(pos.x, pos.y);
|
|
651
|
-
});
|
|
652
|
-
```
|
|
653
|
-
|
|
654
|
-
If `with()` has not been called, `update` automatically expands the implicit component predicate to require the watched component.
|
|
655
|
-
|
|
656
|
-
#### `.each(components, callback)`
|
|
657
|
-
|
|
658
|
-
Fires every tick for **every tracked entity**, regardless of whether anything changed. Use it for per-entity logic that must run every frame. Implies `.track()`. Only one `each` per system.
|
|
659
|
-
|
|
660
|
-
```ts
|
|
661
|
-
.with(Position, Velocity)
|
|
662
|
-
.each([Position, Velocity], (e, [pos, vel]) => {
|
|
663
|
-
pos.x += vel.vx;
|
|
664
|
-
});
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
#### `.orderBy(components, compare)`
|
|
668
|
-
|
|
669
|
-
Store matched entities in a custom order determined by `compare`. Implies `.track()`. Iterating the system, `forEach`, and `each` walks entities in sorted order.
|
|
670
|
-
|
|
671
|
-
```ts
|
|
672
|
-
world
|
|
673
|
-
.system("Render")
|
|
674
|
-
.with(Position, Sprite)
|
|
675
|
-
.orderBy([Position], (_entityA, [posA], _entityB, [posB]) => posA.z - posB.z)
|
|
676
|
-
.each([Position, Sprite], (e, [pos, sprite]) => sprite.draw(pos.x, pos.y));
|
|
677
|
-
```
|
|
678
|
-
|
|
679
|
-
#### `.track()`
|
|
680
|
-
|
|
681
|
-
Enable entity tracking without an `each` callback — exposes matched entities via `system.count`, `system.has(e)`, and direct iteration. `each` and `orderBy` imply `track` automatically. Backfills existing matched entities when the system is built.
|
|
682
|
-
|
|
683
|
-
#### `.run(callback)`
|
|
684
|
-
|
|
685
|
-
Fires every tick when the system runs in the pipeline, regardless of entity state. Use for polling, network I/O, timers, etc.
|
|
686
|
-
|
|
687
|
-
```ts
|
|
688
|
-
.run((now, delta) => {
|
|
689
|
-
sendNetworkPacket(now);
|
|
690
|
-
});
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
#### `.disable()` / `.enable()`
|
|
694
|
-
|
|
695
|
-
Pause and resume a system at runtime. While disabled the system is effectively invisible: the inbox is cleared immediately, any new `enter`, `exit`, or `update` events are silently dropped, `run` and `each` callbacks do not fire, and the system skips its `_run` entirely. Entity membership in the underlying query is still maintained, so the tracked set remains correct and the system resumes cleanly when re-enabled. Events that occurred while the system was disabled are **not** replayed.
|
|
696
|
-
|
|
697
|
-
```ts
|
|
698
|
-
const ai = world.system("AI").with(Enemy).run(tickAI);
|
|
699
|
-
|
|
700
|
-
// Pause AI processing during a cutscene:
|
|
701
|
-
ai.disable();
|
|
702
|
-
|
|
703
|
-
// Resume normal processing:
|
|
704
|
-
ai.enable();
|
|
705
|
-
```
|
|
706
|
-
|
|
707
|
-
Both methods return `this` for chaining and are idempotent (calling `disable()` on an already-disabled system, or `enable()` on an already-enabled system, is a no-op).
|
|
708
|
-
|
|
709
|
-
#### `.destroy()`
|
|
710
|
-
|
|
711
|
-
Permanently remove this system from the world. Calls `disable()` first (clearing the inbox), then unregisters its backing entity. No `exit` callbacks fire. Use a standalone `Query` if you need a temporary reactive set that can be destroyed mid-session.
|
|
712
|
-
|
|
713
|
-
---
|
|
714
|
-
|
|
715
|
-
### `Query`
|
|
716
|
-
|
|
717
|
-
`world.query(name)` returns a standalone reactive entity set, configured through the same builder API as `System`. It has no per-tick callbacks.
|
|
718
|
-
|
|
719
|
-
```ts
|
|
720
|
-
const projectiles = world
|
|
721
|
-
.query("Projectiles")
|
|
722
|
-
.with(Position, Velocity)
|
|
723
|
-
.orderBy([Position], (_entityA, [a], _entityB, [b]) => a.z - b.z)
|
|
724
|
-
.enter([Position], (e, [pos]) => {
|
|
725
|
-
pos.x = spawnX;
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
projectiles.forEach((e) => { ... });
|
|
729
|
-
for (const e of projectiles) { ... }
|
|
730
|
-
console.log(projectiles.count, "active projectiles");
|
|
731
|
-
```
|
|
732
|
-
|
|
733
|
-
| Method | Description |
|
|
734
|
-
| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
735
|
-
| `.with(...specs)` | Add membership predicates using `QuerySpec` expressions. |
|
|
736
|
-
| `.without(dsl)` | Add a negated membership predicate; shorthand for `.with({ not: dsl })`. |
|
|
737
|
-
| `.enter(callback)` / `.enter(inject, callback)` | Fires when an entity joins the query. |
|
|
738
|
-
| `.exit(callback)` / `.exit(inject, callback)` | Fires when an entity leaves the query. |
|
|
739
|
-
| `.update(C, callback)` / `.update(C, inject, callback)` | Fires when `C` is modified on a tracked entity. Callback receives `(entity, component, injected?)`. |
|
|
740
|
-
| `.orderBy(components, compare)` | Store matched entities in sorted order. Comparator receives `(entityA, tupleA, entityB, tupleB)`. |
|
|
741
|
-
| `.track()` | Enable tracking. Backfills existing matches when the query is built. |
|
|
742
|
-
| `.belongs(e)` | Returns `true` if the entity satisfies the predicate. |
|
|
743
|
-
| `.count` | Number of currently tracked entities. |
|
|
744
|
-
| `.has(e)` | Returns `true` if the entity is currently tracked. |
|
|
745
|
-
| `[Symbol.iterator]()` | Iterate currently tracked entities with `for (const e of query)`. |
|
|
746
|
-
| `.forEach(callback)` | Iterate currently tracked entities. |
|
|
747
|
-
| `.forEach(components, callback)` | Iterate with component injection. |
|
|
748
|
-
| `.destroy()` | Remove the query from the world and from every entity (no exit fires). |
|
|
749
|
-
|
|
750
|
-
#### `.destroy()` semantics
|
|
751
|
-
|
|
752
|
-
`destroy()` permanently removes a standalone query from the world. Entity references are silently purged (no `exit` callbacks fire), the tracked set is cleared, and the `world` reference is set to `undefined`. Any further use of the object is **undefined behavior**.
|
|
753
|
-
|
|
754
|
-
```ts
|
|
755
|
-
const q = world.query("Temporary").with(Position);
|
|
756
|
-
// ... use q.count, q.has(e), or iterate q ...
|
|
757
|
-
q.destroy();
|
|
758
|
-
```
|
|
759
|
-
|
|
760
|
-
`System` shares the same DSL, callback, sorting, and tracking machinery — `System` extends `Query` and adds `run`, `each`, and an inbox replayed on every tick.
|
|
761
|
-
|
|
762
|
-
---
|
|
763
|
-
|
|
764
|
-
### `Filter`
|
|
765
|
-
|
|
766
|
-
`world.filter(dsl)` returns a `Filter` that performs a non-reactive scan. It accepts the same `QueryDSL` expressions as systems and queries.
|
|
767
|
-
|
|
768
|
-
```ts
|
|
769
|
-
const f = world.filter([Position, Velocity]);
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
| Method | Description |
|
|
773
|
-
| -------------------------------- | -------------------------------------------------------------------------- |
|
|
774
|
-
| `.forEach(callback)` | Walk all world entities; invoke callback on each match. |
|
|
775
|
-
| `.forEach(components, callback)` | Same, with component injection and non-null types for required components. |
|
|
776
|
-
|
|
777
|
-
`forEach` runs inside a deferred scope, so mutations made by the callback are batched and become visible after iteration finishes.
|
|
778
|
-
|
|
779
|
-
**Type inference.** Component classes the type system can extract from the DSL (plain component classes, plain arrays, `only`, and `all` of those) are non-nullable in the callback tuple. For the rest, supply a `hint` field:
|
|
780
|
-
|
|
781
|
-
```ts
|
|
782
|
-
// Auto-deduced — both non-null:
|
|
783
|
-
world.filter([Position, Velocity]).forEach([Position, Velocity], (e, [pos, vel]) => { ... });
|
|
784
|
-
|
|
785
|
-
// Manual hint for any / not / test:
|
|
786
|
-
world.filter({ with: { any: [Position, Velocity] }, hint: [Position] }).forEach([Position], (e, [pos]) => pos.x);
|
|
787
|
-
```
|
|
788
|
-
|
|
789
|
-
A `Filter` holds no tracked set, makes no registration calls, and needs no `destroy()`.
|
|
790
|
-
|
|
791
|
-
---
|
|
792
|
-
|
|
793
|
-
### `Bitset`
|
|
794
|
-
|
|
795
|
-
A compact, growable set of non-negative integers backed by 32-bit words. Used internally for entity archetypes and watchlists, and exposed in the public API so component data can use it for bit-flag fields.
|
|
796
|
-
|
|
797
|
-
| Method | Description |
|
|
798
|
-
| ------------------ | ------------------------------------------------------------------------------------- |
|
|
799
|
-
| `add(n)` | Set bit `n`. |
|
|
800
|
-
| `addBit(bptr)` | Set the bit at a pre-computed `BitPtr` (fast path). |
|
|
801
|
-
| `delete(n)` | Clear bit `n`. Storage is not compacted automatically; call `compact()` when needed. |
|
|
802
|
-
| `deleteBit(bptr)` | Clear the bit at a pre-computed `BitPtr` (fast path). Does not compact automatically. |
|
|
803
|
-
| `compact()` | Trim trailing zero words from backing storage. |
|
|
804
|
-
| `clear()` | Remove every set bit. |
|
|
805
|
-
| `has(n)` | Returns `true` if bit `n` is set. |
|
|
806
|
-
| `hasBit(bptr)` | Fast check via a pre-computed `BitPtr`. |
|
|
807
|
-
| `equal(other)` | Returns `true` when both bitsets have the same bits set. |
|
|
808
|
-
| `hasBitset(other)` | Returns `true` when every bit set in `other` is also set in this bitset. |
|
|
809
|
-
| `forEach(cb)` | Visit each set bit index in ascending order. |
|
|
810
|
-
| `indices()` | Return all set bit indices as a `number[]`. |
|
|
811
|
-
|
|
812
|
-
```ts
|
|
813
|
-
class Tags {
|
|
814
|
-
tags = new Bitset();
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
tags.tags.add(TAG_VISIBLE);
|
|
818
|
-
if (tags.tags.has(TAG_VISIBLE)) { ... }
|
|
819
|
-
```
|
|
820
|
-
|
|
821
|
-
---
|
|
72
|
+
Plus the [Utilities](./docs/utilities.md) guide for helper classes and the
|
|
73
|
+
[Glossary](./docs/glossary.md) for terminology. The exhaustive per-symbol reference is the JSDoc on
|
|
74
|
+
the source under [`src/`](./src/) — read it in your IDE.
|
|
822
75
|
|
|
823
76
|
## Build & Test
|
|
824
77
|
|
|
78
|
+
```sh
|
|
79
|
+
npm run build
|
|
80
|
+
npm run test
|
|
81
|
+
npm run lint
|
|
825
82
|
```
|
|
826
|
-
yarn build
|
|
827
|
-
yarn test
|
|
828
|
-
yarn lint
|
|
829
|
-
```
|
|
830
|
-
|
|
831
|
-
---
|
|
832
83
|
|
|
833
84
|
## License
|
|
834
85
|
|