@vworlds/vecs 1.0.10 → 1.0.12
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/.husky/pre-commit +1 -0
- package/README.md +299 -228
- 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 +2 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/package.json +3 -1
- package/dist/phase.d.ts +12 -30
- 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 +170 -86
- package/dist/system.js +253 -114
- package/dist/system.js.map +1 -1
- package/dist/timer.d.ts +50 -0
- package/dist/timer.js +154 -0
- package/dist/timer.js.map +1 -0
- 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 +230 -218
- package/dist/world.js +422 -327
- 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 ───────────────────────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
const bullet = world.entity();
|
|
118
|
-
bullet.set(Position, { x: 0, y: 0 });
|
|
104
|
+
world.start(); // freeze registration, distribute systems into phases
|
|
119
105
|
|
|
120
|
-
|
|
106
|
+
// ─── Spawn entities ────────────────────────────────────────────────────────
|
|
121
107
|
|
|
122
|
-
|
|
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,147 @@ 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());
|
|
205
|
+
```
|
|
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.beginFrame(now, delta);
|
|
221
|
+
try {
|
|
222
|
+
world.runPhase(preUpdate, now, delta);
|
|
223
|
+
world.runPhase(update, now, delta);
|
|
224
|
+
world.runPhase(send, now, delta);
|
|
225
|
+
} finally {
|
|
226
|
+
world.endFrame();
|
|
227
|
+
}
|
|
200
228
|
```
|
|
201
229
|
|
|
230
|
+
Systems with no explicit phase are placed in the built-in `"update"` phase.
|
|
231
|
+
|
|
202
232
|
#### Systems
|
|
203
233
|
|
|
204
234
|
```ts
|
|
205
|
-
|
|
206
|
-
|
|
235
|
+
world
|
|
236
|
+
.system("MySystem")
|
|
207
237
|
.phase("update")
|
|
208
238
|
.requires(A, B)
|
|
209
239
|
.enter(...)
|
|
210
240
|
.update(...)
|
|
241
|
+
.each(...)
|
|
211
242
|
.exit(...);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
#### Timers and rate filters
|
|
246
|
+
|
|
247
|
+
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`.
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import { IntervalTickSource, RateTickSource } from "@vworlds/vecs";
|
|
251
|
+
|
|
252
|
+
world
|
|
253
|
+
.system("Move")
|
|
254
|
+
.interval(1.0)
|
|
255
|
+
.each([Position], (e, [pos]) => {
|
|
256
|
+
// 1 Hz
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
world
|
|
260
|
+
.system("Move")
|
|
261
|
+
.rate(2)
|
|
262
|
+
.each([Position], (e, [pos]) => {
|
|
263
|
+
// every 2nd frame
|
|
264
|
+
});
|
|
212
265
|
|
|
213
|
-
|
|
266
|
+
const second = new IntervalTickSource(1.0);
|
|
267
|
+
|
|
268
|
+
world
|
|
269
|
+
.system("Move")
|
|
270
|
+
.tickSource(second)
|
|
271
|
+
.each([Position], (e, [pos]) => {
|
|
272
|
+
// driven by a shared timer
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
second.stop();
|
|
276
|
+
second.start();
|
|
277
|
+
|
|
278
|
+
const minute = new RateTickSource(60, second);
|
|
279
|
+
const hour = world
|
|
280
|
+
.system("Hour")
|
|
281
|
+
.tickSource(minute)
|
|
282
|
+
.rate(60)
|
|
283
|
+
.run((now, delta) => {
|
|
284
|
+
console.log("hour tick", now, delta);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Systems can also be tick sources for each other.
|
|
288
|
+
const eachSecond = world
|
|
289
|
+
.system("EachSecond")
|
|
290
|
+
.interval(1)
|
|
291
|
+
.run(() => {
|
|
292
|
+
// ...
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const eachMinute = world
|
|
296
|
+
.system("EachMinute")
|
|
297
|
+
.tickSource(eachSecond)
|
|
298
|
+
.rate(60)
|
|
299
|
+
.run(() => {
|
|
300
|
+
// ...
|
|
301
|
+
});
|
|
214
302
|
```
|
|
215
303
|
|
|
216
|
-
|
|
304
|
+
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.
|
|
217
305
|
|
|
218
|
-
|
|
306
|
+
#### Queries
|
|
219
307
|
|
|
220
308
|
```ts
|
|
221
309
|
const enemies = world
|
|
222
310
|
.query("Enemies")
|
|
223
311
|
.requires(Enemy, Health)
|
|
224
|
-
.enter((e) => console.log("enemy spawned", e.eid))
|
|
225
|
-
.exit((e) => console.log("enemy died", e.eid));
|
|
312
|
+
.enter((e) => console.log("enemy spawned", e.eid));
|
|
226
313
|
|
|
227
314
|
world.start();
|
|
228
|
-
// enemies.entities is kept up-to-date automatically
|
|
315
|
+
// enemies.entities is kept up-to-date automatically.
|
|
229
316
|
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
// lateQuery.entities immediately contains all current Wall entities
|
|
317
|
+
// Standalone queries can also be created after start(); existing matched
|
|
318
|
+
// entities are backfilled immediately.
|
|
233
319
|
```
|
|
234
320
|
|
|
235
321
|
#### Filters
|
|
236
322
|
|
|
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
323
|
```ts
|
|
240
324
|
// Entity only:
|
|
241
325
|
world.filter([Position]).forEach((e) => console.log(e.eid));
|
|
@@ -249,55 +333,30 @@ world.filter([Position, Velocity]).forEach([Position, Velocity], (e, [pos, vel])
|
|
|
249
333
|
world
|
|
250
334
|
.filter({ AND: [{ HAS: Position }, { HAS: Velocity }] })
|
|
251
335
|
.forEach([Position, Velocity], (e, [pos, vel]) => {
|
|
252
|
-
pos.x += vel.vx;
|
|
336
|
+
pos.x += vel.vx;
|
|
253
337
|
});
|
|
254
338
|
|
|
255
|
-
// Manual
|
|
339
|
+
// Manual hint for queries the type extractor can't see through:
|
|
256
340
|
world.filter({ OR: [Position, Velocity] }, [Position]).forEach([Position], (e, [pos]) => pos.x);
|
|
257
341
|
```
|
|
258
342
|
|
|
259
|
-
|
|
343
|
+
A `Filter` requires no name, no `world.start()`, and no `destroy()` — create it anywhere and discard freely.
|
|
260
344
|
|
|
261
|
-
####
|
|
345
|
+
#### Pipeline control
|
|
262
346
|
|
|
263
347
|
```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.
|
|
279
|
-
|
|
280
|
-
#### Hooks
|
|
281
|
-
|
|
282
|
-
A hook is a shorthand for reacting to a single component's lifecycle without writing a full system:
|
|
348
|
+
world.start(); // freeze registration, distribute systems
|
|
349
|
+
world.disableComponentRegistration(); // freeze registration without sorting
|
|
283
350
|
|
|
284
|
-
|
|
285
|
-
world
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
.onRemove((sprite) => sprite.destroy())
|
|
289
|
-
.onSet((sprite) => sprite.syncToScene());
|
|
351
|
+
world.flush(); // drain queued top-level mutations
|
|
352
|
+
world.defer(() => { ... }); // run a block in deferred mode
|
|
353
|
+
world.beginDefer(); // pair with endDefer() for finer scoping
|
|
354
|
+
world.endDefer();
|
|
290
355
|
```
|
|
291
356
|
|
|
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
357
|
---
|
|
297
358
|
|
|
298
|
-
### Component
|
|
299
|
-
|
|
300
|
-
Extend `Component` to define your data:
|
|
359
|
+
### `Component`
|
|
301
360
|
|
|
302
361
|
```ts
|
|
303
362
|
class Position extends Component {
|
|
@@ -312,90 +371,80 @@ const pos = entity.get(Position)!;
|
|
|
312
371
|
pos.x = 100;
|
|
313
372
|
pos.modified(); // tell the world this component changed
|
|
314
373
|
|
|
315
|
-
//
|
|
374
|
+
// Equivalent — set assigns props and fires onSet automatically:
|
|
316
375
|
entity.set(Position, { x: 100 });
|
|
317
376
|
```
|
|
318
377
|
|
|
319
|
-
Every component instance exposes:
|
|
320
|
-
|
|
321
378
|
| Property / Method | Description |
|
|
322
379
|
| ----------------- | --------------------------------------------------------------------- |
|
|
323
380
|
| `entity` | The `Entity` this component belongs to. |
|
|
324
|
-
| `meta` | `ComponentMeta` —
|
|
381
|
+
| `meta` | `ComponentMeta` — type id, display name, and bit-pointer. |
|
|
325
382
|
| `type` | Numeric type id (shorthand for `meta.type`). |
|
|
383
|
+
| `bitPtr` | `BitPtr` (shorthand for `meta.bitPtr`). |
|
|
326
384
|
| `modified()` | Queue an `onSet` / `update` notification. Call after mutating fields. |
|
|
385
|
+
| `toString()` | Returns the registered component name. |
|
|
327
386
|
|
|
328
387
|
---
|
|
329
388
|
|
|
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
|
-
|
|
389
|
+
### `Entity`
|
|
390
|
+
|
|
391
|
+
Created via `world.entity()` (auto-assigned id) or `world.getOrCreateEntity(id, ...)` (caller-supplied id).
|
|
392
|
+
|
|
393
|
+
| Property / Method | Description |
|
|
394
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
395
|
+
| `eid` | Unique numeric entity id. |
|
|
396
|
+
| `world` | The `World` that owns this entity. |
|
|
397
|
+
| `componentBitmask` | `Bitset` of component type ids attached to this entity. Used by archetype matching. |
|
|
398
|
+
| `properties` | `Map<string, any>` free-form bag for module-level bookkeeping. |
|
|
399
|
+
| `add(Class)` | Attach a component (idempotent). Returns the entity for chaining. |
|
|
400
|
+
| `set(Class, props)` | Attach a component and assign `props`; fires `onSet`. Returns the entity for chaining. |
|
|
401
|
+
| `modified(component)` | Queue an `onSet` / `update` notification. Returns the entity for chaining. |
|
|
402
|
+
| `get(Class)` | Return the component instance, or `undefined`. |
|
|
403
|
+
| `remove(Class)` | Detach a component (fires `onRemove` and `exit`). |
|
|
404
|
+
| `destroy()` | Remove all components, unregister from the world, recurse into children. |
|
|
405
|
+
| `components` | `ReadonlyArrayMap<Component>` — read-only view of attached components keyed by type id. Supports `forEach`, `get`, `has`, and `size`. |
|
|
406
|
+
| `empty` | `true` when no components are attached. |
|
|
407
|
+
| `parent` | Parent entity, or `undefined` for a root entity. |
|
|
408
|
+
| `children` | `ReadonlySet<Entity>` of direct children (lazy). |
|
|
409
|
+
| `setParent(p)` | Reparent the entity. `undefined` makes it a root entity. Throws on cycles. |
|
|
410
|
+
| `events` | Typed event emitter. Currently emits `"destroy"` just before teardown. |
|
|
411
|
+
| `toString()` | Returns `"EntityN"`. |
|
|
412
|
+
|
|
413
|
+
`entity.modified(c)` is equivalent to `c.modified()` but returns the entity so it can chain:
|
|
354
414
|
|
|
355
415
|
```ts
|
|
356
|
-
// Mutate fields then signal the change inline:
|
|
357
416
|
const vel = entity.get(Velocity)!;
|
|
358
417
|
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)!);
|
|
418
|
+
entity.modified(vel); // chainable
|
|
363
419
|
```
|
|
364
420
|
|
|
365
|
-
#### Parent
|
|
421
|
+
#### Parent / child hierarchy
|
|
366
422
|
|
|
367
423
|
```ts
|
|
368
|
-
child.parent
|
|
369
|
-
parent.children.
|
|
424
|
+
child.setParent(parent);
|
|
425
|
+
parent.children.has(child); // true
|
|
370
426
|
|
|
371
427
|
// Destroying a parent recursively destroys all children:
|
|
372
428
|
parent.destroy();
|
|
373
429
|
```
|
|
374
430
|
|
|
375
|
-
Archetype queries that use `{ PARENT: ... }` are
|
|
431
|
+
`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
432
|
|
|
377
433
|
---
|
|
378
434
|
|
|
379
|
-
### System
|
|
435
|
+
### `System`
|
|
380
436
|
|
|
381
|
-
Systems are created via `world.system(name)` and configured through a fluent builder
|
|
437
|
+
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
438
|
|
|
383
439
|
#### `.requires(...components)` and `.query(q)`
|
|
384
440
|
|
|
385
|
-
Declare which entities the system
|
|
441
|
+
Declare which entities the system tracks.
|
|
386
442
|
|
|
387
443
|
```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] }] })
|
|
444
|
+
.requires(Position, Velocity) // shorthand for HAS
|
|
445
|
+
.query({ HAS: [Position, Velocity] }) // explicit
|
|
446
|
+
.query({ PARENT: { AND: [Player, Container] } }) // parent-aware
|
|
447
|
+
.query({ AND: [Position, { OR: [Sprite, Container] }] }) // compound
|
|
399
448
|
.query({ NOT: Invisible })
|
|
400
449
|
```
|
|
401
450
|
|
|
@@ -409,130 +458,128 @@ Declare which entities the system should track:
|
|
|
409
458
|
| `{ OR: [q1, q2] }` | Either sub-query matches |
|
|
410
459
|
| `{ NOT: q }` | Sub-query must not match |
|
|
411
460
|
| `{ PARENT: q }` | Entity's parent matches q |
|
|
412
|
-
| An array `[A, B]` | Shorthand for `HAS: [A, B]`
|
|
461
|
+
| An array `[A, B]` | Shorthand for `{ HAS: [A, B] }` |
|
|
462
|
+
| A single class / id | Shorthand for `{ HAS: [C] }` |
|
|
463
|
+
| A predicate function | Custom membership logic |
|
|
413
464
|
|
|
414
|
-
**Type inference
|
|
465
|
+
**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
466
|
|
|
416
467
|
```ts
|
|
417
468
|
.query({ AND: [{ HAS: Position }, { HAS: Velocity }] }, [Position, Velocity])
|
|
418
469
|
.each([Position, Velocity], (e, [pos, vel]) => {
|
|
419
470
|
pos.x += vel.vx; // pos and vel are non-null
|
|
420
|
-
})
|
|
471
|
+
});
|
|
421
472
|
```
|
|
422
473
|
|
|
423
474
|
#### `.phase(p)`
|
|
424
475
|
|
|
425
|
-
Assign the system to a
|
|
476
|
+
Assign the system to a phase by name or `IPhase` reference. Default phase is `"update"`.
|
|
426
477
|
|
|
427
478
|
```ts
|
|
428
|
-
.phase("preupdate")
|
|
429
|
-
.phase(myPhase)
|
|
479
|
+
.phase("preupdate")
|
|
480
|
+
.phase(myPhase)
|
|
430
481
|
```
|
|
431
482
|
|
|
432
483
|
#### `.enter(callback)` / `.enter(inject, callback)`
|
|
433
484
|
|
|
434
|
-
|
|
485
|
+
Fires once when an entity first matches the system.
|
|
435
486
|
|
|
436
487
|
```ts
|
|
437
|
-
|
|
438
|
-
.enter((e) => { console.log("entity joined", e.eid); })
|
|
439
|
-
|
|
440
|
-
// With injection — component instances resolved from the entity:
|
|
488
|
+
.enter((e) => { ... })
|
|
441
489
|
.enter([Position, Sprite], (e, [pos, sprite]) => {
|
|
442
490
|
sprite.setPosition(pos.x, pos.y);
|
|
443
491
|
})
|
|
444
492
|
|
|
445
|
-
// Resolve from parent:
|
|
493
|
+
// Resolve from the entity's parent:
|
|
446
494
|
.enter([{ parent: Container }], (e, [container]) => {
|
|
447
495
|
container.add(e.get(Sprite)!.gameObject);
|
|
448
|
-
})
|
|
496
|
+
});
|
|
449
497
|
```
|
|
450
498
|
|
|
451
499
|
#### `.exit(callback)` / `.exit(inject, callback)`
|
|
452
500
|
|
|
453
|
-
|
|
501
|
+
Fires when an entity leaves the system (component removed or entity destroyed). Components removed in the same frame are still resolvable in `inject`.
|
|
454
502
|
|
|
455
503
|
```ts
|
|
456
|
-
.exit([Sprite], (e, [sprite]) =>
|
|
457
|
-
sprite.destroy();
|
|
458
|
-
})
|
|
504
|
+
.exit([Sprite], (e, [sprite]) => sprite.destroy());
|
|
459
505
|
```
|
|
460
506
|
|
|
461
507
|
#### `.update(ComponentClass, callback)` / `.update(ComponentClass, inject, callback)`
|
|
462
508
|
|
|
463
|
-
|
|
509
|
+
Fires when `component.modified()` is called for the watched component on a tracked entity.
|
|
464
510
|
|
|
465
511
|
```ts
|
|
466
|
-
|
|
467
|
-
.update(Position, (pos) => {
|
|
468
|
-
renderer.setPosition(pos.x, pos.y);
|
|
469
|
-
})
|
|
512
|
+
.update(Position, (pos) => renderer.setPosition(pos.x, pos.y));
|
|
470
513
|
|
|
471
|
-
// With injection — receives the modified component and extra components:
|
|
472
514
|
.update(Position, [Sprite], (pos, [sprite]) => {
|
|
473
515
|
sprite.sprite.setPosition(pos.x, pos.y);
|
|
474
|
-
})
|
|
516
|
+
});
|
|
475
517
|
```
|
|
476
518
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
Calling `update` also adds that component type to the system's implicit `HAS` query (unless you called `query()` first).
|
|
519
|
+
If `query()` has not been called, `update` automatically expands the implicit `HAS` predicate to require the watched component.
|
|
480
520
|
|
|
481
521
|
#### `.each(components, callback)`
|
|
482
522
|
|
|
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.
|
|
523
|
+
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
524
|
|
|
487
525
|
```ts
|
|
488
526
|
.requires(Position, Velocity)
|
|
489
527
|
.each([Position, Velocity], (e, [pos, vel]) => {
|
|
490
|
-
pos.x += vel.vx;
|
|
491
|
-
|
|
492
|
-
})
|
|
528
|
+
pos.x += vel.vx;
|
|
529
|
+
});
|
|
493
530
|
```
|
|
494
531
|
|
|
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
532
|
#### `.sort(components, compare)`
|
|
498
533
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
Components declared via `requires()` are non-null in the compare callback.
|
|
534
|
+
Store matched entities in a custom order determined by `compare`. Implies `.track()`. Iterating `system.entities`, `forEach`, and `each` walks entities in sorted order.
|
|
502
535
|
|
|
503
536
|
```ts
|
|
504
537
|
world
|
|
505
538
|
.system("Render")
|
|
506
539
|
.requires(Position, Sprite)
|
|
507
540
|
.sort([Position], ([posA], [posB]) => posA.z - posB.z)
|
|
508
|
-
.each([Position, Sprite], (e, [pos, sprite]) =>
|
|
509
|
-
sprite.draw(pos.x, pos.y);
|
|
510
|
-
});
|
|
541
|
+
.each([Position, Sprite], (e, [pos, sprite]) => sprite.draw(pos.x, pos.y));
|
|
511
542
|
```
|
|
512
543
|
|
|
513
|
-
Iterating `system.entities` after a phase run yields entities in the sorted order.
|
|
514
|
-
|
|
515
544
|
#### `.track()`
|
|
516
545
|
|
|
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.
|
|
546
|
+
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
547
|
|
|
521
548
|
#### `.run(callback)`
|
|
522
549
|
|
|
523
|
-
|
|
550
|
+
Fires every tick when the system's phase runs, regardless of entity state. Use for polling, network I/O, timers, etc.
|
|
524
551
|
|
|
525
552
|
```ts
|
|
526
553
|
.run((now, delta) => {
|
|
527
554
|
sendNetworkPacket(now);
|
|
528
|
-
})
|
|
555
|
+
});
|
|
529
556
|
```
|
|
530
557
|
|
|
558
|
+
#### `.disable()` / `.enable()`
|
|
559
|
+
|
|
560
|
+
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.
|
|
561
|
+
|
|
562
|
+
```ts
|
|
563
|
+
const ai = world.system("AI").requires(Enemy).run(tickAI);
|
|
564
|
+
|
|
565
|
+
// Pause AI processing during a cutscene:
|
|
566
|
+
ai.disable();
|
|
567
|
+
|
|
568
|
+
// Resume normal processing:
|
|
569
|
+
ai.enable();
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
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).
|
|
573
|
+
|
|
574
|
+
#### `.destroy()`
|
|
575
|
+
|
|
576
|
+
**Not supported on `System`** — calling it throws. Systems live for the duration of the world. Use a standalone `Query` for temporary reactive sets.
|
|
577
|
+
|
|
531
578
|
---
|
|
532
579
|
|
|
533
|
-
### Query
|
|
580
|
+
### `Query`
|
|
534
581
|
|
|
535
|
-
|
|
582
|
+
`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
583
|
|
|
537
584
|
```ts
|
|
538
585
|
const projectiles = world
|
|
@@ -545,46 +592,42 @@ const projectiles = world
|
|
|
545
592
|
|
|
546
593
|
world.start();
|
|
547
594
|
|
|
548
|
-
|
|
549
|
-
projectiles.forEach((e) => {
|
|
550
|
-
/* ... */
|
|
551
|
-
});
|
|
595
|
+
projectiles.forEach((e) => { ... });
|
|
552
596
|
console.log(projectiles.entities.size, "active projectiles");
|
|
553
597
|
```
|
|
554
598
|
|
|
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
|
-
| `.
|
|
599
|
+
| Method | Description |
|
|
600
|
+
| ------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
601
|
+
| `.requires(...components)` | Set the membership predicate to `HAS(...components)` and start tracking. |
|
|
602
|
+
| `.query(expr, _guaranteed?)` | Set the membership predicate using a `QueryDSL` expression. |
|
|
603
|
+
| `.enter(callback)` / `.enter(inject, callback)` | Fires when an entity joins the query. |
|
|
604
|
+
| `.exit(callback)` / `.exit(inject, callback)` | Fires when an entity leaves the query. |
|
|
605
|
+
| `.update(C, callback)` / `.update(C, inject, callback)` | Fires when `C` is modified on a tracked entity. |
|
|
606
|
+
| `.sort(components, compare)` | Store matched entities in sorted order. |
|
|
607
|
+
| `.track()` | Enable tracking. Backfills when called after `start()`. |
|
|
608
|
+
| `.belongs(e)` | Returns `true` if the entity satisfies the predicate. |
|
|
609
|
+
| `.forEach(callback)` | Iterate currently tracked entities. |
|
|
610
|
+
| `.forEach(components, callback)` | Iterate with component injection. |
|
|
611
|
+
| `.entities` | `ReadonlySet<Entity>` of currently tracked entities. |
|
|
612
|
+
| `.destroy()` | Remove the query from the world and from every entity (no exit fires). |
|
|
568
613
|
|
|
569
|
-
#### `.destroy()`
|
|
614
|
+
#### `.destroy()` semantics
|
|
570
615
|
|
|
571
|
-
|
|
616
|
+
`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
617
|
|
|
573
618
|
```ts
|
|
574
619
|
const q = world.query("Temporary").requires(Position);
|
|
575
620
|
// ... use q.entities ...
|
|
576
|
-
q.destroy();
|
|
621
|
+
q.destroy();
|
|
577
622
|
```
|
|
578
623
|
|
|
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.
|
|
624
|
+
`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
625
|
|
|
583
626
|
---
|
|
584
627
|
|
|
585
|
-
### Filter
|
|
628
|
+
### `Filter`
|
|
586
629
|
|
|
587
|
-
|
|
630
|
+
`world.filter(dsl)` returns a `Filter` that performs a non-reactive scan. It accepts the same `QueryDSL` expressions as systems and queries.
|
|
588
631
|
|
|
589
632
|
```ts
|
|
590
633
|
const f = world.filter([Position, Velocity]);
|
|
@@ -592,30 +635,58 @@ const f = world.filter([Position, Velocity]);
|
|
|
592
635
|
|
|
593
636
|
| Method | Description |
|
|
594
637
|
| -------------------------------- | -------------------------------------------------------------------------- |
|
|
595
|
-
| `.forEach(callback)` | Walk all world entities; invoke callback
|
|
638
|
+
| `.forEach(callback)` | Walk all world entities; invoke callback on each match. |
|
|
596
639
|
| `.forEach(components, callback)` | Same, with component injection and non-null types for required components. |
|
|
597
640
|
|
|
598
|
-
|
|
641
|
+
`forEach` runs inside a deferred scope, so mutations made by the callback are batched and become visible after iteration finishes.
|
|
642
|
+
|
|
643
|
+
**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
644
|
|
|
600
645
|
```ts
|
|
601
646
|
// Auto-deduced — both non-null:
|
|
602
|
-
world.filter([Position, Velocity])
|
|
603
|
-
.forEach([Position, Velocity], (e, [pos, vel]) => { ... });
|
|
647
|
+
world.filter([Position, Velocity]).forEach([Position, Velocity], (e, [pos, vel]) => { ... });
|
|
604
648
|
|
|
605
649
|
// Manual hint for OR / NOT / PARENT / custom function:
|
|
606
|
-
world.filter({ OR: [Position, Velocity] }, [Position])
|
|
607
|
-
.forEach([Position], (e, [pos]) => pos.x);
|
|
650
|
+
world.filter({ OR: [Position, Velocity] }, [Position]).forEach([Position], (e, [pos]) => pos.x);
|
|
608
651
|
```
|
|
609
652
|
|
|
610
653
|
A `Filter` holds no tracked set, makes no registration calls, and needs no `destroy()`.
|
|
611
654
|
|
|
612
655
|
---
|
|
613
656
|
|
|
657
|
+
### `Bitset`
|
|
658
|
+
|
|
659
|
+
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.
|
|
660
|
+
|
|
661
|
+
| Method | Description |
|
|
662
|
+
| ------------------ | ------------------------------------------------------------------------ |
|
|
663
|
+
| `add(n)` | Set bit `n`. |
|
|
664
|
+
| `addBit(bptr)` | Set the bit at a pre-computed `BitPtr`. |
|
|
665
|
+
| `delete(n)` | Clear bit `n`. Trims trailing zero words. |
|
|
666
|
+
| `has(n)` | Returns `true` if bit `n` is set. |
|
|
667
|
+
| `hasBit(bptr)` | Fast check via a pre-computed `BitPtr`. |
|
|
668
|
+
| `equal(other)` | Returns `true` when both bitsets have the same bits set. |
|
|
669
|
+
| `hasBitset(other)` | Returns `true` when every bit set in `other` is also set in this bitset. |
|
|
670
|
+
| `forEach(cb)` | Visit each set bit index in ascending order. |
|
|
671
|
+
| `indices()` | Return all set bit indices as a `number[]`. |
|
|
672
|
+
|
|
673
|
+
```ts
|
|
674
|
+
class Tags extends Component {
|
|
675
|
+
tags = new Bitset();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
tags.tags.add(TAG_VISIBLE);
|
|
679
|
+
if (tags.tags.has(TAG_VISIBLE)) { ... }
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
---
|
|
683
|
+
|
|
614
684
|
## Build & Test
|
|
615
685
|
|
|
616
686
|
```
|
|
617
687
|
yarn build
|
|
618
688
|
yarn test
|
|
689
|
+
yarn lint
|
|
619
690
|
```
|
|
620
691
|
|
|
621
692
|
---
|