archetype-ecs-lib 0.4.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/LICENSE +21 -0
- package/README.md +239 -0
- package/lib/ecs/Archetype.d.ts +19 -0
- package/lib/ecs/Archetype.js +119 -0
- package/lib/ecs/Commands.d.ts +25 -0
- package/lib/ecs/Commands.js +27 -0
- package/lib/ecs/EntityManager.d.ts +9 -0
- package/lib/ecs/EntityManager.js +38 -0
- package/lib/ecs/Schedule.d.ts +10 -0
- package/lib/ecs/Schedule.js +65 -0
- package/lib/ecs/Signature.d.ts +8 -0
- package/lib/ecs/Signature.js +50 -0
- package/lib/ecs/TypeRegistry.d.ts +5 -0
- package/lib/ecs/TypeRegistry.js +16 -0
- package/lib/ecs/Types.d.ts +24 -0
- package/lib/ecs/Types.js +2 -0
- package/lib/ecs/World.d.ts +43 -0
- package/lib/ecs/World.js +365 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +21 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jean-Laurent Duzant
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Archetype ECS Lib
|
|
2
|
+
|
|
3
|
+
A tiny **archetype-based ECS** (Entity Component System) for TypeScript.
|
|
4
|
+
|
|
5
|
+
- **Archetypes (tables)** store entities in a **SoA** layout (one column per component type).
|
|
6
|
+
- **Queries** iterate matching archetypes efficiently.
|
|
7
|
+
- **Commands** let you **defer structural changes** (spawn/despawn/add/remove) safely.
|
|
8
|
+
- A minimal **Schedule** runs systems by phases and flushes commands between phases.
|
|
9
|
+
|
|
10
|
+
Exports are defined in `index.ts`:
|
|
11
|
+
- `Types`, `TypeRegistry`, `Commands`, `World`, `Schedule`
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm i archetype-ecs-lib
|
|
19
|
+
# or
|
|
20
|
+
pnpm add archetype-ecs-lib
|
|
21
|
+
# or
|
|
22
|
+
yarn add archetype-ecs-lib
|
|
23
|
+
````
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Core concepts
|
|
28
|
+
|
|
29
|
+
### Entity
|
|
30
|
+
|
|
31
|
+
An entity is a lightweight handle:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
type Entity = { id: number; gen: number };
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The `gen` (generation) prevents using stale entity handles after despawn/reuse.
|
|
38
|
+
|
|
39
|
+
### Component
|
|
40
|
+
|
|
41
|
+
A component is any class used as a type key:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
class Position { constructor(public x = 0, public y = 0) {} }
|
|
45
|
+
class Velocity { constructor(public x = 0, public y = 0) {} }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Internally, constructors are mapped to a stable numeric `TypeId` via `typeId()`.
|
|
49
|
+
|
|
50
|
+
### World
|
|
51
|
+
|
|
52
|
+
`World` owns entities, archetypes, commands, and systems.
|
|
53
|
+
|
|
54
|
+
Structural operations:
|
|
55
|
+
|
|
56
|
+
* `spawn()`, `despawn(e)`
|
|
57
|
+
* `add(e, Ctor, value)`, `remove(e, Ctor)`
|
|
58
|
+
* `has(e, Ctor)`, `get(e, Ctor)`, `set(e, Ctor, value)`
|
|
59
|
+
* `query(...ctors)` to iterate entities with required components
|
|
60
|
+
* `cmd()` to enqueue deferred commands
|
|
61
|
+
* `flush()` applies queued commands
|
|
62
|
+
* `update(dt)` runs registered systems and flushes at the end
|
|
63
|
+
|
|
64
|
+
### Deferred structural changes (important)
|
|
65
|
+
|
|
66
|
+
While iterating a query (or while systems are running), doing structural changes directly can throw:
|
|
67
|
+
|
|
68
|
+
> “Cannot do structural change (…) while iterating. Use world.cmd() and flush …”
|
|
69
|
+
|
|
70
|
+
Use `world.cmd()` inside systems / loops, and let `world.flush()` apply changes safely.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Quick start
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { World, Schedule } from "archetype-ecs-lib";
|
|
78
|
+
|
|
79
|
+
class Position { constructor(public x = 0, public y = 0) {} }
|
|
80
|
+
class Velocity { constructor(public x = 0, public y = 0) {} }
|
|
81
|
+
|
|
82
|
+
const world = new World();
|
|
83
|
+
|
|
84
|
+
// Spawn immediately
|
|
85
|
+
const e = world.spawn();
|
|
86
|
+
world.add(e, Position, new Position(0, 0));
|
|
87
|
+
world.add(e, Velocity, new Velocity(1, 0));
|
|
88
|
+
|
|
89
|
+
// A simple system
|
|
90
|
+
world.addSystem((w: any, dt: number) => {
|
|
91
|
+
for (const { e, c1: pos, c2: vel } of w.query(Position, Velocity)) {
|
|
92
|
+
pos.x += vel.x * dt;
|
|
93
|
+
pos.y += vel.y * dt;
|
|
94
|
+
|
|
95
|
+
// Defer structural changes safely
|
|
96
|
+
if (pos.x > 10) w.cmd().despawn(e);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
world.update(1 / 60);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
> Note: `SystemFn` is typed as `(world: WorldI, dt) => void` where `WorldI` only requires `flush()`.
|
|
104
|
+
> In practice, you’ll typically use the concrete `World` API in systems (cast `world` or type your function accordingly).
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Query API
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
for (const row of world.query(Position, Velocity)) {
|
|
112
|
+
// row.e -> Entity
|
|
113
|
+
// row.c1 -> Position
|
|
114
|
+
// row.c2 -> Velocity
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`query(...ctors)` yields objects shaped like:
|
|
119
|
+
|
|
120
|
+
* `e`: the entity
|
|
121
|
+
* `c1`, `c2`, `c3`, …: component values **in the same order** as the ctor arguments
|
|
122
|
+
|
|
123
|
+
So if you call `query(A, B, C)` you’ll get `{ e, c1: A, c2: B, c3: C }`.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Commands API (deferred ops)
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const cmd = world.cmd();
|
|
131
|
+
|
|
132
|
+
cmd.spawn((e) => {
|
|
133
|
+
cmd.add(e, Position, new Position(0, 0));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
cmd.add(entity, Velocity, new Velocity(1, 0));
|
|
137
|
+
cmd.remove(entity, Velocity);
|
|
138
|
+
cmd.despawn(entity);
|
|
139
|
+
|
|
140
|
+
// Apply them (World.update() also flushes automatically at end)
|
|
141
|
+
world.flush();
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Supported commands (`Commands.ts`):
|
|
145
|
+
|
|
146
|
+
* `spawn(init?)`
|
|
147
|
+
* `despawn(e)`
|
|
148
|
+
* `add(e, ctor, value)`
|
|
149
|
+
* `remove(e, ctor)`
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Schedule (phases)
|
|
154
|
+
|
|
155
|
+
`Schedule` is a small phase runner:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { World, Schedule } from "archetype-ecs-lib";
|
|
159
|
+
|
|
160
|
+
const world = new World();
|
|
161
|
+
const sched = new Schedule();
|
|
162
|
+
|
|
163
|
+
sched
|
|
164
|
+
.add("input", (w: any) => { /* read input, enqueue commands */ })
|
|
165
|
+
.add("sim", (w: any, dt) => { /* update movement */ })
|
|
166
|
+
.add("render",(w: any) => { /* build render data */ });
|
|
167
|
+
|
|
168
|
+
const phases = ["input", "sim", "render"];
|
|
169
|
+
|
|
170
|
+
// Runs each phase in order and calls world.flush() after each phase.
|
|
171
|
+
sched.run(world, 1/60, phases);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
This is handy when you want deterministic ordering and command application points.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## World API summary
|
|
179
|
+
|
|
180
|
+
### Entity lifecycle
|
|
181
|
+
|
|
182
|
+
* `spawn(): Entity`
|
|
183
|
+
* `despawn(e: Entity): void`
|
|
184
|
+
* `isAlive(e: Entity): boolean`
|
|
185
|
+
|
|
186
|
+
### Components
|
|
187
|
+
|
|
188
|
+
* `has(e, Ctor): boolean`
|
|
189
|
+
* `get(e, Ctor): T | undefined`
|
|
190
|
+
* `set(e, Ctor, value): void` *(requires the component to exist; otherwise throws)*
|
|
191
|
+
* `add(e, Ctor, value): void` *(structural: may move entity between archetypes)*
|
|
192
|
+
* `remove(e, Ctor): void` *(structural: may move entity between archetypes)*
|
|
193
|
+
|
|
194
|
+
### Systems / frame
|
|
195
|
+
|
|
196
|
+
* `addSystem(fn): this`
|
|
197
|
+
* `update(dt): void` *(runs systems in order, then flushes)*
|
|
198
|
+
* `cmd(): Commands`
|
|
199
|
+
* `flush(): void`
|
|
200
|
+
|
|
201
|
+
### Queries
|
|
202
|
+
|
|
203
|
+
* `query(...ctors): Iterable<{ e: Entity; c1?: any; c2?: any; ... }>`
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Notes & limitations
|
|
208
|
+
|
|
209
|
+
* This is intentionally minimal: **no parallelism**, no borrow-checking, no automatic conflict detection.
|
|
210
|
+
* Query results use `c1/c2/...` fields for stability and speed; you can wrap this in helpers if you prefer tuple returns.
|
|
211
|
+
* `TypeId` assignment is process-local and based on constructor identity (`WeakMap`).
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
MIT License
|
|
219
|
+
|
|
220
|
+
Copyright (c) 2025 Jean-Laurent Duzant
|
|
221
|
+
|
|
222
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
223
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
224
|
+
in the Software without restriction, including without limitation the rights
|
|
225
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
226
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
227
|
+
furnished to do so, subject to the following conditions:
|
|
228
|
+
|
|
229
|
+
The above copyright notice and this permission notice shall be included in all
|
|
230
|
+
copies or substantial portions of the Software.
|
|
231
|
+
|
|
232
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
233
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
234
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
235
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
236
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
237
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
238
|
+
SOFTWARE.
|
|
239
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Column, Entity, Signature, TypeId } from "./Types";
|
|
2
|
+
export declare class Archetype {
|
|
3
|
+
readonly id: number;
|
|
4
|
+
readonly sig: Signature;
|
|
5
|
+
private readonly cols;
|
|
6
|
+
readonly entities: Entity[];
|
|
7
|
+
constructor(id: number, sig: Signature);
|
|
8
|
+
/**
|
|
9
|
+
* Adds a new row. The caller must push per-column values in the same order.
|
|
10
|
+
* Returns row index.
|
|
11
|
+
*/
|
|
12
|
+
addRow(e: Entity): number;
|
|
13
|
+
/**
|
|
14
|
+
* Swap-remove a row (O(1)). Returns the entity that moved into `row`, or null if none.
|
|
15
|
+
*/
|
|
16
|
+
removeRow(row: number): Entity | null;
|
|
17
|
+
has(t: TypeId): boolean;
|
|
18
|
+
column<T>(t: TypeId): Column<T>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __values = (this && this.__values) || function(o) {
|
|
3
|
+
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
4
|
+
if (m) return m.call(o);
|
|
5
|
+
if (o && typeof o.length === "number") return {
|
|
6
|
+
next: function () {
|
|
7
|
+
if (o && i >= o.length) o = void 0;
|
|
8
|
+
return { value: o && o[i++], done: !o };
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
12
|
+
};
|
|
13
|
+
var __read = (this && this.__read) || function (o, n) {
|
|
14
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
15
|
+
if (!m) return o;
|
|
16
|
+
var i = m.call(o), r, ar = [], e;
|
|
17
|
+
try {
|
|
18
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
19
|
+
}
|
|
20
|
+
catch (error) { e = { error: error }; }
|
|
21
|
+
finally {
|
|
22
|
+
try {
|
|
23
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
24
|
+
}
|
|
25
|
+
finally { if (e) throw e.error; }
|
|
26
|
+
}
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.Archetype = void 0;
|
|
31
|
+
var Archetype = /** @class */ (function () {
|
|
32
|
+
function Archetype(id, sig) {
|
|
33
|
+
var e_1, _a;
|
|
34
|
+
// One column per component type id (SoA layout)
|
|
35
|
+
this.cols = new Map();
|
|
36
|
+
// Entity handles stored per row
|
|
37
|
+
this.entities = [];
|
|
38
|
+
this.id = id;
|
|
39
|
+
this.sig = sig;
|
|
40
|
+
try {
|
|
41
|
+
for (var sig_1 = __values(sig), sig_1_1 = sig_1.next(); !sig_1_1.done; sig_1_1 = sig_1.next()) {
|
|
42
|
+
var t = sig_1_1.value;
|
|
43
|
+
this.cols.set(t, []);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
47
|
+
finally {
|
|
48
|
+
try {
|
|
49
|
+
if (sig_1_1 && !sig_1_1.done && (_a = sig_1.return)) _a.call(sig_1);
|
|
50
|
+
}
|
|
51
|
+
finally { if (e_1) throw e_1.error; }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Adds a new row. The caller must push per-column values in the same order.
|
|
56
|
+
* Returns row index.
|
|
57
|
+
*/
|
|
58
|
+
Archetype.prototype.addRow = function (e) {
|
|
59
|
+
var row = this.entities.length;
|
|
60
|
+
this.entities.push(e);
|
|
61
|
+
return row;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Swap-remove a row (O(1)). Returns the entity that moved into `row`, or null if none.
|
|
65
|
+
*/
|
|
66
|
+
Archetype.prototype.removeRow = function (row) {
|
|
67
|
+
var e_2, _a, e_3, _b;
|
|
68
|
+
var last = this.entities.length - 1;
|
|
69
|
+
if (row < 0 || row > last)
|
|
70
|
+
throw new Error("removeRow out of range: ".concat(row));
|
|
71
|
+
if (row !== last) {
|
|
72
|
+
var moved = this.entities[last];
|
|
73
|
+
this.entities[row] = moved;
|
|
74
|
+
this.entities.pop();
|
|
75
|
+
try {
|
|
76
|
+
for (var _c = __values(this.cols), _d = _c.next(); !_d.done; _d = _c.next()) {
|
|
77
|
+
var _e = __read(_d.value, 2), _ = _e[0], col = _e[1];
|
|
78
|
+
col[row] = col[last];
|
|
79
|
+
col.pop();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
83
|
+
finally {
|
|
84
|
+
try {
|
|
85
|
+
if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
|
|
86
|
+
}
|
|
87
|
+
finally { if (e_2) throw e_2.error; }
|
|
88
|
+
}
|
|
89
|
+
return moved;
|
|
90
|
+
}
|
|
91
|
+
// removing last
|
|
92
|
+
this.entities.pop();
|
|
93
|
+
try {
|
|
94
|
+
for (var _f = __values(this.cols), _g = _f.next(); !_g.done; _g = _f.next()) {
|
|
95
|
+
var _h = __read(_g.value, 2), col = _h[1];
|
|
96
|
+
col.pop();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (e_3_1) { e_3 = { error: e_3_1 }; }
|
|
100
|
+
finally {
|
|
101
|
+
try {
|
|
102
|
+
if (_g && !_g.done && (_b = _f.return)) _b.call(_f);
|
|
103
|
+
}
|
|
104
|
+
finally { if (e_3) throw e_3.error; }
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
};
|
|
108
|
+
Archetype.prototype.has = function (t) {
|
|
109
|
+
return this.cols.has(t);
|
|
110
|
+
};
|
|
111
|
+
Archetype.prototype.column = function (t) {
|
|
112
|
+
var c = this.cols.get(t);
|
|
113
|
+
if (!c)
|
|
114
|
+
throw new Error("Archetype ".concat(this.id, " missing column for type ").concat(t));
|
|
115
|
+
return c;
|
|
116
|
+
};
|
|
117
|
+
return Archetype;
|
|
118
|
+
}());
|
|
119
|
+
exports.Archetype = Archetype;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ComponentCtor, Entity } from "./Types";
|
|
2
|
+
export type Command = {
|
|
3
|
+
k: "spawn";
|
|
4
|
+
init?: (e: Entity) => void;
|
|
5
|
+
} | {
|
|
6
|
+
k: "despawn";
|
|
7
|
+
e: Entity;
|
|
8
|
+
} | {
|
|
9
|
+
k: "add";
|
|
10
|
+
e: Entity;
|
|
11
|
+
ctor: ComponentCtor<any>;
|
|
12
|
+
value: any;
|
|
13
|
+
} | {
|
|
14
|
+
k: "remove";
|
|
15
|
+
e: Entity;
|
|
16
|
+
ctor: ComponentCtor<any>;
|
|
17
|
+
};
|
|
18
|
+
export declare class Commands {
|
|
19
|
+
private readonly q;
|
|
20
|
+
spawn(init?: (e: Entity) => void): void;
|
|
21
|
+
despawn(e: Entity): void;
|
|
22
|
+
add<T>(e: Entity, ctor: ComponentCtor<T>, value: T): void;
|
|
23
|
+
remove<T>(e: Entity, ctor: ComponentCtor<T>): void;
|
|
24
|
+
drain(): Command[];
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Commands = void 0;
|
|
4
|
+
var Commands = /** @class */ (function () {
|
|
5
|
+
function Commands() {
|
|
6
|
+
this.q = [];
|
|
7
|
+
}
|
|
8
|
+
Commands.prototype.spawn = function (init) {
|
|
9
|
+
this.q.push({ k: "spawn", init: init });
|
|
10
|
+
};
|
|
11
|
+
Commands.prototype.despawn = function (e) {
|
|
12
|
+
this.q.push({ k: "despawn", e: e });
|
|
13
|
+
};
|
|
14
|
+
Commands.prototype.add = function (e, ctor, value) {
|
|
15
|
+
this.q.push({ k: "add", e: e, ctor: ctor, value: value });
|
|
16
|
+
};
|
|
17
|
+
Commands.prototype.remove = function (e, ctor) {
|
|
18
|
+
this.q.push({ k: "remove", e: e, ctor: ctor });
|
|
19
|
+
};
|
|
20
|
+
Commands.prototype.drain = function () {
|
|
21
|
+
var out = this.q.slice();
|
|
22
|
+
this.q.length = 0;
|
|
23
|
+
return out;
|
|
24
|
+
};
|
|
25
|
+
return Commands;
|
|
26
|
+
}());
|
|
27
|
+
exports.Commands = Commands;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EntityManager = void 0;
|
|
4
|
+
var EntityManager = /** @class */ (function () {
|
|
5
|
+
function EntityManager() {
|
|
6
|
+
this._nextId = 1;
|
|
7
|
+
this._free = [];
|
|
8
|
+
this.meta = []; // indexed by EntityId; meta[0] unused
|
|
9
|
+
}
|
|
10
|
+
EntityManager.prototype.create = function () {
|
|
11
|
+
var id;
|
|
12
|
+
if (this._free.length > 0) {
|
|
13
|
+
id = this._free.pop();
|
|
14
|
+
var m = this.meta[id];
|
|
15
|
+
m.alive = true;
|
|
16
|
+
m.gen += 1; // bump generation on reuse
|
|
17
|
+
m.arch = 0;
|
|
18
|
+
m.row = 0;
|
|
19
|
+
return { id: id, gen: m.gen };
|
|
20
|
+
}
|
|
21
|
+
id = this._nextId++;
|
|
22
|
+
this.meta[id] = { gen: 1, alive: true, arch: 0, row: 0 };
|
|
23
|
+
return { id: id, gen: 1 };
|
|
24
|
+
};
|
|
25
|
+
EntityManager.prototype.isAlive = function (e) {
|
|
26
|
+
var m = this.meta[e.id];
|
|
27
|
+
return !!m && m.alive && m.gen === e.gen;
|
|
28
|
+
};
|
|
29
|
+
EntityManager.prototype.kill = function (e) {
|
|
30
|
+
var m = this.meta[e.id];
|
|
31
|
+
if (!m || !m.alive || m.gen !== e.gen)
|
|
32
|
+
return;
|
|
33
|
+
m.alive = false;
|
|
34
|
+
this._free.push(e.id);
|
|
35
|
+
};
|
|
36
|
+
return EntityManager;
|
|
37
|
+
}());
|
|
38
|
+
exports.EntityManager = EntityManager;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SystemFn, WorldI } from "./Types";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal scheduler that supports phases, without borrow-checking.
|
|
4
|
+
* (Add conflict detection later if you want parallelism.)
|
|
5
|
+
*/
|
|
6
|
+
export declare class Schedule {
|
|
7
|
+
private readonly phases;
|
|
8
|
+
add(phase: string, fn: SystemFn): this;
|
|
9
|
+
run(world: WorldI, dt: number, phaseOrder: string[]): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __values = (this && this.__values) || function(o) {
|
|
3
|
+
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
4
|
+
if (m) return m.call(o);
|
|
5
|
+
if (o && typeof o.length === "number") return {
|
|
6
|
+
next: function () {
|
|
7
|
+
if (o && i >= o.length) o = void 0;
|
|
8
|
+
return { value: o && o[i++], done: !o };
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.Schedule = void 0;
|
|
15
|
+
/**
|
|
16
|
+
* Minimal scheduler that supports phases, without borrow-checking.
|
|
17
|
+
* (Add conflict detection later if you want parallelism.)
|
|
18
|
+
*/
|
|
19
|
+
var Schedule = /** @class */ (function () {
|
|
20
|
+
function Schedule() {
|
|
21
|
+
this.phases = new Map();
|
|
22
|
+
}
|
|
23
|
+
Schedule.prototype.add = function (phase, fn) {
|
|
24
|
+
var _a;
|
|
25
|
+
var list = (_a = this.phases.get(phase)) !== null && _a !== void 0 ? _a : [];
|
|
26
|
+
list.push(fn);
|
|
27
|
+
this.phases.set(phase, list);
|
|
28
|
+
return this;
|
|
29
|
+
};
|
|
30
|
+
Schedule.prototype.run = function (world, dt, phaseOrder) {
|
|
31
|
+
var e_1, _a, e_2, _b;
|
|
32
|
+
try {
|
|
33
|
+
for (var phaseOrder_1 = __values(phaseOrder), phaseOrder_1_1 = phaseOrder_1.next(); !phaseOrder_1_1.done; phaseOrder_1_1 = phaseOrder_1.next()) {
|
|
34
|
+
var phase = phaseOrder_1_1.value;
|
|
35
|
+
var list = this.phases.get(phase);
|
|
36
|
+
if (!list)
|
|
37
|
+
continue;
|
|
38
|
+
try {
|
|
39
|
+
for (var list_1 = (e_2 = void 0, __values(list)), list_1_1 = list_1.next(); !list_1_1.done; list_1_1 = list_1.next()) {
|
|
40
|
+
var fn = list_1_1.value;
|
|
41
|
+
fn(world, dt);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
45
|
+
finally {
|
|
46
|
+
try {
|
|
47
|
+
if (list_1_1 && !list_1_1.done && (_b = list_1.return)) _b.call(list_1);
|
|
48
|
+
}
|
|
49
|
+
finally { if (e_2) throw e_2.error; }
|
|
50
|
+
}
|
|
51
|
+
// apply deferred commands between phases
|
|
52
|
+
world.flush();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
56
|
+
finally {
|
|
57
|
+
try {
|
|
58
|
+
if (phaseOrder_1_1 && !phaseOrder_1_1.done && (_a = phaseOrder_1.return)) _a.call(phaseOrder_1);
|
|
59
|
+
}
|
|
60
|
+
finally { if (e_1) throw e_1.error; }
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
return Schedule;
|
|
64
|
+
}());
|
|
65
|
+
exports.Schedule = Schedule;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Signature, TypeId } from "./Types";
|
|
2
|
+
export declare function signatureKey(sig: Signature): string;
|
|
3
|
+
export declare function mergeSignature(sig: Signature, add: TypeId): TypeId[];
|
|
4
|
+
export declare function subtractSignature(sig: Signature, remove: TypeId): TypeId[];
|
|
5
|
+
/**
|
|
6
|
+
* True if `need` is a subset of `have`. Both must be sorted ascending.
|
|
7
|
+
*/
|
|
8
|
+
export declare function signatureHasAll(have: Signature, need: Signature): boolean;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.signatureKey = signatureKey;
|
|
4
|
+
exports.mergeSignature = mergeSignature;
|
|
5
|
+
exports.subtractSignature = subtractSignature;
|
|
6
|
+
exports.signatureHasAll = signatureHasAll;
|
|
7
|
+
function signatureKey(sig) {
|
|
8
|
+
// canonical: sorted ascending, joined
|
|
9
|
+
return sig.join(",");
|
|
10
|
+
}
|
|
11
|
+
function mergeSignature(sig, add) {
|
|
12
|
+
var out = sig.slice();
|
|
13
|
+
// insert in sorted order (sig is sorted)
|
|
14
|
+
var i = 0;
|
|
15
|
+
while (i < out.length && out[i] < add)
|
|
16
|
+
i++;
|
|
17
|
+
if (out[i] === add)
|
|
18
|
+
return out;
|
|
19
|
+
out.splice(i, 0, add);
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
function subtractSignature(sig, remove) {
|
|
23
|
+
var out = sig.slice();
|
|
24
|
+
var idx = out.indexOf(remove);
|
|
25
|
+
if (idx >= 0)
|
|
26
|
+
out.splice(idx, 1);
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* True if `need` is a subset of `have`. Both must be sorted ascending.
|
|
31
|
+
*/
|
|
32
|
+
function signatureHasAll(have, need) {
|
|
33
|
+
var i = 0;
|
|
34
|
+
var j = 0;
|
|
35
|
+
while (i < have.length && j < need.length) {
|
|
36
|
+
var a = have[i];
|
|
37
|
+
var b = need[j];
|
|
38
|
+
if (a === b) {
|
|
39
|
+
i++;
|
|
40
|
+
j++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (a < b) {
|
|
44
|
+
i++;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
return false; // a > b -> missing b
|
|
48
|
+
}
|
|
49
|
+
return j === need.length;
|
|
50
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.typeId = typeId;
|
|
4
|
+
var ctorToId = new WeakMap();
|
|
5
|
+
var nextId = 1;
|
|
6
|
+
/**
|
|
7
|
+
* Returns a stable numeric TypeId for a component constructor.
|
|
8
|
+
*/
|
|
9
|
+
function typeId(ctor) {
|
|
10
|
+
var existing = ctorToId.get(ctor);
|
|
11
|
+
if (existing != null)
|
|
12
|
+
return existing;
|
|
13
|
+
var id = nextId++;
|
|
14
|
+
ctorToId.set(ctor, id);
|
|
15
|
+
return id;
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type EntityId = number;
|
|
2
|
+
export type Entity = Readonly<{
|
|
3
|
+
id: EntityId;
|
|
4
|
+
gen: number;
|
|
5
|
+
}>;
|
|
6
|
+
export type EntityMeta = {
|
|
7
|
+
gen: number;
|
|
8
|
+
alive: boolean;
|
|
9
|
+
arch: number;
|
|
10
|
+
row: number;
|
|
11
|
+
};
|
|
12
|
+
export type Column<T = unknown> = T[];
|
|
13
|
+
export type Signature = ReadonlyArray<TypeId>;
|
|
14
|
+
export type ComponentCtor<T> = new (...args: any[]) => T;
|
|
15
|
+
/**
|
|
16
|
+
* Internal numeric id for a component "type".
|
|
17
|
+
* (We keep it numeric so signatures can be sorted quickly.)
|
|
18
|
+
*/
|
|
19
|
+
export type TypeId = number;
|
|
20
|
+
export type SystemFn = (world: WorldI, dt: number) => void;
|
|
21
|
+
/** Forward declaration to avoid circular import types-only pain. */
|
|
22
|
+
export interface WorldI {
|
|
23
|
+
flush(): void;
|
|
24
|
+
}
|
package/lib/ecs/Types.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Commands } from "./Commands";
|
|
2
|
+
import { ComponentCtor, Entity, SystemFn, WorldI } from "./Types";
|
|
3
|
+
export declare class World implements WorldI {
|
|
4
|
+
private readonly entities;
|
|
5
|
+
private readonly archetypes;
|
|
6
|
+
private readonly archByKey;
|
|
7
|
+
private readonly systems;
|
|
8
|
+
private readonly commands;
|
|
9
|
+
private _isIterating;
|
|
10
|
+
constructor();
|
|
11
|
+
/** Queue structural changes to apply safely after systems run. */
|
|
12
|
+
cmd(): Commands;
|
|
13
|
+
addSystem(fn: SystemFn): this;
|
|
14
|
+
/**
|
|
15
|
+
* Run a frame:
|
|
16
|
+
* - run systems in order
|
|
17
|
+
* - flush queued commands (structural changes)
|
|
18
|
+
*/
|
|
19
|
+
update(dt: number): void;
|
|
20
|
+
flush(): void;
|
|
21
|
+
spawn(): Entity;
|
|
22
|
+
isAlive(e: Entity): boolean;
|
|
23
|
+
despawn(e: Entity): void;
|
|
24
|
+
has<T>(e: Entity, ctor: ComponentCtor<T>): boolean;
|
|
25
|
+
get<T>(e: Entity, ctor: ComponentCtor<T>): T | undefined;
|
|
26
|
+
set<T>(e: Entity, ctor: ComponentCtor<T>, value: T): void;
|
|
27
|
+
add<T>(e: Entity, ctor: ComponentCtor<T>, value: T): void;
|
|
28
|
+
remove<T>(e: Entity, ctor: ComponentCtor<T>): void;
|
|
29
|
+
/**
|
|
30
|
+
* Query all entities having all required component types.
|
|
31
|
+
* Iterates archetypes (tables) and yields SoA columns for cache-friendly loops.
|
|
32
|
+
*/
|
|
33
|
+
query(...ctors: ComponentCtor<any>[]): Iterable<any>;
|
|
34
|
+
private _ensureNotIterating;
|
|
35
|
+
private _getOrCreateArchetype;
|
|
36
|
+
private _removeFromArchetype;
|
|
37
|
+
/**
|
|
38
|
+
* Move entity from src archetype row to dst archetype, copying columns via `pick`.
|
|
39
|
+
* Then swap-remove from src.
|
|
40
|
+
*/
|
|
41
|
+
private _moveEntity;
|
|
42
|
+
private _apply;
|
|
43
|
+
}
|
package/lib/ecs/World.js
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
3
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
4
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
5
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
6
|
+
function step(op) {
|
|
7
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
8
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
9
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
10
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
11
|
+
switch (op[0]) {
|
|
12
|
+
case 0: case 1: t = op; break;
|
|
13
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
14
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
15
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
16
|
+
default:
|
|
17
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
18
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
19
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
20
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
21
|
+
if (t[2]) _.ops.pop();
|
|
22
|
+
_.trys.pop(); continue;
|
|
23
|
+
}
|
|
24
|
+
op = body.call(thisArg, _);
|
|
25
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
26
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var __values = (this && this.__values) || function(o) {
|
|
30
|
+
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
31
|
+
if (m) return m.call(o);
|
|
32
|
+
if (o && typeof o.length === "number") return {
|
|
33
|
+
next: function () {
|
|
34
|
+
if (o && i >= o.length) o = void 0;
|
|
35
|
+
return { value: o && o[i++], done: !o };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
39
|
+
};
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.World = void 0;
|
|
42
|
+
var Archetype_1 = require("./Archetype");
|
|
43
|
+
var Commands_1 = require("./Commands");
|
|
44
|
+
var EntityManager_1 = require("./EntityManager");
|
|
45
|
+
var Signature_1 = require("./Signature");
|
|
46
|
+
var TypeRegistry_1 = require("./TypeRegistry");
|
|
47
|
+
var World = /** @class */ (function () {
|
|
48
|
+
function World() {
|
|
49
|
+
this.entities = new EntityManager_1.EntityManager();
|
|
50
|
+
this.archetypes = [];
|
|
51
|
+
this.archByKey = new Map();
|
|
52
|
+
this.systems = [];
|
|
53
|
+
this.commands = new Commands_1.Commands();
|
|
54
|
+
this._isIterating = false;
|
|
55
|
+
// Archetype 0: empty signature
|
|
56
|
+
var archetype0 = new Archetype_1.Archetype(0, []);
|
|
57
|
+
this.archetypes[0] = archetype0;
|
|
58
|
+
this.archByKey.set("", archetype0);
|
|
59
|
+
}
|
|
60
|
+
/** Queue structural changes to apply safely after systems run. */
|
|
61
|
+
World.prototype.cmd = function () { return this.commands; };
|
|
62
|
+
World.prototype.addSystem = function (fn) {
|
|
63
|
+
this.systems.push(fn);
|
|
64
|
+
return this;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Run a frame:
|
|
68
|
+
* - run systems in order
|
|
69
|
+
* - flush queued commands (structural changes)
|
|
70
|
+
*/
|
|
71
|
+
World.prototype.update = function (dt) {
|
|
72
|
+
var e_1, _a;
|
|
73
|
+
this._isIterating = true;
|
|
74
|
+
try {
|
|
75
|
+
try {
|
|
76
|
+
for (var _b = __values(this.systems), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
77
|
+
var s = _c.value;
|
|
78
|
+
s(this, dt);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
82
|
+
finally {
|
|
83
|
+
try {
|
|
84
|
+
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
|
85
|
+
}
|
|
86
|
+
finally { if (e_1) throw e_1.error; }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
this._isIterating = false;
|
|
91
|
+
this.flush();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
World.prototype.flush = function () {
|
|
95
|
+
var e_2, _a;
|
|
96
|
+
var ops = this.commands.drain();
|
|
97
|
+
try {
|
|
98
|
+
for (var ops_1 = __values(ops), ops_1_1 = ops_1.next(); !ops_1_1.done; ops_1_1 = ops_1.next()) {
|
|
99
|
+
var op = ops_1_1.value;
|
|
100
|
+
this._apply(op);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
104
|
+
finally {
|
|
105
|
+
try {
|
|
106
|
+
if (ops_1_1 && !ops_1_1.done && (_a = ops_1.return)) _a.call(ops_1);
|
|
107
|
+
}
|
|
108
|
+
finally { if (e_2) throw e_2.error; }
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
//#region ---------- Entity lifecycle ----------
|
|
112
|
+
World.prototype.spawn = function () {
|
|
113
|
+
var entity = this.entities.create();
|
|
114
|
+
// place in archetype 0
|
|
115
|
+
var archetype0 = this.archetypes[0];
|
|
116
|
+
var row = archetype0.addRow(entity);
|
|
117
|
+
var entityMeta = this.entities.meta[entity.id];
|
|
118
|
+
entityMeta.arch = 0;
|
|
119
|
+
entityMeta.row = row;
|
|
120
|
+
return entity;
|
|
121
|
+
};
|
|
122
|
+
World.prototype.isAlive = function (e) {
|
|
123
|
+
return this.entities.isAlive(e);
|
|
124
|
+
};
|
|
125
|
+
World.prototype.despawn = function (e) {
|
|
126
|
+
if (!this.entities.isAlive(e))
|
|
127
|
+
return;
|
|
128
|
+
this._ensureNotIterating("despawn");
|
|
129
|
+
this._removeFromArchetype(e);
|
|
130
|
+
this.entities.kill(e);
|
|
131
|
+
};
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region ---------- Components ----------
|
|
134
|
+
World.prototype.has = function (e, ctor) {
|
|
135
|
+
var meta = this.entities.meta[e.id];
|
|
136
|
+
if (!meta || !meta.alive || meta.gen !== e.gen)
|
|
137
|
+
return false;
|
|
138
|
+
var tid = (0, TypeRegistry_1.typeId)(ctor);
|
|
139
|
+
return this.archetypes[meta.arch].has(tid);
|
|
140
|
+
};
|
|
141
|
+
World.prototype.get = function (e, ctor) {
|
|
142
|
+
var meta = this.entities.meta[e.id];
|
|
143
|
+
if (!meta || !meta.alive || meta.gen !== e.gen)
|
|
144
|
+
return undefined;
|
|
145
|
+
var tid = (0, TypeRegistry_1.typeId)(ctor);
|
|
146
|
+
var a = this.archetypes[meta.arch];
|
|
147
|
+
if (!a.has(tid))
|
|
148
|
+
return undefined;
|
|
149
|
+
return a.column(tid)[meta.row];
|
|
150
|
+
};
|
|
151
|
+
World.prototype.set = function (e, ctor, value) {
|
|
152
|
+
var meta = this.entities.meta[e.id];
|
|
153
|
+
if (!meta || !meta.alive || meta.gen !== e.gen)
|
|
154
|
+
return;
|
|
155
|
+
var tid = (0, TypeRegistry_1.typeId)(ctor);
|
|
156
|
+
var a = this.archetypes[meta.arch];
|
|
157
|
+
if (!a.has(tid))
|
|
158
|
+
throw new Error("set() requires component to exist; use add()");
|
|
159
|
+
a.column(tid)[meta.row] = value;
|
|
160
|
+
};
|
|
161
|
+
World.prototype.add = function (e, ctor, value) {
|
|
162
|
+
if (!this.entities.isAlive(e))
|
|
163
|
+
return;
|
|
164
|
+
this._ensureNotIterating("add");
|
|
165
|
+
var tid = (0, TypeRegistry_1.typeId)(ctor);
|
|
166
|
+
var srcMeta = this.entities.meta[e.id];
|
|
167
|
+
var src = this.archetypes[srcMeta.arch];
|
|
168
|
+
if (src.has(tid)) {
|
|
169
|
+
// overwrite in-place
|
|
170
|
+
src.column(tid)[srcMeta.row] = value;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
var dstSig = (0, Signature_1.mergeSignature)(src.sig, tid);
|
|
174
|
+
var dst = this._getOrCreateArchetype(dstSig);
|
|
175
|
+
this._moveEntity(e, src, srcMeta.row, dst, function (t) {
|
|
176
|
+
if (t === tid)
|
|
177
|
+
return value;
|
|
178
|
+
return src.column(t)[srcMeta.row];
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
World.prototype.remove = function (e, ctor) {
|
|
182
|
+
if (!this.entities.isAlive(e))
|
|
183
|
+
return;
|
|
184
|
+
this._ensureNotIterating("remove");
|
|
185
|
+
var tid = (0, TypeRegistry_1.typeId)(ctor);
|
|
186
|
+
var srcMeta = this.entities.meta[e.id];
|
|
187
|
+
var src = this.archetypes[srcMeta.arch];
|
|
188
|
+
if (!src.has(tid))
|
|
189
|
+
return;
|
|
190
|
+
var dstSig = (0, Signature_1.subtractSignature)(src.sig, tid);
|
|
191
|
+
var dst = this._getOrCreateArchetype(dstSig);
|
|
192
|
+
this._moveEntity(e, src, srcMeta.row, dst, function (t) {
|
|
193
|
+
// copy all but removed, but dstSig guarantees t != tid
|
|
194
|
+
return src.column(t)[srcMeta.row];
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
//#endregion
|
|
198
|
+
//region ---------- Queries ----------
|
|
199
|
+
/**
|
|
200
|
+
* Query all entities having all required component types.
|
|
201
|
+
* Iterates archetypes (tables) and yields SoA columns for cache-friendly loops.
|
|
202
|
+
*/
|
|
203
|
+
World.prototype.query = function () {
|
|
204
|
+
var ctors = [];
|
|
205
|
+
for (var _i = 0; _i < arguments.length; _i++) {
|
|
206
|
+
ctors[_i] = arguments[_i];
|
|
207
|
+
}
|
|
208
|
+
var requested = ctors.map(TypeRegistry_1.typeId); // preserve caller order
|
|
209
|
+
var needSorted = Array.from(new Set(requested)).sort(function (a, b) { return a - b; }); // for signatureHasAll
|
|
210
|
+
function gen(world) {
|
|
211
|
+
var _loop_1, _a, _b, a, e_3_1;
|
|
212
|
+
var e_3, _c;
|
|
213
|
+
return __generator(this, function (_d) {
|
|
214
|
+
switch (_d.label) {
|
|
215
|
+
case 0:
|
|
216
|
+
world._isIterating = true;
|
|
217
|
+
_d.label = 1;
|
|
218
|
+
case 1:
|
|
219
|
+
_d.trys.push([1, , 10, 11]);
|
|
220
|
+
_loop_1 = function (a) {
|
|
221
|
+
var cols, row, e, out, i;
|
|
222
|
+
return __generator(this, function (_e) {
|
|
223
|
+
switch (_e.label) {
|
|
224
|
+
case 0:
|
|
225
|
+
if (!a)
|
|
226
|
+
return [2 /*return*/, "continue"];
|
|
227
|
+
// if (!signatureHasAll(a.sig, need)) continue;
|
|
228
|
+
if (!(0, Signature_1.signatureHasAll)(a.sig, needSorted))
|
|
229
|
+
return [2 /*return*/, "continue"];
|
|
230
|
+
cols = requested.map(function (t) { return a.column(t); });
|
|
231
|
+
row = 0;
|
|
232
|
+
_e.label = 1;
|
|
233
|
+
case 1:
|
|
234
|
+
if (!(row < a.entities.length)) return [3 /*break*/, 4];
|
|
235
|
+
e = a.entities[row];
|
|
236
|
+
out = { e: e };
|
|
237
|
+
for (i = 0; i < cols.length; i++)
|
|
238
|
+
out["c".concat(i + 1)] = cols[i][row];
|
|
239
|
+
return [4 /*yield*/, out];
|
|
240
|
+
case 2:
|
|
241
|
+
_e.sent();
|
|
242
|
+
_e.label = 3;
|
|
243
|
+
case 3:
|
|
244
|
+
row++;
|
|
245
|
+
return [3 /*break*/, 1];
|
|
246
|
+
case 4: return [2 /*return*/];
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
_d.label = 2;
|
|
251
|
+
case 2:
|
|
252
|
+
_d.trys.push([2, 7, 8, 9]);
|
|
253
|
+
_a = __values(world.archetypes), _b = _a.next();
|
|
254
|
+
_d.label = 3;
|
|
255
|
+
case 3:
|
|
256
|
+
if (!!_b.done) return [3 /*break*/, 6];
|
|
257
|
+
a = _b.value;
|
|
258
|
+
return [5 /*yield**/, _loop_1(a)];
|
|
259
|
+
case 4:
|
|
260
|
+
_d.sent();
|
|
261
|
+
_d.label = 5;
|
|
262
|
+
case 5:
|
|
263
|
+
_b = _a.next();
|
|
264
|
+
return [3 /*break*/, 3];
|
|
265
|
+
case 6: return [3 /*break*/, 9];
|
|
266
|
+
case 7:
|
|
267
|
+
e_3_1 = _d.sent();
|
|
268
|
+
e_3 = { error: e_3_1 };
|
|
269
|
+
return [3 /*break*/, 9];
|
|
270
|
+
case 8:
|
|
271
|
+
try {
|
|
272
|
+
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
273
|
+
}
|
|
274
|
+
finally { if (e_3) throw e_3.error; }
|
|
275
|
+
return [7 /*endfinally*/];
|
|
276
|
+
case 9: return [3 /*break*/, 11];
|
|
277
|
+
case 10:
|
|
278
|
+
world._isIterating = false;
|
|
279
|
+
return [7 /*endfinally*/];
|
|
280
|
+
case 11: return [2 /*return*/];
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return gen(this);
|
|
285
|
+
};
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region ---------- Internals ----------
|
|
288
|
+
World.prototype._ensureNotIterating = function (op) {
|
|
289
|
+
if (this._isIterating) {
|
|
290
|
+
throw new Error("Cannot do structural change (".concat(op, ") while iterating. Use world.cmd() and flush at end of frame."));
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
World.prototype._getOrCreateArchetype = function (sig) {
|
|
294
|
+
var key = (0, Signature_1.signatureKey)(sig);
|
|
295
|
+
var existing = this.archByKey.get(key);
|
|
296
|
+
if (existing)
|
|
297
|
+
return existing;
|
|
298
|
+
var id = this.archetypes.length;
|
|
299
|
+
var a = new Archetype_1.Archetype(id, sig.slice().sort(function (x, y) { return x - y; }));
|
|
300
|
+
this.archetypes[id] = a;
|
|
301
|
+
this.archByKey.set(key, a);
|
|
302
|
+
return a;
|
|
303
|
+
};
|
|
304
|
+
World.prototype._removeFromArchetype = function (e) {
|
|
305
|
+
var m = this.entities.meta[e.id];
|
|
306
|
+
var a = this.archetypes[m.arch];
|
|
307
|
+
var moved = a.removeRow(m.row);
|
|
308
|
+
if (moved) {
|
|
309
|
+
// update moved entity meta to new row
|
|
310
|
+
var mm = this.entities.meta[moved.id];
|
|
311
|
+
mm.row = m.row;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Move entity from src archetype row to dst archetype, copying columns via `pick`.
|
|
316
|
+
* Then swap-remove from src.
|
|
317
|
+
*/
|
|
318
|
+
World.prototype._moveEntity = function (e, src, srcRow, dst, pick) {
|
|
319
|
+
var e_4, _a;
|
|
320
|
+
// add row in dst
|
|
321
|
+
var dstRow = dst.addRow(e);
|
|
322
|
+
try {
|
|
323
|
+
for (var _b = __values(dst.sig), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
324
|
+
var t = _c.value;
|
|
325
|
+
dst.column(t).push(pick(t));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch (e_4_1) { e_4 = { error: e_4_1 }; }
|
|
329
|
+
finally {
|
|
330
|
+
try {
|
|
331
|
+
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
|
332
|
+
}
|
|
333
|
+
finally { if (e_4) throw e_4.error; }
|
|
334
|
+
}
|
|
335
|
+
// update meta to dst before removing from src (in case src==dst should never happen here)
|
|
336
|
+
var m = this.entities.meta[e.id];
|
|
337
|
+
m.arch = dst.id;
|
|
338
|
+
m.row = dstRow;
|
|
339
|
+
// remove from src (swap-remove)
|
|
340
|
+
var moved = src.removeRow(srcRow);
|
|
341
|
+
if (moved) {
|
|
342
|
+
var mm = this.entities.meta[moved.id];
|
|
343
|
+
mm.arch = src.id;
|
|
344
|
+
mm.row = srcRow;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
World.prototype._apply = function (op) {
|
|
348
|
+
var _a;
|
|
349
|
+
switch (op.k) {
|
|
350
|
+
case "spawn": {
|
|
351
|
+
var e = this.spawn();
|
|
352
|
+
(_a = op.init) === null || _a === void 0 ? void 0 : _a.call(op, e);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
case "despawn":
|
|
356
|
+
return this.despawn(op.e);
|
|
357
|
+
case "add":
|
|
358
|
+
return this.add(op.e, op.ctor, op.value);
|
|
359
|
+
case "remove":
|
|
360
|
+
return this.remove(op.e, op.ctor);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
return World;
|
|
364
|
+
}());
|
|
365
|
+
exports.World = World;
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./ecs/Types"), exports);
|
|
18
|
+
__exportStar(require("./ecs/TypeRegistry"), exports);
|
|
19
|
+
__exportStar(require("./ecs/Commands"), exports);
|
|
20
|
+
__exportStar(require("./ecs/World"), exports);
|
|
21
|
+
__exportStar(require("./ecs/Schedule"), exports);
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "archetype-ecs-lib",
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "An Archetype Entity Component System (AECS)",
|
|
5
|
+
"main": "./lib/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "jest --coverage",
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"lint": "tslint -c tslint.json src/**/*.ts",
|
|
10
|
+
"reload-packages": "rm -Rf node_modules && npm cache clean --force && npm ci"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"archetype",
|
|
14
|
+
"ecs",
|
|
15
|
+
"lib"
|
|
16
|
+
],
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "PirateJL"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"tslint": "^6.1.3",
|
|
23
|
+
"typescript": "^5.9.3"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"./bin/*",
|
|
27
|
+
"./lib/*"
|
|
28
|
+
],
|
|
29
|
+
"typings": "./lib/index.d.ts",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/jest": "^30.0.0",
|
|
32
|
+
"jest": "^30.2.0",
|
|
33
|
+
"ts-jest": "^29.4.6"
|
|
34
|
+
}
|
|
35
|
+
}
|