@ste_tisci/ecs 0.1.4 → 0.1.6
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/.prettierrc +12 -12
- package/README.md +89 -84
- package/package.json +23 -23
- package/src/ComponentStore.ts +96 -64
- package/src/Ecs.ts +98 -94
- package/src/EntityManager.ts +60 -58
- package/src/index.ts +1 -1
- package/src/types/IComponentRegistry.d.ts +29 -29
- package/src/types/IComponentStore.d.ts +53 -53
- package/src/types/IEcs.d.ts +64 -63
- package/src/types/IEntityManager.d.ts +65 -64
- package/src/types/ISparseSet.d.ts +49 -49
- package/src/types/index.d.ts +43 -42
- package/src/utils/ComponentRegistry.ts +32 -32
- package/src/utils/SparseSet.ts +54 -54
- package/tsconfig.json +17 -17
- package/tsup.config.ts +10 -10
package/.prettierrc
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
{
|
|
2
|
-
"singleQuote": true,
|
|
3
|
-
"printWidth": 150,
|
|
4
|
-
"proseWrap": "always",
|
|
5
|
-
"tabWidth": 2,
|
|
6
|
-
"useTabs": false,
|
|
7
|
-
"trailingComma": "all",
|
|
8
|
-
"bracketSpacing": true,
|
|
9
|
-
"arrowParens": "avoid",
|
|
10
|
-
"singleAttributePerLine": false,
|
|
11
|
-
"semi": true
|
|
12
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"singleQuote": true,
|
|
3
|
+
"printWidth": 150,
|
|
4
|
+
"proseWrap": "always",
|
|
5
|
+
"tabWidth": 2,
|
|
6
|
+
"useTabs": false,
|
|
7
|
+
"trailingComma": "all",
|
|
8
|
+
"bracketSpacing": true,
|
|
9
|
+
"arrowParens": "avoid",
|
|
10
|
+
"singleAttributePerLine": false,
|
|
11
|
+
"semi": true
|
|
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/package.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
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
|
-
"scripts": {
|
|
9
|
-
"build": "tsup"
|
|
10
|
-
},
|
|
11
|
-
"keywords": [],
|
|
12
|
-
"author": "",
|
|
13
|
-
"license": "MIT",
|
|
14
|
-
"repository": {
|
|
15
|
-
"type": "git",
|
|
16
|
-
"url": "https://github.com/SteTisci/ecs"
|
|
17
|
-
},
|
|
18
|
-
"type": "module",
|
|
19
|
-
"devDependencies": {
|
|
20
|
-
"tsup": "^8.5.1",
|
|
21
|
-
"typescript": "^5.9.3"
|
|
22
|
-
}
|
|
23
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@ste_tisci/ecs",
|
|
3
|
+
"version": "0.1.6",
|
|
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
|
+
"scripts": {
|
|
9
|
+
"build": "tsup"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [],
|
|
12
|
+
"author": "",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/SteTisci/ecs"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"tsup": "^8.5.1",
|
|
21
|
+
"typescript": "^5.9.3"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/ComponentStore.ts
CHANGED
|
@@ -1,64 +1,96 @@
|
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
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
CHANGED
|
@@ -1,94 +1,98 @@
|
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
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
|
+
}
|