@ste_tisci/ecs 0.1.5 → 0.1.7
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 +89 -84
- package/dist/index.cjs +26 -8
- package/dist/index.d.cts +92 -90
- package/dist/index.d.ts +92 -90
- package/dist/index.js +26 -8
- package/package.json +27 -23
- package/.prettierrc +0 -12
package/README.md
CHANGED
|
@@ -1,84 +1,89 @@
|
|
|
1
|
-
> [!IMPORTANT]
|
|
2
|
-
> Work in progress.
|
|
3
|
-
|
|
4
|
-
# ECS in TypeScript
|
|
5
|
-
|
|
6
|
-
A minimalistic game engine with zero dependencies based on the **Entity Component System (ECS)** architecture implemented in **TypeScript**
|
|
7
|
-
|
|
8
|
-
## Features
|
|
9
|
-
|
|
10
|
-
- **ECS architecture** with single responsability principles (SRP)
|
|
11
|
-
|
|
12
|
-
- **SparSet** for ultra-fast entities lookup
|
|
13
|
-
|
|
14
|
-
- **Structure of Arrays(SoA)** for optimized memory storage and access
|
|
15
|
-
|
|
16
|
-
- **Bitmask** for efficient component queries
|
|
17
|
-
|
|
18
|
-
## Core Components
|
|
19
|
-
|
|
20
|
-
- **EntityManager**: Manage creation, destruction and entity recycle. Uses a bitmask to track components of every entity.
|
|
21
|
-
|
|
22
|
-
- **ComponentStore**: Generic store that maintains
|
|
23
|
-
|
|
24
|
-
- **ECS**: Central Orchestator that coordinates entity and components, expose an API for queries and CRUD operations
|
|
25
|
-
|
|
26
|
-
- **SparseSet**: Data structure that grants O(1) for insertion, removal and lookups of entities.
|
|
27
|
-
|
|
28
|
-
## Technologies
|
|
29
|
-
|
|
30
|
-
- TypeScript for type safety
|
|
31
|
-
- Data oriented architecture for optimal performance
|
|
32
|
-
- Functional pattern without classes
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
import { ECS } from '@ste_tisci/ecs';
|
|
36
|
-
|
|
37
|
-
// Define the type of the components
|
|
38
|
-
const World = ECS<{
|
|
39
|
-
Position: { x: number; y: number };
|
|
40
|
-
Velocity: { x: number; y: number };
|
|
41
|
-
Size: { width: number; height: number };
|
|
42
|
-
Sprite: { src: HTMLImageElement };
|
|
43
|
-
}>();
|
|
44
|
-
|
|
45
|
-
// Define the structure of components that will be converted
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
World
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
1
|
+
> [!IMPORTANT]
|
|
2
|
+
> Work in progress.
|
|
3
|
+
|
|
4
|
+
# ECS in TypeScript
|
|
5
|
+
|
|
6
|
+
A minimalistic game engine with zero dependencies based on the **Entity Component System (ECS)** architecture implemented in **TypeScript**
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **ECS architecture** with single responsability principles (SRP)
|
|
11
|
+
|
|
12
|
+
- **SparSet** for ultra-fast entities lookup
|
|
13
|
+
|
|
14
|
+
- **Structure of Arrays(SoA)** for optimized memory storage and access — numeric fields are backed by `Float32Array`, non-numeric fields by plain arrays
|
|
15
|
+
|
|
16
|
+
- **Bitmask** for efficient component queries
|
|
17
|
+
|
|
18
|
+
## Core Components
|
|
19
|
+
|
|
20
|
+
- **EntityManager**: Manage creation, destruction and entity recycle. Uses a bitmask to track components of every entity.
|
|
21
|
+
|
|
22
|
+
- **ComponentStore**: Generic store that maintains component data in SoA format to maximise cache locality — numeric fields in a growable `Float32Array`, non-numeric fields (e.g. `HTMLImageElement`) in a plain array.
|
|
23
|
+
|
|
24
|
+
- **ECS**: Central Orchestator that coordinates entity and components, expose an API for queries and CRUD operations
|
|
25
|
+
|
|
26
|
+
- **SparseSet**: Data structure that grants O(1) for insertion, removal and lookups of entities.
|
|
27
|
+
|
|
28
|
+
## Technologies
|
|
29
|
+
|
|
30
|
+
- TypeScript for type safety
|
|
31
|
+
- Data oriented architecture for optimal performance
|
|
32
|
+
- Functional pattern without classes
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { ECS } from '@ste_tisci/ecs';
|
|
36
|
+
|
|
37
|
+
// Define the type of the components
|
|
38
|
+
const World = ECS<{
|
|
39
|
+
Position: { x: number; y: number };
|
|
40
|
+
Velocity: { x: number; y: number };
|
|
41
|
+
Size: { width: number; height: number };
|
|
42
|
+
Sprite: { src: HTMLImageElement };
|
|
43
|
+
}>();
|
|
44
|
+
|
|
45
|
+
// Define the structure of components that will be converted to SoA storage.
|
|
46
|
+
// Numeric fields become Float32Array, everything else stays a plain array.
|
|
47
|
+
// Position = { x: Float32Array, y: Float32Array }
|
|
48
|
+
// Sprite = { src: HTMLImageElement[] }
|
|
49
|
+
// etc.
|
|
50
|
+
World.defineComponents('Position', 'Velocity', 'Size', 'Sprite');
|
|
51
|
+
|
|
52
|
+
const ent = World.createEntity();
|
|
53
|
+
|
|
54
|
+
World.addComponent(ent, 'Position', { x: 100, y: 20 });
|
|
55
|
+
World.addComponent(ent, 'Velocity', { x: 2, y: 1 });
|
|
56
|
+
World.addComponent(ent, 'Size', { width: 20, height: 20 });
|
|
57
|
+
|
|
58
|
+
function movementSystem(World) {
|
|
59
|
+
const { Position, Velocity } = World.components;
|
|
60
|
+
|
|
61
|
+
for (const entity of World.query('Position', 'Velocity')) {
|
|
62
|
+
const posID = entity.Position.id;
|
|
63
|
+
const velID = entity.Velocity.id;
|
|
64
|
+
|
|
65
|
+
Position.x[posID] += Velocity.x[velID];
|
|
66
|
+
Position.y[posID] += Velocity.y[velID];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function gameLoop() {
|
|
71
|
+
movementSystem(World);
|
|
72
|
+
requestAnimationFrame(gameLoop);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
gameLoop();
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Error Handling
|
|
79
|
+
|
|
80
|
+
Every operation validates its inputs and throws a descriptive `Error` instead of failing silently or corrupting state:
|
|
81
|
+
|
|
82
|
+
- `addComponent` / `removeComponent` / `destroyEntity` throw if the entity does not exist
|
|
83
|
+
- `addComponent` throws if the entity already has the component; `removeComponent` throws if it doesn't
|
|
84
|
+
- `query`, `addComponent` and `removeComponent` throw if the component name was never registered via `defineComponents`
|
|
85
|
+
- `defineComponents` throws if a component name is already defined
|
|
86
|
+
|
|
87
|
+
## ⚖️ License
|
|
88
|
+
|
|
89
|
+
MIT © 2025 SteTisci
|
package/dist/index.cjs
CHANGED
|
@@ -64,18 +64,32 @@ function SparseSet() {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// src/ComponentStore.ts
|
|
67
|
+
var INITIAL_CAPACITY = 8;
|
|
68
|
+
function grow(buffer, requiredIndex) {
|
|
69
|
+
let capacity = buffer.length;
|
|
70
|
+
while (capacity <= requiredIndex) capacity *= 2;
|
|
71
|
+
const grown = new Float32Array(capacity);
|
|
72
|
+
grown.set(buffer);
|
|
73
|
+
return grown;
|
|
74
|
+
}
|
|
67
75
|
function ComponentStore() {
|
|
68
76
|
const componentSet = SparseSet();
|
|
69
77
|
const componentData = {};
|
|
78
|
+
const isNumeric = {};
|
|
70
79
|
function add(eid, data) {
|
|
71
80
|
if (componentSet.has(eid)) throw new Error(`Entity ${eid} already has this component`);
|
|
72
81
|
const ID = componentSet.getSize();
|
|
73
82
|
componentSet.add(eid);
|
|
74
83
|
for (const key in data) {
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
const value = data[key];
|
|
85
|
+
if (!(key in componentData)) {
|
|
86
|
+
isNumeric[key] = typeof value === "number";
|
|
87
|
+
componentData[key] = isNumeric[key] ? new Float32Array(INITIAL_CAPACITY) : [];
|
|
88
|
+
}
|
|
89
|
+
if (isNumeric[key] && ID >= componentData[key].length) {
|
|
90
|
+
componentData[key] = grow(componentData[key], ID);
|
|
77
91
|
}
|
|
78
|
-
componentData[key][ID] =
|
|
92
|
+
componentData[key][ID] = value;
|
|
79
93
|
}
|
|
80
94
|
}
|
|
81
95
|
function remove(eid) {
|
|
@@ -89,7 +103,9 @@ function ComponentStore() {
|
|
|
89
103
|
}
|
|
90
104
|
componentSet.remove(eid);
|
|
91
105
|
for (const key in componentData) {
|
|
92
|
-
|
|
106
|
+
if (!isNumeric[key]) {
|
|
107
|
+
componentData[key].pop();
|
|
108
|
+
}
|
|
93
109
|
}
|
|
94
110
|
}
|
|
95
111
|
function getDense() {
|
|
@@ -145,6 +161,7 @@ function EntityManager(registry) {
|
|
|
145
161
|
bitMasks[eid] &= ~(1n << BigInt(cid));
|
|
146
162
|
}
|
|
147
163
|
function getMask(eid) {
|
|
164
|
+
if (!exists(eid)) throw new Error(`Entity ${eid} does not exist`);
|
|
148
165
|
return bitMasks[eid];
|
|
149
166
|
}
|
|
150
167
|
return { exists, hasComponent, create, remove, addComponent, removeComponent, getMask };
|
|
@@ -183,6 +200,7 @@ function ECS() {
|
|
|
183
200
|
const components = {};
|
|
184
201
|
function defineComponents(...names) {
|
|
185
202
|
for (const name of names) {
|
|
203
|
+
if (name in internalStores) throw new Error(`Component ${String(name)} is already defined`);
|
|
186
204
|
ComponentRegistry.register(name);
|
|
187
205
|
internalStores[name] = ComponentStore();
|
|
188
206
|
components[name] = internalStores[name].getData();
|
|
@@ -215,6 +233,10 @@ function ECS() {
|
|
|
215
233
|
}
|
|
216
234
|
function* query(...componentName) {
|
|
217
235
|
if (componentName.length === 0) return;
|
|
236
|
+
let targetMask = 0n;
|
|
237
|
+
for (const name of componentName) {
|
|
238
|
+
targetMask |= 1n << BigInt(ComponentRegistry.getID(name));
|
|
239
|
+
}
|
|
218
240
|
let smallestStore = internalStores[componentName[0]];
|
|
219
241
|
let smallestSize = smallestStore.getSize();
|
|
220
242
|
for (let i = 1; i < componentName.length; i++) {
|
|
@@ -225,10 +247,6 @@ function ECS() {
|
|
|
225
247
|
smallestSize = size;
|
|
226
248
|
}
|
|
227
249
|
}
|
|
228
|
-
let targetMask = 0n;
|
|
229
|
-
for (const name of componentName) {
|
|
230
|
-
targetMask |= 1n << BigInt(ComponentRegistry.getID(name));
|
|
231
|
-
}
|
|
232
250
|
for (const eid of smallestStore.getDense()) {
|
|
233
251
|
if ((Entities.getMask(eid) & targetMask) !== targetMask) continue;
|
|
234
252
|
const result = {};
|
package/dist/index.d.cts
CHANGED
|
@@ -1,95 +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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
+
* Numeric properties are backed by a Float32Array for a compact, homogeneous
|
|
6
|
+
* memory layout; non-numeric properties fall back to a plain JS array.
|
|
7
|
+
* @template T - The component data type
|
|
8
|
+
* @example
|
|
9
|
+
* // For Position component: { x: number, y: number }
|
|
10
|
+
* // ComponentDataArrays would be: { x: Float32Array, y: Float32Array }
|
|
11
|
+
*/
|
|
12
|
+
type ComponentDataArrays<T> = {
|
|
13
|
+
[K in keyof T]: T[K] extends number ? Float32Array : T[K][];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Maps component names to their data arrays for direct access.
|
|
18
|
+
* Used to expose component data to the user without store methods.
|
|
19
|
+
* @template T - Record type defining all component types
|
|
20
|
+
*/
|
|
21
|
+
type StoreDataMap<T> = {
|
|
22
|
+
[K in keyof T]: ComponentDataArrays<T[K]>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Type helper for the query result.
|
|
27
|
+
* Creates an object with an entry for each requested component, holding its store index.
|
|
28
|
+
* @template C - Array of the component names requested in the query.
|
|
29
|
+
*/
|
|
30
|
+
type QueryResult<C extends readonly PropertyKey[]> = {
|
|
31
|
+
[K in C[number]]: { id: number };
|
|
12
32
|
};
|
|
13
33
|
|
|
14
|
-
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* @param eid - The entity ID
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* @
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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>;
|
|
34
|
+
/**
|
|
35
|
+
* Interface for the main ECS registry.
|
|
36
|
+
* Provides high-level API for entity-component management,
|
|
37
|
+
* coordinating between EntityManager and ComponentStores.
|
|
38
|
+
* @template T - Record type defining all component types in the ECS
|
|
39
|
+
*/
|
|
40
|
+
interface IECS<T> {
|
|
41
|
+
/**
|
|
42
|
+
* Initializes the ECS with component definitions.
|
|
43
|
+
* Registers all components and creates their corresponding stores.
|
|
44
|
+
* Must be called before any other operations.
|
|
45
|
+
* @param names -component names
|
|
46
|
+
* @throws Error if a component name is already defined
|
|
47
|
+
*/
|
|
48
|
+
defineComponents<K extends readonly (keyof T)[]>(...names: K): void;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a new entity in the ECS.
|
|
52
|
+
* @returns The ID of the newly created entity
|
|
53
|
+
*/
|
|
54
|
+
createEntity: () => number;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Completely removes an entity and all its components from the ECS.
|
|
58
|
+
* Cleans up all component data and entity records.
|
|
59
|
+
* @param eid - The entity ID to destroy
|
|
60
|
+
*/
|
|
61
|
+
destroyEntity: (eid: number) => void;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Adds a component to an entity with the provided data.
|
|
65
|
+
* Updates both the entity's bitmask and the component store.
|
|
66
|
+
* @template K - The component name type (key of T)
|
|
67
|
+
* @param eid - The entity ID
|
|
68
|
+
* @param name - The component name
|
|
69
|
+
* @param data - The component data to store
|
|
70
|
+
*/
|
|
71
|
+
addComponent: <K extends keyof T>(eid: number, name: K, data: T[K]) => void;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Removes a component from an entity.
|
|
75
|
+
* Updates both the entity's bitmask and the component store.
|
|
76
|
+
* @template K - The component name type (key of T)
|
|
77
|
+
* @param eid - The entity ID
|
|
78
|
+
* @param name - The component name
|
|
79
|
+
*/
|
|
80
|
+
removeComponent: <K extends keyof T>(eid: number, name: K) => void;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Retrieves all entities that contain the specified set of components.
|
|
84
|
+
* Performs a filtered search across component stores and yields each matching entity.
|
|
85
|
+
* The returned generator allows efficient iteration without allocating large arrays.
|
|
86
|
+
*
|
|
87
|
+
* @template C - Array of component names to query for
|
|
88
|
+
* @param components - List of component names that the entity must include
|
|
89
|
+
* @returns A generator that yields an object for each matching entity,
|
|
90
|
+
* containing the entity ID and the related component data
|
|
91
|
+
*/
|
|
92
|
+
query: <C extends (keyof T)[]>(...componentName: C) => Generator<QueryResult<C>>;
|
|
93
|
+
|
|
94
|
+
components: StoreDataMap<T>;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
declare function ECS<T extends Record<string, Record<string, any>>>(): IECS<T>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,95 +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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
+
* Numeric properties are backed by a Float32Array for a compact, homogeneous
|
|
6
|
+
* memory layout; non-numeric properties fall back to a plain JS array.
|
|
7
|
+
* @template T - The component data type
|
|
8
|
+
* @example
|
|
9
|
+
* // For Position component: { x: number, y: number }
|
|
10
|
+
* // ComponentDataArrays would be: { x: Float32Array, y: Float32Array }
|
|
11
|
+
*/
|
|
12
|
+
type ComponentDataArrays<T> = {
|
|
13
|
+
[K in keyof T]: T[K] extends number ? Float32Array : T[K][];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Maps component names to their data arrays for direct access.
|
|
18
|
+
* Used to expose component data to the user without store methods.
|
|
19
|
+
* @template T - Record type defining all component types
|
|
20
|
+
*/
|
|
21
|
+
type StoreDataMap<T> = {
|
|
22
|
+
[K in keyof T]: ComponentDataArrays<T[K]>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Type helper for the query result.
|
|
27
|
+
* Creates an object with an entry for each requested component, holding its store index.
|
|
28
|
+
* @template C - Array of the component names requested in the query.
|
|
29
|
+
*/
|
|
30
|
+
type QueryResult<C extends readonly PropertyKey[]> = {
|
|
31
|
+
[K in C[number]]: { id: number };
|
|
12
32
|
};
|
|
13
33
|
|
|
14
|
-
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* @param eid - The entity ID
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* @
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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>;
|
|
34
|
+
/**
|
|
35
|
+
* Interface for the main ECS registry.
|
|
36
|
+
* Provides high-level API for entity-component management,
|
|
37
|
+
* coordinating between EntityManager and ComponentStores.
|
|
38
|
+
* @template T - Record type defining all component types in the ECS
|
|
39
|
+
*/
|
|
40
|
+
interface IECS<T> {
|
|
41
|
+
/**
|
|
42
|
+
* Initializes the ECS with component definitions.
|
|
43
|
+
* Registers all components and creates their corresponding stores.
|
|
44
|
+
* Must be called before any other operations.
|
|
45
|
+
* @param names -component names
|
|
46
|
+
* @throws Error if a component name is already defined
|
|
47
|
+
*/
|
|
48
|
+
defineComponents<K extends readonly (keyof T)[]>(...names: K): void;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a new entity in the ECS.
|
|
52
|
+
* @returns The ID of the newly created entity
|
|
53
|
+
*/
|
|
54
|
+
createEntity: () => number;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Completely removes an entity and all its components from the ECS.
|
|
58
|
+
* Cleans up all component data and entity records.
|
|
59
|
+
* @param eid - The entity ID to destroy
|
|
60
|
+
*/
|
|
61
|
+
destroyEntity: (eid: number) => void;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Adds a component to an entity with the provided data.
|
|
65
|
+
* Updates both the entity's bitmask and the component store.
|
|
66
|
+
* @template K - The component name type (key of T)
|
|
67
|
+
* @param eid - The entity ID
|
|
68
|
+
* @param name - The component name
|
|
69
|
+
* @param data - The component data to store
|
|
70
|
+
*/
|
|
71
|
+
addComponent: <K extends keyof T>(eid: number, name: K, data: T[K]) => void;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Removes a component from an entity.
|
|
75
|
+
* Updates both the entity's bitmask and the component store.
|
|
76
|
+
* @template K - The component name type (key of T)
|
|
77
|
+
* @param eid - The entity ID
|
|
78
|
+
* @param name - The component name
|
|
79
|
+
*/
|
|
80
|
+
removeComponent: <K extends keyof T>(eid: number, name: K) => void;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Retrieves all entities that contain the specified set of components.
|
|
84
|
+
* Performs a filtered search across component stores and yields each matching entity.
|
|
85
|
+
* The returned generator allows efficient iteration without allocating large arrays.
|
|
86
|
+
*
|
|
87
|
+
* @template C - Array of component names to query for
|
|
88
|
+
* @param components - List of component names that the entity must include
|
|
89
|
+
* @returns A generator that yields an object for each matching entity,
|
|
90
|
+
* containing the entity ID and the related component data
|
|
91
|
+
*/
|
|
92
|
+
query: <C extends (keyof T)[]>(...componentName: C) => Generator<QueryResult<C>>;
|
|
93
|
+
|
|
94
|
+
components: StoreDataMap<T>;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
declare function ECS<T extends Record<string, Record<string, any>>>(): IECS<T>;
|
package/dist/index.js
CHANGED
|
@@ -38,18 +38,32 @@ function SparseSet() {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// src/ComponentStore.ts
|
|
41
|
+
var INITIAL_CAPACITY = 8;
|
|
42
|
+
function grow(buffer, requiredIndex) {
|
|
43
|
+
let capacity = buffer.length;
|
|
44
|
+
while (capacity <= requiredIndex) capacity *= 2;
|
|
45
|
+
const grown = new Float32Array(capacity);
|
|
46
|
+
grown.set(buffer);
|
|
47
|
+
return grown;
|
|
48
|
+
}
|
|
41
49
|
function ComponentStore() {
|
|
42
50
|
const componentSet = SparseSet();
|
|
43
51
|
const componentData = {};
|
|
52
|
+
const isNumeric = {};
|
|
44
53
|
function add(eid, data) {
|
|
45
54
|
if (componentSet.has(eid)) throw new Error(`Entity ${eid} already has this component`);
|
|
46
55
|
const ID = componentSet.getSize();
|
|
47
56
|
componentSet.add(eid);
|
|
48
57
|
for (const key in data) {
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
const value = data[key];
|
|
59
|
+
if (!(key in componentData)) {
|
|
60
|
+
isNumeric[key] = typeof value === "number";
|
|
61
|
+
componentData[key] = isNumeric[key] ? new Float32Array(INITIAL_CAPACITY) : [];
|
|
62
|
+
}
|
|
63
|
+
if (isNumeric[key] && ID >= componentData[key].length) {
|
|
64
|
+
componentData[key] = grow(componentData[key], ID);
|
|
51
65
|
}
|
|
52
|
-
componentData[key][ID] =
|
|
66
|
+
componentData[key][ID] = value;
|
|
53
67
|
}
|
|
54
68
|
}
|
|
55
69
|
function remove(eid) {
|
|
@@ -63,7 +77,9 @@ function ComponentStore() {
|
|
|
63
77
|
}
|
|
64
78
|
componentSet.remove(eid);
|
|
65
79
|
for (const key in componentData) {
|
|
66
|
-
|
|
80
|
+
if (!isNumeric[key]) {
|
|
81
|
+
componentData[key].pop();
|
|
82
|
+
}
|
|
67
83
|
}
|
|
68
84
|
}
|
|
69
85
|
function getDense() {
|
|
@@ -119,6 +135,7 @@ function EntityManager(registry) {
|
|
|
119
135
|
bitMasks[eid] &= ~(1n << BigInt(cid));
|
|
120
136
|
}
|
|
121
137
|
function getMask(eid) {
|
|
138
|
+
if (!exists(eid)) throw new Error(`Entity ${eid} does not exist`);
|
|
122
139
|
return bitMasks[eid];
|
|
123
140
|
}
|
|
124
141
|
return { exists, hasComponent, create, remove, addComponent, removeComponent, getMask };
|
|
@@ -157,6 +174,7 @@ function ECS() {
|
|
|
157
174
|
const components = {};
|
|
158
175
|
function defineComponents(...names) {
|
|
159
176
|
for (const name of names) {
|
|
177
|
+
if (name in internalStores) throw new Error(`Component ${String(name)} is already defined`);
|
|
160
178
|
ComponentRegistry.register(name);
|
|
161
179
|
internalStores[name] = ComponentStore();
|
|
162
180
|
components[name] = internalStores[name].getData();
|
|
@@ -189,6 +207,10 @@ function ECS() {
|
|
|
189
207
|
}
|
|
190
208
|
function* query(...componentName) {
|
|
191
209
|
if (componentName.length === 0) return;
|
|
210
|
+
let targetMask = 0n;
|
|
211
|
+
for (const name of componentName) {
|
|
212
|
+
targetMask |= 1n << BigInt(ComponentRegistry.getID(name));
|
|
213
|
+
}
|
|
192
214
|
let smallestStore = internalStores[componentName[0]];
|
|
193
215
|
let smallestSize = smallestStore.getSize();
|
|
194
216
|
for (let i = 1; i < componentName.length; i++) {
|
|
@@ -199,10 +221,6 @@ function ECS() {
|
|
|
199
221
|
smallestSize = size;
|
|
200
222
|
}
|
|
201
223
|
}
|
|
202
|
-
let targetMask = 0n;
|
|
203
|
-
for (const name of componentName) {
|
|
204
|
-
targetMask |= 1n << BigInt(ComponentRegistry.getID(name));
|
|
205
|
-
}
|
|
206
224
|
for (const eid of smallestStore.getDense()) {
|
|
207
225
|
if ((Entities.getMask(eid) & targetMask) !== targetMask) continue;
|
|
208
226
|
const result = {};
|
package/package.json
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@ste_tisci/ecs",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A minimalistic game engine with zero dependencies based on the Entity Component System (ECS) architecture implemented in TypeScript",
|
|
5
|
-
"main": "./dist/index.cjs",
|
|
6
|
-
"module": "./dist/index.js",
|
|
7
|
-
"types": "./dist/index.d.ts",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@ste_tisci/ecs",
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "A minimalistic game engine with zero dependencies based on the Entity Component System (ECS) architecture implemented in TypeScript",
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [],
|
|
16
|
+
"author": "",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/SteTisci/ecs"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"tsup": "^8.5.1",
|
|
25
|
+
"typescript": "^5.9.3"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/.prettierrc
DELETED