@ste_tisci/ecs 0.1.3 → 0.1.5
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/dist/index.cjs +247 -0
- package/{src/types/IEcs.d.ts → dist/index.d.cts} +36 -2
- package/dist/index.d.ts +97 -0
- package/dist/index.js +220 -0
- package/package.json +1 -1
- package/src/ComponentStore.ts +0 -64
- package/src/Ecs.ts +0 -94
- package/src/EntityManager.ts +0 -58
- 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/IEntityManager.d.ts +0 -64
- package/src/types/ISparseSet.d.ts +0 -49
- package/src/types/index.d.ts +0 -42
- 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.cjs
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ECS: () => ECS
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/utils/SparseSet.ts
|
|
28
|
+
function SparseSet() {
|
|
29
|
+
const sparse = [];
|
|
30
|
+
const dense = [];
|
|
31
|
+
let size = 0;
|
|
32
|
+
function has(eid) {
|
|
33
|
+
const ID = getIndex(eid);
|
|
34
|
+
const exist = ID !== void 0 && dense[ID] === eid;
|
|
35
|
+
const isWhitinBounds = ID >= 0 && ID < size;
|
|
36
|
+
return exist && isWhitinBounds;
|
|
37
|
+
}
|
|
38
|
+
function add(eid) {
|
|
39
|
+
if (has(eid)) throw new Error(`ID ${eid} already present in the set`);
|
|
40
|
+
sparse[eid] = size;
|
|
41
|
+
dense[size] = eid;
|
|
42
|
+
size++;
|
|
43
|
+
}
|
|
44
|
+
function remove(eid) {
|
|
45
|
+
if (!has(eid)) throw new Error(`Cannot remove ID ${eid} because does not exists`);
|
|
46
|
+
const ID = getIndex(eid);
|
|
47
|
+
const last = dense[size - 1];
|
|
48
|
+
dense[ID] = last;
|
|
49
|
+
sparse[last] = ID;
|
|
50
|
+
dense.pop();
|
|
51
|
+
size--;
|
|
52
|
+
delete sparse[eid];
|
|
53
|
+
}
|
|
54
|
+
function getIndex(eid) {
|
|
55
|
+
return sparse[eid];
|
|
56
|
+
}
|
|
57
|
+
function getDense() {
|
|
58
|
+
return dense;
|
|
59
|
+
}
|
|
60
|
+
function getSize() {
|
|
61
|
+
return size;
|
|
62
|
+
}
|
|
63
|
+
return { has, add, remove, getIndex, getDense, getSize };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/ComponentStore.ts
|
|
67
|
+
function ComponentStore() {
|
|
68
|
+
const componentSet = SparseSet();
|
|
69
|
+
const componentData = {};
|
|
70
|
+
function add(eid, data) {
|
|
71
|
+
if (componentSet.has(eid)) throw new Error(`Entity ${eid} already has this component`);
|
|
72
|
+
const ID = componentSet.getSize();
|
|
73
|
+
componentSet.add(eid);
|
|
74
|
+
for (const key in data) {
|
|
75
|
+
if (!componentData[key]) {
|
|
76
|
+
componentData[key] = [];
|
|
77
|
+
}
|
|
78
|
+
componentData[key][ID] = data[key];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function remove(eid) {
|
|
82
|
+
if (!componentSet.has(eid)) throw new Error(`Entity ${eid} does not have this component`);
|
|
83
|
+
const ID = componentSet.getIndex(eid);
|
|
84
|
+
const lastID = componentSet.getSize() - 1;
|
|
85
|
+
if (ID !== lastID) {
|
|
86
|
+
for (const key of Object.keys(componentData)) {
|
|
87
|
+
componentData[key][ID] = componentData[key][lastID];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
componentSet.remove(eid);
|
|
91
|
+
for (const key in componentData) {
|
|
92
|
+
componentData[key].pop();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function getDense() {
|
|
96
|
+
return componentSet.getDense();
|
|
97
|
+
}
|
|
98
|
+
function getData() {
|
|
99
|
+
return componentData;
|
|
100
|
+
}
|
|
101
|
+
function getIndex(eid) {
|
|
102
|
+
return componentSet.getIndex(eid);
|
|
103
|
+
}
|
|
104
|
+
function getSize() {
|
|
105
|
+
return componentSet.getSize();
|
|
106
|
+
}
|
|
107
|
+
return { add, remove, getIndex, getDense, getData, getSize };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/EntityManager.ts
|
|
111
|
+
function EntityManager(registry) {
|
|
112
|
+
let nextID = 0;
|
|
113
|
+
const recycledIDs = [];
|
|
114
|
+
const bitMasks = [];
|
|
115
|
+
function exists(eid) {
|
|
116
|
+
const isWithinBounds = eid >= 0 && eid < nextID;
|
|
117
|
+
const hasMask = bitMasks[eid] !== void 0;
|
|
118
|
+
return isWithinBounds && hasMask;
|
|
119
|
+
}
|
|
120
|
+
function hasComponent(eid, name) {
|
|
121
|
+
if (!exists(eid)) throw new Error(`Entity ${eid} does not exist`);
|
|
122
|
+
const cid = registry.getID(name);
|
|
123
|
+
return (bitMasks[eid] & 1n << BigInt(cid)) !== 0n;
|
|
124
|
+
}
|
|
125
|
+
function create() {
|
|
126
|
+
const ID = recycledIDs.length > 0 ? recycledIDs.pop() : nextID++;
|
|
127
|
+
bitMasks[ID] = 0n;
|
|
128
|
+
return ID;
|
|
129
|
+
}
|
|
130
|
+
function remove(eid) {
|
|
131
|
+
if (!exists(eid)) throw new Error(`Entity ${eid} does not exist`);
|
|
132
|
+
bitMasks[eid] = void 0;
|
|
133
|
+
recycledIDs.push(eid);
|
|
134
|
+
}
|
|
135
|
+
function addComponent(eid, name) {
|
|
136
|
+
if (!exists(eid)) throw new Error(`Entity ${eid} does not exists`);
|
|
137
|
+
if (hasComponent(eid, name)) throw new Error(`Entity ${eid} already has component ${name}`);
|
|
138
|
+
const cid = registry.getID(name);
|
|
139
|
+
bitMasks[eid] |= 1n << BigInt(cid);
|
|
140
|
+
}
|
|
141
|
+
function removeComponent(eid, name) {
|
|
142
|
+
if (!exists(eid)) throw new Error(`Entity ${eid} does not exists`);
|
|
143
|
+
if (!hasComponent(eid, name)) throw new Error(`Entity ${eid} does not have component ${name}`);
|
|
144
|
+
const cid = registry.getID(name);
|
|
145
|
+
bitMasks[eid] &= ~(1n << BigInt(cid));
|
|
146
|
+
}
|
|
147
|
+
function getMask(eid) {
|
|
148
|
+
return bitMasks[eid];
|
|
149
|
+
}
|
|
150
|
+
return { exists, hasComponent, create, remove, addComponent, removeComponent, getMask };
|
|
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 = {};
|
|
183
|
+
const components = {};
|
|
184
|
+
function defineComponents(...names) {
|
|
185
|
+
for (const name of names) {
|
|
186
|
+
ComponentRegistry.register(name);
|
|
187
|
+
internalStores[name] = ComponentStore();
|
|
188
|
+
components[name] = internalStores[name].getData();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function createEntity() {
|
|
192
|
+
return Entities.create();
|
|
193
|
+
}
|
|
194
|
+
function destroyEntity(eid) {
|
|
195
|
+
const entityMask = Entities.getMask(eid);
|
|
196
|
+
let cid = 0;
|
|
197
|
+
let tempMask = entityMask;
|
|
198
|
+
while (tempMask !== 0n) {
|
|
199
|
+
if ((tempMask & 1n) !== 0n) {
|
|
200
|
+
const name = ComponentRegistry.getName(cid);
|
|
201
|
+
internalStores[name].remove(eid);
|
|
202
|
+
}
|
|
203
|
+
tempMask >>= 1n;
|
|
204
|
+
cid++;
|
|
205
|
+
}
|
|
206
|
+
Entities.remove(eid);
|
|
207
|
+
}
|
|
208
|
+
function addComponent(eid, name, data) {
|
|
209
|
+
Entities.addComponent(eid, name);
|
|
210
|
+
internalStores[name].add(eid, data);
|
|
211
|
+
}
|
|
212
|
+
function removeComponent(eid, name) {
|
|
213
|
+
Entities.removeComponent(eid, name);
|
|
214
|
+
internalStores[name].remove(eid);
|
|
215
|
+
}
|
|
216
|
+
function* query(...componentName) {
|
|
217
|
+
if (componentName.length === 0) return;
|
|
218
|
+
let smallestStore = internalStores[componentName[0]];
|
|
219
|
+
let smallestSize = smallestStore.getSize();
|
|
220
|
+
for (let i = 1; i < componentName.length; i++) {
|
|
221
|
+
const store = internalStores[componentName[i]];
|
|
222
|
+
const size = store.getSize();
|
|
223
|
+
if (size < smallestSize) {
|
|
224
|
+
smallestStore = store;
|
|
225
|
+
smallestSize = size;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
let targetMask = 0n;
|
|
229
|
+
for (const name of componentName) {
|
|
230
|
+
targetMask |= 1n << BigInt(ComponentRegistry.getID(name));
|
|
231
|
+
}
|
|
232
|
+
for (const eid of smallestStore.getDense()) {
|
|
233
|
+
if ((Entities.getMask(eid) & targetMask) !== targetMask) continue;
|
|
234
|
+
const result = {};
|
|
235
|
+
for (const name of componentName) {
|
|
236
|
+
const id = internalStores[name].getIndex(eid);
|
|
237
|
+
result[name] = { id };
|
|
238
|
+
}
|
|
239
|
+
yield result;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { defineComponents, createEntity, destroyEntity, addComponent, removeComponent, query, components };
|
|
243
|
+
}
|
|
244
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
245
|
+
0 && (module.exports = {
|
|
246
|
+
ECS
|
|
247
|
+
});
|
|
@@ -1,4 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Represents component data in Structure of Arrays (SoA) format.
|
|
3
|
+
* Each property of the component is stored in a separate array,
|
|
4
|
+
* enabling cache-efficient iteration and SIMD operations.
|
|
5
|
+
* @template T - The component data type
|
|
6
|
+
* @example
|
|
7
|
+
* // For Position component: { x: number, y: number }
|
|
8
|
+
* // ComponentDataArrays would be: { x: number[], y: number[] }
|
|
9
|
+
*/
|
|
10
|
+
type ComponentDataArrays<T> = {
|
|
11
|
+
[K in keyof T]: T[K][];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Maps component names to their data arrays for direct access.
|
|
16
|
+
* Used to expose component data to the user without store methods.
|
|
17
|
+
* @template T - Record type defining all component types
|
|
18
|
+
*/
|
|
19
|
+
type StoreDataMap<T> = {
|
|
20
|
+
[K in keyof T]: ComponentDataArrays<T[K]>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Type helper for the query result.
|
|
25
|
+
* Creates an object with eid and all required components typed correctly.
|
|
26
|
+
* @template T - Record type with all component types.
|
|
27
|
+
* @template C - Array of the component names requested in the query.
|
|
28
|
+
*/
|
|
29
|
+
type QueryResult<T, C extends (keyof T)[]> = {
|
|
30
|
+
[K in C[number]]: { id: number };
|
|
31
|
+
};
|
|
2
32
|
|
|
3
33
|
/**
|
|
4
34
|
* Interface for the main ECS registry.
|
|
@@ -6,7 +36,7 @@ import type { QueryResult, StoreDataMap } from './index.js';
|
|
|
6
36
|
* coordinating between EntityManager and ComponentStores.
|
|
7
37
|
* @template T - Record type defining all component types in the ECS
|
|
8
38
|
*/
|
|
9
|
-
|
|
39
|
+
interface IECS<T> {
|
|
10
40
|
/**
|
|
11
41
|
* Initializes the ECS with component definitions.
|
|
12
42
|
* Registers all components and creates their corresponding stores.
|
|
@@ -61,3 +91,7 @@ export interface IECS<T> {
|
|
|
61
91
|
|
|
62
92
|
components: StoreDataMap<T>;
|
|
63
93
|
}
|
|
94
|
+
|
|
95
|
+
declare function ECS<T extends Record<string, Record<string, any>>>(): IECS<T>;
|
|
96
|
+
|
|
97
|
+
export { ECS };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents component data in Structure of Arrays (SoA) format.
|
|
3
|
+
* Each property of the component is stored in a separate array,
|
|
4
|
+
* enabling cache-efficient iteration and SIMD operations.
|
|
5
|
+
* @template T - The component data type
|
|
6
|
+
* @example
|
|
7
|
+
* // For Position component: { x: number, y: number }
|
|
8
|
+
* // ComponentDataArrays would be: { x: number[], y: number[] }
|
|
9
|
+
*/
|
|
10
|
+
type ComponentDataArrays<T> = {
|
|
11
|
+
[K in keyof T]: T[K][];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Maps component names to their data arrays for direct access.
|
|
16
|
+
* Used to expose component data to the user without store methods.
|
|
17
|
+
* @template T - Record type defining all component types
|
|
18
|
+
*/
|
|
19
|
+
type StoreDataMap<T> = {
|
|
20
|
+
[K in keyof T]: ComponentDataArrays<T[K]>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Type helper for the query result.
|
|
25
|
+
* Creates an object with eid and all required components typed correctly.
|
|
26
|
+
* @template T - Record type with all component types.
|
|
27
|
+
* @template C - Array of the component names requested in the query.
|
|
28
|
+
*/
|
|
29
|
+
type QueryResult<T, C extends (keyof T)[]> = {
|
|
30
|
+
[K in C[number]]: { id: number };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Interface for the main ECS registry.
|
|
35
|
+
* Provides high-level API for entity-component management,
|
|
36
|
+
* coordinating between EntityManager and ComponentStores.
|
|
37
|
+
* @template T - Record type defining all component types in the ECS
|
|
38
|
+
*/
|
|
39
|
+
interface IECS<T> {
|
|
40
|
+
/**
|
|
41
|
+
* Initializes the ECS with component definitions.
|
|
42
|
+
* Registers all components and creates their corresponding stores.
|
|
43
|
+
* Must be called before any other operations.
|
|
44
|
+
* @param names -component names
|
|
45
|
+
*/
|
|
46
|
+
defineComponents<K extends readonly (keyof T)[]>(...names: K): void;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates a new entity in the ECS.
|
|
50
|
+
* @returns The ID of the newly created entity
|
|
51
|
+
*/
|
|
52
|
+
createEntity: () => number;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Completely removes an entity and all its components from the ECS.
|
|
56
|
+
* Cleans up all component data and entity records.
|
|
57
|
+
* @param eid - The entity ID to destroy
|
|
58
|
+
*/
|
|
59
|
+
destroyEntity: (eid: number) => void;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Adds a component to an entity with the provided data.
|
|
63
|
+
* Updates both the entity's bitmask and the component store.
|
|
64
|
+
* @template K - The component name type (key of T)
|
|
65
|
+
* @param eid - The entity ID
|
|
66
|
+
* @param name - The component name
|
|
67
|
+
* @param data - The component data to store
|
|
68
|
+
*/
|
|
69
|
+
addComponent: <K extends keyof T>(eid: number, name: K, data: T[K]) => void;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Removes a component from an entity.
|
|
73
|
+
* Updates both the entity's bitmask and the component store.
|
|
74
|
+
* @template K - The component name type (key of T)
|
|
75
|
+
* @param eid - The entity ID
|
|
76
|
+
* @param name - The component name
|
|
77
|
+
*/
|
|
78
|
+
removeComponent: <K extends keyof T>(eid: number, name: K) => void;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Retrieves all entities that contain the specified set of components.
|
|
82
|
+
* Performs a filtered search across component stores and yields each matching entity.
|
|
83
|
+
* The returned generator allows efficient iteration without allocating large arrays.
|
|
84
|
+
*
|
|
85
|
+
* @template C - Array of component names to query for
|
|
86
|
+
* @param components - List of component names that the entity must include
|
|
87
|
+
* @returns A generator that yields an object for each matching entity,
|
|
88
|
+
* containing the entity ID and the related component data
|
|
89
|
+
*/
|
|
90
|
+
query: <C extends (keyof T)[]>(...componentName: C) => Generator<QueryResult<T, C>>;
|
|
91
|
+
|
|
92
|
+
components: StoreDataMap<T>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
declare function ECS<T extends Record<string, Record<string, any>>>(): IECS<T>;
|
|
96
|
+
|
|
97
|
+
export { ECS };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
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
|
+
return { has, add, remove, getIndex, getDense, getSize };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/ComponentStore.ts
|
|
41
|
+
function ComponentStore() {
|
|
42
|
+
const componentSet = SparseSet();
|
|
43
|
+
const componentData = {};
|
|
44
|
+
function add(eid, data) {
|
|
45
|
+
if (componentSet.has(eid)) throw new Error(`Entity ${eid} already has this component`);
|
|
46
|
+
const ID = componentSet.getSize();
|
|
47
|
+
componentSet.add(eid);
|
|
48
|
+
for (const key in data) {
|
|
49
|
+
if (!componentData[key]) {
|
|
50
|
+
componentData[key] = [];
|
|
51
|
+
}
|
|
52
|
+
componentData[key][ID] = data[key];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function remove(eid) {
|
|
56
|
+
if (!componentSet.has(eid)) throw new Error(`Entity ${eid} does not have this component`);
|
|
57
|
+
const ID = componentSet.getIndex(eid);
|
|
58
|
+
const lastID = componentSet.getSize() - 1;
|
|
59
|
+
if (ID !== lastID) {
|
|
60
|
+
for (const key of Object.keys(componentData)) {
|
|
61
|
+
componentData[key][ID] = componentData[key][lastID];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
componentSet.remove(eid);
|
|
65
|
+
for (const key in componentData) {
|
|
66
|
+
componentData[key].pop();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function getDense() {
|
|
70
|
+
return componentSet.getDense();
|
|
71
|
+
}
|
|
72
|
+
function getData() {
|
|
73
|
+
return componentData;
|
|
74
|
+
}
|
|
75
|
+
function getIndex(eid) {
|
|
76
|
+
return componentSet.getIndex(eid);
|
|
77
|
+
}
|
|
78
|
+
function getSize() {
|
|
79
|
+
return componentSet.getSize();
|
|
80
|
+
}
|
|
81
|
+
return { add, remove, getIndex, getDense, getData, getSize };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/EntityManager.ts
|
|
85
|
+
function EntityManager(registry) {
|
|
86
|
+
let nextID = 0;
|
|
87
|
+
const recycledIDs = [];
|
|
88
|
+
const bitMasks = [];
|
|
89
|
+
function exists(eid) {
|
|
90
|
+
const isWithinBounds = eid >= 0 && eid < nextID;
|
|
91
|
+
const hasMask = bitMasks[eid] !== void 0;
|
|
92
|
+
return isWithinBounds && hasMask;
|
|
93
|
+
}
|
|
94
|
+
function hasComponent(eid, name) {
|
|
95
|
+
if (!exists(eid)) throw new Error(`Entity ${eid} does not exist`);
|
|
96
|
+
const cid = registry.getID(name);
|
|
97
|
+
return (bitMasks[eid] & 1n << BigInt(cid)) !== 0n;
|
|
98
|
+
}
|
|
99
|
+
function create() {
|
|
100
|
+
const ID = recycledIDs.length > 0 ? recycledIDs.pop() : nextID++;
|
|
101
|
+
bitMasks[ID] = 0n;
|
|
102
|
+
return ID;
|
|
103
|
+
}
|
|
104
|
+
function remove(eid) {
|
|
105
|
+
if (!exists(eid)) throw new Error(`Entity ${eid} does not exist`);
|
|
106
|
+
bitMasks[eid] = void 0;
|
|
107
|
+
recycledIDs.push(eid);
|
|
108
|
+
}
|
|
109
|
+
function addComponent(eid, name) {
|
|
110
|
+
if (!exists(eid)) throw new Error(`Entity ${eid} does not exists`);
|
|
111
|
+
if (hasComponent(eid, name)) throw new Error(`Entity ${eid} already has component ${name}`);
|
|
112
|
+
const cid = registry.getID(name);
|
|
113
|
+
bitMasks[eid] |= 1n << BigInt(cid);
|
|
114
|
+
}
|
|
115
|
+
function removeComponent(eid, name) {
|
|
116
|
+
if (!exists(eid)) throw new Error(`Entity ${eid} does not exists`);
|
|
117
|
+
if (!hasComponent(eid, name)) throw new Error(`Entity ${eid} does not have component ${name}`);
|
|
118
|
+
const cid = registry.getID(name);
|
|
119
|
+
bitMasks[eid] &= ~(1n << BigInt(cid));
|
|
120
|
+
}
|
|
121
|
+
function getMask(eid) {
|
|
122
|
+
return bitMasks[eid];
|
|
123
|
+
}
|
|
124
|
+
return { exists, hasComponent, create, remove, addComponent, removeComponent, getMask };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/utils/ComponentRegistry.ts
|
|
128
|
+
function createComponentRegistry() {
|
|
129
|
+
const nameToID = /* @__PURE__ */ new Map();
|
|
130
|
+
const IDtoName = /* @__PURE__ */ new Map();
|
|
131
|
+
let nextID = 0;
|
|
132
|
+
function register(name) {
|
|
133
|
+
if (!nameToID.has(name)) {
|
|
134
|
+
const eid = nextID++;
|
|
135
|
+
nameToID.set(name, eid);
|
|
136
|
+
IDtoName.set(eid, name);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function getID(name) {
|
|
140
|
+
const eid = nameToID.get(name);
|
|
141
|
+
if (eid === void 0) throw new Error(`Component ${name} not registered`);
|
|
142
|
+
return eid;
|
|
143
|
+
}
|
|
144
|
+
function getName(cid) {
|
|
145
|
+
const name = IDtoName.get(cid);
|
|
146
|
+
if (!name) throw new Error(`Component ID ${cid} not registered`);
|
|
147
|
+
return name;
|
|
148
|
+
}
|
|
149
|
+
return { register, getID, getName };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/Ecs.ts
|
|
153
|
+
function ECS() {
|
|
154
|
+
const ComponentRegistry = createComponentRegistry();
|
|
155
|
+
const Entities = EntityManager(ComponentRegistry);
|
|
156
|
+
const internalStores = {};
|
|
157
|
+
const components = {};
|
|
158
|
+
function defineComponents(...names) {
|
|
159
|
+
for (const name of names) {
|
|
160
|
+
ComponentRegistry.register(name);
|
|
161
|
+
internalStores[name] = ComponentStore();
|
|
162
|
+
components[name] = internalStores[name].getData();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function createEntity() {
|
|
166
|
+
return Entities.create();
|
|
167
|
+
}
|
|
168
|
+
function destroyEntity(eid) {
|
|
169
|
+
const entityMask = Entities.getMask(eid);
|
|
170
|
+
let cid = 0;
|
|
171
|
+
let tempMask = entityMask;
|
|
172
|
+
while (tempMask !== 0n) {
|
|
173
|
+
if ((tempMask & 1n) !== 0n) {
|
|
174
|
+
const name = ComponentRegistry.getName(cid);
|
|
175
|
+
internalStores[name].remove(eid);
|
|
176
|
+
}
|
|
177
|
+
tempMask >>= 1n;
|
|
178
|
+
cid++;
|
|
179
|
+
}
|
|
180
|
+
Entities.remove(eid);
|
|
181
|
+
}
|
|
182
|
+
function addComponent(eid, name, data) {
|
|
183
|
+
Entities.addComponent(eid, name);
|
|
184
|
+
internalStores[name].add(eid, data);
|
|
185
|
+
}
|
|
186
|
+
function removeComponent(eid, name) {
|
|
187
|
+
Entities.removeComponent(eid, name);
|
|
188
|
+
internalStores[name].remove(eid);
|
|
189
|
+
}
|
|
190
|
+
function* query(...componentName) {
|
|
191
|
+
if (componentName.length === 0) return;
|
|
192
|
+
let smallestStore = internalStores[componentName[0]];
|
|
193
|
+
let smallestSize = smallestStore.getSize();
|
|
194
|
+
for (let i = 1; i < componentName.length; i++) {
|
|
195
|
+
const store = internalStores[componentName[i]];
|
|
196
|
+
const size = store.getSize();
|
|
197
|
+
if (size < smallestSize) {
|
|
198
|
+
smallestStore = store;
|
|
199
|
+
smallestSize = size;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
let targetMask = 0n;
|
|
203
|
+
for (const name of componentName) {
|
|
204
|
+
targetMask |= 1n << BigInt(ComponentRegistry.getID(name));
|
|
205
|
+
}
|
|
206
|
+
for (const eid of smallestStore.getDense()) {
|
|
207
|
+
if ((Entities.getMask(eid) & targetMask) !== targetMask) continue;
|
|
208
|
+
const result = {};
|
|
209
|
+
for (const name of componentName) {
|
|
210
|
+
const id = internalStores[name].getIndex(eid);
|
|
211
|
+
result[name] = { id };
|
|
212
|
+
}
|
|
213
|
+
yield result;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return { defineComponents, createEntity, destroyEntity, addComponent, removeComponent, query, components };
|
|
217
|
+
}
|
|
218
|
+
export {
|
|
219
|
+
ECS
|
|
220
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ste_tisci/ecs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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",
|
package/src/ComponentStore.ts
DELETED
|
@@ -1,64 +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
|
-
export function ComponentStore<T extends Record<string, any>>(): IComponentStore<T> {
|
|
6
|
-
const componentSet = SparseSet();
|
|
7
|
-
const componentData = {} as ComponentDataArrays<T>;
|
|
8
|
-
|
|
9
|
-
function add(eid: number, data: T): void {
|
|
10
|
-
if (componentSet.has(eid)) throw new Error(`Entity ${eid} already has this component`);
|
|
11
|
-
|
|
12
|
-
// Use the current size (next index) to maintain data coherence with the dense array
|
|
13
|
-
const ID = componentSet.getSize();
|
|
14
|
-
|
|
15
|
-
componentSet.add(eid);
|
|
16
|
-
|
|
17
|
-
// Store each property of the component into its corresponding array
|
|
18
|
-
for (const key in data) {
|
|
19
|
-
if (!componentData[key]) {
|
|
20
|
-
componentData[key] = [] as T[typeof key][];
|
|
21
|
-
}
|
|
22
|
-
componentData[key][ID] = data[key];
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function remove(eid: number): void {
|
|
27
|
-
if (!componentSet.has(eid)) throw new Error(`Entity ${eid} does not have this component`);
|
|
28
|
-
|
|
29
|
-
const ID = componentSet.getIndex(eid);
|
|
30
|
-
const lastID = componentSet.getSize() - 1;
|
|
31
|
-
|
|
32
|
-
// Swap the component data only if is not the last one inserted
|
|
33
|
-
if (ID !== lastID) {
|
|
34
|
-
for (const key of Object.keys(componentData)) {
|
|
35
|
-
componentData[key][ID] = componentData[key][lastID];
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
componentSet.remove(eid);
|
|
40
|
-
|
|
41
|
-
// Keep component arrays dense and in sync with the SparseSet's dense array
|
|
42
|
-
for (const key in componentData) {
|
|
43
|
-
componentData[key].pop();
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function getDense(): number[] {
|
|
48
|
-
return componentSet.getDense();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function getData(): ComponentDataArrays<T> {
|
|
52
|
-
return componentData;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function getIndex(eid: number): number {
|
|
56
|
-
return componentSet.getIndex(eid);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function getSize(): number {
|
|
60
|
-
return componentSet.getSize();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return { add, remove, getIndex, getDense, getData, getSize };
|
|
64
|
-
}
|
package/src/Ecs.ts
DELETED
|
@@ -1,94 +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
|
-
ComponentRegistry.register(name as string);
|
|
17
|
-
internalStores[name] = ComponentStore<T[typeof name]>();
|
|
18
|
-
components[name] = internalStores[name].getData();
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function createEntity(): number {
|
|
23
|
-
return Entities.create();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function destroyEntity(eid: number): void {
|
|
27
|
-
const entityMask = Entities.getMask(eid);
|
|
28
|
-
|
|
29
|
-
let cid = 0;
|
|
30
|
-
let tempMask = entityMask;
|
|
31
|
-
|
|
32
|
-
// Bit operation on the entity mask to track the corresponding components and remove them
|
|
33
|
-
while (tempMask !== 0n) {
|
|
34
|
-
if ((tempMask & 1n) !== 0n) {
|
|
35
|
-
const name = ComponentRegistry.getName(cid);
|
|
36
|
-
internalStores[name].remove(eid);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
tempMask >>= 1n;
|
|
40
|
-
cid++;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
Entities.remove(eid);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function addComponent<K extends keyof T>(eid: number, name: K, data: T[K]): void {
|
|
47
|
-
Entities.addComponent(eid, name as keyof T & string);
|
|
48
|
-
internalStores[name].add(eid, data);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function removeComponent<K extends keyof T>(eid: number, name: K): void {
|
|
52
|
-
Entities.removeComponent(eid, name as keyof T & string);
|
|
53
|
-
internalStores[name].remove(eid);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Generator function to retrive the indexes of the entity components searched
|
|
57
|
-
function* query<C extends (keyof T)[]>(...componentName: C): Generator<QueryResult<T, C>> {
|
|
58
|
-
if (componentName.length === 0) return;
|
|
59
|
-
|
|
60
|
-
let smallestStore = internalStores[componentName[0]];
|
|
61
|
-
let smallestSize = smallestStore.getSize();
|
|
62
|
-
|
|
63
|
-
for (let i = 1; i < componentName.length; i++) {
|
|
64
|
-
const store = internalStores[componentName[i]];
|
|
65
|
-
const size = store.getSize();
|
|
66
|
-
|
|
67
|
-
if (size < smallestSize) {
|
|
68
|
-
smallestStore = store;
|
|
69
|
-
smallestSize = size;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
let targetMask = 0n;
|
|
74
|
-
for (const name of componentName) {
|
|
75
|
-
targetMask |= 1n << BigInt(ComponentRegistry.getID(name as keyof T & string));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
for (const eid of smallestStore.getDense()) {
|
|
79
|
-
if ((Entities.getMask(eid) & targetMask) !== targetMask) continue;
|
|
80
|
-
|
|
81
|
-
const result = {} as any;
|
|
82
|
-
|
|
83
|
-
for (const name of componentName) {
|
|
84
|
-
const id = internalStores[name].getIndex(eid);
|
|
85
|
-
|
|
86
|
-
result[name] = { id };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
yield result as QueryResult<T, C>;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return { defineComponents, createEntity, destroyEntity, addComponent, removeComponent, query, components };
|
|
94
|
-
}
|
package/src/EntityManager.ts
DELETED
|
@@ -1,58 +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
|
-
return bitMasks[eid];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return { exists, hasComponent, create, remove, addComponent, removeComponent, getMask };
|
|
58
|
-
}
|
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
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interface for managing entities and their component associations.
|
|
3
|
-
* Uses bitmasks to efficiently track which components each entity has,
|
|
4
|
-
* enabling fast component checks and queries.
|
|
5
|
-
*/
|
|
6
|
-
export interface IEntityManager<K extends string> {
|
|
7
|
-
/**
|
|
8
|
-
* Checks if an entity exists in the system.
|
|
9
|
-
* @param eid - The entity ID to check
|
|
10
|
-
* @returns True if the entity exists, false otherwise
|
|
11
|
-
*/
|
|
12
|
-
exists: (eid: number) => boolean;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Checks if an entity has a specific component.
|
|
16
|
-
* Uses bitwise operations on the entity's bitmask for fast lookups.
|
|
17
|
-
* @param eid - The entity ID
|
|
18
|
-
* @param name - The component name
|
|
19
|
-
* @returns True if the entity has the component, false otherwise
|
|
20
|
-
*/
|
|
21
|
-
hasComponent: (eid: number, name: K) => boolean;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Creates a new entity.
|
|
25
|
-
* Recycles entity IDs when possible, otherwise assigns a new incremental ID.
|
|
26
|
-
* @returns The ID of the newly created entity
|
|
27
|
-
*/
|
|
28
|
-
create: () => number;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Removes an entity from the system.
|
|
32
|
-
* Marks the entity ID for recycling.
|
|
33
|
-
* @param eid - The entity ID to remove
|
|
34
|
-
* @throws Error if the entity doesn't exist
|
|
35
|
-
*/
|
|
36
|
-
remove: (eid: number) => void;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Adds a component flag to an entity's bitmask.
|
|
40
|
-
* Updates the entity's bitmask to indicate it has the specified component.
|
|
41
|
-
* @param eid - The entity ID
|
|
42
|
-
* @param name - The component name
|
|
43
|
-
* @throws Error if the entity doesn't exist or already has the component
|
|
44
|
-
*/
|
|
45
|
-
addComponent: (eid: number, name: K) => void;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Removes a component flag from an entity's bitmask.
|
|
49
|
-
* Updates the entity's bitmask to indicate it no longer has the specified component.
|
|
50
|
-
* @param eid - The entity ID
|
|
51
|
-
* @param name - The component name
|
|
52
|
-
* @throws Error if the entity doesn't exist
|
|
53
|
-
*/
|
|
54
|
-
removeComponent: (eid: number, name: K) => void;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Retrieves the complete bitmask for an entity.
|
|
58
|
-
* The bitmask represents all components the entity has,
|
|
59
|
-
* with each bit corresponding to a component ID.
|
|
60
|
-
* @param eid - The entity ID
|
|
61
|
-
* @returns The entity's component bitmask
|
|
62
|
-
*/
|
|
63
|
-
getMask: (eid: number) => bigint;
|
|
64
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interface used for fast entity lookups, insertion and removal.
|
|
3
|
-
*
|
|
4
|
-
* Maintain two parallel arrays:
|
|
5
|
-
*
|
|
6
|
-
* - sparse: a direct lookup table that maps an entity ID (eid) to its index in the dense array.
|
|
7
|
-
* - dense: a compact array that stores all active entity IDs contiguously.
|
|
8
|
-
*/
|
|
9
|
-
export interface ISparseSet {
|
|
10
|
-
/**
|
|
11
|
-
* Check if an entity ID exists in the set
|
|
12
|
-
* @param eid The entity ID to check
|
|
13
|
-
* @returns True if the entity ID exists, false otherwise
|
|
14
|
-
*/
|
|
15
|
-
has: (eid: number) => boolean;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Add the entity ID to the set
|
|
19
|
-
* @param eid The entity ID to add
|
|
20
|
-
* @thows Error if the entity ID is already been added
|
|
21
|
-
*/
|
|
22
|
-
add: (eid: number) => void;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Remove an Entity ID from the set
|
|
26
|
-
* @param eid The entity ID to remove
|
|
27
|
-
* @throws Error if the ID does not exists in the set
|
|
28
|
-
*/
|
|
29
|
-
remove: (eid: number) => void;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Retrive the index of the entity ID in the set
|
|
33
|
-
* @param eid The entity ID
|
|
34
|
-
* @returns The index corresponding to an entity ID in the set
|
|
35
|
-
*/
|
|
36
|
-
getIndex: (eid: number) => number;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Retrive the full dense array that contain all the entity IDs of the set
|
|
40
|
-
* @returns an Array with all the current entity IDs
|
|
41
|
-
*/
|
|
42
|
-
getDense: () => number[];
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Retrive the size representing the number of active entities in the set.
|
|
46
|
-
* @returns The current size of the Dense array
|
|
47
|
-
*/
|
|
48
|
-
getSize: () => number;
|
|
49
|
-
}
|
package/src/types/index.d.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { IComponentStore } from './IComponentStore.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Represents component data in Structure of Arrays (SoA) format.
|
|
5
|
-
* Each property of the component is stored in a separate array,
|
|
6
|
-
* enabling cache-efficient iteration and SIMD operations.
|
|
7
|
-
* @template T - The component data type
|
|
8
|
-
* @example
|
|
9
|
-
* // For Position component: { x: number, y: number }
|
|
10
|
-
* // ComponentDataArrays would be: { x: number[], y: number[] }
|
|
11
|
-
*/
|
|
12
|
-
export type ComponentDataArrays<T> = {
|
|
13
|
-
[K in keyof T]: T[K][];
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Maps component names to their corresponding ComponentStore instances.
|
|
18
|
-
* Used internally by CreateWorld to manage all component stores.
|
|
19
|
-
* @template T - Record type defining all component types
|
|
20
|
-
*/
|
|
21
|
-
export type StoreMap<T> = {
|
|
22
|
-
[K in keyof T]: IComponentStore<T[K]>;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Maps component names to their data arrays for direct access.
|
|
27
|
-
* Used to expose component data to the user without store methods.
|
|
28
|
-
* @template T - Record type defining all component types
|
|
29
|
-
*/
|
|
30
|
-
export type StoreDataMap<T> = {
|
|
31
|
-
[K in keyof T]: ComponentDataArrays<T[K]>;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Type helper for the query result.
|
|
36
|
-
* Creates an object with eid and all required components typed correctly.
|
|
37
|
-
* @template T - Record type with all component types.
|
|
38
|
-
* @template C - Array of the component names requested in the query.
|
|
39
|
-
*/
|
|
40
|
-
export type QueryResult<T, C extends (keyof T)[]> = {
|
|
41
|
-
[K in C[number]]: { id: number };
|
|
42
|
-
};
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { IComponentRegistry } from '../types/IComponentRegistry.js';
|
|
2
|
-
|
|
3
|
-
export function createComponentRegistry<K extends string>(): IComponentRegistry<K> {
|
|
4
|
-
const nameToID = new Map<K, number>();
|
|
5
|
-
const IDtoName = new Map<number, K>();
|
|
6
|
-
let nextID: number = 0;
|
|
7
|
-
|
|
8
|
-
function register(name: K): void {
|
|
9
|
-
if (!nameToID.has(name)) {
|
|
10
|
-
const eid = nextID++;
|
|
11
|
-
|
|
12
|
-
nameToID.set(name, eid);
|
|
13
|
-
IDtoName.set(eid, name);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function getID(name: K): number {
|
|
18
|
-
const eid = nameToID.get(name);
|
|
19
|
-
if (eid === undefined) throw new Error(`Component ${name} not registered`);
|
|
20
|
-
|
|
21
|
-
return eid;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function getName(cid: number): K {
|
|
25
|
-
const name = IDtoName.get(cid);
|
|
26
|
-
if (!name) throw new Error(`Component ID ${cid} not registered`);
|
|
27
|
-
|
|
28
|
-
return name;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return { register, getID, getName };
|
|
32
|
-
}
|
package/src/utils/SparseSet.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { ISparseSet } from '../types/ISparseSet.js';
|
|
2
|
-
|
|
3
|
-
export function SparseSet(): ISparseSet {
|
|
4
|
-
const sparse: number[] = [];
|
|
5
|
-
const dense: number[] = [];
|
|
6
|
-
let size = 0;
|
|
7
|
-
|
|
8
|
-
function has(eid: number): boolean {
|
|
9
|
-
const ID = getIndex(eid);
|
|
10
|
-
|
|
11
|
-
const exist = ID !== undefined && dense[ID] === eid;
|
|
12
|
-
const isWhitinBounds = ID >= 0 && ID < size;
|
|
13
|
-
|
|
14
|
-
return exist && isWhitinBounds;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function add(eid: number): void {
|
|
18
|
-
if (has(eid)) throw new Error(`ID ${eid} already present in the set`);
|
|
19
|
-
|
|
20
|
-
sparse[eid] = size;
|
|
21
|
-
dense[size] = eid;
|
|
22
|
-
size++;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function remove(eid: number): void {
|
|
26
|
-
if (!has(eid)) throw new Error(`Cannot remove ID ${eid} because does not exists`);
|
|
27
|
-
|
|
28
|
-
const ID = getIndex(eid);
|
|
29
|
-
const last = dense[size - 1];
|
|
30
|
-
|
|
31
|
-
// Swap with the last element inserted
|
|
32
|
-
dense[ID] = last;
|
|
33
|
-
sparse[last] = ID;
|
|
34
|
-
|
|
35
|
-
// Remove the element
|
|
36
|
-
dense.pop();
|
|
37
|
-
size--;
|
|
38
|
-
delete sparse[eid];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function getIndex(eid: number): number {
|
|
42
|
-
return sparse[eid];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function getDense(): number[] {
|
|
46
|
-
return dense;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function getSize(): number {
|
|
50
|
-
return size;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return { has, add, remove, getIndex, getDense, getSize };
|
|
54
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "es2024",
|
|
4
|
-
"allowSyntheticDefaultImports": true,
|
|
5
|
-
"esModuleInterop": true,
|
|
6
|
-
"outDir": "dist",
|
|
7
|
-
"forceConsistentCasingInFileNames": true,
|
|
8
|
-
"resolveJsonModule": true,
|
|
9
|
-
"strict": true,
|
|
10
|
-
"sourceMap": true,
|
|
11
|
-
"rootDir": "src",
|
|
12
|
-
"moduleResolution": "nodenext",
|
|
13
|
-
"module": "node20"
|
|
14
|
-
},
|
|
15
|
-
"exclude": ["node_modules/"],
|
|
16
|
-
"include": ["src"]
|
|
17
|
-
}
|