@vworlds/vecs 1.0.10 → 1.0.11
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 +218 -229
- package/dist/command.d.ts +1 -46
- package/dist/component.d.ts +51 -59
- package/dist/component.js +31 -25
- package/dist/component.js.map +1 -1
- package/dist/dsl.d.ts +34 -26
- package/dist/dsl.js +46 -20
- package/dist/dsl.js.map +1 -1
- package/dist/entity.d.ts +96 -106
- package/dist/entity.js +261 -190
- package/dist/entity.js.map +1 -1
- package/dist/filter.d.ts +31 -23
- package/dist/filter.js +24 -17
- package/dist/filter.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/package.json +3 -1
- package/dist/phase.d.ts +5 -28
- package/dist/phase.js +11 -10
- package/dist/phase.js.map +1 -1
- package/dist/query.d.ts +107 -144
- package/dist/query.js +200 -169
- package/dist/query.js.map +1 -1
- package/dist/system.d.ts +59 -87
- package/dist/system.js +114 -114
- package/dist/system.js.map +1 -1
- package/dist/util/array_map.d.ts +4 -55
- package/dist/util/array_map.js +35 -37
- package/dist/util/array_map.js.map +1 -1
- package/dist/util/bitset.d.ts +40 -50
- package/dist/util/bitset.js +76 -62
- package/dist/util/bitset.js.map +1 -1
- package/dist/util/events.d.ts +14 -18
- package/dist/util/events.js +24 -3
- package/dist/util/events.js.map +1 -1
- package/dist/util/ordered_set.d.ts +1 -17
- package/dist/util/ordered_set.js +74 -25
- package/dist/util/ordered_set.js.map +1 -1
- package/dist/world.d.ts +212 -224
- package/dist/world.js +368 -330
- package/dist/world.js.map +1 -1
- package/eslint-rules/internal-underscore.js +60 -0
- package/eslint.config.js +5 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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** (
|
|
5
|
+
`vecs` lets you model game state as **entities** (numeric ids) with **components** (typed data bags) attached to them. **Systems** declare which component combinations they care about and receive automatic callbacks when entities enter or leave their query, when component data changes, and on every tick. A **World** ties it all together and drives the update loop.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -14,18 +14,20 @@ yarn add @vworlds/vecs
|
|
|
14
14
|
|
|
15
15
|
| Concept | What it is |
|
|
16
16
|
| ------------------------ | ------------------------------------------------------------------------------- |
|
|
17
|
-
| **World** | Central container. Owns
|
|
17
|
+
| **World** | Central container. Owns every entity, query, system, and pipeline phase. |
|
|
18
18
|
| **Component** | A plain data class. Extend `Component` and attach instances to entities. |
|
|
19
|
-
| **Entity** |
|
|
20
|
-
| **Query** | A reactive, always-
|
|
21
|
-
| **System** | A `Query` with per-tick
|
|
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 phase placement and per-tick logic (`update`, `each`, `run`). |
|
|
22
22
|
| **Filter** | A non-reactive, one-shot scan: walks all world entities on each `forEach` call. |
|
|
23
|
-
| **
|
|
23
|
+
| **Hook** | Lightweight `onAdd` / `onRemove` / `onSet` callbacks per component class. |
|
|
24
|
+
| **Phase** | Named ordered bucket of systems within the update pipeline. |
|
|
25
|
+
| **Exclusive components** | A group of components where at most one may exist on any entity at a time. |
|
|
24
26
|
|
|
25
27
|
### Lifecycle in brief
|
|
26
28
|
|
|
27
29
|
```
|
|
28
|
-
registerComponent() × N → system() / query() × N → start() → progress() every frame
|
|
30
|
+
registerComponent() × N → addPhase() / system() / query() × N → start() → progress() every frame
|
|
29
31
|
```
|
|
30
32
|
|
|
31
33
|
After `start()`, component registration is disabled. Systems and queries can still be created — standalone queries backfill existing matched entities immediately.
|
|
@@ -34,8 +36,6 @@ After `start()`, component registration is disabled. Systems and queries can sti
|
|
|
34
36
|
|
|
35
37
|
## Example
|
|
36
38
|
|
|
37
|
-
The example below defines three components, two systems, a phase, and a hook, then runs a simple "move and despawn" loop.
|
|
38
|
-
|
|
39
39
|
```ts
|
|
40
40
|
import { World, Component, IPhase } from "@vworlds/vecs";
|
|
41
41
|
|
|
@@ -70,22 +70,15 @@ const cleanup: IPhase = world.addPhase("cleanup");
|
|
|
70
70
|
|
|
71
71
|
// ─── Systems ───────────────────────────────────────────────────────────────
|
|
72
72
|
|
|
73
|
-
// MoveSystem:
|
|
73
|
+
// MoveSystem: integrates Velocity into Position every tick.
|
|
74
74
|
world
|
|
75
75
|
.system("Move")
|
|
76
76
|
.phase(update)
|
|
77
77
|
.requires(Position, Velocity)
|
|
78
|
-
.
|
|
79
|
-
console.log(`entity ${e.eid} entered Move with pos=(${pos.x},${pos.y})`);
|
|
80
|
-
})
|
|
81
|
-
.update(Velocity, [Position], (vel, [pos]) => {
|
|
82
|
-
// Called whenever vel.modified() is queued.
|
|
78
|
+
.each([Position, Velocity], (e, [pos, vel]) => {
|
|
83
79
|
pos.x += vel.vx;
|
|
84
80
|
pos.y += vel.vy;
|
|
85
|
-
pos.modified(); //
|
|
86
|
-
})
|
|
87
|
-
.exit((e) => {
|
|
88
|
-
console.log(`entity ${e.eid} left Move`);
|
|
81
|
+
pos.modified(); // signal that Position changed so other systems react
|
|
89
82
|
});
|
|
90
83
|
|
|
91
84
|
// HealthSystem: despawns entities whose HP drops to zero.
|
|
@@ -101,8 +94,6 @@ world
|
|
|
101
94
|
|
|
102
95
|
// ─── Hooks ─────────────────────────────────────────────────────────────────
|
|
103
96
|
|
|
104
|
-
// Hooks are a lightweight alternative to systems for side effects on a single
|
|
105
|
-
// component type — no per-entity query, just callbacks on add/remove/set.
|
|
106
97
|
world
|
|
107
98
|
.hook(Health)
|
|
108
99
|
.onAdd((h) => console.log(`entity ${h.entity.eid} spawned with hp=${h.hp}`))
|
|
@@ -110,16 +101,11 @@ world
|
|
|
110
101
|
|
|
111
102
|
// ─── Start ─────────────────────────────────────────────────────────────────
|
|
112
103
|
|
|
113
|
-
world.start(); // freeze registration,
|
|
114
|
-
|
|
115
|
-
// ─── Create entities ───────────────────────────────────────────────────────
|
|
104
|
+
world.start(); // freeze registration, distribute systems into phases
|
|
116
105
|
|
|
117
|
-
|
|
118
|
-
bullet.set(Position, { x: 0, y: 0 });
|
|
106
|
+
// ─── Spawn entities ────────────────────────────────────────────────────────
|
|
119
107
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const hp = bullet.set(Health, { hp: 3 }).get(Health)!;
|
|
108
|
+
world.entity().set(Position, { x: 0, y: 0 }).set(Velocity, { vx: 5, vy: 0 }).set(Health, { hp: 3 });
|
|
123
109
|
|
|
124
110
|
// ─── Game loop ─────────────────────────────────────────────────────────────
|
|
125
111
|
|
|
@@ -132,11 +118,25 @@ for (let tick = 0; tick < 5; tick++) {
|
|
|
132
118
|
|
|
133
119
|
---
|
|
134
120
|
|
|
121
|
+
## Deferred mode
|
|
122
|
+
|
|
123
|
+
Inside a system body, a `Query.forEach`, or any `world.defer(...)` block, the world is in **deferred mode**: entity mutations (`add` / `set` / `remove` / `destroy` / `setParent` / `modified`) are queued instead of applied inline. The queue drains on the boundary that opened the deferred scope.
|
|
124
|
+
|
|
125
|
+
Concretely, while deferred:
|
|
126
|
+
|
|
127
|
+
- `entity.get(C)` returns `undefined` after `entity.add(C)` (no instance has been created yet).
|
|
128
|
+
- `entity.get(C)` returns the **previous** value after `entity.set(C, props)`.
|
|
129
|
+
- `entity.get(C)` still returns the component after `entity.remove(C)`.
|
|
130
|
+
|
|
131
|
+
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`.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
135
|
## API Reference
|
|
136
136
|
|
|
137
|
-
### World
|
|
137
|
+
### `World`
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
Create one per game session.
|
|
140
140
|
|
|
141
141
|
```ts
|
|
142
142
|
const world = new World();
|
|
@@ -145,24 +145,22 @@ const world = new World();
|
|
|
145
145
|
#### Component registration
|
|
146
146
|
|
|
147
147
|
```ts
|
|
148
|
-
// Auto-assigned type id (
|
|
148
|
+
// Auto-assigned type id (≥ 256 for "local" components):
|
|
149
149
|
world.registerComponent(Position);
|
|
150
150
|
|
|
151
|
-
// Explicit numeric type id (
|
|
151
|
+
// Explicit numeric type id (e.g. server-assigned):
|
|
152
152
|
world.registerComponent(Position, 1);
|
|
153
153
|
|
|
154
|
-
//
|
|
154
|
+
// Explicit display name (e.g. when the class name differs from the network name):
|
|
155
155
|
world.registerComponent(Position, "pos");
|
|
156
156
|
|
|
157
157
|
// Pre-register a name → id mapping before the class is available:
|
|
158
158
|
world.registerComponentType("Position", 1);
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
After `world.start()` any further call to `registerComponent` throws.
|
|
162
|
-
|
|
163
|
-
#### Exclusive components
|
|
161
|
+
After `world.start()` (or `world.disableComponentRegistration()`) any further call to `registerComponent` throws.
|
|
164
162
|
|
|
165
|
-
|
|
163
|
+
#### Exclusive component groups
|
|
166
164
|
|
|
167
165
|
```ts
|
|
168
166
|
world.setExclusiveComponents(Walking, Running, Idle);
|
|
@@ -170,12 +168,9 @@ world.setExclusiveComponents(Walking, Running, Idle);
|
|
|
170
168
|
const e = world.entity();
|
|
171
169
|
e.add(Walking);
|
|
172
170
|
e.add(Running); // Walking is automatically removed first
|
|
173
|
-
// e.get(Walking) === undefined, e.get(Running) is defined
|
|
174
171
|
```
|
|
175
172
|
|
|
176
|
-
Each call
|
|
177
|
-
|
|
178
|
-
`setExclusiveComponents` may be called before or after `world.start()`.
|
|
173
|
+
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 before or after `world.start()`.
|
|
179
174
|
|
|
180
175
|
#### Entity management
|
|
181
176
|
|
|
@@ -184,58 +179,81 @@ Each call to `setExclusiveComponents` defines one independent group. Components
|
|
|
184
179
|
const e = world.entity();
|
|
185
180
|
|
|
186
181
|
// Look up by id (returns undefined if not found):
|
|
187
|
-
const
|
|
182
|
+
const found = world.entity(42);
|
|
188
183
|
|
|
189
184
|
// Server-assigned id; creates the entity if it doesn't exist yet:
|
|
190
|
-
const
|
|
185
|
+
const net = world.getOrCreateEntity(serverId, (newEntity) => {
|
|
191
186
|
tracked.add(newEntity);
|
|
192
187
|
});
|
|
193
188
|
|
|
189
|
+
// Reserve a high id range for locally created entities so they don't collide
|
|
190
|
+
// with server-assigned ids (call before world.start()):
|
|
191
|
+
world.setEntityIdRange(0x10000);
|
|
192
|
+
|
|
194
193
|
// Destroy everything (e.g. on level reset):
|
|
195
194
|
world.clearAllEntities();
|
|
195
|
+
```
|
|
196
196
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
#### Hooks
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
world
|
|
201
|
+
.hook(Sprite)
|
|
202
|
+
.onAdd((sprite) => sprite.initialize(scene))
|
|
203
|
+
.onRemove((sprite) => sprite.destroy())
|
|
204
|
+
.onSet((sprite) => sprite.syncToScene());
|
|
200
205
|
```
|
|
201
206
|
|
|
207
|
+
`onAdd` fires when the component is first attached. `onRemove` fires when it is removed (or the entity is destroyed). `onSet` fires whenever `component.modified()` (or `entity.modified(c)`) is called, and when `entity.set(C, props)` is applied to an entity that already has the component.
|
|
208
|
+
|
|
209
|
+
#### Phases
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const preUpdate = world.addPhase("preupdate");
|
|
213
|
+
const update = world.addPhase("update");
|
|
214
|
+
const send = world.addPhase("send");
|
|
215
|
+
|
|
216
|
+
// Drive every phase in registration order:
|
|
217
|
+
world.progress(now, delta);
|
|
218
|
+
|
|
219
|
+
// ...or run individual phases manually:
|
|
220
|
+
world.runPhase(preUpdate, now, delta);
|
|
221
|
+
world.runPhase(update, now, delta);
|
|
222
|
+
world.runPhase(send, now, delta);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Systems with no explicit phase are placed in the built-in `"update"` phase.
|
|
226
|
+
|
|
202
227
|
#### Systems
|
|
203
228
|
|
|
204
229
|
```ts
|
|
205
|
-
|
|
206
|
-
|
|
230
|
+
world
|
|
231
|
+
.system("MySystem")
|
|
207
232
|
.phase("update")
|
|
208
233
|
.requires(A, B)
|
|
209
234
|
.enter(...)
|
|
210
235
|
.update(...)
|
|
236
|
+
.each(...)
|
|
211
237
|
.exit(...);
|
|
212
|
-
|
|
213
|
-
world.start(); // distributes systems to phases, freezes component registration
|
|
214
238
|
```
|
|
215
239
|
|
|
216
240
|
#### Queries
|
|
217
241
|
|
|
218
|
-
A standalone `Query` is a reactive entity set without a phase or per-tick callbacks. Use it when you need the matched set kept up-to-date automatically — for example to enumerate scene nodes or find the nearest enemy.
|
|
219
|
-
|
|
220
242
|
```ts
|
|
221
243
|
const enemies = world
|
|
222
244
|
.query("Enemies")
|
|
223
245
|
.requires(Enemy, Health)
|
|
224
|
-
.enter((e) => console.log("enemy spawned", e.eid))
|
|
225
|
-
.exit((e) => console.log("enemy died", e.eid));
|
|
246
|
+
.enter((e) => console.log("enemy spawned", e.eid));
|
|
226
247
|
|
|
227
248
|
world.start();
|
|
228
|
-
// enemies.entities is kept up-to-date automatically
|
|
249
|
+
// enemies.entities is kept up-to-date automatically.
|
|
229
250
|
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
// lateQuery.entities immediately contains all current Wall entities
|
|
251
|
+
// Standalone queries can also be created after start(); existing matched
|
|
252
|
+
// entities are backfilled immediately.
|
|
233
253
|
```
|
|
234
254
|
|
|
235
255
|
#### Filters
|
|
236
256
|
|
|
237
|
-
A `Filter` is a non-reactive, one-shot scan. It holds no tracked entity set — each `forEach` call walks all world entities at that moment. Use it for ad-hoc lookups that don't need to stay live.
|
|
238
|
-
|
|
239
257
|
```ts
|
|
240
258
|
// Entity only:
|
|
241
259
|
world.filter([Position]).forEach((e) => console.log(e.eid));
|
|
@@ -249,55 +267,30 @@ world.filter([Position, Velocity]).forEach([Position, Velocity], (e, [pos, vel])
|
|
|
249
267
|
world
|
|
250
268
|
.filter({ AND: [{ HAS: Position }, { HAS: Velocity }] })
|
|
251
269
|
.forEach([Position, Velocity], (e, [pos, vel]) => {
|
|
252
|
-
pos.x += vel.vx;
|
|
270
|
+
pos.x += vel.vx;
|
|
253
271
|
});
|
|
254
272
|
|
|
255
|
-
// Manual
|
|
273
|
+
// Manual hint for queries the type extractor can't see through:
|
|
256
274
|
world.filter({ OR: [Position, Velocity] }, [Position]).forEach([Position], (e, [pos]) => pos.x);
|
|
257
275
|
```
|
|
258
276
|
|
|
259
|
-
|
|
277
|
+
A `Filter` requires no name, no `world.start()`, and no `destroy()` — create it anywhere and discard freely.
|
|
260
278
|
|
|
261
|
-
####
|
|
279
|
+
#### Pipeline control
|
|
262
280
|
|
|
263
281
|
```ts
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
const update = world.addPhase("update");
|
|
267
|
-
const send = world.addPhase("send");
|
|
268
|
-
|
|
269
|
-
// Each frame, run all phases in registration order:
|
|
270
|
-
world.progress(Date.now(), deltaMs);
|
|
271
|
-
|
|
272
|
-
// Or drive individual phases manually:
|
|
273
|
-
world.runPhase(preUpdate, Date.now(), deltaMs);
|
|
274
|
-
world.runPhase(update, Date.now(), deltaMs);
|
|
275
|
-
world.runPhase(send, Date.now(), deltaMs);
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
Systems with no explicit phase go into a built-in `"update"` phase.
|
|
282
|
+
world.start(); // freeze registration, distribute systems
|
|
283
|
+
world.disableComponentRegistration(); // freeze registration without sorting
|
|
279
284
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
```ts
|
|
285
|
-
world
|
|
286
|
-
.hook(Sprite)
|
|
287
|
-
.onAdd((sprite) => sprite.initialize(scene))
|
|
288
|
-
.onRemove((sprite) => sprite.destroy())
|
|
289
|
-
.onSet((sprite) => sprite.syncToScene());
|
|
285
|
+
world.flush(); // drain queued top-level mutations
|
|
286
|
+
world.defer(() => { ... }); // run a block in deferred mode
|
|
287
|
+
world.beginDefer(); // pair with endDefer() for finer scoping
|
|
288
|
+
world.endDefer();
|
|
290
289
|
```
|
|
291
290
|
|
|
292
|
-
`onSet` fires whenever `component.modified()` is called.
|
|
293
|
-
`onAdd` fires when the component is first attached to an entity.
|
|
294
|
-
`onRemove` fires when it is removed or the entity is destroyed.
|
|
295
|
-
|
|
296
291
|
---
|
|
297
292
|
|
|
298
|
-
### Component
|
|
299
|
-
|
|
300
|
-
Extend `Component` to define your data:
|
|
293
|
+
### `Component`
|
|
301
294
|
|
|
302
295
|
```ts
|
|
303
296
|
class Position extends Component {
|
|
@@ -312,90 +305,80 @@ const pos = entity.get(Position)!;
|
|
|
312
305
|
pos.x = 100;
|
|
313
306
|
pos.modified(); // tell the world this component changed
|
|
314
307
|
|
|
315
|
-
//
|
|
308
|
+
// Equivalent — set assigns props and fires onSet automatically:
|
|
316
309
|
entity.set(Position, { x: 100 });
|
|
317
310
|
```
|
|
318
311
|
|
|
319
|
-
Every component instance exposes:
|
|
320
|
-
|
|
321
312
|
| Property / Method | Description |
|
|
322
313
|
| ----------------- | --------------------------------------------------------------------- |
|
|
323
314
|
| `entity` | The `Entity` this component belongs to. |
|
|
324
|
-
| `meta` | `ComponentMeta` —
|
|
315
|
+
| `meta` | `ComponentMeta` — type id, display name, and bit-pointer. |
|
|
325
316
|
| `type` | Numeric type id (shorthand for `meta.type`). |
|
|
317
|
+
| `bitPtr` | `BitPtr` (shorthand for `meta.bitPtr`). |
|
|
326
318
|
| `modified()` | Queue an `onSet` / `update` notification. Call after mutating fields. |
|
|
319
|
+
| `toString()` | Returns the registered component name. |
|
|
327
320
|
|
|
328
321
|
---
|
|
329
322
|
|
|
330
|
-
### Entity
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
|
337
|
-
|
|
|
338
|
-
| `
|
|
339
|
-
| `
|
|
340
|
-
| `add(Class)`
|
|
341
|
-
| `set(Class, props)`
|
|
342
|
-
| `modified(component)`
|
|
343
|
-
| `get(Class)`
|
|
344
|
-
| `remove(Class)`
|
|
345
|
-
| `destroy()`
|
|
346
|
-
| `
|
|
347
|
-
| `
|
|
348
|
-
| `parent`
|
|
349
|
-
| `children`
|
|
350
|
-
| `
|
|
351
|
-
| `
|
|
352
|
-
|
|
353
|
-
|
|
323
|
+
### `Entity`
|
|
324
|
+
|
|
325
|
+
Created via `world.entity()` (auto-assigned id) or `world.getOrCreateEntity(id, ...)` (caller-supplied id).
|
|
326
|
+
|
|
327
|
+
| Property / Method | Description |
|
|
328
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
329
|
+
| `eid` | Unique numeric entity id. |
|
|
330
|
+
| `world` | The `World` that owns this entity. |
|
|
331
|
+
| `componentBitmask` | `Bitset` of component type ids attached to this entity. Used by archetype matching. |
|
|
332
|
+
| `properties` | `Map<string, any>` free-form bag for module-level bookkeeping. |
|
|
333
|
+
| `add(Class)` | Attach a component (idempotent). Returns the entity for chaining. |
|
|
334
|
+
| `set(Class, props)` | Attach a component and assign `props`; fires `onSet`. Returns the entity for chaining. |
|
|
335
|
+
| `modified(component)` | Queue an `onSet` / `update` notification. Returns the entity for chaining. |
|
|
336
|
+
| `get(Class)` | Return the component instance, or `undefined`. |
|
|
337
|
+
| `remove(Class)` | Detach a component (fires `onRemove` and `exit`). |
|
|
338
|
+
| `destroy()` | Remove all components, unregister from the world, recurse into children. |
|
|
339
|
+
| `components` | `ReadonlyArrayMap<Component>` — read-only view of attached components keyed by type id. Supports `forEach`, `get`, `has`, and `size`. |
|
|
340
|
+
| `empty` | `true` when no components are attached. |
|
|
341
|
+
| `parent` | Parent entity, or `undefined` for a root entity. |
|
|
342
|
+
| `children` | `ReadonlySet<Entity>` of direct children (lazy). |
|
|
343
|
+
| `setParent(p)` | Reparent the entity. `undefined` makes it a root entity. Throws on cycles. |
|
|
344
|
+
| `events` | Typed event emitter. Currently emits `"destroy"` just before teardown. |
|
|
345
|
+
| `toString()` | Returns `"EntityN"`. |
|
|
346
|
+
|
|
347
|
+
`entity.modified(c)` is equivalent to `c.modified()` but returns the entity so it can chain:
|
|
354
348
|
|
|
355
349
|
```ts
|
|
356
|
-
// Mutate fields then signal the change inline:
|
|
357
350
|
const vel = entity.get(Velocity)!;
|
|
358
351
|
vel.vx += accel;
|
|
359
|
-
entity.modified(vel); //
|
|
360
|
-
|
|
361
|
-
// Or in a chain — add without initial notification, then notify later:
|
|
362
|
-
entity.add(Position, false).modified(entity.get(Position)!);
|
|
352
|
+
entity.modified(vel); // chainable
|
|
363
353
|
```
|
|
364
354
|
|
|
365
|
-
#### Parent
|
|
355
|
+
#### Parent / child hierarchy
|
|
366
356
|
|
|
367
357
|
```ts
|
|
368
|
-
child.parent
|
|
369
|
-
parent.children.
|
|
358
|
+
child.setParent(parent);
|
|
359
|
+
parent.children.has(child); // true
|
|
370
360
|
|
|
371
361
|
// Destroying a parent recursively destroys all children:
|
|
372
362
|
parent.destroy();
|
|
373
363
|
```
|
|
374
364
|
|
|
375
|
-
Archetype queries that use `{ PARENT: ... }` are
|
|
365
|
+
`setParent` throws if the new parent is a descendant of the entity. Archetype queries that use `{ PARENT: ... }` are re-evaluated automatically when a parent's component set changes.
|
|
376
366
|
|
|
377
367
|
---
|
|
378
368
|
|
|
379
|
-
### System
|
|
369
|
+
### `System`
|
|
380
370
|
|
|
381
|
-
Systems are created via `world.system(name)` and configured through a fluent builder
|
|
371
|
+
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.
|
|
382
372
|
|
|
383
373
|
#### `.requires(...components)` and `.query(q)`
|
|
384
374
|
|
|
385
|
-
Declare which entities the system
|
|
375
|
+
Declare which entities the system tracks.
|
|
386
376
|
|
|
387
377
|
```ts
|
|
388
|
-
//
|
|
389
|
-
.
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
.query({ HAS: [Position, Velocity] })
|
|
393
|
-
|
|
394
|
-
// Entities that have a parent with Player AND Container:
|
|
395
|
-
.query({ PARENT: { AND: [Player, Container] } })
|
|
396
|
-
|
|
397
|
-
// Compound queries:
|
|
398
|
-
.query({ AND: [Position, { OR: [Sprite, Container] }] })
|
|
378
|
+
.requires(Position, Velocity) // shorthand for HAS
|
|
379
|
+
.query({ HAS: [Position, Velocity] }) // explicit
|
|
380
|
+
.query({ PARENT: { AND: [Player, Container] } }) // parent-aware
|
|
381
|
+
.query({ AND: [Position, { OR: [Sprite, Container] }] }) // compound
|
|
399
382
|
.query({ NOT: Invisible })
|
|
400
383
|
```
|
|
401
384
|
|
|
@@ -409,130 +392,112 @@ Declare which entities the system should track:
|
|
|
409
392
|
| `{ OR: [q1, q2] }` | Either sub-query matches |
|
|
410
393
|
| `{ NOT: q }` | Sub-query must not match |
|
|
411
394
|
| `{ PARENT: q }` | Entity's parent matches q |
|
|
412
|
-
| An array `[A, B]` | Shorthand for `HAS: [A, B]`
|
|
395
|
+
| An array `[A, B]` | Shorthand for `{ HAS: [A, B] }` |
|
|
396
|
+
| A single class / id | Shorthand for `{ HAS: [C] }` |
|
|
397
|
+
| A predicate function | Custom membership logic |
|
|
413
398
|
|
|
414
|
-
**Type inference
|
|
399
|
+
**Type inference.** `requires()` records the listed classes as a type parameter `R` on the system. Callbacks in `.sort()`, `.each()`, and `.update()` injection treat those components as non-nullable — no `!` needed. For complex `query()` expressions the type system can't introspect, supply a `_guaranteed` second argument:
|
|
415
400
|
|
|
416
401
|
```ts
|
|
417
402
|
.query({ AND: [{ HAS: Position }, { HAS: Velocity }] }, [Position, Velocity])
|
|
418
403
|
.each([Position, Velocity], (e, [pos, vel]) => {
|
|
419
404
|
pos.x += vel.vx; // pos and vel are non-null
|
|
420
|
-
})
|
|
405
|
+
});
|
|
421
406
|
```
|
|
422
407
|
|
|
423
408
|
#### `.phase(p)`
|
|
424
409
|
|
|
425
|
-
Assign the system to a
|
|
410
|
+
Assign the system to a phase by name or `IPhase` reference. Default phase is `"update"`.
|
|
426
411
|
|
|
427
412
|
```ts
|
|
428
|
-
.phase("preupdate")
|
|
429
|
-
.phase(myPhase)
|
|
413
|
+
.phase("preupdate")
|
|
414
|
+
.phase(myPhase)
|
|
430
415
|
```
|
|
431
416
|
|
|
432
417
|
#### `.enter(callback)` / `.enter(inject, callback)`
|
|
433
418
|
|
|
434
|
-
|
|
419
|
+
Fires once when an entity first matches the system.
|
|
435
420
|
|
|
436
421
|
```ts
|
|
437
|
-
|
|
438
|
-
.enter((e) => { console.log("entity joined", e.eid); })
|
|
439
|
-
|
|
440
|
-
// With injection — component instances resolved from the entity:
|
|
422
|
+
.enter((e) => { ... })
|
|
441
423
|
.enter([Position, Sprite], (e, [pos, sprite]) => {
|
|
442
424
|
sprite.setPosition(pos.x, pos.y);
|
|
443
425
|
})
|
|
444
426
|
|
|
445
|
-
// Resolve from parent:
|
|
427
|
+
// Resolve from the entity's parent:
|
|
446
428
|
.enter([{ parent: Container }], (e, [container]) => {
|
|
447
429
|
container.add(e.get(Sprite)!.gameObject);
|
|
448
|
-
})
|
|
430
|
+
});
|
|
449
431
|
```
|
|
450
432
|
|
|
451
433
|
#### `.exit(callback)` / `.exit(inject, callback)`
|
|
452
434
|
|
|
453
|
-
|
|
435
|
+
Fires when an entity leaves the system (component removed or entity destroyed). Components removed in the same frame are still resolvable in `inject`.
|
|
454
436
|
|
|
455
437
|
```ts
|
|
456
|
-
.exit([Sprite], (e, [sprite]) =>
|
|
457
|
-
sprite.destroy();
|
|
458
|
-
})
|
|
438
|
+
.exit([Sprite], (e, [sprite]) => sprite.destroy());
|
|
459
439
|
```
|
|
460
440
|
|
|
461
441
|
#### `.update(ComponentClass, callback)` / `.update(ComponentClass, inject, callback)`
|
|
462
442
|
|
|
463
|
-
|
|
443
|
+
Fires when `component.modified()` is called for the watched component on a tracked entity.
|
|
464
444
|
|
|
465
445
|
```ts
|
|
466
|
-
|
|
467
|
-
.update(Position, (pos) => {
|
|
468
|
-
renderer.setPosition(pos.x, pos.y);
|
|
469
|
-
})
|
|
446
|
+
.update(Position, (pos) => renderer.setPosition(pos.x, pos.y));
|
|
470
447
|
|
|
471
|
-
// With injection — receives the modified component and extra components:
|
|
472
448
|
.update(Position, [Sprite], (pos, [sprite]) => {
|
|
473
449
|
sprite.sprite.setPosition(pos.x, pos.y);
|
|
474
|
-
})
|
|
450
|
+
});
|
|
475
451
|
```
|
|
476
452
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
Calling `update` also adds that component type to the system's implicit `HAS` query (unless you called `query()` first).
|
|
453
|
+
If `query()` has not been called, `update` automatically expands the implicit `HAS` predicate to require the watched component.
|
|
480
454
|
|
|
481
455
|
#### `.each(components, callback)`
|
|
482
456
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
The callback receives the entity and a tuple of resolved component instances. Components declared via `requires()` are guaranteed non-null; any others are `undefined` if the entity lacks them.
|
|
457
|
+
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.
|
|
486
458
|
|
|
487
459
|
```ts
|
|
488
460
|
.requires(Position, Velocity)
|
|
489
461
|
.each([Position, Velocity], (e, [pos, vel]) => {
|
|
490
|
-
pos.x += vel.vx;
|
|
491
|
-
|
|
492
|
-
})
|
|
462
|
+
pos.x += vel.vx;
|
|
463
|
+
});
|
|
493
464
|
```
|
|
494
465
|
|
|
495
|
-
`each` does not modify the system's query — define membership with `requires(...)` or `query(...)` as usual. Only one `each` may be registered per system; a second call throws.
|
|
496
|
-
|
|
497
466
|
#### `.sort(components, compare)`
|
|
498
467
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
Components declared via `requires()` are non-null in the compare callback.
|
|
468
|
+
Store matched entities in a custom order determined by `compare`. Implies `.track()`. Iterating `system.entities`, `forEach`, and `each` walks entities in sorted order.
|
|
502
469
|
|
|
503
470
|
```ts
|
|
504
471
|
world
|
|
505
472
|
.system("Render")
|
|
506
473
|
.requires(Position, Sprite)
|
|
507
474
|
.sort([Position], ([posA], [posB]) => posA.z - posB.z)
|
|
508
|
-
.each([Position, Sprite], (e, [pos, sprite]) =>
|
|
509
|
-
sprite.draw(pos.x, pos.y);
|
|
510
|
-
});
|
|
475
|
+
.each([Position, Sprite], (e, [pos, sprite]) => sprite.draw(pos.x, pos.y));
|
|
511
476
|
```
|
|
512
477
|
|
|
513
|
-
Iterating `system.entities` after a phase run yields entities in the sorted order.
|
|
514
|
-
|
|
515
478
|
#### `.track()`
|
|
516
479
|
|
|
517
|
-
Enable entity tracking without an `each` callback — matched entities
|
|
518
|
-
|
|
519
|
-
When called after `world.start()`, `track()` immediately backfills existing entities that satisfy the query predicate.
|
|
480
|
+
Enable entity tracking without an `each` callback — exposes matched entities via `system.entities`. `each` and `sort` imply `track` automatically. When called after `world.start()`, immediately backfills existing matched entities.
|
|
520
481
|
|
|
521
482
|
#### `.run(callback)`
|
|
522
483
|
|
|
523
|
-
|
|
484
|
+
Fires every tick when the system's phase runs, regardless of entity state. Use for polling, network I/O, timers, etc.
|
|
524
485
|
|
|
525
486
|
```ts
|
|
526
487
|
.run((now, delta) => {
|
|
527
488
|
sendNetworkPacket(now);
|
|
528
|
-
})
|
|
489
|
+
});
|
|
529
490
|
```
|
|
530
491
|
|
|
492
|
+
#### `.destroy()`
|
|
493
|
+
|
|
494
|
+
**Not supported on `System`** — calling it throws. Systems live for the duration of the world. Use a standalone `Query` for temporary reactive sets.
|
|
495
|
+
|
|
531
496
|
---
|
|
532
497
|
|
|
533
|
-
### Query
|
|
498
|
+
### `Query`
|
|
534
499
|
|
|
535
|
-
|
|
500
|
+
`world.query(name)` returns a standalone reactive entity set, configured through the same builder API as `System`. It has no phase and no per-tick callbacks.
|
|
536
501
|
|
|
537
502
|
```ts
|
|
538
503
|
const projectiles = world
|
|
@@ -545,46 +510,42 @@ const projectiles = world
|
|
|
545
510
|
|
|
546
511
|
world.start();
|
|
547
512
|
|
|
548
|
-
|
|
549
|
-
projectiles.forEach((e) => {
|
|
550
|
-
/* ... */
|
|
551
|
-
});
|
|
513
|
+
projectiles.forEach((e) => { ... });
|
|
552
514
|
console.log(projectiles.entities.size, "active projectiles");
|
|
553
515
|
```
|
|
554
516
|
|
|
555
|
-
| Method
|
|
556
|
-
|
|
|
557
|
-
| `.requires(...components)`
|
|
558
|
-
| `.query(expr)`
|
|
559
|
-
| `.enter(callback)` / `.enter(inject, callback)`
|
|
560
|
-
| `.exit(callback)` / `.exit(inject, callback)`
|
|
561
|
-
| `.
|
|
562
|
-
| `.
|
|
563
|
-
| `.
|
|
564
|
-
| `.
|
|
565
|
-
| `.forEach(
|
|
566
|
-
| `.
|
|
567
|
-
| `.
|
|
517
|
+
| Method | Description |
|
|
518
|
+
| ------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
519
|
+
| `.requires(...components)` | Set the membership predicate to `HAS(...components)` and start tracking. |
|
|
520
|
+
| `.query(expr, _guaranteed?)` | Set the membership predicate using a `QueryDSL` expression. |
|
|
521
|
+
| `.enter(callback)` / `.enter(inject, callback)` | Fires when an entity joins the query. |
|
|
522
|
+
| `.exit(callback)` / `.exit(inject, callback)` | Fires when an entity leaves the query. |
|
|
523
|
+
| `.update(C, callback)` / `.update(C, inject, callback)` | Fires when `C` is modified on a tracked entity. |
|
|
524
|
+
| `.sort(components, compare)` | Store matched entities in sorted order. |
|
|
525
|
+
| `.track()` | Enable tracking. Backfills when called after `start()`. |
|
|
526
|
+
| `.belongs(e)` | Returns `true` if the entity satisfies the predicate. |
|
|
527
|
+
| `.forEach(callback)` | Iterate currently tracked entities. |
|
|
528
|
+
| `.forEach(components, callback)` | Iterate with component injection. |
|
|
529
|
+
| `.entities` | `ReadonlySet<Entity>` of currently tracked entities. |
|
|
530
|
+
| `.destroy()` | Remove the query from the world and from every entity (no exit fires). |
|
|
568
531
|
|
|
569
|
-
#### `.destroy()`
|
|
532
|
+
#### `.destroy()` semantics
|
|
570
533
|
|
|
571
|
-
|
|
534
|
+
`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**.
|
|
572
535
|
|
|
573
536
|
```ts
|
|
574
537
|
const q = world.query("Temporary").requires(Position);
|
|
575
538
|
// ... use q.entities ...
|
|
576
|
-
q.destroy();
|
|
539
|
+
q.destroy();
|
|
577
540
|
```
|
|
578
541
|
|
|
579
|
-
`System`
|
|
580
|
-
|
|
581
|
-
Both `System` and `Query` share the same query DSL, enter/exit callbacks, sort, and `entities` set — `System` extends `Query` and layers phase execution on top.
|
|
542
|
+
`System` shares the same DSL, callback, sorting, and tracking machinery — `System` extends `Query` and adds phase placement, `run`, `each`, and an inbox replayed on every tick.
|
|
582
543
|
|
|
583
544
|
---
|
|
584
545
|
|
|
585
|
-
### Filter
|
|
546
|
+
### `Filter`
|
|
586
547
|
|
|
587
|
-
|
|
548
|
+
`world.filter(dsl)` returns a `Filter` that performs a non-reactive scan. It accepts the same `QueryDSL` expressions as systems and queries.
|
|
588
549
|
|
|
589
550
|
```ts
|
|
590
551
|
const f = world.filter([Position, Velocity]);
|
|
@@ -592,30 +553,58 @@ const f = world.filter([Position, Velocity]);
|
|
|
592
553
|
|
|
593
554
|
| Method | Description |
|
|
594
555
|
| -------------------------------- | -------------------------------------------------------------------------- |
|
|
595
|
-
| `.forEach(callback)` | Walk all world entities; invoke callback
|
|
556
|
+
| `.forEach(callback)` | Walk all world entities; invoke callback on each match. |
|
|
596
557
|
| `.forEach(components, callback)` | Same, with component injection and non-null types for required components. |
|
|
597
558
|
|
|
598
|
-
|
|
559
|
+
`forEach` runs inside a deferred scope, so mutations made by the callback are batched and become visible after iteration finishes.
|
|
560
|
+
|
|
561
|
+
**Type inference.** Component classes the type system can extract from the DSL (`HAS`, `HAS_ONLY`, plain arrays, `AND` of those) are non-nullable in the callback tuple. For the rest, supply a `_guaranteed` second argument to `world.filter()`:
|
|
599
562
|
|
|
600
563
|
```ts
|
|
601
564
|
// Auto-deduced — both non-null:
|
|
602
|
-
world.filter([Position, Velocity])
|
|
603
|
-
.forEach([Position, Velocity], (e, [pos, vel]) => { ... });
|
|
565
|
+
world.filter([Position, Velocity]).forEach([Position, Velocity], (e, [pos, vel]) => { ... });
|
|
604
566
|
|
|
605
567
|
// Manual hint for OR / NOT / PARENT / custom function:
|
|
606
|
-
world.filter({ OR: [Position, Velocity] }, [Position])
|
|
607
|
-
.forEach([Position], (e, [pos]) => pos.x);
|
|
568
|
+
world.filter({ OR: [Position, Velocity] }, [Position]).forEach([Position], (e, [pos]) => pos.x);
|
|
608
569
|
```
|
|
609
570
|
|
|
610
571
|
A `Filter` holds no tracked set, makes no registration calls, and needs no `destroy()`.
|
|
611
572
|
|
|
612
573
|
---
|
|
613
574
|
|
|
575
|
+
### `Bitset`
|
|
576
|
+
|
|
577
|
+
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.
|
|
578
|
+
|
|
579
|
+
| Method | Description |
|
|
580
|
+
| ------------------ | ------------------------------------------------------------------------ |
|
|
581
|
+
| `add(n)` | Set bit `n`. |
|
|
582
|
+
| `addBit(bptr)` | Set the bit at a pre-computed `BitPtr`. |
|
|
583
|
+
| `delete(n)` | Clear bit `n`. Trims trailing zero words. |
|
|
584
|
+
| `has(n)` | Returns `true` if bit `n` is set. |
|
|
585
|
+
| `hasBit(bptr)` | Fast check via a pre-computed `BitPtr`. |
|
|
586
|
+
| `equal(other)` | Returns `true` when both bitsets have the same bits set. |
|
|
587
|
+
| `hasBitset(other)` | Returns `true` when every bit set in `other` is also set in this bitset. |
|
|
588
|
+
| `forEach(cb)` | Visit each set bit index in ascending order. |
|
|
589
|
+
| `indices()` | Return all set bit indices as a `number[]`. |
|
|
590
|
+
|
|
591
|
+
```ts
|
|
592
|
+
class Tags extends Component {
|
|
593
|
+
tags = new Bitset();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
tags.tags.add(TAG_VISIBLE);
|
|
597
|
+
if (tags.tags.has(TAG_VISIBLE)) { ... }
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
614
602
|
## Build & Test
|
|
615
603
|
|
|
616
604
|
```
|
|
617
605
|
yarn build
|
|
618
606
|
yarn test
|
|
607
|
+
yarn lint
|
|
619
608
|
```
|
|
620
609
|
|
|
621
610
|
---
|