@ste_tisci/ecs 0.1.6 → 0.1.8
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 +33 -0
- package/dist/index.cjs +309 -0
- package/dist/index.d.cts +142 -0
- package/dist/index.d.ts +142 -0
- package/dist/index.js +282 -0
- package/package.json +6 -2
- package/.prettierrc +0 -12
- package/src/ComponentStore.ts +0 -96
- package/src/Ecs.ts +0 -98
- package/src/EntityManager.ts +0 -60
- package/src/index.ts +0 -1
- package/src/types/IComponentRegistry.d.ts +0 -29
- package/src/types/IComponentStore.d.ts +0 -53
- package/src/types/IEcs.d.ts +0 -64
- package/src/types/IEntityManager.d.ts +0 -65
- package/src/types/ISparseSet.d.ts +0 -49
- package/src/types/index.d.ts +0 -43
- package/src/utils/ComponentRegistry.ts +0 -32
- package/src/utils/SparseSet.ts +0 -54
- package/tsconfig.json +0 -17
- package/tsup.config.ts +0 -10
package/dist/index.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// src/utils/SparseSet.ts
|
|
2
|
+
function SparseSet() {
|
|
3
|
+
const sparse = [];
|
|
4
|
+
const dense = [];
|
|
5
|
+
let size = 0;
|
|
6
|
+
function has(eid) {
|
|
7
|
+
const ID = getIndex(eid);
|
|
8
|
+
const exist = ID !== void 0 && dense[ID] === eid;
|
|
9
|
+
const isWhitinBounds = ID >= 0 && ID < size;
|
|
10
|
+
return exist && isWhitinBounds;
|
|
11
|
+
}
|
|
12
|
+
function add(eid) {
|
|
13
|
+
if (has(eid)) throw new Error(`ID ${eid} already present in the set`);
|
|
14
|
+
sparse[eid] = size;
|
|
15
|
+
dense[size] = eid;
|
|
16
|
+
size++;
|
|
17
|
+
}
|
|
18
|
+
function remove(eid) {
|
|
19
|
+
if (!has(eid)) throw new Error(`Cannot remove ID ${eid} because does not exists`);
|
|
20
|
+
const ID = getIndex(eid);
|
|
21
|
+
const last = dense[size - 1];
|
|
22
|
+
dense[ID] = last;
|
|
23
|
+
sparse[last] = ID;
|
|
24
|
+
dense.pop();
|
|
25
|
+
size--;
|
|
26
|
+
delete sparse[eid];
|
|
27
|
+
}
|
|
28
|
+
function getIndex(eid) {
|
|
29
|
+
return sparse[eid];
|
|
30
|
+
}
|
|
31
|
+
function getDense() {
|
|
32
|
+
return dense;
|
|
33
|
+
}
|
|
34
|
+
function getSize() {
|
|
35
|
+
return size;
|
|
36
|
+
}
|
|
37
|
+
function getSparse() {
|
|
38
|
+
return sparse;
|
|
39
|
+
}
|
|
40
|
+
return { has, add, remove, getIndex, getDense, getSize, getSparse };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/ComponentStore.ts
|
|
44
|
+
var INITIAL_CAPACITY = 8;
|
|
45
|
+
function grow(buffer, requiredIndex) {
|
|
46
|
+
let capacity = buffer.length || INITIAL_CAPACITY;
|
|
47
|
+
while (capacity <= requiredIndex) capacity *= 2;
|
|
48
|
+
const grown = new Float32Array(capacity);
|
|
49
|
+
grown.set(buffer);
|
|
50
|
+
return grown;
|
|
51
|
+
}
|
|
52
|
+
function ComponentStore() {
|
|
53
|
+
const componentSet = SparseSet();
|
|
54
|
+
const componentData = /* @__PURE__ */ Object.create(null);
|
|
55
|
+
const isNumeric = /* @__PURE__ */ Object.create(null);
|
|
56
|
+
function add(eid, data) {
|
|
57
|
+
if (componentSet.has(eid)) throw new Error(`Entity ${eid} already has this component`);
|
|
58
|
+
const ID = componentSet.getSize();
|
|
59
|
+
componentSet.add(eid);
|
|
60
|
+
for (const key in data) {
|
|
61
|
+
const value = data[key];
|
|
62
|
+
if (!(key in componentData)) {
|
|
63
|
+
isNumeric[key] = typeof value === "number";
|
|
64
|
+
componentData[key] = isNumeric[key] ? new Float32Array(INITIAL_CAPACITY) : [];
|
|
65
|
+
}
|
|
66
|
+
if (isNumeric[key] && ID >= componentData[key].length) {
|
|
67
|
+
componentData[key] = grow(componentData[key], ID);
|
|
68
|
+
}
|
|
69
|
+
componentData[key][ID] = value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function remove(eid) {
|
|
73
|
+
if (!componentSet.has(eid)) throw new Error(`Entity ${eid} does not have this component`);
|
|
74
|
+
const ID = componentSet.getIndex(eid);
|
|
75
|
+
const lastID = componentSet.getSize() - 1;
|
|
76
|
+
if (ID !== lastID) {
|
|
77
|
+
for (const key of Object.keys(componentData)) {
|
|
78
|
+
componentData[key][ID] = componentData[key][lastID];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
componentSet.remove(eid);
|
|
82
|
+
for (const key in componentData) {
|
|
83
|
+
if (!isNumeric[key]) {
|
|
84
|
+
componentData[key].pop();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function getDense() {
|
|
89
|
+
return componentSet.getDense();
|
|
90
|
+
}
|
|
91
|
+
function getData() {
|
|
92
|
+
return componentData;
|
|
93
|
+
}
|
|
94
|
+
function getIndex(eid) {
|
|
95
|
+
return componentSet.getIndex(eid);
|
|
96
|
+
}
|
|
97
|
+
function getSize() {
|
|
98
|
+
return componentSet.getSize();
|
|
99
|
+
}
|
|
100
|
+
function getSparse() {
|
|
101
|
+
return componentSet.getSparse();
|
|
102
|
+
}
|
|
103
|
+
return { add, remove, getIndex, getDense, getData, getSize, getSparse };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/EntityManager.ts
|
|
107
|
+
function EntityManager(registry) {
|
|
108
|
+
let nextID = 0;
|
|
109
|
+
const recycledIDs = [];
|
|
110
|
+
const bitMasks = [];
|
|
111
|
+
function exists(eid) {
|
|
112
|
+
const isWithinBounds = eid >= 0 && eid < nextID;
|
|
113
|
+
const hasMask = bitMasks[eid] !== void 0;
|
|
114
|
+
return isWithinBounds && hasMask;
|
|
115
|
+
}
|
|
116
|
+
function hasComponent(eid, name) {
|
|
117
|
+
if (!exists(eid)) throw new Error(`Entity ${eid} does not exist`);
|
|
118
|
+
const cid = registry.getID(name);
|
|
119
|
+
return (bitMasks[eid] & 1n << BigInt(cid)) !== 0n;
|
|
120
|
+
}
|
|
121
|
+
function create() {
|
|
122
|
+
const ID = recycledIDs.length > 0 ? recycledIDs.pop() : nextID++;
|
|
123
|
+
bitMasks[ID] = 0n;
|
|
124
|
+
return ID;
|
|
125
|
+
}
|
|
126
|
+
function remove(eid) {
|
|
127
|
+
if (!exists(eid)) throw new Error(`Entity ${eid} does not exist`);
|
|
128
|
+
bitMasks[eid] = void 0;
|
|
129
|
+
recycledIDs.push(eid);
|
|
130
|
+
}
|
|
131
|
+
function addComponent(eid, name) {
|
|
132
|
+
if (!exists(eid)) throw new Error(`Entity ${eid} does not exists`);
|
|
133
|
+
if (hasComponent(eid, name)) throw new Error(`Entity ${eid} already has component ${name}`);
|
|
134
|
+
const cid = registry.getID(name);
|
|
135
|
+
bitMasks[eid] |= 1n << BigInt(cid);
|
|
136
|
+
}
|
|
137
|
+
function removeComponent(eid, name) {
|
|
138
|
+
if (!exists(eid)) throw new Error(`Entity ${eid} does not exists`);
|
|
139
|
+
if (!hasComponent(eid, name)) throw new Error(`Entity ${eid} does not have component ${name}`);
|
|
140
|
+
const cid = registry.getID(name);
|
|
141
|
+
bitMasks[eid] &= ~(1n << BigInt(cid));
|
|
142
|
+
}
|
|
143
|
+
function getMask(eid) {
|
|
144
|
+
if (!exists(eid)) throw new Error(`Entity ${eid} does not exist`);
|
|
145
|
+
return bitMasks[eid];
|
|
146
|
+
}
|
|
147
|
+
function tryGetMask(eid) {
|
|
148
|
+
return eid >= 0 && eid < nextID ? bitMasks[eid] : void 0;
|
|
149
|
+
}
|
|
150
|
+
return { exists, hasComponent, create, remove, addComponent, removeComponent, getMask, tryGetMask };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/utils/ComponentRegistry.ts
|
|
154
|
+
function createComponentRegistry() {
|
|
155
|
+
const nameToID = /* @__PURE__ */ new Map();
|
|
156
|
+
const IDtoName = /* @__PURE__ */ new Map();
|
|
157
|
+
let nextID = 0;
|
|
158
|
+
function register(name) {
|
|
159
|
+
if (!nameToID.has(name)) {
|
|
160
|
+
const eid = nextID++;
|
|
161
|
+
nameToID.set(name, eid);
|
|
162
|
+
IDtoName.set(eid, name);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function getID(name) {
|
|
166
|
+
const eid = nameToID.get(name);
|
|
167
|
+
if (eid === void 0) throw new Error(`Component ${name} not registered`);
|
|
168
|
+
return eid;
|
|
169
|
+
}
|
|
170
|
+
function getName(cid) {
|
|
171
|
+
const name = IDtoName.get(cid);
|
|
172
|
+
if (!name) throw new Error(`Component ID ${cid} not registered`);
|
|
173
|
+
return name;
|
|
174
|
+
}
|
|
175
|
+
return { register, getID, getName };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/Ecs.ts
|
|
179
|
+
function ECS() {
|
|
180
|
+
const ComponentRegistry = createComponentRegistry();
|
|
181
|
+
const Entities = EntityManager(ComponentRegistry);
|
|
182
|
+
const internalStores = /* @__PURE__ */ Object.create(null);
|
|
183
|
+
const components = /* @__PURE__ */ Object.create(null);
|
|
184
|
+
const indices = /* @__PURE__ */ Object.create(null);
|
|
185
|
+
function defineComponents(...names) {
|
|
186
|
+
for (const name of names) {
|
|
187
|
+
if (Object.hasOwn(internalStores, name))
|
|
188
|
+
throw new Error(`Component ${String(name)} is already defined`);
|
|
189
|
+
ComponentRegistry.register(name);
|
|
190
|
+
internalStores[name] = ComponentStore();
|
|
191
|
+
components[name] = internalStores[name].getData();
|
|
192
|
+
indices[name] = internalStores[name].getSparse();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function createEntity() {
|
|
196
|
+
return Entities.create();
|
|
197
|
+
}
|
|
198
|
+
function destroyEntity(eid) {
|
|
199
|
+
const entityMask = Entities.getMask(eid);
|
|
200
|
+
let cid = 0;
|
|
201
|
+
let tempMask = entityMask;
|
|
202
|
+
while (tempMask !== 0n) {
|
|
203
|
+
if ((tempMask & 1n) !== 0n) {
|
|
204
|
+
const name = ComponentRegistry.getName(cid);
|
|
205
|
+
internalStores[name].remove(eid);
|
|
206
|
+
}
|
|
207
|
+
tempMask >>= 1n;
|
|
208
|
+
cid++;
|
|
209
|
+
}
|
|
210
|
+
Entities.remove(eid);
|
|
211
|
+
}
|
|
212
|
+
function addComponent(eid, name, data) {
|
|
213
|
+
Entities.addComponent(eid, name);
|
|
214
|
+
internalStores[name].add(eid, data);
|
|
215
|
+
}
|
|
216
|
+
function removeComponent(eid, name) {
|
|
217
|
+
Entities.removeComponent(eid, name);
|
|
218
|
+
internalStores[name].remove(eid);
|
|
219
|
+
}
|
|
220
|
+
function planQuery(componentName) {
|
|
221
|
+
let targetMask = 0n;
|
|
222
|
+
for (const name of componentName) {
|
|
223
|
+
targetMask |= 1n << BigInt(ComponentRegistry.getID(name));
|
|
224
|
+
}
|
|
225
|
+
let pivot = internalStores[componentName[0]];
|
|
226
|
+
let pivotSize = pivot.getSize();
|
|
227
|
+
for (let i = 1; i < componentName.length; i++) {
|
|
228
|
+
const store = internalStores[componentName[i]];
|
|
229
|
+
const size = store.getSize();
|
|
230
|
+
if (size < pivotSize) {
|
|
231
|
+
pivot = store;
|
|
232
|
+
pivotSize = size;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { targetMask, pivot };
|
|
236
|
+
}
|
|
237
|
+
function* query(...componentName) {
|
|
238
|
+
if (componentName.length === 0) return;
|
|
239
|
+
const { targetMask, pivot } = planQuery(componentName);
|
|
240
|
+
const count = componentName.length;
|
|
241
|
+
const sparses = new Array(count);
|
|
242
|
+
for (let i = 0; i < count; i++) sparses[i] = internalStores[componentName[i]].getSparse();
|
|
243
|
+
const entities = [...pivot.getDense()];
|
|
244
|
+
for (let i = 0; i < entities.length; i++) {
|
|
245
|
+
const eid = entities[i];
|
|
246
|
+
const mask = Entities.tryGetMask(eid);
|
|
247
|
+
if (mask === void 0 || (mask & targetMask) !== targetMask) continue;
|
|
248
|
+
const result = {};
|
|
249
|
+
for (let c = 0; c < count; c++) {
|
|
250
|
+
result[componentName[c]] = { id: sparses[c][eid] };
|
|
251
|
+
}
|
|
252
|
+
yield result;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function queryIds(...componentName) {
|
|
256
|
+
const matches = [];
|
|
257
|
+
if (componentName.length === 0) return matches;
|
|
258
|
+
const { targetMask, pivot } = planQuery(componentName);
|
|
259
|
+
const dense = pivot.getDense();
|
|
260
|
+
for (let i = 0; i < dense.length; i++) {
|
|
261
|
+
const eid = dense[i];
|
|
262
|
+
const mask = Entities.tryGetMask(eid);
|
|
263
|
+
if (mask === void 0 || (mask & targetMask) !== targetMask) continue;
|
|
264
|
+
matches.push(eid);
|
|
265
|
+
}
|
|
266
|
+
return matches;
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
defineComponents,
|
|
270
|
+
createEntity,
|
|
271
|
+
destroyEntity,
|
|
272
|
+
addComponent,
|
|
273
|
+
removeComponent,
|
|
274
|
+
query,
|
|
275
|
+
queryIds,
|
|
276
|
+
components,
|
|
277
|
+
indices
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
export {
|
|
281
|
+
ECS
|
|
282
|
+
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ste_tisci/ecs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "A minimalistic game engine with zero dependencies based on the Entity Component System (ECS) architecture implemented in TypeScript",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
8
11
|
"scripts": {
|
|
9
|
-
"build": "tsup"
|
|
12
|
+
"build": "tsup",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
10
14
|
},
|
|
11
15
|
"keywords": [],
|
|
12
16
|
"author": "",
|
package/.prettierrc
DELETED
package/src/ComponentStore.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import type { IComponentStore } from './types/IComponentStore.js';
|
|
2
|
-
import type { ComponentDataArrays } from './types/index.js';
|
|
3
|
-
import { SparseSet } from './utils/SparseSet.js';
|
|
4
|
-
|
|
5
|
-
const INITIAL_CAPACITY = 8;
|
|
6
|
-
|
|
7
|
-
// Float32Array has a fixed length, so numeric fields grow by doubling capacity
|
|
8
|
-
// (like a dynamic array reallocation) instead of relying on push/pop.
|
|
9
|
-
function grow(buffer: Float32Array, requiredIndex: number): Float32Array {
|
|
10
|
-
let capacity = buffer.length;
|
|
11
|
-
while (capacity <= requiredIndex) capacity *= 2;
|
|
12
|
-
|
|
13
|
-
const grown = new Float32Array(capacity);
|
|
14
|
-
grown.set(buffer);
|
|
15
|
-
return grown;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Internally every field is either a Float32Array (numeric) or a plain array (anything else).
|
|
19
|
-
// The precise, per-field ComponentDataArrays<T> type is only applied at the public getData() boundary.
|
|
20
|
-
type FieldStore = Float32Array | unknown[];
|
|
21
|
-
|
|
22
|
-
export function ComponentStore<T extends Record<string, any>>(): IComponentStore<T> {
|
|
23
|
-
const componentSet = SparseSet();
|
|
24
|
-
const componentData: Record<string, FieldStore> = {};
|
|
25
|
-
|
|
26
|
-
// Tracks which fields are numeric (Float32Array-backed) vs plain arrays, keyed by field name
|
|
27
|
-
const isNumeric: Record<string, boolean> = {};
|
|
28
|
-
|
|
29
|
-
function add(eid: number, data: T): void {
|
|
30
|
-
if (componentSet.has(eid)) throw new Error(`Entity ${eid} already has this component`);
|
|
31
|
-
|
|
32
|
-
// Use the current size (next index) to maintain data coherence with the dense array
|
|
33
|
-
const ID = componentSet.getSize();
|
|
34
|
-
|
|
35
|
-
componentSet.add(eid);
|
|
36
|
-
|
|
37
|
-
// Store each property of the component into its corresponding array
|
|
38
|
-
for (const key in data) {
|
|
39
|
-
const value = data[key];
|
|
40
|
-
|
|
41
|
-
if (!(key in componentData)) {
|
|
42
|
-
isNumeric[key] = typeof value === 'number';
|
|
43
|
-
componentData[key] = isNumeric[key] ? new Float32Array(INITIAL_CAPACITY) : [];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (isNumeric[key] && ID >= componentData[key].length) {
|
|
47
|
-
componentData[key] = grow(componentData[key] as Float32Array, ID);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
(componentData[key] as unknown[])[ID] = value;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function remove(eid: number): void {
|
|
55
|
-
if (!componentSet.has(eid)) throw new Error(`Entity ${eid} does not have this component`);
|
|
56
|
-
|
|
57
|
-
const ID = componentSet.getIndex(eid);
|
|
58
|
-
const lastID = componentSet.getSize() - 1;
|
|
59
|
-
|
|
60
|
-
// Swap the component data only if is not the last one inserted
|
|
61
|
-
if (ID !== lastID) {
|
|
62
|
-
for (const key of Object.keys(componentData)) {
|
|
63
|
-
(componentData[key] as unknown[])[ID] = componentData[key][lastID];
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
componentSet.remove(eid);
|
|
68
|
-
|
|
69
|
-
// Plain arrays are trimmed to release references and stay in sync with the dense array.
|
|
70
|
-
// Float32Array slots beyond the current size are simply unused (unreachable via query/getIndex),
|
|
71
|
-
// so there's nothing to release and the capacity is kept as-is.
|
|
72
|
-
for (const key in componentData) {
|
|
73
|
-
if (!isNumeric[key]) {
|
|
74
|
-
(componentData[key] as unknown[]).pop();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function getDense(): number[] {
|
|
80
|
-
return componentSet.getDense();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function getData(): ComponentDataArrays<T> {
|
|
84
|
-
return componentData as ComponentDataArrays<T>;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function getIndex(eid: number): number {
|
|
88
|
-
return componentSet.getIndex(eid);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function getSize(): number {
|
|
92
|
-
return componentSet.getSize();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return { add, remove, getIndex, getDense, getData, getSize };
|
|
96
|
-
}
|
package/src/Ecs.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import type { QueryResult, StoreDataMap, StoreMap } from './types/index.js';
|
|
2
|
-
import type { IECS } from './types/IEcs.js';
|
|
3
|
-
import { ComponentStore } from './ComponentStore.js';
|
|
4
|
-
import { EntityManager } from './EntityManager.js';
|
|
5
|
-
import { createComponentRegistry } from './utils/ComponentRegistry.js';
|
|
6
|
-
|
|
7
|
-
export function ECS<T extends Record<string, Record<string, any>>>(): IECS<T> {
|
|
8
|
-
const ComponentRegistry = createComponentRegistry<keyof T & string>();
|
|
9
|
-
const Entities = EntityManager(ComponentRegistry);
|
|
10
|
-
const internalStores = {} as StoreMap<T>;
|
|
11
|
-
const components = {} as StoreDataMap<T>;
|
|
12
|
-
|
|
13
|
-
// Initialize the structure of the components used
|
|
14
|
-
function defineComponents<K extends readonly (keyof T)[]>(...names: K): void {
|
|
15
|
-
for (const name of names) {
|
|
16
|
-
if (name in internalStores) throw new Error(`Component ${String(name)} is already defined`);
|
|
17
|
-
|
|
18
|
-
ComponentRegistry.register(name as string);
|
|
19
|
-
internalStores[name] = ComponentStore<T[typeof name]>();
|
|
20
|
-
components[name] = internalStores[name].getData();
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function createEntity(): number {
|
|
25
|
-
return Entities.create();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function destroyEntity(eid: number): void {
|
|
29
|
-
const entityMask = Entities.getMask(eid);
|
|
30
|
-
|
|
31
|
-
let cid = 0;
|
|
32
|
-
let tempMask = entityMask;
|
|
33
|
-
|
|
34
|
-
// Bit operation on the entity mask to track the corresponding components and remove them
|
|
35
|
-
while (tempMask !== 0n) {
|
|
36
|
-
if ((tempMask & 1n) !== 0n) {
|
|
37
|
-
const name = ComponentRegistry.getName(cid);
|
|
38
|
-
internalStores[name].remove(eid);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
tempMask >>= 1n;
|
|
42
|
-
cid++;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
Entities.remove(eid);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function addComponent<K extends keyof T>(eid: number, name: K, data: T[K]): void {
|
|
49
|
-
Entities.addComponent(eid, name as keyof T & string);
|
|
50
|
-
internalStores[name].add(eid, data);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function removeComponent<K extends keyof T>(eid: number, name: K): void {
|
|
54
|
-
Entities.removeComponent(eid, name as keyof T & string);
|
|
55
|
-
internalStores[name].remove(eid);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Generator function to retrive the indexes of the entity components searched
|
|
59
|
-
function* query<C extends (keyof T)[]>(...componentName: C): Generator<QueryResult<C>> {
|
|
60
|
-
if (componentName.length === 0) return;
|
|
61
|
-
|
|
62
|
-
// Validate that every requested component is registered before touching internalStores,
|
|
63
|
-
// so an unknown name throws a clear error instead of a raw "undefined" access below.
|
|
64
|
-
let targetMask = 0n;
|
|
65
|
-
for (const name of componentName) {
|
|
66
|
-
targetMask |= 1n << BigInt(ComponentRegistry.getID(name as keyof T & string));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
let smallestStore = internalStores[componentName[0]];
|
|
70
|
-
let smallestSize = smallestStore.getSize();
|
|
71
|
-
|
|
72
|
-
for (let i = 1; i < componentName.length; i++) {
|
|
73
|
-
const store = internalStores[componentName[i]];
|
|
74
|
-
const size = store.getSize();
|
|
75
|
-
|
|
76
|
-
if (size < smallestSize) {
|
|
77
|
-
smallestStore = store;
|
|
78
|
-
smallestSize = size;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
for (const eid of smallestStore.getDense()) {
|
|
83
|
-
if ((Entities.getMask(eid) & targetMask) !== targetMask) continue;
|
|
84
|
-
|
|
85
|
-
const result = {} as any;
|
|
86
|
-
|
|
87
|
-
for (const name of componentName) {
|
|
88
|
-
const id = internalStores[name].getIndex(eid);
|
|
89
|
-
|
|
90
|
-
result[name] = { id };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
yield result as QueryResult<C>;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return { defineComponents, createEntity, destroyEntity, addComponent, removeComponent, query, components };
|
|
98
|
-
}
|
package/src/EntityManager.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import type { IComponentRegistry } from './types/IComponentRegistry.js';
|
|
2
|
-
import type { IEntityManager } from './types/IEntityManager.js';
|
|
3
|
-
|
|
4
|
-
export function EntityManager<K extends string>(registry: IComponentRegistry<K>): IEntityManager<K> {
|
|
5
|
-
let nextID: number = 0;
|
|
6
|
-
const recycledIDs: number[] = [];
|
|
7
|
-
const bitMasks: bigint[] = [];
|
|
8
|
-
|
|
9
|
-
function exists(eid: number): boolean {
|
|
10
|
-
const isWithinBounds = eid >= 0 && eid < nextID;
|
|
11
|
-
const hasMask = bitMasks[eid] !== undefined;
|
|
12
|
-
|
|
13
|
-
return isWithinBounds && hasMask;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function hasComponent(eid: number, name: K): boolean {
|
|
17
|
-
if (!exists(eid)) throw new Error(`Entity ${eid} does not exist`);
|
|
18
|
-
|
|
19
|
-
const cid = registry.getID(name);
|
|
20
|
-
return (bitMasks[eid] & (1n << BigInt(cid))) !== 0n;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function create(): number {
|
|
24
|
-
const ID = recycledIDs.length > 0 ? recycledIDs.pop()! : nextID++;
|
|
25
|
-
bitMasks[ID] = 0n;
|
|
26
|
-
|
|
27
|
-
return ID;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function remove(eid: number): void {
|
|
31
|
-
if (!exists(eid)) throw new Error(`Entity ${eid} does not exist`);
|
|
32
|
-
|
|
33
|
-
bitMasks[eid] = undefined as any;
|
|
34
|
-
recycledIDs.push(eid);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function addComponent(eid: number, name: K): void {
|
|
38
|
-
if (!exists(eid)) throw new Error(`Entity ${eid} does not exists`);
|
|
39
|
-
if (hasComponent(eid, name)) throw new Error(`Entity ${eid} already has component ${name}`);
|
|
40
|
-
|
|
41
|
-
const cid = registry.getID(name);
|
|
42
|
-
bitMasks[eid] |= 1n << BigInt(cid);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function removeComponent(eid: number, name: K): void {
|
|
46
|
-
if (!exists(eid)) throw new Error(`Entity ${eid} does not exists`);
|
|
47
|
-
if (!hasComponent(eid, name)) throw new Error(`Entity ${eid} does not have component ${name}`);
|
|
48
|
-
|
|
49
|
-
const cid = registry.getID(name);
|
|
50
|
-
bitMasks[eid] &= ~(1n << BigInt(cid));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function getMask(eid: number): bigint {
|
|
54
|
-
if (!exists(eid)) throw new Error(`Entity ${eid} does not exist`);
|
|
55
|
-
|
|
56
|
-
return bitMasks[eid];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return { exists, hasComponent, create, remove, addComponent, removeComponent, getMask };
|
|
60
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { ECS } from './Ecs.js';
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interface for managing component registration and ID mapping.
|
|
3
|
-
* Provides a centralized registry that maps component names to unique IDs
|
|
4
|
-
* and vice versa, used for efficient bitmask operations in the ECS.
|
|
5
|
-
*/
|
|
6
|
-
export interface IComponentRegistry<K extends string> {
|
|
7
|
-
/**
|
|
8
|
-
* Registers a new component by name.
|
|
9
|
-
* Assigns a unique incremental ID to the component if not already registered.
|
|
10
|
-
* @param name - The name of the component to register
|
|
11
|
-
*/
|
|
12
|
-
register: (name: K) => void;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Retrieves the unique ID associated with a component name.
|
|
16
|
-
* @param name - The name of the component
|
|
17
|
-
* @returns The unique ID assigned to the component
|
|
18
|
-
* @throws Error if the component is not registered
|
|
19
|
-
*/
|
|
20
|
-
getID: (name: K) => number;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Retrieves the component name associated with a given ID.
|
|
24
|
-
* @param cid - The unique ID of the component
|
|
25
|
-
* @returns The name of the component
|
|
26
|
-
* @throws Error if the ID is not registered
|
|
27
|
-
*/
|
|
28
|
-
getName: (cid: number) => K;
|
|
29
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import type { ComponentDataArrays } from './index.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Interface for storing and managing component data using Structure of Arrays (SoA) pattern.
|
|
5
|
-
* Each component type has its own store that maintains entity-component associations
|
|
6
|
-
* and stores component data in separate arrays per property for cache efficiency.
|
|
7
|
-
* @template T - The component data type (object with properties)
|
|
8
|
-
*/
|
|
9
|
-
export interface IComponentStore<T> {
|
|
10
|
-
/**
|
|
11
|
-
* Adds a component to an entity with the provided data.
|
|
12
|
-
* Stores each property of the component in separate arrays for efficient iteration.
|
|
13
|
-
* @param eid - The entity ID to add the component to
|
|
14
|
-
* @param data - The component data to store
|
|
15
|
-
* @throws Error if the entity already has this component
|
|
16
|
-
*/
|
|
17
|
-
add: (eid: number, data: T) => void;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Removes a component from an entity.
|
|
21
|
-
* Uses swap-and-pop technique to maintain dense arrays.
|
|
22
|
-
* @param eid - The entity ID to remove the component from
|
|
23
|
-
* @throws Error if the entity doesn't have this component
|
|
24
|
-
*/
|
|
25
|
-
remove: (eid: number) => void;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Gets the index of an entity's component data in the dense arrays.
|
|
29
|
-
* @param eid - The entity ID
|
|
30
|
-
* @returns The index in the data arrays where this entity's component data is stored
|
|
31
|
-
*/
|
|
32
|
-
getIndex: (eid: number) => number;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Returns the dense array of entity IDs that have this component.
|
|
36
|
-
* Useful for efficient iteration over all entities with this component.
|
|
37
|
-
* @returns Array of entity IDs
|
|
38
|
-
*/
|
|
39
|
-
getDense: () => number[];
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Returns the component data arrays organized by property.
|
|
43
|
-
* Each property of the component type has its own array.
|
|
44
|
-
* @returns Object mapping property names to their value arrays
|
|
45
|
-
*/
|
|
46
|
-
getData: () => ComponentDataArrays<T>;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Returns the number of entities that have this component.
|
|
50
|
-
* @returns The count of entities with this component
|
|
51
|
-
*/
|
|
52
|
-
getSize: () => number;
|
|
53
|
-
}
|