@oasys/oecs 0.1.2 → 0.2.1
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 +172 -164
- package/dist/archetype.d.ts +50 -13
- package/dist/archetype.d.ts.map +1 -1
- package/dist/component.d.ts +27 -10
- package/dist/component.d.ts.map +1 -1
- package/dist/ecs.d.ts +104 -0
- package/dist/ecs.d.ts.map +1 -0
- package/dist/entity.d.ts.map +1 -1
- package/dist/event.d.ts +2 -2
- package/dist/event.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +673 -321
- package/dist/query.d.ts +28 -31
- package/dist/query.d.ts.map +1 -1
- package/dist/ref.d.ts +19 -0
- package/dist/ref.d.ts.map +1 -0
- package/dist/resource.d.ts +23 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/schedule.d.ts +3 -3
- package/dist/schedule.d.ts.map +1 -1
- package/dist/store.d.ts +48 -29
- package/dist/store.d.ts.map +1 -1
- package/dist/system.d.ts +2 -2
- package/dist/system.d.ts.map +1 -1
- package/dist/type_primitives/assertions.d.ts +1 -0
- package/dist/type_primitives/assertions.d.ts.map +1 -1
- package/dist/type_primitives/bitset/bitset.d.ts +2 -2
- package/dist/type_primitives/bitset/bitset.d.ts.map +1 -1
- package/dist/type_primitives/sparse_map/sparse_map.d.ts.map +1 -1
- package/dist/type_primitives/sparse_set/sparse_set.d.ts.map +1 -1
- package/dist/type_primitives/typed_arrays/typed_arrays.d.ts +9 -0
- package/dist/type_primitives/typed_arrays/typed_arrays.d.ts.map +1 -1
- package/dist/utils/constants.d.ts +20 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/error.d.ts +2 -9
- package/dist/utils/error.d.ts.map +1 -1
- package/package.json +3 -2
- package/dist/world.d.ts +0 -73
- package/dist/world.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -2,27 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
A fast, minimal archetype-based Entity Component System written in TypeScript.
|
|
4
4
|
|
|
5
|
-
- **
|
|
6
|
-
- **
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
5
|
+
- **Structure-of-Arrays (SoA)** — each component field is a contiguous typed array column (`Float64Array`, `Int32Array`, etc.), enabling cache-friendly inner loops.
|
|
6
|
+
- **Phantom-typed components** — `ComponentDef<{x:"f64",y:"f64"}>` is just a number at runtime, but enforces field names and types at compile time.
|
|
7
|
+
- **Batch iteration** — `for..of` over a query yields non-empty archetypes. Access SoA columns via `get_column()` and write the inner loop.
|
|
8
|
+
- **Single-entity refs** — `ctx.ref(Pos, entity)` gives you a cached accessor with prototype-backed getters/setters.
|
|
9
|
+
- **Resources** — typed global singletons (time, input, config) with live readers.
|
|
10
|
+
- **Events & signals** — fire-and-forget SoA channels, auto-cleared each frame.
|
|
9
11
|
- **Deferred structural changes** — add/remove component and destroy entity are buffered during system execution and flushed between phases.
|
|
10
12
|
- **Topological system ordering** — systems within a phase are sorted by before/after constraints using Kahn's algorithm.
|
|
13
|
+
- **Fixed timestep** — configurable fixed update loop with accumulator and interpolation alpha.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Using npm (or equivalent):
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
npm install @oasys/oecs
|
|
21
|
+
```
|
|
11
22
|
|
|
12
23
|
## Quick start
|
|
13
24
|
|
|
14
25
|
```ts
|
|
15
|
-
import {
|
|
26
|
+
import { ECS, SCHEDULE } from "@oasys/oecs";
|
|
27
|
+
|
|
28
|
+
const world = new ECS();
|
|
16
29
|
|
|
17
|
-
|
|
30
|
+
// Record syntax — per-field type control
|
|
31
|
+
const Pos = world.register_component({ x: "f64", y: "f64" });
|
|
18
32
|
|
|
19
|
-
//
|
|
20
|
-
const Pos = world.register_component(["x", "y"] as const);
|
|
33
|
+
// Array shorthand — uniform type, defaults to "f64"
|
|
21
34
|
const Vel = world.register_component(["vx", "vy"] as const);
|
|
22
35
|
|
|
23
36
|
// Tags have no fields
|
|
24
37
|
const IsEnemy = world.register_tag();
|
|
25
38
|
|
|
39
|
+
// Resources are global singletons
|
|
40
|
+
const Time = world.register_resource(["delta", "elapsed"] as const, {
|
|
41
|
+
delta: 0,
|
|
42
|
+
elapsed: 0,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Events are fire-and-forget messages
|
|
46
|
+
const Damage = world.register_event(["target", "amount"] as const);
|
|
47
|
+
|
|
26
48
|
// Create entities and attach components
|
|
27
49
|
const e = world.create_entity();
|
|
28
50
|
world.add_component(e, Pos, { x: 0, y: 0 });
|
|
@@ -32,12 +54,17 @@ world.add_component(e, IsEnemy);
|
|
|
32
54
|
// Register a system with a typed query
|
|
33
55
|
const moveSys = world.register_system(
|
|
34
56
|
(q, _ctx, dt) => {
|
|
35
|
-
|
|
57
|
+
for (const arch of q) {
|
|
58
|
+
const px = arch.get_column(Pos, "x");
|
|
59
|
+
const py = arch.get_column(Pos, "y");
|
|
60
|
+
const vx = arch.get_column(Vel, "vx");
|
|
61
|
+
const vy = arch.get_column(Vel, "vy");
|
|
62
|
+
const n = arch.entity_count;
|
|
36
63
|
for (let i = 0; i < n; i++) {
|
|
37
|
-
|
|
38
|
-
|
|
64
|
+
px[i] += vx[i] * dt;
|
|
65
|
+
py[i] += vy[i] * dt;
|
|
39
66
|
}
|
|
40
|
-
}
|
|
67
|
+
}
|
|
41
68
|
},
|
|
42
69
|
(qb) => qb.every(Pos, Vel),
|
|
43
70
|
);
|
|
@@ -50,125 +77,105 @@ world.startup();
|
|
|
50
77
|
|
|
51
78
|
// Game loop
|
|
52
79
|
function frame(dt: number) {
|
|
80
|
+
world.set_resource(Time, { delta: dt, elapsed: performance.now() / 1000 });
|
|
53
81
|
world.update(dt);
|
|
54
82
|
}
|
|
55
83
|
```
|
|
56
84
|
|
|
57
|
-
##
|
|
85
|
+
## World Options
|
|
58
86
|
|
|
59
|
-
|
|
87
|
+
`ECS` accepts an optional configuration object:
|
|
60
88
|
|
|
61
89
|
```ts
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
const world = new ECS({
|
|
91
|
+
initial_capacity: 4096, // pre-allocate archetype storage (default: 1024, grows automatically)
|
|
92
|
+
fixed_timestep: 1 / 50, // fixed update interval in seconds (default: 1/60)
|
|
93
|
+
max_fixed_steps: 4, // cap fixed updates per frame to prevent spiral of death (default: 5)
|
|
94
|
+
});
|
|
65
95
|
```
|
|
66
96
|
|
|
67
|
-
|
|
97
|
+
| Option | Type | Default | Description |
|
|
98
|
+
|---|---|---|---|
|
|
99
|
+
| `initial_capacity` | `number` | `1024` | Starting size for every archetype's backing typed arrays (entity IDs and all SoA component columns). Arrays double when exceeded — set this close to your expected entity count per archetype to avoid early re-allocations. |
|
|
100
|
+
| `fixed_timestep` | `number` | `1/60` | Interval (seconds) for `FIXED_UPDATE` systems. |
|
|
101
|
+
| `max_fixed_steps` | `number` | `5` | Maximum `FIXED_UPDATE` iterations per frame. |
|
|
102
|
+
|
|
103
|
+
## Components
|
|
68
104
|
|
|
69
|
-
|
|
105
|
+
Components map field names to typed array tags. All field values are `number`, but storage uses the specified typed array (`Float64Array`, `Int32Array`, etc.) for cache-friendly iteration.
|
|
70
106
|
|
|
71
107
|
```ts
|
|
72
|
-
|
|
108
|
+
// Record syntax — per-field type control
|
|
109
|
+
const Position = world.register_component({ x: "f64", y: "f64" });
|
|
110
|
+
const Health = world.register_component({ current: "i32", max: "i32" });
|
|
73
111
|
|
|
74
|
-
//
|
|
75
|
-
world.
|
|
76
|
-
world.add_component(e, Health, { current: 100, max: 100 });
|
|
112
|
+
// Array shorthand — all fields default to "f64"
|
|
113
|
+
const Vel = world.register_component(["vx", "vy"] as const);
|
|
77
114
|
|
|
78
|
-
// Tags
|
|
79
|
-
world.
|
|
115
|
+
// Tags — no fields
|
|
116
|
+
const IsEnemy = world.register_tag();
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Supported typed array tags: `"f32"`, `"f64"`, `"i8"`, `"i16"`, `"i32"`, `"u8"`, `"u16"`, `"u32"`.
|
|
120
|
+
|
|
121
|
+
Add components individually or via `add_components` (single archetype transition):
|
|
80
122
|
|
|
81
|
-
|
|
123
|
+
```ts
|
|
124
|
+
world.add_component(e, Position, { x: 10, y: 20 });
|
|
82
125
|
world.add_components(e, [
|
|
83
126
|
{ def: Position, values: { x: 10, y: 20 } },
|
|
84
127
|
{ def: Health, values: { current: 100, max: 100 } },
|
|
85
128
|
]);
|
|
86
129
|
```
|
|
87
130
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
world.remove_component(e, Health);
|
|
92
|
-
world.has_component(e, IsEnemy); // true
|
|
93
|
-
```
|
|
131
|
+
See [docs/api/components.md](docs/api/components.md) for full API.
|
|
94
132
|
|
|
95
133
|
## Queries
|
|
96
134
|
|
|
97
|
-
Queries are live views over all archetypes matching a component mask.
|
|
98
|
-
|
|
99
|
-
### Batch iteration with `each()`
|
|
100
|
-
|
|
101
|
-
`each()` calls your function once per matching archetype, passing column-group objects and the entity count. You write the inner loop.
|
|
135
|
+
Queries are live views over all archetypes matching a component mask.
|
|
102
136
|
|
|
103
137
|
```ts
|
|
104
138
|
const q = world.query(Position, Velocity);
|
|
105
139
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// n is the entity count in this archetype
|
|
109
|
-
for (let i = 0; i < n; i++) {
|
|
110
|
-
pos.x[i] += vel.vx[i];
|
|
111
|
-
pos.y[i] += vel.vy[i];
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### Query chaining
|
|
117
|
-
|
|
118
|
-
```ts
|
|
119
|
-
// Extend required components
|
|
120
|
-
const q = world.query(Position).and(Velocity);
|
|
121
|
-
|
|
122
|
-
// Exclude archetypes that have a component
|
|
123
|
-
const alive = world.query(Position).not(Dead);
|
|
124
|
-
|
|
125
|
-
// Require at least one of these
|
|
126
|
-
const damaged = world.query(Health).or(Poison, Fire);
|
|
127
|
-
|
|
128
|
-
// Combine
|
|
129
|
-
const targets = world.query(Position).and(Health).not(Shield).or(IsEnemy, IsBoss);
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### Manual iteration
|
|
133
|
-
|
|
134
|
-
For dynamic component access, iterate archetypes directly:
|
|
135
|
-
|
|
136
|
-
```ts
|
|
137
|
-
for (const arch of world.query(Position)) {
|
|
140
|
+
// Iterate non-empty archetypes, access SoA columns, write the inner loop
|
|
141
|
+
for (const arch of q) {
|
|
138
142
|
const px = arch.get_column(Position, "x");
|
|
139
143
|
const py = arch.get_column(Position, "y");
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
const vx = arch.get_column(Velocity, "vx");
|
|
145
|
+
const vy = arch.get_column(Velocity, "vy");
|
|
146
|
+
const n = arch.entity_count;
|
|
147
|
+
for (let i = 0; i < n; i++) {
|
|
148
|
+
px[i] += vx[i];
|
|
149
|
+
py[i] += vy[i];
|
|
145
150
|
}
|
|
146
151
|
}
|
|
152
|
+
|
|
153
|
+
// Chaining
|
|
154
|
+
const targets = world
|
|
155
|
+
.query(Position)
|
|
156
|
+
.and(Health)
|
|
157
|
+
.not(Shield)
|
|
158
|
+
.any_of(IsEnemy, IsBoss);
|
|
147
159
|
```
|
|
148
160
|
|
|
161
|
+
See [docs/api/queries.md](docs/api/queries.md) for full API.
|
|
162
|
+
|
|
149
163
|
## Systems
|
|
150
164
|
|
|
151
165
|
Systems are plain functions registered with a query and scheduled into lifecycle phases.
|
|
152
166
|
|
|
153
|
-
### Registration
|
|
154
|
-
|
|
155
|
-
Two registration styles:
|
|
156
|
-
|
|
157
167
|
```ts
|
|
158
|
-
//
|
|
168
|
+
// With a typed query
|
|
159
169
|
const moveSys = world.register_system(
|
|
160
170
|
(q, ctx, dt) => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
pos.y[i] += vel.vy[i] * dt;
|
|
165
|
-
}
|
|
166
|
-
});
|
|
171
|
+
for (const arch of q) {
|
|
172
|
+
/* ... */
|
|
173
|
+
}
|
|
167
174
|
},
|
|
168
175
|
(qb) => qb.every(Pos, Vel),
|
|
169
176
|
);
|
|
170
177
|
|
|
171
|
-
//
|
|
178
|
+
// Without a query
|
|
172
179
|
const logSys = world.register_system({
|
|
173
180
|
fn(ctx, dt) {
|
|
174
181
|
console.log("frame", dt);
|
|
@@ -176,119 +183,118 @@ const logSys = world.register_system({
|
|
|
176
183
|
});
|
|
177
184
|
```
|
|
178
185
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
Inside systems, use `ctx` (SystemContext) for structural changes. These are buffered and applied between phases.
|
|
186
|
+
Inside systems, use `ctx` for deferred structural changes and per-entity access:
|
|
182
187
|
|
|
183
188
|
```ts
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
ctx.destroy_entity(someOtherEntity);
|
|
189
|
-
ctx.remove_component(anotherEntity, Health);
|
|
190
|
-
// Changes are applied after all systems in this phase complete
|
|
191
|
-
},
|
|
192
|
-
});
|
|
189
|
+
const e = ctx.create_entity();
|
|
190
|
+
ctx.add_component(e, Pos, { x: 0, y: 0 });
|
|
191
|
+
ctx.destroy_entity(someEntity);
|
|
192
|
+
ctx.remove_component(entity, Health);
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
See [docs/api/systems.md](docs/api/systems.md) for full API.
|
|
196
196
|
|
|
197
|
-
|
|
197
|
+
## Resources
|
|
198
|
+
|
|
199
|
+
Resources are typed global singletons — time, input state, camera config.
|
|
198
200
|
|
|
199
201
|
```ts
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
ctx.set_field(Health, targetEntity, "current", hp - 10);
|
|
204
|
-
},
|
|
202
|
+
const Time = world.register_resource(["delta", "elapsed"] as const, {
|
|
203
|
+
delta: 0,
|
|
204
|
+
elapsed: 0,
|
|
205
205
|
});
|
|
206
|
+
|
|
207
|
+
// Write
|
|
208
|
+
world.set_resource(Time, { delta: dt, elapsed: total });
|
|
209
|
+
|
|
210
|
+
// Read — scalar values, not arrays
|
|
211
|
+
const time = world.resource(Time);
|
|
212
|
+
time.delta; // number
|
|
213
|
+
time.elapsed; // number
|
|
206
214
|
```
|
|
207
215
|
|
|
208
|
-
|
|
216
|
+
See [docs/api/resources.md](docs/api/resources.md) for full API.
|
|
217
|
+
|
|
218
|
+
## Events & Signals
|
|
209
219
|
|
|
210
|
-
|
|
220
|
+
Events are fire-and-forget SoA channels, auto-cleared each frame.
|
|
211
221
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
```ts
|
|
223
|
+
// Data events carry fields
|
|
224
|
+
const Damage = world.register_event(["target", "amount"] as const);
|
|
225
|
+
ctx.emit(Damage, { target: entityId, amount: 50 });
|
|
226
|
+
|
|
227
|
+
const dmg = ctx.read(Damage);
|
|
228
|
+
for (let i = 0; i < dmg.length; i++) {
|
|
229
|
+
dmg.target[i]; // number
|
|
230
|
+
dmg.amount[i]; // number
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Signals carry no data — just a count
|
|
234
|
+
const OnReset = world.register_signal();
|
|
235
|
+
ctx.emit(OnReset);
|
|
236
|
+
if (ctx.read(OnReset).length > 0) {
|
|
237
|
+
/* fired */
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
See [docs/api/events.md](docs/api/events.md) for full API.
|
|
220
242
|
|
|
221
|
-
|
|
243
|
+
## Refs
|
|
244
|
+
|
|
245
|
+
Refs provide cached single-entity field access — faster than `get_field`/`set_field` for repeated access.
|
|
222
246
|
|
|
223
247
|
```ts
|
|
224
|
-
|
|
225
|
-
|
|
248
|
+
const pos = ctx.ref(Pos, entity);
|
|
249
|
+
const vel = ctx.ref(Vel, entity);
|
|
250
|
+
pos.x += vel.vx * dt;
|
|
251
|
+
pos.y += vel.vy * dt;
|
|
226
252
|
```
|
|
227
253
|
|
|
228
|
-
|
|
254
|
+
See [docs/api/refs.md](docs/api/refs.md) for full API.
|
|
255
|
+
|
|
256
|
+
## Schedule
|
|
257
|
+
|
|
258
|
+
Seven lifecycle phases, executed in order:
|
|
259
|
+
|
|
260
|
+
| Phase | When | Use case |
|
|
261
|
+
| -------------- | --------------------- | ----------------------- |
|
|
262
|
+
| `PRE_STARTUP` | Once, before startup | Resource loading |
|
|
263
|
+
| `STARTUP` | Once | Initial entity spawning |
|
|
264
|
+
| `POST_STARTUP` | Once, after startup | Validation |
|
|
265
|
+
| `FIXED_UPDATE` | Every tick (fixed dt) | Physics, simulation |
|
|
266
|
+
| `PRE_UPDATE` | Every frame, first | Input handling |
|
|
267
|
+
| `UPDATE` | Every frame | Game logic |
|
|
268
|
+
| `POST_UPDATE` | Every frame, last | Rendering, cleanup |
|
|
229
269
|
|
|
230
270
|
```ts
|
|
271
|
+
world.add_systems(SCHEDULE.UPDATE, moveSys, physicsSys);
|
|
272
|
+
world.add_systems(SCHEDULE.POST_UPDATE, renderSys);
|
|
273
|
+
|
|
274
|
+
// Ordering constraints
|
|
231
275
|
world.add_systems(SCHEDULE.UPDATE, moveSys, {
|
|
232
276
|
system: physicsSys,
|
|
233
277
|
ordering: { after: [moveSys] },
|
|
234
278
|
});
|
|
235
|
-
|
|
236
|
-
world.add_systems(SCHEDULE.UPDATE, {
|
|
237
|
-
system: aiSys,
|
|
238
|
-
ordering: { before: [moveSys] },
|
|
239
|
-
});
|
|
240
279
|
```
|
|
241
280
|
|
|
242
|
-
|
|
281
|
+
See [docs/api/schedule.md](docs/api/schedule.md) for full API.
|
|
243
282
|
|
|
244
283
|
## Entity lifecycle
|
|
245
284
|
|
|
246
285
|
```ts
|
|
247
286
|
const e = world.create_entity();
|
|
248
|
-
|
|
249
287
|
world.is_alive(e); // true
|
|
250
|
-
|
|
251
|
-
world.
|
|
252
|
-
world.flush(); // apply now
|
|
253
|
-
|
|
288
|
+
world.destroy_entity_deferred(e); // deferred
|
|
289
|
+
world.flush();
|
|
254
290
|
world.is_alive(e); // false
|
|
255
291
|
```
|
|
256
292
|
|
|
257
|
-
Entity IDs are generational: destroying an entity increments its slot's generation, so stale IDs are
|
|
258
|
-
|
|
259
|
-
## Lifecycle hooks
|
|
260
|
-
|
|
261
|
-
Systems can define lifecycle hooks:
|
|
262
|
-
|
|
263
|
-
```ts
|
|
264
|
-
const sys = world.register_system({
|
|
265
|
-
fn(ctx, dt) { /* runs every frame */ },
|
|
266
|
-
|
|
267
|
-
on_added(store) {
|
|
268
|
-
// Called once during world.startup()
|
|
269
|
-
},
|
|
270
|
-
|
|
271
|
-
on_removed() {
|
|
272
|
-
// Called when system is unregistered via world.remove_system()
|
|
273
|
-
},
|
|
274
|
-
|
|
275
|
-
dispose() {
|
|
276
|
-
// Called during world.dispose()
|
|
277
|
-
},
|
|
278
|
-
});
|
|
279
|
-
```
|
|
293
|
+
Entity IDs are generational: destroying an entity increments its slot's generation, so stale IDs are detected as dead.
|
|
280
294
|
|
|
281
295
|
## Dev / Prod modes
|
|
282
296
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
- Bounds checking on entity IDs
|
|
286
|
-
- Validation on branded type construction
|
|
287
|
-
- Duplicate system detection
|
|
288
|
-
- Circular dependency detection
|
|
289
|
-
- Helpful error messages with context
|
|
290
|
-
|
|
291
|
-
All dev-only checks are tree-shaken in production builds.
|
|
297
|
+
`__DEV__` compile-time flags enable bounds checking, dead entity detection, and duplicate system detection. Circular dependency detection is always active (not tree-shaken). All other dev checks are tree-shaken in production builds.
|
|
292
298
|
|
|
293
299
|
## Development
|
|
294
300
|
|
|
@@ -300,6 +306,8 @@ pnpm build # vite library build
|
|
|
300
306
|
pnpm tsc --noEmit # type check
|
|
301
307
|
```
|
|
302
308
|
|
|
303
|
-
##
|
|
309
|
+
## Guides
|
|
304
310
|
|
|
305
|
-
|
|
311
|
+
- [Getting Started](docs/GETTING_STARTED.md) — step-by-step tutorial
|
|
312
|
+
- [Best Practices](docs/BEST_PRACTICES.md) — performance tips and patterns
|
|
313
|
+
- [Architecture](docs/ARCHITECTURE.md) — internal design details
|
package/dist/archetype.d.ts
CHANGED
|
@@ -1,41 +1,52 @@
|
|
|
1
|
-
import { Brand, BitSet } from './type_primitives';
|
|
2
|
-
import { ComponentID,
|
|
1
|
+
import { Brand, GrowableTypedArray, AnyTypedArray, TypedArrayTag, BitSet } from './type_primitives';
|
|
2
|
+
import { ComponentID, ComponentDef, ComponentSchema, TagToTypedArray } from './component';
|
|
3
3
|
import { EntityID } from './entity';
|
|
4
4
|
export type ArchetypeID = Brand<number, "archetype_id">;
|
|
5
5
|
export declare const as_archetype_id: (value: number) => ArchetypeID;
|
|
6
6
|
export interface ArchetypeEdge {
|
|
7
7
|
add: ArchetypeID | null;
|
|
8
8
|
remove: ArchetypeID | null;
|
|
9
|
+
/** Pre-computed column mapping for add direction: this → add target. */
|
|
10
|
+
add_map: Int16Array | null;
|
|
11
|
+
/** Pre-computed column mapping for remove direction: this → remove target. */
|
|
12
|
+
remove_map: Int16Array | null;
|
|
9
13
|
}
|
|
10
14
|
export interface ArchetypeColumnLayout {
|
|
11
15
|
component_id: ComponentID;
|
|
12
16
|
field_names: string[];
|
|
13
17
|
field_index: Record<string, number>;
|
|
18
|
+
field_types: TypedArrayTag[];
|
|
14
19
|
}
|
|
15
20
|
interface ArchetypeColumnGroup {
|
|
16
21
|
layout: ArchetypeColumnLayout;
|
|
17
|
-
columns:
|
|
18
|
-
record: Record<string, number[]>;
|
|
22
|
+
columns: GrowableTypedArray<AnyTypedArray>[];
|
|
19
23
|
}
|
|
20
24
|
export declare class Archetype {
|
|
21
25
|
readonly id: ArchetypeID;
|
|
22
26
|
readonly mask: BitSet;
|
|
23
27
|
readonly has_columns: boolean;
|
|
24
|
-
|
|
28
|
+
private readonly _entity_ids;
|
|
25
29
|
length: number;
|
|
26
|
-
private edges;
|
|
30
|
+
private readonly edges;
|
|
31
|
+
readonly _flat_columns: GrowableTypedArray<AnyTypedArray>[];
|
|
32
|
+
readonly _col_offset: number[];
|
|
33
|
+
readonly _field_count: number[];
|
|
34
|
+
private readonly _field_index;
|
|
35
|
+
private readonly _field_names;
|
|
27
36
|
readonly column_groups: (ArchetypeColumnGroup | undefined)[];
|
|
28
|
-
|
|
29
|
-
constructor(id: ArchetypeID, mask: BitSet, layouts?: ArchetypeColumnLayout[]);
|
|
37
|
+
readonly _column_ids: number[];
|
|
38
|
+
constructor(id: ArchetypeID, mask: BitSet, layouts?: ArchetypeColumnLayout[], initial_capacity?: number);
|
|
30
39
|
get entity_count(): number;
|
|
31
|
-
|
|
40
|
+
/** Raw entity ID buffer. Valid data at indices 0..entity_count-1. */
|
|
41
|
+
get entity_ids(): Uint32Array;
|
|
42
|
+
get entity_list(): Uint32Array;
|
|
32
43
|
has_component(id: ComponentID): boolean;
|
|
33
44
|
matches(required: BitSet): boolean;
|
|
34
45
|
/** Get a single field's column. Valid data: indices 0..entity_count-1. */
|
|
35
|
-
get_column<
|
|
36
|
-
/** Get all columns for a component as { fieldName: number[] }. */
|
|
37
|
-
get_column_group<F extends ComponentFields>(def: ComponentDef<F>): ColumnsForSchema<F>;
|
|
46
|
+
get_column<S extends ComponentSchema, K extends string & keyof S>(def: ComponentDef<S>, field: K): TagToTypedArray[S[K]];
|
|
38
47
|
write_fields(row: number, component_id: ComponentID, values: Record<string, number>): void;
|
|
48
|
+
/** Fast positional write: values[i] → field[i] in declaration order. No string lookup. */
|
|
49
|
+
write_fields_positional(row: number, component_id: ComponentID, values: ArrayLike<number>): void;
|
|
39
50
|
read_field(row: number, component_id: ComponentID, field: string): number;
|
|
40
51
|
/** Copy all shared component columns from source archetype at src_row into dst_row. */
|
|
41
52
|
copy_shared_from(source: Archetype, src_row: number, dst_row: number): void;
|
|
@@ -47,15 +58,41 @@ export declare class Archetype {
|
|
|
47
58
|
/**
|
|
48
59
|
* Remove entity at row via swap-and-pop. Swaps the last entity into the
|
|
49
60
|
* vacated row to keep data dense. Returns the entity_index of the swapped
|
|
50
|
-
* entity (so Store can update its row), or
|
|
61
|
+
* entity (so Store can update its row), or NO_SWAP if no swap was needed.
|
|
51
62
|
*/
|
|
52
63
|
remove_entity(row: number): number;
|
|
53
64
|
/** Tag-optimized add: skip column push entirely (no data to store). */
|
|
54
65
|
add_entity_tag(entity_id: EntityID): number;
|
|
55
66
|
/** Tag-optimized remove via swap-and-pop: skip column swap/pop entirely. */
|
|
56
67
|
remove_entity_tag(row: number): number;
|
|
68
|
+
/**
|
|
69
|
+
* Move an entity from src archetype into this archetype in a single pass.
|
|
70
|
+
* Combines add_entity + copy_shared_from + remove_entity(src).
|
|
71
|
+
* Uses a pre-computed transition map for branchless column copy.
|
|
72
|
+
* Writes dst_row to _move_result[0], swapped entity index to _move_result[1].
|
|
73
|
+
*/
|
|
74
|
+
move_entity_from(src: Archetype, src_row: number, entity_id: EntityID, transition_map: Int16Array): void;
|
|
75
|
+
/**
|
|
76
|
+
* Move an entity from src into this archetype (tag-only: no columns to copy).
|
|
77
|
+
* Writes dst_row to _move_result[0], swapped entity index to _move_result[1].
|
|
78
|
+
*/
|
|
79
|
+
move_entity_from_tag(src: Archetype, src_row: number, entity_id: EntityID): void;
|
|
80
|
+
/**
|
|
81
|
+
* Bulk-move ALL entities from src into this archetype using TypedArray.set().
|
|
82
|
+
* Much faster than per-entity move_entity_from when the entire source is moving.
|
|
83
|
+
* After this call, src is empty. Returns the starting dst_row for the batch.
|
|
84
|
+
*/
|
|
85
|
+
bulk_move_all_from(src: Archetype, transition_map: Int16Array): number;
|
|
57
86
|
get_edge(component_id: ComponentID): ArchetypeEdge | undefined;
|
|
58
87
|
set_edge(component_id: ComponentID, edge: ArchetypeEdge): void;
|
|
59
88
|
}
|
|
89
|
+
/** Reusable result buffer for move_entity_from/move_entity_from_tag. [dst_row, swapped_index] */
|
|
90
|
+
export declare const _move_result: [number, number];
|
|
91
|
+
/**
|
|
92
|
+
* Build a transition map from src archetype to dst archetype.
|
|
93
|
+
* For each column in dst, stores the index of the corresponding column in src,
|
|
94
|
+
* or -1 if the column is new (no source).
|
|
95
|
+
*/
|
|
96
|
+
export declare function build_transition_map(src: Archetype, dst: Archetype): Int16Array;
|
|
60
97
|
export {};
|
|
61
98
|
//# sourceMappingURL=archetype.d.ts.map
|
package/dist/archetype.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archetype.d.ts","sourceRoot":"","sources":["../src/archetype.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;KAmBK;AAEL,OAAO,EACL,KAAK,
|
|
1
|
+
{"version":3,"file":"archetype.d.ts","sourceRoot":"","sources":["../src/archetype.ts"],"names":[],"mappings":"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"}
|
package/dist/component.d.ts
CHANGED
|
@@ -1,18 +1,35 @@
|
|
|
1
|
-
import { Brand } from './type_primitives';
|
|
1
|
+
import { Brand, TypedArrayTag } from './type_primitives';
|
|
2
2
|
export type ComponentID = Brand<number, "component_id">;
|
|
3
3
|
export declare const as_component_id: (value: number) => ComponentID;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/** Core schema type: maps field names to typed array tags. */
|
|
5
|
+
export type ComponentSchema = Readonly<Record<string, TypedArrayTag>>;
|
|
6
|
+
/** Compile-time tag → TypedArray mapping. */
|
|
7
|
+
export type TagToTypedArray = {
|
|
8
|
+
f32: Float32Array;
|
|
9
|
+
f64: Float64Array;
|
|
10
|
+
i8: Int8Array;
|
|
11
|
+
i16: Int16Array;
|
|
12
|
+
i32: Int32Array;
|
|
13
|
+
u8: Uint8Array;
|
|
14
|
+
u16: Uint16Array;
|
|
15
|
+
u32: Uint32Array;
|
|
16
|
+
};
|
|
17
|
+
/** Maps schema fields to their value object: { x: number, y: number }. */
|
|
18
|
+
export type FieldValues<S extends ComponentSchema> = {
|
|
19
|
+
readonly [K in keyof S]: number;
|
|
8
20
|
};
|
|
9
|
-
/** Maps
|
|
10
|
-
export type ColumnsForSchema<
|
|
11
|
-
readonly [K in
|
|
21
|
+
/** Maps schema fields to their specific typed array columns. */
|
|
22
|
+
export type ColumnsForSchema<S extends ComponentSchema> = {
|
|
23
|
+
readonly [K in keyof S]: TagToTypedArray[S[K]];
|
|
24
|
+
};
|
|
25
|
+
export type ComponentFields = readonly string[];
|
|
26
|
+
/** Maps component fields to column arrays (used by events — always Float64Array). */
|
|
27
|
+
export type ColumnsForFields<F extends ComponentFields> = {
|
|
28
|
+
readonly [K in F[number]]: Float64Array;
|
|
12
29
|
};
|
|
13
30
|
declare const __schema: unique symbol;
|
|
14
|
-
export type ComponentDef<
|
|
15
|
-
readonly [__schema]:
|
|
31
|
+
export type ComponentDef<S extends ComponentSchema = ComponentSchema> = ComponentID & {
|
|
32
|
+
readonly [__schema]: S;
|
|
16
33
|
};
|
|
17
34
|
export {};
|
|
18
35
|
//# sourceMappingURL=component.d.ts.map
|
package/dist/component.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;KAuBK;AAEL,OAAO,EACL,KAAK,EAGL,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AACxD,eAAO,MAAM,eAAe,UAAW,MAAM,gBAK1C,CAAC;AAEJ,8DAA8D;AAC9D,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;AAEtE,6CAA6C;AAC7C,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,YAAY,CAAC;IAClB,GAAG,EAAE,YAAY,CAAC;IAClB,EAAE,EAAE,SAAS,CAAC;IACd,GAAG,EAAE,UAAU,CAAC;IAChB,GAAG,EAAE,UAAU,CAAC;IAChB,EAAE,EAAE,UAAU,CAAC;IACf,GAAG,EAAE,WAAW,CAAC;IACjB,GAAG,EAAE,WAAW,CAAC;CAClB,CAAC;AAEF,0EAA0E;AAC1E,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,eAAe,IAAI;IACnD,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM;CAChC,CAAC;AAEF,gEAAgE;AAChE,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,eAAe,IAAI;IACxD,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC/C,CAAC;AAGF,MAAM,MAAM,eAAe,GAAG,SAAS,MAAM,EAAE,CAAC;AAEhD,qFAAqF;AACrF,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,eAAe,IAAI;IACxD,QAAQ,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,GAAG,YAAY;CACxC,CAAC;AAMF,OAAO,CAAC,MAAM,QAAQ,EAAE,OAAO,MAAM,CAAC;AAEtC,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,IAClE,WAAW,GAAG;IAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;CAAE,CAAC"}
|