iris-ecs 0.0.9 → 0.0.10
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 +250 -61
- package/dist/archetype.d.ts +18 -1
- package/dist/archetype.d.ts.map +1 -1
- package/dist/archetype.js +60 -15
- package/dist/archetype.js.map +1 -1
- package/dist/component.d.ts +106 -7
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +86 -4
- package/dist/component.js.map +1 -1
- package/dist/directed-acyclic-graph.d.ts +123 -0
- package/dist/directed-acyclic-graph.d.ts.map +1 -0
- package/dist/directed-acyclic-graph.js +235 -0
- package/dist/directed-acyclic-graph.js.map +1 -0
- package/dist/entity.d.ts +18 -0
- package/dist/entity.d.ts.map +1 -1
- package/dist/entity.js +5 -15
- package/dist/entity.js.map +1 -1
- package/dist/error.d.ts +2 -2
- package/dist/error.js +2 -2
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/name.d.ts +2 -2
- package/dist/name.d.ts.map +1 -1
- package/dist/name.js +3 -3
- package/dist/name.js.map +1 -1
- package/dist/query.d.ts +65 -5
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +34 -3
- package/dist/query.js.map +1 -1
- package/dist/resource.d.ts +62 -4
- package/dist/resource.d.ts.map +1 -1
- package/dist/resource.js +10 -1
- package/dist/resource.js.map +1 -1
- package/dist/scheduler.d.ts +119 -11
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +168 -68
- package/dist/scheduler.js.map +1 -1
- package/dist/schema.d.ts +97 -40
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +31 -69
- package/dist/schema.js.map +1 -1
- package/dist/world.d.ts +10 -1
- package/dist/world.d.ts.map +1 -1
- package/dist/world.js +3 -0
- package/dist/world.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,14 @@ ECS is not a good fit for everything. Simple CRUD applications, form-heavy UIs,
|
|
|
30
30
|
npm install iris-ecs
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## AI Skills
|
|
34
|
+
|
|
35
|
+
Install the [iris-ecs skill](https://skills.sh) so AI coding agents (Claude Code, Cursor, etc.) understand the iris-ecs API:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx skills add https://github.com/r04423/iris --skill iris-ecs
|
|
39
|
+
```
|
|
40
|
+
|
|
33
41
|
## Quick Start
|
|
34
42
|
|
|
35
43
|
```typescript
|
|
@@ -40,43 +48,40 @@ import {
|
|
|
40
48
|
defineSystem,
|
|
41
49
|
defineTag,
|
|
42
50
|
cacheQuery,
|
|
43
|
-
|
|
44
|
-
getComponentValue,
|
|
45
|
-
setComponentValue,
|
|
51
|
+
getComponentVectorView,
|
|
46
52
|
queryEntities,
|
|
47
53
|
addSystem,
|
|
48
54
|
runOnce,
|
|
49
55
|
Type,
|
|
50
56
|
} from "iris-ecs";
|
|
51
57
|
|
|
52
|
-
// Define components
|
|
53
|
-
const Position = defineComponent("Position", {
|
|
54
|
-
const Velocity = defineComponent("Velocity", {
|
|
58
|
+
// Define components -- vector fields store x,y interleaved in one TypedArray
|
|
59
|
+
const Position = defineComponent("Position", { value: Type.f32(2) });
|
|
60
|
+
const Velocity = defineComponent("Velocity", { value: Type.f32(2) });
|
|
55
61
|
const Player = defineTag("Player");
|
|
56
62
|
|
|
57
63
|
// Create world and entities
|
|
58
64
|
const world = createWorld();
|
|
59
65
|
|
|
60
|
-
const player = createEntity(world
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
const player = createEntity(world, [
|
|
67
|
+
[Position, { value: [0, 0] }],
|
|
68
|
+
[Velocity, { value: [1, 0] }],
|
|
69
|
+
Player,
|
|
70
|
+
]);
|
|
64
71
|
|
|
65
72
|
// Define a system -- init runs once, tick runs every frame
|
|
66
73
|
const movementSystem = defineSystem("movementSystem", (world) => {
|
|
67
74
|
// Init: cache queries once
|
|
68
|
-
const movers = cacheQuery(world, Position, Velocity);
|
|
75
|
+
const movers = cacheQuery(world, [Position, Velocity]);
|
|
69
76
|
|
|
70
77
|
return () => {
|
|
71
78
|
// Tick: runs every frame
|
|
72
79
|
queryEntities(world, movers, (e) => {
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const vx = getComponentValue(world, e, Velocity, "x");
|
|
76
|
-
const vy = getComponentValue(world, e, Velocity, "y");
|
|
80
|
+
const pos = getComponentVectorView(world, e, Position, "value");
|
|
81
|
+
const vel = getComponentVectorView(world, e, Velocity, "value");
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
pos[0] += vel[0];
|
|
84
|
+
pos[1] += vel[1];
|
|
80
85
|
});
|
|
81
86
|
};
|
|
82
87
|
});
|
|
@@ -85,7 +90,7 @@ const movementSystem = defineSystem("movementSystem", (world) => {
|
|
|
85
90
|
addSystem(world, movementSystem);
|
|
86
91
|
await runOnce(world);
|
|
87
92
|
|
|
88
|
-
// Position is now
|
|
93
|
+
// Position is now [1, 0]
|
|
89
94
|
```
|
|
90
95
|
|
|
91
96
|
## Core Concepts
|
|
@@ -108,6 +113,12 @@ const world = createWorld();
|
|
|
108
113
|
const player = createEntity(world);
|
|
109
114
|
const enemy = createEntity(world);
|
|
110
115
|
|
|
116
|
+
// Create with initial components
|
|
117
|
+
const npc = createEntity(world, [
|
|
118
|
+
[Position, { value: [10, 20] }],
|
|
119
|
+
Enemy,
|
|
120
|
+
]);
|
|
121
|
+
|
|
111
122
|
destroyEntity(world, enemy);
|
|
112
123
|
isEntityAlive(world, enemy); // false
|
|
113
124
|
isEntityAlive(world, player); // true
|
|
@@ -116,7 +127,7 @@ isEntityAlive(world, player); // true
|
|
|
116
127
|
resetWorld(world);
|
|
117
128
|
```
|
|
118
129
|
|
|
119
|
-
Create entities with `createEntity()`,
|
|
130
|
+
Create entities with `createEntity()`, optionally passing an array of component entries to attach in one call. Destroy them with `destroyEntity()`. Use `isEntityAlive()` to check if an entity reference is still valid. Call `resetWorld()` to clear all entities and state while preserving definitions -- useful for level reloads or testing.
|
|
120
131
|
|
|
121
132
|
⚠️ **Entity IDs are recycled.** After destroying an entity, its ID may be reused for a new entity. Never store entity IDs long-term without checking `isEntityAlive()` first -- your old reference might now point to a different entity.
|
|
122
133
|
|
|
@@ -138,7 +149,7 @@ getName(world, player); // "player-1"
|
|
|
138
149
|
lookupByName(world, "player-1"); // player entity
|
|
139
150
|
|
|
140
151
|
// Validate components during lookup -- returns entity only if it has both
|
|
141
|
-
lookupByName(world, "player-1", Position, Health);
|
|
152
|
+
lookupByName(world, "player-1", [Position, Health]);
|
|
142
153
|
|
|
143
154
|
removeName(world, player);
|
|
144
155
|
lookupByName(world, "player-1"); // undefined
|
|
@@ -177,18 +188,26 @@ import {
|
|
|
177
188
|
defineComponent,
|
|
178
189
|
Type,
|
|
179
190
|
addComponent,
|
|
191
|
+
addComponents,
|
|
180
192
|
getComponentValue,
|
|
181
193
|
setComponentValue,
|
|
194
|
+
getComponentVectorValue,
|
|
195
|
+
setComponentVectorValue,
|
|
196
|
+
getComponentVectorView,
|
|
182
197
|
} from "iris-ecs";
|
|
183
198
|
|
|
184
|
-
const Position = defineComponent("Position", {
|
|
199
|
+
const Position = defineComponent("Position", { value: Type.f32(2) });
|
|
185
200
|
const Health = defineComponent("Health", { current: Type.i32(), max: Type.i32() });
|
|
186
201
|
|
|
187
|
-
addComponent(world, entity, Position, {
|
|
202
|
+
addComponent(world, entity, Position, { value: [0, 0] });
|
|
188
203
|
addComponent(world, entity, Health, { current: 100, max: 100 });
|
|
189
204
|
|
|
190
|
-
|
|
191
|
-
|
|
205
|
+
// Scalar fields use getComponentValue / setComponentValue
|
|
206
|
+
const hp = getComponentValue(world, entity, Health, "current"); // 100
|
|
207
|
+
setComponentValue(world, entity, Health, "current", 80);
|
|
208
|
+
|
|
209
|
+
// Vector fields use dedicated access functions
|
|
210
|
+
const pos = getComponentVectorView(world, entity, Position, "value"); // Float32Array [0, 0]
|
|
192
211
|
```
|
|
193
212
|
|
|
194
213
|
#### Schema Types
|
|
@@ -207,6 +226,8 @@ The `Type` namespace provides storage-optimized types:
|
|
|
207
226
|
| `Type.string()` | Array | Text data |
|
|
208
227
|
| `Type.object<T>()` | Array | Complex nested objects |
|
|
209
228
|
|
|
229
|
+
All numeric type factories accept an optional size parameter (2-16) to create **vector fields** -- see [Vector Fields](#vector-fields) below.
|
|
230
|
+
|
|
210
231
|
Numeric types use TypedArrays for cache-friendly memory layout. Use the smallest type that fits your data.
|
|
211
232
|
|
|
212
233
|
#### Adding Components is Idempotent
|
|
@@ -214,14 +235,82 @@ Numeric types use TypedArrays for cache-friendly memory layout. Use the smallest
|
|
|
214
235
|
Adding a component that already exists does nothing -- the existing data is preserved.
|
|
215
236
|
|
|
216
237
|
```typescript
|
|
217
|
-
addComponent(world, entity,
|
|
218
|
-
addComponent(world, entity,
|
|
238
|
+
addComponent(world, entity, Health, { current: 100, max: 100 });
|
|
239
|
+
addComponent(world, entity, Health, { current: 50, max: 50 }); // ignored
|
|
219
240
|
|
|
220
|
-
getComponentValue(world, entity,
|
|
241
|
+
getComponentValue(world, entity, Health, "current"); // still 100
|
|
221
242
|
```
|
|
222
243
|
|
|
223
244
|
💡 **Tip:** Use `hasComponent()` to check first if you need conditional addition, or `setComponentValue()` to update existing data.
|
|
224
245
|
|
|
246
|
+
#### Batch Adding Components
|
|
247
|
+
|
|
248
|
+
Use `addComponents()` to attach multiple components in one call:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
addComponents(world, entity, [
|
|
252
|
+
[Position, { value: [0, 0] }],
|
|
253
|
+
[Velocity, { value: [1, 0] }],
|
|
254
|
+
Player,
|
|
255
|
+
]);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Each entry is either a standalone ID (tag, entity, schema-less pair) or a `[component, data]` tuple for data components.
|
|
259
|
+
|
|
260
|
+
#### Vector Fields
|
|
261
|
+
|
|
262
|
+
When component fields represent logically grouped numbers (positions, colors, directions), use **vector fields** to store them interleaved in a single TypedArray column. Pass a size (2-16) to any numeric type factory:
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import {
|
|
266
|
+
defineComponent,
|
|
267
|
+
addComponent,
|
|
268
|
+
getComponentVectorValue,
|
|
269
|
+
setComponentVectorValue,
|
|
270
|
+
getComponentVectorView,
|
|
271
|
+
Type,
|
|
272
|
+
} from "iris-ecs";
|
|
273
|
+
|
|
274
|
+
const Position = defineComponent("Position", { value: Type.f32(2) });
|
|
275
|
+
const Color = defineComponent("Color", { value: Type.u32(4) });
|
|
276
|
+
|
|
277
|
+
const entity = createEntity(world);
|
|
278
|
+
addComponent(world, entity, Position, { value: [10, 20] });
|
|
279
|
+
addComponent(world, entity, Color, { value: [255, 128, 0, 255] });
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Vector fields use dedicated access functions instead of the scalar `getComponentValue` / `setComponentValue`:
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// Copy-based read -- returns a tuple (e.g., [number, number])
|
|
286
|
+
const pos = getComponentVectorValue(world, entity, Position, "value");
|
|
287
|
+
|
|
288
|
+
// Copy-based write
|
|
289
|
+
setComponentVectorValue(world, entity, Position, "value", [30, 40]);
|
|
290
|
+
|
|
291
|
+
// Zero-copy view -- returns a TypedArray subarray backed by the column buffer
|
|
292
|
+
const view = getComponentVectorView(world, entity, Position, "value");
|
|
293
|
+
view[0] += 1.0; // direct mutation, no copy
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The zero-copy view shares the underlying buffer -- mutations are immediate. Views are invalidated if the archetype resizes (when new entities are added and capacity grows). Use views within a system tick; do not cache across frames.
|
|
297
|
+
|
|
298
|
+
Components can mix scalar and vector fields:
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
const Particle = defineComponent("Particle", {
|
|
302
|
+
position: Type.f32(3),
|
|
303
|
+
mass: Type.f32(),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
addComponent(world, entity, Particle, { position: [0, 0, 0], mass: 1.0 });
|
|
307
|
+
|
|
308
|
+
const mass = getComponentValue(world, entity, Particle, "mass"); // number
|
|
309
|
+
const pos = getComponentVectorValue(world, entity, Particle, "position"); // [number, number, number]
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
TypeScript enforces the scalar/vector boundary at the type level -- `getComponentValue` rejects vector fields, and `getComponentVectorValue` rejects scalar fields.
|
|
313
|
+
|
|
225
314
|
### Resources
|
|
226
315
|
|
|
227
316
|
A **Resource** is a global singleton -- world-level data that isn't attached to any specific entity. Define resources using regular components and store them with `addResource()`.
|
|
@@ -261,6 +350,32 @@ queryEntities(world, [Time], (entity) => {
|
|
|
261
350
|
|
|
262
351
|
Use resources for frame timing, configuration, asset registry, input state, physics settings, or any global data that systems need but doesn't belong to a specific entity.
|
|
263
352
|
|
|
353
|
+
Resources with vector fields use dedicated access functions, mirroring the component vector API:
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
import {
|
|
357
|
+
defineComponent,
|
|
358
|
+
addResource,
|
|
359
|
+
getResourceVectorValue,
|
|
360
|
+
setResourceVectorValue,
|
|
361
|
+
getResourceVectorView,
|
|
362
|
+
Type,
|
|
363
|
+
} from "iris-ecs";
|
|
364
|
+
|
|
365
|
+
const Gravity = defineComponent("Gravity", { value: Type.f64(3) });
|
|
366
|
+
addResource(world, Gravity, { value: [0, -9.81, 0] });
|
|
367
|
+
|
|
368
|
+
// Copy-based read
|
|
369
|
+
const g = getResourceVectorValue(world, Gravity, "value"); // [number, number, number]
|
|
370
|
+
|
|
371
|
+
// Copy-based write
|
|
372
|
+
setResourceVectorValue(world, Gravity, "value", [0, -20, 0]);
|
|
373
|
+
|
|
374
|
+
// Zero-copy view
|
|
375
|
+
const view = getResourceVectorView(world, Gravity, "value"); // Float64Array
|
|
376
|
+
view[1] = -15; // direct mutation
|
|
377
|
+
```
|
|
378
|
+
|
|
264
379
|
### Relations
|
|
265
380
|
|
|
266
381
|
A **Relation** describes a directed connection between two entities. Combine a relation with a target using `pair()` to create a pair -- pairs are added to entities like components.
|
|
@@ -358,22 +473,24 @@ An **Archetype** groups entities that share the same component set. All entities
|
|
|
358
473
|
|
|
359
474
|
```
|
|
360
475
|
Archetype [Position, Velocity]
|
|
361
|
-
|
|
362
|
-
│ Entity │
|
|
363
|
-
|
|
364
|
-
│ bullet1 │ 10, 5
|
|
365
|
-
│ bullet2 │ 15, 8
|
|
366
|
-
|
|
476
|
+
┌─────────┬──────────────────┬──────────────────┐
|
|
477
|
+
│ Entity │ Position (vec2) │ Velocity (vec2) │
|
|
478
|
+
├─────────┼──────────────────┼──────────────────┤
|
|
479
|
+
│ bullet1 │ [10, 5] │ [1, 0] │
|
|
480
|
+
│ bullet2 │ [15, 8] │ [1, 0] │
|
|
481
|
+
└─────────┴──────────────────┴──────────────────┘
|
|
367
482
|
|
|
368
483
|
Archetype [Position, Velocity, Health]
|
|
369
|
-
|
|
370
|
-
│ Entity │
|
|
371
|
-
|
|
372
|
-
│ player │
|
|
373
|
-
│ enemy │ 50, 20
|
|
374
|
-
|
|
484
|
+
┌─────────┬──────────────────┬──────────────────┬─────────┐
|
|
485
|
+
│ Entity │ Position (vec2) │ Velocity (vec2) │ Health │
|
|
486
|
+
├─────────┼──────────────────┼──────────────────┼─────────┤
|
|
487
|
+
│ player │ [0, 0] │ [1, 0] │ 100 │
|
|
488
|
+
│ enemy │ [50, 20] │ [-1, 0] │ 50 │
|
|
489
|
+
└─────────┴──────────────────┴──────────────────┴─────────┘
|
|
375
490
|
```
|
|
376
491
|
|
|
492
|
+
Vector fields like Position store all elements interleaved in a single TypedArray column: `[x0, y0, x1, y1, ...]`. This keeps each entity's vector contiguous in memory for cache-friendly access.
|
|
493
|
+
|
|
377
494
|
Within an archetype, component data is stored in **columns** (TypedArrays for numeric types). When a query iterates entities with `Position` and `Velocity`, it walks through archetypes that contain both components. This columnar layout keeps data contiguous rather than scattered across objects, reducing memory overhead and enabling efficient iteration.
|
|
378
495
|
|
|
379
496
|
Adding or removing a component moves an entity to a different archetype. This is more expensive than reading or writing component values, so prefer stable component sets for entities that update frequently.
|
|
@@ -389,7 +506,7 @@ import { queryEntities, queryFirstEntity, cacheQuery, not } from "iris-ecs";
|
|
|
389
506
|
|
|
390
507
|
// Iterate all entities with Position and Velocity
|
|
391
508
|
queryEntities(world, [Position, Velocity], (entity) => {
|
|
392
|
-
const
|
|
509
|
+
const pos = getComponentVectorView(world, entity, Position, "value")!;
|
|
393
510
|
// ...
|
|
394
511
|
});
|
|
395
512
|
|
|
@@ -401,7 +518,7 @@ Inline terms (arrays) work anywhere, but in systems it's advised to pre-build qu
|
|
|
401
518
|
|
|
402
519
|
```typescript
|
|
403
520
|
const mySystem = defineSystem("mySystem", (world) => {
|
|
404
|
-
const movers = cacheQuery(world, Position, Velocity);
|
|
521
|
+
const movers = cacheQuery(world, [Position, Velocity]);
|
|
405
522
|
|
|
406
523
|
return () => {
|
|
407
524
|
queryEntities(world, movers, (entity) => {
|
|
@@ -433,6 +550,50 @@ queryEntities(world, [Position, Velocity, not(Frozen), not(Disabled)], (entity)
|
|
|
433
550
|
|
|
434
551
|
Queries match archetypes where all required components are present and no excluded components exist. Matched archetypes are cached and auto-update when archetypes are created or destroyed.
|
|
435
552
|
|
|
553
|
+
#### Column Iteration
|
|
554
|
+
|
|
555
|
+
For performance-critical systems, `queryColumns()` provides direct access to the underlying TypedArray columns instead of iterating entity-by-entity. The callback receives the entity array and a columns tuple for each matching archetype. Each element in the tuple corresponds to a data-bearing component in query term order:
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
import { queryColumns, cacheQuery, not } from "iris-ecs";
|
|
559
|
+
|
|
560
|
+
queryColumns(world, [Player, Position, Velocity, not(Dead)], (entities, [pos, vel]) => {
|
|
561
|
+
// pos.value and vel.value are raw TypedArrays (e.g. Float32Array)
|
|
562
|
+
// entities.length tells you how many entities are in this archetype
|
|
563
|
+
for (let i = 0; i < entities.length; i++) {
|
|
564
|
+
const offset = i * 2; // vec2 stride
|
|
565
|
+
pos.value[offset] += vel.value[offset];
|
|
566
|
+
pos.value[offset + 1] += vel.value[offset + 1];
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
Tags and data-less pairs are omitted from the columns tuple -- only data-bearing components and relation pairs appear as elements. The element order matches the order of data-bearing terms in the query.
|
|
572
|
+
|
|
573
|
+
Pre-cached queries work the same way:
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
const movementSystem = defineSystem("movementSystem", (world) => {
|
|
577
|
+
const movers = cacheQuery(world, [Position, Enemy, Velocity, not(Frozen)]);
|
|
578
|
+
|
|
579
|
+
return () => {
|
|
580
|
+
queryColumns(world, movers, (entities, [pos, vel]) => {
|
|
581
|
+
for (let i = 0; i < entities.length; i++) {
|
|
582
|
+
const offset = i * 2;
|
|
583
|
+
pos.value[offset] += vel.value[offset];
|
|
584
|
+
pos.value[offset + 1] += vel.value[offset + 1];
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
};
|
|
588
|
+
});
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
Return `false` from the callback to stop iteration early (same as `queryEntities`).
|
|
592
|
+
|
|
593
|
+
`queryColumns` does not support `added()` or `changed()` modifiers -- use `queryEntities` for change detection.
|
|
594
|
+
|
|
595
|
+
💡 **Tip:** Use `queryColumns` when you need to process large numbers of entities with tight loops over TypedArray data. Use `queryEntities` for entity-level logic, change detection, or when you need per-entity API calls like `getComponentValue`.
|
|
596
|
+
|
|
436
597
|
### Systems
|
|
437
598
|
|
|
438
599
|
A **System** is a function that operates on the world. Systems query entities, read and write components, emit events, and implement game logic.
|
|
@@ -449,24 +610,24 @@ import {
|
|
|
449
610
|
run,
|
|
450
611
|
stop,
|
|
451
612
|
queryEntities,
|
|
452
|
-
|
|
453
|
-
setComponentValue,
|
|
613
|
+
getComponentVectorView,
|
|
454
614
|
getResourceValue,
|
|
455
615
|
} from "iris-ecs";
|
|
456
616
|
|
|
457
617
|
const movementSystem = defineSystem("movementSystem", (world) => {
|
|
458
618
|
// Init: cache queries and action getters once
|
|
459
|
-
const movers = cacheQuery(world, Position, Velocity);
|
|
619
|
+
const movers = cacheQuery(world, [Position, Velocity]);
|
|
460
620
|
|
|
461
621
|
return () => {
|
|
462
622
|
// Tick: runs every frame
|
|
463
623
|
const dt = getResourceValue(world, Time, "delta") ?? 0;
|
|
464
624
|
|
|
465
625
|
queryEntities(world, movers, (e) => {
|
|
466
|
-
const
|
|
467
|
-
const
|
|
626
|
+
const pos = getComponentVectorView(world, e, Position, "value");
|
|
627
|
+
const vel = getComponentVectorView(world, e, Velocity, "value");
|
|
468
628
|
|
|
469
|
-
|
|
629
|
+
pos[0] += vel[0] * dt;
|
|
630
|
+
pos[1] += vel[1] * dt;
|
|
470
631
|
});
|
|
471
632
|
};
|
|
472
633
|
});
|
|
@@ -484,8 +645,8 @@ For simple systems that don't need one-time setup, plain functions also work, an
|
|
|
484
645
|
|
|
485
646
|
```typescript
|
|
486
647
|
function debugSystem(world) {
|
|
487
|
-
queryEntities(world, [
|
|
488
|
-
console.log(getComponentValue(world, e,
|
|
648
|
+
queryEntities(world, [Health], (e) => {
|
|
649
|
+
console.log(getComponentValue(world, e, Health, "current"));
|
|
489
650
|
});
|
|
490
651
|
}
|
|
491
652
|
|
|
@@ -517,6 +678,34 @@ Without constraints, systems run in registration order. Use arrays for multiple
|
|
|
517
678
|
|
|
518
679
|
`defineSystem` factory can be registered multiple times with different names via the `name` option: `addSystem(world, movementSystem, { name: "lateMovement" })`.
|
|
519
680
|
|
|
681
|
+
#### System Sets
|
|
682
|
+
|
|
683
|
+
**System sets** are named groups for ordering entire groups of systems relative to each other. Instead of wiring individual `before`/`after` between every physics and render system, declare the group-level constraint once:
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
import { defineSystemSet, addSystemSet, addSystem } from "iris-ecs";
|
|
687
|
+
|
|
688
|
+
const PhysicsSystems = defineSystemSet("PhysicsSystems");
|
|
689
|
+
const RenderSystems = defineSystemSet("RenderSystems");
|
|
690
|
+
|
|
691
|
+
addSystemSet(world, PhysicsSystems, { before: RenderSystems });
|
|
692
|
+
addSystemSet(world, RenderSystems);
|
|
693
|
+
|
|
694
|
+
addSystem(world, applyGravity, { set: PhysicsSystems });
|
|
695
|
+
addSystem(world, detectCollisions, { set: PhysicsSystems, after: applyGravity });
|
|
696
|
+
addSystem(world, drawSprites, { set: RenderSystems });
|
|
697
|
+
addSystem(world, drawParticles, { set: RenderSystems });
|
|
698
|
+
// All physics systems run before all render systems
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
Systems within a set still respect their own `before`/`after` constraints. A system can also order itself relative to a set without joining it:
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
addSystem(world, debugOverlay, { after: PhysicsSystems, before: RenderSystems });
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
A system uses either `schedule` or `set`, not both -- the set inherits its schedule from `addSystemSet`.
|
|
708
|
+
|
|
520
709
|
#### Schedules
|
|
521
710
|
|
|
522
711
|
Systems are grouped into **schedules** -- named execution phases. The default pipeline runs these schedules every frame:
|
|
@@ -600,20 +789,20 @@ addSystem(world, loadAssetsSystem, { schedule: Startup });
|
|
|
600
789
|
**Actions** bundle reusable operations with a world captured in closure. Define actions once, then call them without repeatedly passing the world.
|
|
601
790
|
|
|
602
791
|
```typescript
|
|
603
|
-
import { defineActions, createEntity
|
|
792
|
+
import { defineActions, createEntity } from "iris-ecs";
|
|
604
793
|
|
|
605
794
|
const spawnActions = defineActions((world) => ({
|
|
606
795
|
player(x: number, y: number) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
796
|
+
return createEntity(world, [
|
|
797
|
+
[Position, { value: [x, y] }],
|
|
798
|
+
Player,
|
|
799
|
+
]);
|
|
611
800
|
},
|
|
612
801
|
enemy(x: number, y: number) {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
802
|
+
return createEntity(world, [
|
|
803
|
+
[Position, { value: [x, y] }],
|
|
804
|
+
Enemy,
|
|
805
|
+
]);
|
|
617
806
|
},
|
|
618
807
|
}));
|
|
619
808
|
|
|
@@ -729,7 +918,7 @@ import {
|
|
|
729
918
|
} from "iris-ecs";
|
|
730
919
|
|
|
731
920
|
const physicsSetupSystem = defineSystem("physicsSetupSystem", (world) => {
|
|
732
|
-
const newBodies = cacheQuery(world, added(Position));
|
|
921
|
+
const newBodies = cacheQuery(world, [added(Position)]);
|
|
733
922
|
|
|
734
923
|
return () => {
|
|
735
924
|
// Entities where Position was added since this system's last run
|
|
@@ -740,7 +929,7 @@ const physicsSetupSystem = defineSystem("physicsSetupSystem", (world) => {
|
|
|
740
929
|
});
|
|
741
930
|
|
|
742
931
|
const healthBarSystem = defineSystem("healthBarSystem", (world) => {
|
|
743
|
-
const damaged = cacheQuery(world, changed(Health));
|
|
932
|
+
const damaged = cacheQuery(world, [changed(Health)]);
|
|
744
933
|
|
|
745
934
|
return () => {
|
|
746
935
|
// Entities where Health was modified (added OR value changed)
|
|
@@ -752,7 +941,7 @@ const healthBarSystem = defineSystem("healthBarSystem", (world) => {
|
|
|
752
941
|
|
|
753
942
|
const minimapSystem = defineSystem("minimapSystem", (world) => {
|
|
754
943
|
// Combine change detection with regular filters
|
|
755
|
-
const movedPlayers = cacheQuery(world, Player, changed(Position), not(Dead));
|
|
944
|
+
const movedPlayers = cacheQuery(world, [Player, changed(Position), not(Dead)]);
|
|
756
945
|
|
|
757
946
|
return () => {
|
|
758
947
|
queryEntities(world, movedPlayers, (e) => {
|
package/dist/archetype.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EntityId } from "./encoding.js";
|
|
2
|
-
import type { SchemaRecord } from "./schema.js";
|
|
2
|
+
import type { Schema, SchemaRecord, TypedArrayInstance } from "./schema.js";
|
|
3
3
|
import type { World } from "./world.js";
|
|
4
4
|
/**
|
|
5
5
|
* Column storage type.
|
|
@@ -15,6 +15,16 @@ export type Column = Int8Array | Int16Array | Int32Array | Uint32Array | Float32
|
|
|
15
15
|
export type FieldColumns = {
|
|
16
16
|
[fieldName: string]: Column;
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Map a schema record to its column types.
|
|
20
|
+
*
|
|
21
|
+
* Each field maps to the runtime array type used for storage.
|
|
22
|
+
*
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export type FieldColumnsOf<S extends SchemaRecord> = {
|
|
26
|
+
[K in keyof S]: S[K] extends Schema<number> ? TypedArrayInstance : S[K] extends Schema<number[]> ? TypedArrayInstance : S[K] extends Schema<boolean> ? boolean[] : S[K] extends Schema<string> ? string[] : unknown[];
|
|
27
|
+
};
|
|
18
28
|
/**
|
|
19
29
|
* Component tick storage for change detection.
|
|
20
30
|
*
|
|
@@ -40,6 +50,13 @@ export type Archetype = {
|
|
|
40
50
|
capacity: number;
|
|
41
51
|
ticks: Map<EntityId, ComponentTicks>;
|
|
42
52
|
};
|
|
53
|
+
/**
|
|
54
|
+
* Derives the stride of a column from its length and the archetype capacity.
|
|
55
|
+
* Scalar columns return 1, vector columns return their stride (e.g., 2 for vec2).
|
|
56
|
+
*
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
export declare function getColumnStride(column: Column, capacity: number): number;
|
|
43
60
|
/**
|
|
44
61
|
* Hashes a sorted array of type IDs into a unique archetype key.
|
|
45
62
|
*
|
package/dist/archetype.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archetype.d.ts","sourceRoot":"","sources":["../src/archetype.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAG9C,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"archetype.d.ts","sourceRoot":"","sources":["../src/archetype.ts"],"names":[],"mappings":"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"}
|