@oasys/oecs 0.2.1 → 0.3.0
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/LICENSE +1 -1
- package/README.md +169 -182
- package/dist/archetype.d.ts +12 -9
- package/dist/archetype.d.ts.map +1 -1
- package/dist/component.d.ts +10 -0
- package/dist/component.d.ts.map +1 -1
- package/dist/ecs.d.ts +14 -14
- package/dist/ecs.d.ts.map +1 -1
- package/dist/entity.d.ts.map +1 -1
- package/dist/event.d.ts +6 -0
- package/dist/event.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +621 -459
- package/dist/query.d.ts +36 -16
- package/dist/query.d.ts.map +1 -1
- package/dist/ref.d.ts +4 -0
- package/dist/ref.d.ts.map +1 -1
- package/dist/resource.d.ts +21 -21
- package/dist/resource.d.ts.map +1 -1
- package/dist/schedule.d.ts +8 -6
- package/dist/schedule.d.ts.map +1 -1
- package/dist/store.d.ts +13 -6
- package/dist/store.d.ts.map +1 -1
- 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/binary_heap/binary_heap.d.ts +33 -0
- package/dist/type_primitives/binary_heap/binary_heap.d.ts.map +1 -0
- package/dist/type_primitives/bitset/bitset.d.ts +5 -0
- package/dist/type_primitives/bitset/bitset.d.ts.map +1 -1
- package/dist/type_primitives/error.d.ts +2 -1
- package/dist/type_primitives/error.d.ts.map +1 -1
- package/dist/type_primitives/index.d.ts +2 -0
- package/dist/type_primitives/index.d.ts.map +1 -1
- package/dist/type_primitives/topological_sort/topological_sort.d.ts +25 -0
- package/dist/type_primitives/topological_sort/topological_sort.d.ts.map +1 -0
- package/dist/type_primitives/typed_arrays/typed_arrays.d.ts +2 -0
- package/dist/type_primitives/typed_arrays/typed_arrays.d.ts.map +1 -1
- package/dist/utils/arrays.d.ts.map +1 -1
- package/dist/utils/constants.d.ts +0 -8
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/error.d.ts +4 -1
- package/dist/utils/error.d.ts.map +1 -1
- package/package.json +1 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,313 +1,300 @@
|
|
|
1
|
-
#
|
|
1
|
+
# oecs
|
|
2
2
|
|
|
3
|
-
A fast, minimal archetype-based Entity Component System
|
|
3
|
+
A fast, minimal, archetype-based Entity Component System for TypeScript.
|
|
4
4
|
|
|
5
|
-
|
|
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.
|
|
11
|
-
- **Deferred structural changes** — add/remove component and destroy entity are buffered during system execution and flushed between phases.
|
|
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.
|
|
5
|
+
## Features
|
|
14
6
|
|
|
15
|
-
|
|
7
|
+
- **Archetype-based SoA storage.** Entities sharing a component set share contiguous typed-array columns — cache-friendly loops, no per-entity object allocation.
|
|
8
|
+
- **Phantom-typed components.** `ComponentDef<{ x: "f64", y: "f64" }>` is a branded integer at runtime and a fully-typed schema at compile time. Misspelled fields are compile errors.
|
|
9
|
+
- **Callback iteration.** `query.for_each(arch => ...)` yields non-empty archetypes; you write the row loop over typed-array columns.
|
|
10
|
+
- **Tick-based change detection.** Each `(archetype, component)` tracks a change tick. `query.changed(Pos).for_each(...)` visits only archetypes mutated since the system's last run.
|
|
11
|
+
- **Key-based events and resources.** `event_key<F>` / `resource_key<T>` create module-scope symbol handles carrying their schema as a phantom — import the key anywhere.
|
|
12
|
+
- **Cached single-entity refs.** `ctx.ref` / `ctx.ref_mut` give ergonomic `pos.x += vel.vx * dt` that compiles to a direct typed-array index op.
|
|
13
|
+
- **Deferred structural changes.** `ctx.add_component` / `ctx.remove_component` / `ctx.destroy_entity` buffer until the schedule flushes between phases, so iterators stay valid.
|
|
14
|
+
- **Topological scheduler.** Per-phase Kahn's-algorithm sort over a binary heap, with insertion order as a deterministic tiebreaker.
|
|
15
|
+
- **Fixed timestep.** Accumulator loop with configurable `fixed_timestep` and spiral-of-death protection.
|
|
16
|
+
- **Reusable primitives.** `BitSet`, `SparseSet`, `SparseMap`, `GrowableTypedArray`, `BinaryHeap`, `topological_sort` all exported.
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
## Installation
|
|
18
19
|
|
|
19
|
-
```
|
|
20
|
-
|
|
20
|
+
```bash
|
|
21
|
+
pnpm add @oasys/oecs
|
|
21
22
|
```
|
|
22
23
|
|
|
23
24
|
## Quick start
|
|
24
25
|
|
|
25
26
|
```ts
|
|
26
|
-
import { ECS, SCHEDULE } from "@oasys/oecs";
|
|
27
|
+
import { ECS, SCHEDULE, event_key, resource_key } from "@oasys/oecs";
|
|
28
|
+
|
|
29
|
+
// Keys — module scope, phantom-typed
|
|
30
|
+
const Time = resource_key<{ delta: number; elapsed: number }>("Time");
|
|
31
|
+
const DamageEvent = event_key<readonly ["target", "amount"]>("Damage");
|
|
27
32
|
|
|
28
33
|
const world = new ECS();
|
|
29
34
|
|
|
30
|
-
//
|
|
35
|
+
// Components
|
|
31
36
|
const Pos = world.register_component({ x: "f64", y: "f64" });
|
|
32
|
-
|
|
33
|
-
// Array shorthand — uniform type, defaults to "f64"
|
|
34
37
|
const Vel = world.register_component(["vx", "vy"] as const);
|
|
35
38
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// Resources are global singletons
|
|
40
|
-
const Time = world.register_resource(["delta", "elapsed"] as const, {
|
|
41
|
-
delta: 0,
|
|
42
|
-
elapsed: 0,
|
|
43
|
-
});
|
|
39
|
+
// Resources & events
|
|
40
|
+
world.register_resource(Time, { delta: 0, elapsed: 0 });
|
|
41
|
+
world.register_event(DamageEvent, ["target", "amount"] as const);
|
|
44
42
|
|
|
45
|
-
//
|
|
46
|
-
const Damage = world.register_event(["target", "amount"] as const);
|
|
47
|
-
|
|
48
|
-
// Create entities and attach components
|
|
43
|
+
// Entities
|
|
49
44
|
const e = world.create_entity();
|
|
50
|
-
world.
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
world.add_components(e, [
|
|
46
|
+
{ def: Pos, values: { x: 0, y: 0 } },
|
|
47
|
+
{ def: Vel, values: { vx: 100, vy: 50 } },
|
|
48
|
+
]);
|
|
53
49
|
|
|
54
|
-
//
|
|
50
|
+
// System — query resolved once at registration
|
|
55
51
|
const moveSys = world.register_system(
|
|
56
|
-
(q,
|
|
57
|
-
|
|
58
|
-
const px = arch.
|
|
59
|
-
const py = arch.
|
|
52
|
+
(q, ctx, dt) => {
|
|
53
|
+
q.for_each((arch) => {
|
|
54
|
+
const px = arch.get_column_mut(Pos, "x", ctx.world_tick);
|
|
55
|
+
const py = arch.get_column_mut(Pos, "y", ctx.world_tick);
|
|
60
56
|
const vx = arch.get_column(Vel, "vx");
|
|
61
57
|
const vy = arch.get_column(Vel, "vy");
|
|
62
|
-
|
|
63
|
-
for (let i = 0; i < n; i++) {
|
|
58
|
+
for (let i = 0; i < arch.entity_count; i++) {
|
|
64
59
|
px[i] += vx[i] * dt;
|
|
65
60
|
py[i] += vy[i] * dt;
|
|
66
61
|
}
|
|
67
|
-
}
|
|
62
|
+
});
|
|
68
63
|
},
|
|
69
64
|
(qb) => qb.every(Pos, Vel),
|
|
70
65
|
);
|
|
71
66
|
|
|
72
|
-
// Schedule the system
|
|
73
67
|
world.add_systems(SCHEDULE.UPDATE, moveSys);
|
|
74
|
-
|
|
75
|
-
// Initialize
|
|
76
68
|
world.startup();
|
|
77
69
|
|
|
78
|
-
|
|
79
|
-
function frame(
|
|
80
|
-
|
|
70
|
+
let last = performance.now();
|
|
71
|
+
function frame() {
|
|
72
|
+
const now = performance.now();
|
|
73
|
+
const dt = (now - last) / 1000;
|
|
74
|
+
last = now;
|
|
75
|
+
const t = world.resource(Time);
|
|
76
|
+
world.set_resource(Time, { delta: dt, elapsed: t.elapsed + dt });
|
|
81
77
|
world.update(dt);
|
|
78
|
+
requestAnimationFrame(frame);
|
|
82
79
|
}
|
|
80
|
+
requestAnimationFrame(frame);
|
|
83
81
|
```
|
|
84
82
|
|
|
85
|
-
## World
|
|
86
|
-
|
|
87
|
-
`ECS` accepts an optional configuration object:
|
|
83
|
+
## World options
|
|
88
84
|
|
|
89
85
|
```ts
|
|
90
86
|
const world = new ECS({
|
|
91
|
-
initial_capacity: 4096,
|
|
92
|
-
fixed_timestep: 1 / 50,
|
|
93
|
-
max_fixed_steps: 4,
|
|
87
|
+
initial_capacity: 4096,
|
|
88
|
+
fixed_timestep: 1 / 50,
|
|
89
|
+
max_fixed_steps: 4,
|
|
94
90
|
});
|
|
95
91
|
```
|
|
96
92
|
|
|
97
|
-
| Option
|
|
98
|
-
|
|
99
|
-
| `initial_capacity` | `number` | `1024`
|
|
100
|
-
| `fixed_timestep`
|
|
101
|
-
| `max_fixed_steps`
|
|
93
|
+
| Option | Type | Default | Description |
|
|
94
|
+
| ------------------ | -------- | ------- | ----------- |
|
|
95
|
+
| `initial_capacity` | `number` | `1024` | Starting size of each archetype's entity-ID and column buffers. Buffers double on overflow; pick close to your expected per-archetype entity count to avoid early reallocations. |
|
|
96
|
+
| `fixed_timestep` | `number` | `1/60` | Interval (seconds) at which `SCHEDULE.FIXED_UPDATE` systems run. |
|
|
97
|
+
| `max_fixed_steps` | `number` | `5` | Hard cap on fixed-update iterations per frame. Protects against spiral of death. |
|
|
102
98
|
|
|
103
99
|
## Components
|
|
104
100
|
|
|
105
|
-
|
|
101
|
+
Records give per-field type control; array shorthand defaults to `f64`. Tags have no fields.
|
|
106
102
|
|
|
107
103
|
```ts
|
|
108
|
-
|
|
109
|
-
const Position = world.register_component({ x: "f64", y: "f64" });
|
|
104
|
+
const Pos = world.register_component({ x: "f64", y: "f64" });
|
|
110
105
|
const Health = world.register_component({ current: "i32", max: "i32" });
|
|
111
|
-
|
|
112
|
-
// Array shorthand — all fields default to "f64"
|
|
113
106
|
const Vel = world.register_component(["vx", "vy"] as const);
|
|
114
|
-
|
|
115
|
-
// Tags — no fields
|
|
116
107
|
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):
|
|
122
108
|
|
|
123
|
-
```ts
|
|
124
|
-
world.add_component(e, Position, { x: 10, y: 20 });
|
|
125
109
|
world.add_components(e, [
|
|
126
|
-
{ def:
|
|
127
|
-
{ def:
|
|
110
|
+
{ def: Pos, values: { x: 0, y: 0 } },
|
|
111
|
+
{ def: Vel, values: { vx: 1, vy: 0 } },
|
|
112
|
+
{ def: IsEnemy },
|
|
128
113
|
]);
|
|
129
114
|
```
|
|
130
115
|
|
|
131
|
-
|
|
116
|
+
Supported tags: `f32`, `f64`, `i8`, `i16`, `i32`, `u8`, `u16`, `u32`.
|
|
117
|
+
|
|
118
|
+
See [docs/api/components.md](docs/api/components.md).
|
|
132
119
|
|
|
133
120
|
## Queries
|
|
134
121
|
|
|
135
|
-
|
|
122
|
+
Live, cached views over matching archetypes. Iterate with `for_each`.
|
|
136
123
|
|
|
137
124
|
```ts
|
|
138
|
-
const q = world.query(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
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];
|
|
150
|
-
}
|
|
151
|
-
}
|
|
125
|
+
const q = world.query(Pos, Vel);
|
|
126
|
+
|
|
127
|
+
q.for_each((arch) => {
|
|
128
|
+
const px = arch.get_column(Pos, "x");
|
|
129
|
+
const py = arch.get_column(Pos, "y");
|
|
130
|
+
for (let i = 0; i < arch.entity_count; i++) { /* ... */ }
|
|
131
|
+
});
|
|
152
132
|
|
|
153
|
-
// Chaining
|
|
154
|
-
const targets = world
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
.any_of(IsEnemy, IsBoss);
|
|
133
|
+
// Chaining returns new cached queries
|
|
134
|
+
const targets = world.query(Pos).and(Health).not(Shield).any_of(IsEnemy, IsBoss);
|
|
135
|
+
|
|
136
|
+
// Change detection — only archetypes whose Pos column changed since last run
|
|
137
|
+
q.changed(Pos).for_each((arch) => { /* ... */ });
|
|
159
138
|
```
|
|
160
139
|
|
|
161
|
-
See [docs/api/queries.md](docs/api/queries.md)
|
|
140
|
+
See [docs/api/queries.md](docs/api/queries.md) and [docs/api/change-detection.md](docs/api/change-detection.md).
|
|
162
141
|
|
|
163
142
|
## Systems
|
|
164
143
|
|
|
165
|
-
Systems are plain functions
|
|
144
|
+
Systems are plain functions. Three registration shapes all return a `SystemDescriptor`.
|
|
166
145
|
|
|
167
146
|
```ts
|
|
168
|
-
//
|
|
147
|
+
// Bare function
|
|
148
|
+
const logSys = world.register_system((ctx, dt) => { /* ... */ });
|
|
149
|
+
|
|
150
|
+
// Function + query builder (query resolved once at registration)
|
|
169
151
|
const moveSys = world.register_system(
|
|
170
|
-
(q, ctx, dt) => {
|
|
171
|
-
for (const arch of q) {
|
|
172
|
-
/* ... */
|
|
173
|
-
}
|
|
174
|
-
},
|
|
152
|
+
(q, ctx, dt) => { q.for_each((arch) => { /* ... */ }); },
|
|
175
153
|
(qb) => qb.every(Pos, Vel),
|
|
176
154
|
);
|
|
177
155
|
|
|
178
|
-
//
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
},
|
|
156
|
+
// Full config — lifecycle hooks, name
|
|
157
|
+
const spawnSys = world.register_system({
|
|
158
|
+
name: "spawn",
|
|
159
|
+
fn(ctx, dt) { /* every frame */ },
|
|
160
|
+
on_added(ctx) { /* once during world.startup() */ },
|
|
161
|
+
dispose() { /* during world.dispose() */ },
|
|
183
162
|
});
|
|
184
163
|
```
|
|
185
164
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
```ts
|
|
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
|
-
```
|
|
165
|
+
`SystemContext` exposes deferred structural ops, per-entity access, events, resources, and tick bookkeeping (`ctx.world_tick`, `ctx.last_run_tick`).
|
|
194
166
|
|
|
195
|
-
See [docs/api/systems.md](docs/api/systems.md)
|
|
167
|
+
See [docs/api/systems.md](docs/api/systems.md).
|
|
196
168
|
|
|
197
169
|
## Resources
|
|
198
170
|
|
|
199
|
-
|
|
171
|
+
Global singletons keyed by `ResourceKey<T>`. Values can be any type — objects, typed arrays, class instances.
|
|
200
172
|
|
|
201
173
|
```ts
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
174
|
+
import { resource_key } from "@oasys/oecs";
|
|
175
|
+
|
|
176
|
+
const Time = resource_key<{ delta: number; elapsed: number }>("Time");
|
|
177
|
+
const Assets = resource_key<Map<string, ImageBitmap>>("Assets");
|
|
206
178
|
|
|
207
|
-
|
|
208
|
-
world.
|
|
179
|
+
world.register_resource(Time, { delta: 0, elapsed: 0 });
|
|
180
|
+
world.register_resource(Assets, new Map());
|
|
209
181
|
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
time.delta; // number
|
|
213
|
-
time.elapsed; // number
|
|
182
|
+
const t = world.resource(Time); // typed as { delta, elapsed }
|
|
183
|
+
world.set_resource(Time, { delta: 0.016, elapsed: 0 }); // swap in a new value
|
|
214
184
|
```
|
|
215
185
|
|
|
216
|
-
See [docs/api/resources.md](docs/api/resources.md)
|
|
186
|
+
See [docs/api/resources.md](docs/api/resources.md).
|
|
217
187
|
|
|
218
|
-
## Events
|
|
188
|
+
## Events
|
|
219
189
|
|
|
220
|
-
|
|
190
|
+
Fire-and-forget SoA channels. Data events carry typed fields; signals carry only a count. Cleared at the end of each `world.update(dt)`.
|
|
221
191
|
|
|
222
192
|
```ts
|
|
223
|
-
|
|
224
|
-
const Damage = world.register_event(["target", "amount"] as const);
|
|
225
|
-
ctx.emit(Damage, { target: entityId, amount: 50 });
|
|
193
|
+
import { event_key, signal_key } from "@oasys/oecs";
|
|
226
194
|
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
195
|
+
const DamageEvent = event_key<readonly ["target", "amount"]>("Damage");
|
|
196
|
+
const GameOver = signal_key("GameOver");
|
|
197
|
+
|
|
198
|
+
world.register_event(DamageEvent, ["target", "amount"] as const);
|
|
199
|
+
world.register_signal(GameOver);
|
|
200
|
+
|
|
201
|
+
ctx.emit(DamageEvent, { target: victimId, amount: 25 });
|
|
202
|
+
ctx.emit(GameOver);
|
|
232
203
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (ctx.read(OnReset).length > 0) {
|
|
237
|
-
/* fired */
|
|
204
|
+
const dmg = ctx.read(DamageEvent);
|
|
205
|
+
for (let i = 0; i < dmg.length; i++) {
|
|
206
|
+
dmg.target[i]; dmg.amount[i]; // number columns
|
|
238
207
|
}
|
|
208
|
+
if (ctx.read(GameOver).length > 0) { /* fired */ }
|
|
239
209
|
```
|
|
240
210
|
|
|
241
|
-
See [docs/api/events.md](docs/api/events.md)
|
|
211
|
+
See [docs/api/events.md](docs/api/events.md).
|
|
242
212
|
|
|
243
213
|
## Refs
|
|
244
214
|
|
|
245
|
-
|
|
215
|
+
Cached single-entity handles — resolve archetype + row + column once, then read/write fields by name.
|
|
246
216
|
|
|
247
217
|
```ts
|
|
248
|
-
const pos = ctx.
|
|
249
|
-
const vel = ctx.ref(Vel, entity);
|
|
218
|
+
const pos = ctx.ref_mut(Pos, entity); // writable; bumps Pos change tick
|
|
219
|
+
const vel = ctx.ref(Vel, entity); // readonly
|
|
250
220
|
pos.x += vel.vx * dt;
|
|
251
221
|
pos.y += vel.vy * dt;
|
|
252
222
|
```
|
|
253
223
|
|
|
254
|
-
|
|
224
|
+
Prefer `ctx.ref` by default; reach for `ctx.ref_mut` at the point of mutation. Do not hold refs across archetype transitions or phase flushes.
|
|
225
|
+
|
|
226
|
+
See [docs/api/refs.md](docs/api/refs.md).
|
|
255
227
|
|
|
256
228
|
## Schedule
|
|
257
229
|
|
|
258
|
-
Seven
|
|
230
|
+
Seven phases run in a fixed order:
|
|
259
231
|
|
|
260
|
-
| Phase | When
|
|
261
|
-
| -------------- |
|
|
262
|
-
| `PRE_STARTUP` | Once, before
|
|
263
|
-
| `STARTUP` | Once
|
|
264
|
-
| `POST_STARTUP` | Once, after
|
|
265
|
-
| `FIXED_UPDATE` |
|
|
266
|
-
| `PRE_UPDATE` | Every frame, first
|
|
267
|
-
| `UPDATE` | Every frame
|
|
268
|
-
| `POST_UPDATE` | Every frame, last
|
|
232
|
+
| Phase | When | Typical use |
|
|
233
|
+
| -------------- | -------------------------------- | ----------------------- |
|
|
234
|
+
| `PRE_STARTUP` | Once, before `STARTUP` | Resource loading |
|
|
235
|
+
| `STARTUP` | Once | Initial entity spawning |
|
|
236
|
+
| `POST_STARTUP` | Once, after `STARTUP` | Validation |
|
|
237
|
+
| `FIXED_UPDATE` | Zero+ times per frame (fixed dt) | Physics, simulation |
|
|
238
|
+
| `PRE_UPDATE` | Every frame, first | Input, time |
|
|
239
|
+
| `UPDATE` | Every frame | Game logic, AI |
|
|
240
|
+
| `POST_UPDATE` | Every frame, last | Rendering, cleanup |
|
|
269
241
|
|
|
270
242
|
```ts
|
|
271
|
-
world.add_systems(SCHEDULE.UPDATE, moveSys,
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// Ordering constraints
|
|
275
|
-
world.add_systems(SCHEDULE.UPDATE, moveSys, {
|
|
276
|
-
system: physicsSys,
|
|
277
|
-
ordering: { after: [moveSys] },
|
|
243
|
+
world.add_systems(SCHEDULE.UPDATE, moveSys, damageSys, {
|
|
244
|
+
system: deathSys,
|
|
245
|
+
ordering: { after: [damageSys] },
|
|
278
246
|
});
|
|
279
247
|
```
|
|
280
248
|
|
|
281
|
-
|
|
249
|
+
Within a phase, systems are topologically sorted by `before` / `after` constraints. `ctx.flush()` runs automatically between phases.
|
|
250
|
+
|
|
251
|
+
See [docs/api/schedule.md](docs/api/schedule.md).
|
|
282
252
|
|
|
283
253
|
## Entity lifecycle
|
|
284
254
|
|
|
285
255
|
```ts
|
|
286
256
|
const e = world.create_entity();
|
|
287
|
-
world.is_alive(e);
|
|
288
|
-
world.destroy_entity_deferred(e);
|
|
257
|
+
world.is_alive(e); // true
|
|
258
|
+
world.destroy_entity_deferred(e);
|
|
289
259
|
world.flush();
|
|
290
|
-
world.is_alive(e);
|
|
260
|
+
world.is_alive(e); // false
|
|
291
261
|
```
|
|
292
262
|
|
|
293
|
-
|
|
263
|
+
`EntityID` is a packed 31-bit integer (20-bit slot index, 11-bit generation). Destroying an entity bumps its slot's generation, so stale handles are detected as dead. Inside systems, use `ctx.create_entity()` (immediate) and `ctx.destroy_entity(e)` (deferred).
|
|
264
|
+
|
|
265
|
+
See [docs/api/entities.md](docs/api/entities.md).
|
|
294
266
|
|
|
295
|
-
## Dev
|
|
267
|
+
## Dev vs Prod modes
|
|
296
268
|
|
|
297
|
-
|
|
269
|
+
A compile-time `__DEV__` flag gates runtime sanity checks: bounds checks, liveness checks, duplicate-system detection, and registration validation. These are tree-shaken out of production bundles by the Vite build. Scheduler cycle detection is always active and throws `ECS_ERROR.CIRCULAR_SYSTEM_DEPENDENCY` on the first offending run.
|
|
298
270
|
|
|
299
271
|
## Development
|
|
300
272
|
|
|
301
273
|
```bash
|
|
302
274
|
pnpm install
|
|
303
|
-
pnpm test
|
|
304
|
-
pnpm bench
|
|
305
|
-
pnpm build
|
|
306
|
-
pnpm tsc --noEmit
|
|
275
|
+
pnpm test # vitest
|
|
276
|
+
pnpm bench # vitest bench
|
|
277
|
+
pnpm build # vite library build
|
|
278
|
+
pnpm tsc --noEmit # type check
|
|
307
279
|
```
|
|
308
280
|
|
|
309
281
|
## Guides
|
|
310
282
|
|
|
311
|
-
- [Getting Started](docs/GETTING_STARTED.md) — step-by-step tutorial
|
|
312
|
-
- [Best Practices](docs/BEST_PRACTICES.md) —
|
|
313
|
-
- [Architecture](docs/ARCHITECTURE.md) —
|
|
283
|
+
- [Getting Started](docs/GETTING_STARTED.md) — step-by-step tutorial.
|
|
284
|
+
- [Best Practices](docs/BEST_PRACTICES.md) — component design, query patterns, pitfalls.
|
|
285
|
+
- [Architecture](docs/ARCHITECTURE.md) — data layout, flush model, cache invalidation.
|
|
286
|
+
- API reference:
|
|
287
|
+
[components](docs/api/components.md) ·
|
|
288
|
+
[entities](docs/api/entities.md) ·
|
|
289
|
+
[queries](docs/api/queries.md) ·
|
|
290
|
+
[systems](docs/api/systems.md) ·
|
|
291
|
+
[schedule](docs/api/schedule.md) ·
|
|
292
|
+
[resources](docs/api/resources.md) ·
|
|
293
|
+
[events](docs/api/events.md) ·
|
|
294
|
+
[refs](docs/api/refs.md) ·
|
|
295
|
+
[change detection](docs/api/change-detection.md) ·
|
|
296
|
+
[type primitives](docs/api/type-primitives.md)
|
|
297
|
+
|
|
298
|
+
## License
|
|
299
|
+
|
|
300
|
+
MIT
|
package/dist/archetype.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Brand, GrowableTypedArray, AnyTypedArray, TypedArrayTag, BitSet } from './type_primitives';
|
|
2
|
-
import { ComponentID, ComponentDef, ComponentSchema, TagToTypedArray } from './component';
|
|
2
|
+
import { ComponentID, ComponentDef, ComponentSchema, TagToTypedArray, ReadonlyColumn, ReadonlyUint32Array } 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;
|
|
@@ -35,21 +35,24 @@ export declare class Archetype {
|
|
|
35
35
|
private readonly _field_names;
|
|
36
36
|
readonly column_groups: (ArchetypeColumnGroup | undefined)[];
|
|
37
37
|
readonly _column_ids: number[];
|
|
38
|
+
readonly _changed_tick: number[];
|
|
38
39
|
constructor(id: ArchetypeID, mask: BitSet, layouts?: ArchetypeColumnLayout[], initial_capacity?: number);
|
|
39
40
|
get entity_count(): number;
|
|
40
41
|
/** Raw entity ID buffer. Valid data at indices 0..entity_count-1. */
|
|
41
|
-
get entity_ids():
|
|
42
|
+
get entity_ids(): ReadonlyUint32Array;
|
|
42
43
|
get entity_list(): Uint32Array;
|
|
43
44
|
has_component(id: ComponentID): boolean;
|
|
44
45
|
matches(required: BitSet): boolean;
|
|
45
|
-
/** Get a single field's column. Valid data: indices 0..entity_count-1. */
|
|
46
|
-
get_column<S extends ComponentSchema, K extends string & keyof S>(def: ComponentDef<S>, field: K):
|
|
47
|
-
|
|
46
|
+
/** Get a single field's column (read-only). Valid data: indices 0..entity_count-1. */
|
|
47
|
+
get_column<S extends ComponentSchema, K extends string & keyof S>(def: ComponentDef<S>, field: K): ReadonlyColumn;
|
|
48
|
+
/** Get a single field's column (mutable). Marks the component as changed at the given tick. */
|
|
49
|
+
get_column_mut<S extends ComponentSchema, K extends string & keyof S>(def: ComponentDef<S>, field: K, tick: number): TagToTypedArray[S[K]];
|
|
50
|
+
write_fields(row: number, component_id: ComponentID, values: Record<string, number>, tick: number): void;
|
|
48
51
|
/** 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
|
|
52
|
+
write_fields_positional(row: number, component_id: ComponentID, values: ArrayLike<number>, tick: number): void;
|
|
50
53
|
read_field(row: number, component_id: ComponentID, field: string): number;
|
|
51
54
|
/** Copy all shared component columns from source archetype at src_row into dst_row. */
|
|
52
|
-
copy_shared_from(source: Archetype, src_row: number, dst_row: number): void;
|
|
55
|
+
copy_shared_from(source: Archetype, src_row: number, dst_row: number, tick: number): void;
|
|
53
56
|
/**
|
|
54
57
|
* Add an entity. Pushes zeroes into all columns and returns the assigned row.
|
|
55
58
|
* Store is responsible for tracking entity_index → row.
|
|
@@ -71,7 +74,7 @@ export declare class Archetype {
|
|
|
71
74
|
* Uses a pre-computed transition map for branchless column copy.
|
|
72
75
|
* Writes dst_row to _move_result[0], swapped entity index to _move_result[1].
|
|
73
76
|
*/
|
|
74
|
-
move_entity_from(src: Archetype, src_row: number, entity_id: EntityID, transition_map: Int16Array): void;
|
|
77
|
+
move_entity_from(src: Archetype, src_row: number, entity_id: EntityID, transition_map: Int16Array, tick: number): void;
|
|
75
78
|
/**
|
|
76
79
|
* Move an entity from src into this archetype (tag-only: no columns to copy).
|
|
77
80
|
* Writes dst_row to _move_result[0], swapped entity index to _move_result[1].
|
|
@@ -82,7 +85,7 @@ export declare class Archetype {
|
|
|
82
85
|
* Much faster than per-entity move_entity_from when the entire source is moving.
|
|
83
86
|
* After this call, src is empty. Returns the starting dst_row for the batch.
|
|
84
87
|
*/
|
|
85
|
-
bulk_move_all_from(src: Archetype, transition_map: Int16Array): number;
|
|
88
|
+
bulk_move_all_from(src: Archetype, transition_map: Int16Array, tick: number): number;
|
|
86
89
|
get_edge(component_id: ComponentID): ArchetypeEdge | undefined;
|
|
87
90
|
set_edge(component_id: ComponentID, edge: ArchetypeEdge): void;
|
|
88
91
|
}
|
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,EAGL,kBAAkB,EAGlB,KAAK,aAAa,EAClB,KAAK,aAAa,EACnB,MAAM,
|
|
1
|
+
{"version":3,"file":"archetype.d.ts","sourceRoot":"","sources":["../src/archetype.ts"],"names":[],"mappings":"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"}
|
package/dist/component.d.ts
CHANGED
|
@@ -31,5 +31,15 @@ declare const __schema: unique symbol;
|
|
|
31
31
|
export type ComponentDef<S extends ComponentSchema = ComponentSchema> = ComponentID & {
|
|
32
32
|
readonly [__schema]: S;
|
|
33
33
|
};
|
|
34
|
+
/** Compile-time readonly view of a typed array column. Prevents index writes. */
|
|
35
|
+
export interface ReadonlyColumn {
|
|
36
|
+
readonly [index: number]: number;
|
|
37
|
+
readonly length: number;
|
|
38
|
+
}
|
|
39
|
+
/** Compile-time readonly view of a Uint32Array. Prevents index writes. */
|
|
40
|
+
export interface ReadonlyUint32Array {
|
|
41
|
+
readonly [index: number]: number;
|
|
42
|
+
readonly length: number;
|
|
43
|
+
}
|
|
34
44
|
export {};
|
|
35
45
|
//# 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;;;;;;;;;;;;;;;;;;;;;;;KAuBK;AAEL,OAAO,EACL,KAAK,EAGL,KAAK,aAAa,EACnB,MAAM,
|
|
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,mBAAmB,CAAC;AAE3B,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,IAAI,WAAW,GAAG;IACpF,QAAQ,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;CACxB,CAAC;AAEF,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,0EAA0E;AAC1E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB"}
|