@ue-too/ecs 0.9.5 → 0.11.0
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 +361 -5
- package/index.d.ts +255 -1
- package/index.js +2 -266
- package/index.js.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,364 @@
|
|
|
1
|
-
# ecs
|
|
2
|
-
This is an experimental implementation of the Entity Component System architecture.
|
|
1
|
+
# @ue-too/ecs
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
High-performance Entity Component System (ECS) architecture for TypeScript.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@ue-too/ecs)
|
|
6
|
+
[](https://github.com/ue-too/ue-too/blob/main/LICENSE.txt)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
> **Experimental**: This package is an experimental implementation based on [Austin Morlan's ECS tutorial](https://austinmorlan.com/posts/entity_component_system/). Please **DO NOT** use this in production.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
`@ue-too/ecs` provides a lightweight Entity Component System implementation for TypeScript. ECS is an architectural pattern commonly used in game development that promotes composition over inheritance and enables high-performance iteration over game objects.
|
|
13
|
+
|
|
14
|
+
### Key Features
|
|
15
|
+
|
|
16
|
+
- **Efficient Storage**: Component arrays using sparse-set data structure for O(1) lookups
|
|
17
|
+
- **Fast Iteration**: Dense packing enables cache-friendly iteration over components
|
|
18
|
+
- **Type Safety**: Full TypeScript generics for component type safety
|
|
19
|
+
- **Signature Matching**: Automatic system updates when entity component composition changes
|
|
20
|
+
- **Entity Pooling**: Entity ID recycling for memory efficiency
|
|
21
|
+
- **Minimal Overhead**: Lightweight architecture with predictable performance
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
Using Bun:
|
|
26
|
+
```bash
|
|
27
|
+
bun add @ue-too/ecs
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Using npm:
|
|
31
|
+
```bash
|
|
32
|
+
npm install @ue-too/ecs
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
Here's a simple example demonstrating the core ECS workflow:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { Coordinator } from '@ue-too/ecs';
|
|
41
|
+
|
|
42
|
+
// 1. Define component types
|
|
43
|
+
type Position = { x: number; y: number };
|
|
44
|
+
type Velocity = { x: number; y: number };
|
|
45
|
+
type Health = { current: number; max: number };
|
|
46
|
+
|
|
47
|
+
// 2. Create coordinator
|
|
48
|
+
const ecs = new Coordinator();
|
|
49
|
+
|
|
50
|
+
// 3. Register components
|
|
51
|
+
ecs.registerComponent<Position>('Position');
|
|
52
|
+
ecs.registerComponent<Velocity>('Velocity');
|
|
53
|
+
ecs.registerComponent<Health>('Health');
|
|
54
|
+
|
|
55
|
+
// 4. Create entities and add components
|
|
56
|
+
const player = ecs.createEntity();
|
|
57
|
+
ecs.addComponentToEntity('Position', player, { x: 0, y: 0 });
|
|
58
|
+
ecs.addComponentToEntity('Velocity', player, { x: 5, y: 0 });
|
|
59
|
+
ecs.addComponentToEntity('Health', player, { current: 100, max: 100 });
|
|
60
|
+
|
|
61
|
+
const enemy = ecs.createEntity();
|
|
62
|
+
ecs.addComponentToEntity('Position', enemy, { x: 50, y: 50 });
|
|
63
|
+
ecs.addComponentToEntity('Health', enemy, { current: 50, max: 50 });
|
|
64
|
+
|
|
65
|
+
// 5. Query and update components
|
|
66
|
+
const playerPos = ecs.getComponentFromEntity<Position>('Position', player);
|
|
67
|
+
const playerVel = ecs.getComponentFromEntity<Velocity>('Velocity', player);
|
|
68
|
+
|
|
69
|
+
if (playerPos && playerVel) {
|
|
70
|
+
playerPos.x += playerVel.x;
|
|
71
|
+
playerPos.y += playerVel.y;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 6. Clean up
|
|
75
|
+
ecs.destroyEntity(enemy);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## ECS Architecture
|
|
79
|
+
|
|
80
|
+
The Entity Component System pattern separates data from logic:
|
|
81
|
+
|
|
82
|
+
- **Entities**: Unique identifiers (numbers) representing game objects
|
|
83
|
+
- **Components**: Plain data containers (no logic)
|
|
84
|
+
- **Systems**: Functions that operate on entities with specific component combinations
|
|
85
|
+
|
|
86
|
+
### Why ECS?
|
|
87
|
+
|
|
88
|
+
Traditional object-oriented hierarchies can become complex and rigid. ECS promotes:
|
|
89
|
+
|
|
90
|
+
- **Composition over inheritance**: Build entities by combining components
|
|
91
|
+
- **Data locality**: Components are stored in dense arrays for better cache performance
|
|
92
|
+
- **Flexibility**: Easy to add/remove behaviors by adding/removing components
|
|
93
|
+
- **Parallelization**: Systems can operate independently on entity subsets
|
|
94
|
+
|
|
95
|
+
## Core APIs
|
|
96
|
+
|
|
97
|
+
### Coordinator
|
|
98
|
+
|
|
99
|
+
The main ECS coordinator that manages all subsystems.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const ecs = new Coordinator();
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Entity Management:**
|
|
106
|
+
- `createEntity(): Entity` - Creates a new entity, returns entity ID
|
|
107
|
+
- `destroyEntity(entity: Entity): void` - Destroys entity and removes all components
|
|
108
|
+
|
|
109
|
+
**Component Management:**
|
|
110
|
+
- `registerComponent<T>(name: string): void` - Registers a component type
|
|
111
|
+
- `addComponentToEntity<T>(name: string, entity: Entity, component: T): void` - Adds component to entity
|
|
112
|
+
- `removeComponentFromEntity<T>(name: string, entity: Entity): void` - Removes component from entity
|
|
113
|
+
- `getComponentFromEntity<T>(name: string, entity: Entity): T | null` - Retrieves component data
|
|
114
|
+
- `getComponentType(name: string): ComponentType | null` - Gets component type ID
|
|
115
|
+
|
|
116
|
+
**System Management:**
|
|
117
|
+
- `registerSystem(name: string, system: System): void` - Registers a system
|
|
118
|
+
- `setSystemSignature(name: string, signature: ComponentSignature): void` - Sets which components a system requires
|
|
119
|
+
|
|
120
|
+
### System Interface
|
|
121
|
+
|
|
122
|
+
Systems maintain a set of entities that match their component signature:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
interface System {
|
|
126
|
+
entities: Set<Entity>;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Component Signature
|
|
131
|
+
|
|
132
|
+
Bit flags indicating which components an entity has:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
type ComponentSignature = number; // Bit field
|
|
136
|
+
type ComponentType = number; // Component type ID (0-31)
|
|
137
|
+
type Entity = number; // Entity ID
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Common Use Cases
|
|
141
|
+
|
|
142
|
+
### Movement System
|
|
143
|
+
|
|
144
|
+
Update positions based on velocities:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { Coordinator, System } from '@ue-too/ecs';
|
|
148
|
+
|
|
149
|
+
const ecs = new Coordinator();
|
|
150
|
+
|
|
151
|
+
// Register components
|
|
152
|
+
ecs.registerComponent<Position>('Position');
|
|
153
|
+
ecs.registerComponent<Velocity>('Velocity');
|
|
154
|
+
|
|
155
|
+
// Create movement system
|
|
156
|
+
const movementSystem: System = {
|
|
157
|
+
entities: new Set()
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
ecs.registerSystem('Movement', movementSystem);
|
|
161
|
+
|
|
162
|
+
// Set signature: entities with Position AND Velocity
|
|
163
|
+
const posType = ecs.getComponentType('Position')!;
|
|
164
|
+
const velType = ecs.getComponentType('Velocity')!;
|
|
165
|
+
const signature = (1 << posType) | (1 << velType);
|
|
166
|
+
ecs.setSystemSignature('Movement', signature);
|
|
167
|
+
|
|
168
|
+
// Update loop
|
|
169
|
+
function update(deltaTime: number) {
|
|
170
|
+
movementSystem.entities.forEach(entity => {
|
|
171
|
+
const pos = ecs.getComponentFromEntity<Position>('Position', entity)!;
|
|
172
|
+
const vel = ecs.getComponentFromEntity<Velocity>('Velocity', entity)!;
|
|
173
|
+
|
|
174
|
+
pos.x += vel.x * deltaTime;
|
|
175
|
+
pos.y += vel.y * deltaTime;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Game loop
|
|
180
|
+
setInterval(() => update(0.016), 16); // ~60 FPS
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Damage System
|
|
184
|
+
|
|
185
|
+
Process health and damage components:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
type Health = { current: number; max: number };
|
|
189
|
+
type Damage = { amount: number; source: Entity };
|
|
190
|
+
|
|
191
|
+
ecs.registerComponent<Health>('Health');
|
|
192
|
+
ecs.registerComponent<Damage>('Damage');
|
|
193
|
+
|
|
194
|
+
const damageSystem: System = { entities: new Set() };
|
|
195
|
+
ecs.registerSystem('Damage', damageSystem);
|
|
196
|
+
|
|
197
|
+
const healthType = ecs.getComponentType('Health')!;
|
|
198
|
+
const damageType = ecs.getComponentType('Damage')!;
|
|
199
|
+
const damageSignature = (1 << healthType) | (1 << damageType);
|
|
200
|
+
ecs.setSystemSignature('Damage', damageSignature);
|
|
201
|
+
|
|
202
|
+
function processDamage() {
|
|
203
|
+
damageSystem.entities.forEach(entity => {
|
|
204
|
+
const health = ecs.getComponentFromEntity<Health>('Health', entity)!;
|
|
205
|
+
const damage = ecs.getComponentFromEntity<Damage>('Damage', entity)!;
|
|
206
|
+
|
|
207
|
+
health.current -= damage.amount;
|
|
208
|
+
|
|
209
|
+
if (health.current <= 0) {
|
|
210
|
+
console.log(`Entity ${entity} destroyed`);
|
|
211
|
+
ecs.destroyEntity(entity);
|
|
212
|
+
} else {
|
|
213
|
+
// Remove damage component after processing
|
|
214
|
+
ecs.removeComponentFromEntity<Damage>('Damage', entity);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Rendering System
|
|
221
|
+
|
|
222
|
+
Render entities with position and sprite components:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
type Sprite = { imageSrc: string; width: number; height: number };
|
|
226
|
+
|
|
227
|
+
ecs.registerComponent<Sprite>('Sprite');
|
|
228
|
+
|
|
229
|
+
const renderSystem: System = { entities: new Set() };
|
|
230
|
+
ecs.registerSystem('Render', renderSystem);
|
|
231
|
+
|
|
232
|
+
const spriteType = ecs.getComponentType('Sprite')!;
|
|
233
|
+
const renderSignature = (1 << posType) | (1 << spriteType);
|
|
234
|
+
ecs.setSystemSignature('Render', renderSignature);
|
|
235
|
+
|
|
236
|
+
function render(ctx: CanvasRenderingContext2D) {
|
|
237
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
238
|
+
|
|
239
|
+
renderSystem.entities.forEach(entity => {
|
|
240
|
+
const pos = ecs.getComponentFromEntity<Position>('Position', entity)!;
|
|
241
|
+
const sprite = ecs.getComponentFromEntity<Sprite>('Sprite', entity)!;
|
|
242
|
+
|
|
243
|
+
// Draw sprite at position
|
|
244
|
+
const img = new Image();
|
|
245
|
+
img.src = sprite.imageSrc;
|
|
246
|
+
ctx.drawImage(img, pos.x, pos.y, sprite.width, sprite.height);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Component Signature Building
|
|
252
|
+
|
|
253
|
+
Build complex component requirements:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
// Entities that have Position, Velocity, AND Sprite
|
|
257
|
+
const movingRenderables =
|
|
258
|
+
(1 << ecs.getComponentType('Position')!) |
|
|
259
|
+
(1 << ecs.getComponentType('Velocity')!) |
|
|
260
|
+
(1 << ecs.getComponentType('Sprite')!);
|
|
261
|
+
|
|
262
|
+
// Helper function for cleaner syntax
|
|
263
|
+
function buildSignature(ecs: Coordinator, ...componentNames: string[]): number {
|
|
264
|
+
return componentNames.reduce((signature, name) => {
|
|
265
|
+
const type = ecs.getComponentType(name);
|
|
266
|
+
return type !== null ? signature | (1 << type) : signature;
|
|
267
|
+
}, 0);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Usage
|
|
271
|
+
const signature = buildSignature(ecs, 'Position', 'Velocity', 'Health');
|
|
272
|
+
ecs.setSystemSignature('MySystem', signature);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Configuration
|
|
276
|
+
|
|
277
|
+
The package provides configuration constants:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
export const MAX_ENTITIES = 10000; // Maximum simultaneous entities
|
|
281
|
+
export const MAX_COMPONENTS = 32; // Maximum component types (bit limit)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
To customize, you can create your own EntityManager:
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import { EntityManager } from '@ue-too/ecs';
|
|
288
|
+
|
|
289
|
+
const entityManager = new EntityManager(5000); // Custom max entities
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## API Reference
|
|
293
|
+
|
|
294
|
+
For complete API documentation with detailed type information, see the [TypeDoc-generated documentation](../../docs/ecs).
|
|
295
|
+
|
|
296
|
+
## TypeScript Support
|
|
297
|
+
|
|
298
|
+
This package is written in TypeScript with complete type definitions:
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// Component types are fully typed
|
|
302
|
+
type Position = { x: number; y: number };
|
|
303
|
+
ecs.registerComponent<Position>('Position');
|
|
304
|
+
|
|
305
|
+
// Type-safe component retrieval
|
|
306
|
+
const pos = ecs.getComponentFromEntity<Position>('Position', entity);
|
|
307
|
+
if (pos) {
|
|
308
|
+
pos.x += 10; // TypeScript knows pos has x and y properties
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Generic component arrays
|
|
312
|
+
import { ComponentArray } from '@ue-too/ecs';
|
|
313
|
+
const positions = new ComponentArray<Position>(1000);
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Design Principles
|
|
317
|
+
|
|
318
|
+
This ECS implementation follows these principles:
|
|
319
|
+
|
|
320
|
+
- **Simplicity**: Minimal API surface for easy learning
|
|
321
|
+
- **Performance**: Sparse-set data structure for O(1) operations
|
|
322
|
+
- **Type Safety**: Leverage TypeScript's type system
|
|
323
|
+
- **Flexibility**: Components are plain data objects
|
|
324
|
+
- **Explicit**: No magic, predictable behavior
|
|
325
|
+
|
|
326
|
+
## Performance Considerations
|
|
327
|
+
|
|
328
|
+
- **Entity Creation**: O(1) - pops from available entity pool
|
|
329
|
+
- **Component Lookup**: O(1) - sparse-set provides constant-time access
|
|
330
|
+
- **Component Iteration**: O(n) - dense array iteration for cache efficiency
|
|
331
|
+
- **Signature Matching**: O(m) where m is number of systems (typically small)
|
|
332
|
+
|
|
333
|
+
**Performance Tips:**
|
|
334
|
+
- Keep component data small and focused
|
|
335
|
+
- Process components in batches (system-by-system) rather than entity-by-entity
|
|
336
|
+
- Reuse entities when possible instead of create/destroy cycles
|
|
337
|
+
- Limit number of component types (max 32 due to bit signature)
|
|
338
|
+
|
|
339
|
+
## Limitations
|
|
340
|
+
|
|
341
|
+
- **Max 32 component types**: Component signatures use 32-bit integers
|
|
342
|
+
- **No component queries**: Must register systems with signatures upfront
|
|
343
|
+
- **No hierarchical entities**: Flat entity structure only
|
|
344
|
+
- **No built-in serialization**: Component data must be manually serialized
|
|
345
|
+
|
|
346
|
+
## Related Packages
|
|
347
|
+
|
|
348
|
+
- **[@ue-too/being](../being)**: State machine library for entity AI and behavior
|
|
349
|
+
- **[@ue-too/math](../math)**: Vector and transformation utilities for component data
|
|
350
|
+
- **[@ue-too/board](../board)**: Canvas rendering system that can integrate with ECS
|
|
351
|
+
|
|
352
|
+
## Further Reading
|
|
353
|
+
|
|
354
|
+
- [Austin Morlan's ECS Tutorial](https://austinmorlan.com/posts/entity_component_system/) - Original tutorial this implementation is based on
|
|
355
|
+
- [ECS FAQ](https://github.com/SanderMertens/ecs-faq) - Comprehensive ECS concepts and patterns
|
|
356
|
+
- [Data-Oriented Design](https://www.dataorienteddesign.com/dodbook/) - Principles behind ECS architecture
|
|
357
|
+
|
|
358
|
+
## License
|
|
359
|
+
|
|
360
|
+
MIT
|
|
361
|
+
|
|
362
|
+
## Repository
|
|
363
|
+
|
|
364
|
+
[https://github.com/ue-too/ue-too](https://github.com/ue-too/ue-too)
|
package/index.d.ts
CHANGED
|
@@ -1,9 +1,134 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* Entity Component System (ECS) implementation for TypeScript.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* The `@ue-too/ecs` package provides a high-performance Entity Component System architecture
|
|
7
|
+
* based on the tutorial from https://austinmorlan.com/posts/entity_component_system/
|
|
8
|
+
*
|
|
9
|
+
* ## ECS Architecture
|
|
10
|
+
*
|
|
11
|
+
* - **Entities**: Unique identifiers (numbers) representing game objects
|
|
12
|
+
* - **Components**: Data containers attached to entities
|
|
13
|
+
* - **Systems**: Logic that operates on entities with specific component combinations
|
|
14
|
+
* - **Signatures**: Bit flags indicating which components an entity has
|
|
15
|
+
*
|
|
16
|
+
* ## Key Features
|
|
17
|
+
*
|
|
18
|
+
* - **Efficient Storage**: Component arrays using sparse-set data structure
|
|
19
|
+
* - **Fast Iteration**: Dense packing for cache-friendly iteration
|
|
20
|
+
* - **Type-Safe**: TypeScript generics for component type safety
|
|
21
|
+
* - **Signature Matching**: Automatic system updates when entity signatures change
|
|
22
|
+
* - **Pooling**: Entity ID recycling for memory efficiency
|
|
23
|
+
*
|
|
24
|
+
* ## Core Classes
|
|
25
|
+
*
|
|
26
|
+
* - {@link Coordinator}: Main ECS coordinator managing all subsystems
|
|
27
|
+
* - {@link EntityManager}: Creates and destroys entities
|
|
28
|
+
* - {@link ComponentManager}: Registers components and manages component data
|
|
29
|
+
* - {@link SystemManager}: Registers systems and maintains entity sets
|
|
30
|
+
* - {@link ComponentArray}: Efficient sparse-set storage for component data
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* Basic ECS usage
|
|
34
|
+
* ```typescript
|
|
35
|
+
* import { Coordinator } from '@ue-too/ecs';
|
|
36
|
+
*
|
|
37
|
+
* // Define component types
|
|
38
|
+
* type Position = { x: number; y: number };
|
|
39
|
+
* type Velocity = { x: number; y: number };
|
|
40
|
+
*
|
|
41
|
+
* // Create coordinator
|
|
42
|
+
* const coordinator = new Coordinator();
|
|
43
|
+
*
|
|
44
|
+
* // Register components
|
|
45
|
+
* coordinator.registerComponent<Position>('Position');
|
|
46
|
+
* coordinator.registerComponent<Velocity>('Velocity');
|
|
47
|
+
*
|
|
48
|
+
* // Create entity with components
|
|
49
|
+
* const entity = coordinator.createEntity();
|
|
50
|
+
* coordinator.addComponentToEntity('Position', entity, { x: 0, y: 0 });
|
|
51
|
+
* coordinator.addComponentToEntity('Velocity', entity, { x: 1, y: 1 });
|
|
52
|
+
*
|
|
53
|
+
* // Query components
|
|
54
|
+
* const pos = coordinator.getComponentFromEntity<Position>('Position', entity);
|
|
55
|
+
* console.log('Position:', pos);
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* System registration
|
|
60
|
+
* ```typescript
|
|
61
|
+
* import { Coordinator, System } from '@ue-too/ecs';
|
|
62
|
+
*
|
|
63
|
+
* const coordinator = new Coordinator();
|
|
64
|
+
* coordinator.registerComponent<Position>('Position');
|
|
65
|
+
* coordinator.registerComponent<Velocity>('Velocity');
|
|
66
|
+
*
|
|
67
|
+
* // Create a movement system
|
|
68
|
+
* const movementSystem: System = {
|
|
69
|
+
* entities: new Set()
|
|
70
|
+
* };
|
|
71
|
+
*
|
|
72
|
+
* coordinator.registerSystem('Movement', movementSystem);
|
|
73
|
+
*
|
|
74
|
+
* // Set signature (entities with Position AND Velocity)
|
|
75
|
+
* const posType = coordinator.getComponentType('Position')!;
|
|
76
|
+
* const velType = coordinator.getComponentType('Velocity')!;
|
|
77
|
+
* const signature = (1 << posType) | (1 << velType);
|
|
78
|
+
* coordinator.setSystemSignature('Movement', signature);
|
|
79
|
+
*
|
|
80
|
+
* // Update loop
|
|
81
|
+
* function update(deltaTime: number) {
|
|
82
|
+
* movementSystem.entities.forEach(entity => {
|
|
83
|
+
* const pos = coordinator.getComponentFromEntity<Position>('Position', entity)!;
|
|
84
|
+
* const vel = coordinator.getComponentFromEntity<Velocity>('Velocity', entity)!;
|
|
85
|
+
* pos.x += vel.x * deltaTime;
|
|
86
|
+
* pos.y += vel.y * deltaTime;
|
|
87
|
+
* });
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* @see {@link Coordinator} for the main ECS API
|
|
92
|
+
*/
|
|
93
|
+
/**
|
|
94
|
+
* Maximum number of entities that can exist simultaneously.
|
|
95
|
+
* @category Configuration
|
|
96
|
+
*/
|
|
2
97
|
export declare const MAX_ENTITIES = 10000;
|
|
98
|
+
/**
|
|
99
|
+
* Maximum number of component types that can be registered.
|
|
100
|
+
* @category Configuration
|
|
101
|
+
*/
|
|
3
102
|
export declare const MAX_COMPONENTS = 32;
|
|
103
|
+
/**
|
|
104
|
+
* Component signature type (bit field indicating which components an entity has).
|
|
105
|
+
* @category Types
|
|
106
|
+
*/
|
|
4
107
|
export type ComponentSignature = number;
|
|
108
|
+
/**
|
|
109
|
+
* Component type identifier.
|
|
110
|
+
* @category Types
|
|
111
|
+
*/
|
|
5
112
|
export type ComponentType = number;
|
|
113
|
+
/**
|
|
114
|
+
* Entity identifier (unique number).
|
|
115
|
+
* @category Types
|
|
116
|
+
*/
|
|
6
117
|
export type Entity = number;
|
|
118
|
+
/**
|
|
119
|
+
* Manages entity lifecycle and signatures.
|
|
120
|
+
*
|
|
121
|
+
* @remarks
|
|
122
|
+
* The EntityManager handles:
|
|
123
|
+
* - Creating new entities (recycling IDs from a pool)
|
|
124
|
+
* - Destroying entities (returning IDs to the pool)
|
|
125
|
+
* - Storing and updating component signatures for each entity
|
|
126
|
+
*
|
|
127
|
+
* Entities are represented as simple numbers (IDs) and the manager maintains
|
|
128
|
+
* a signature (bit field) for each entity indicating which components it has.
|
|
129
|
+
*
|
|
130
|
+
* @category Managers
|
|
131
|
+
*/
|
|
7
132
|
export declare class EntityManager {
|
|
8
133
|
private _availableEntities;
|
|
9
134
|
private _signatures;
|
|
@@ -15,9 +140,32 @@ export declare class EntityManager {
|
|
|
15
140
|
setSignature(entity: Entity, signature: ComponentSignature): void;
|
|
16
141
|
getSignature(entity: Entity): ComponentSignature | null;
|
|
17
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Internal interface for component array lifecycle hooks.
|
|
145
|
+
* @internal
|
|
146
|
+
*/
|
|
18
147
|
export interface CArray {
|
|
19
148
|
entityDestroyed(entity: Entity): void;
|
|
20
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Efficient sparse-set data structure for storing component data.
|
|
152
|
+
*
|
|
153
|
+
* @remarks
|
|
154
|
+
* ComponentArray uses a sparse-set implementation for O(1) insertion, deletion,
|
|
155
|
+
* and lookup while maintaining dense packing for cache-efficient iteration.
|
|
156
|
+
*
|
|
157
|
+
* The sparse-set consists of:
|
|
158
|
+
* - **Dense array**: Packed component data for iteration
|
|
159
|
+
* - **Sparse array**: Maps entity ID to dense array index
|
|
160
|
+
* - **Reverse array**: Maps dense array index back to entity ID
|
|
161
|
+
*
|
|
162
|
+
* This structure allows fast component access by entity ID and fast iteration
|
|
163
|
+
* over all components without gaps.
|
|
164
|
+
*
|
|
165
|
+
* @typeParam T - The component data type
|
|
166
|
+
*
|
|
167
|
+
* @category Data Structures
|
|
168
|
+
*/
|
|
21
169
|
export declare class ComponentArray<T> implements CArray {
|
|
22
170
|
private denseArray;
|
|
23
171
|
private sparse;
|
|
@@ -29,6 +177,21 @@ export declare class ComponentArray<T> implements CArray {
|
|
|
29
177
|
removeData(entity: Entity): void;
|
|
30
178
|
entityDestroyed(entity: Entity): void;
|
|
31
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Manages component registration and component data storage.
|
|
182
|
+
*
|
|
183
|
+
* @remarks
|
|
184
|
+
* The ComponentManager handles:
|
|
185
|
+
* - Registering new component types and assigning unique type IDs
|
|
186
|
+
* - Creating ComponentArray storage for each component type
|
|
187
|
+
* - Adding, removing, and querying component data for entities
|
|
188
|
+
* - Cleaning up component data when entities are destroyed
|
|
189
|
+
*
|
|
190
|
+
* Each component type gets a unique ID (0-31) and its own ComponentArray
|
|
191
|
+
* for efficient storage and retrieval.
|
|
192
|
+
*
|
|
193
|
+
* @category Managers
|
|
194
|
+
*/
|
|
32
195
|
export declare class ComponentManager {
|
|
33
196
|
private _componentNameToTypeMap;
|
|
34
197
|
private _nextAvailableComponentType;
|
|
@@ -40,9 +203,58 @@ export declare class ComponentManager {
|
|
|
40
203
|
entityDestroyed(entity: Entity): void;
|
|
41
204
|
private _getComponentArray;
|
|
42
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* System interface for processing entities with specific component combinations.
|
|
208
|
+
*
|
|
209
|
+
* @remarks
|
|
210
|
+
* A System maintains a set of entities that match its component signature.
|
|
211
|
+
* The ECS automatically updates this set when entities are created, destroyed,
|
|
212
|
+
* or have their components modified.
|
|
213
|
+
*
|
|
214
|
+
* Systems contain only the logic for processing entities - the `entities` set
|
|
215
|
+
* is automatically managed by the SystemManager.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```typescript
|
|
219
|
+
* const movementSystem: System = {
|
|
220
|
+
* entities: new Set()
|
|
221
|
+
* };
|
|
222
|
+
*
|
|
223
|
+
* // System logic (called in game loop)
|
|
224
|
+
* function updateMovement(deltaTime: number) {
|
|
225
|
+
* movementSystem.entities.forEach(entity => {
|
|
226
|
+
* const pos = ecs.getComponentFromEntity<Position>('Position', entity);
|
|
227
|
+
* const vel = ecs.getComponentFromEntity<Velocity>('Velocity', entity);
|
|
228
|
+
* if (pos && vel) {
|
|
229
|
+
* pos.x += vel.x * deltaTime;
|
|
230
|
+
* pos.y += vel.y * deltaTime;
|
|
231
|
+
* }
|
|
232
|
+
* });
|
|
233
|
+
* }
|
|
234
|
+
* ```
|
|
235
|
+
*
|
|
236
|
+
* @category Types
|
|
237
|
+
*/
|
|
43
238
|
export interface System {
|
|
44
239
|
entities: Set<Entity>;
|
|
45
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Manages system registration and entity-system matching.
|
|
243
|
+
*
|
|
244
|
+
* @remarks
|
|
245
|
+
* The SystemManager handles:
|
|
246
|
+
* - Registering systems with their component signature requirements
|
|
247
|
+
* - Maintaining the set of entities that match each system's signature
|
|
248
|
+
* - Automatically adding/removing entities from systems when signatures change
|
|
249
|
+
* - Cleaning up system entity sets when entities are destroyed
|
|
250
|
+
*
|
|
251
|
+
* When an entity's component signature changes (components added/removed),
|
|
252
|
+
* the SystemManager checks all registered systems and updates their entity sets.
|
|
253
|
+
* An entity is added to a system's set if its signature contains all components
|
|
254
|
+
* required by the system's signature.
|
|
255
|
+
*
|
|
256
|
+
* @category Managers
|
|
257
|
+
*/
|
|
46
258
|
export declare class SystemManager {
|
|
47
259
|
private _systems;
|
|
48
260
|
registerSystem(systemName: string, system: System): void;
|
|
@@ -50,6 +262,48 @@ export declare class SystemManager {
|
|
|
50
262
|
entityDestroyed(entity: Entity): void;
|
|
51
263
|
entitySignatureChanged(entity: Entity, signature: ComponentSignature): void;
|
|
52
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Main ECS coordinator that manages entities, components, and systems.
|
|
267
|
+
*
|
|
268
|
+
* @remarks
|
|
269
|
+
* The Coordinator is the central API for working with the ECS. It provides a unified
|
|
270
|
+
* interface for:
|
|
271
|
+
* - Creating and destroying entities
|
|
272
|
+
* - Registering and managing components
|
|
273
|
+
* - Registering and configuring systems
|
|
274
|
+
* - Querying component data
|
|
275
|
+
*
|
|
276
|
+
* The Coordinator automatically keeps entity signatures up-to-date and notifies
|
|
277
|
+
* systems when entities match their component requirements.
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* Complete ECS workflow
|
|
281
|
+
* ```typescript
|
|
282
|
+
* const ecs = new Coordinator();
|
|
283
|
+
*
|
|
284
|
+
* // Setup
|
|
285
|
+
* ecs.registerComponent<Position>('Position');
|
|
286
|
+
* ecs.registerComponent<Velocity>('Velocity');
|
|
287
|
+
*
|
|
288
|
+
* // Create entity
|
|
289
|
+
* const entity = ecs.createEntity();
|
|
290
|
+
* ecs.addComponentToEntity('Position', entity, { x: 0, y: 0 });
|
|
291
|
+
* ecs.addComponentToEntity('Velocity', entity, { x: 1, y: 0 });
|
|
292
|
+
*
|
|
293
|
+
* // Update
|
|
294
|
+
* const pos = ecs.getComponentFromEntity<Position>('Position', entity);
|
|
295
|
+
* const vel = ecs.getComponentFromEntity<Velocity>('Velocity', entity);
|
|
296
|
+
* if (pos && vel) {
|
|
297
|
+
* pos.x += vel.x;
|
|
298
|
+
* pos.y += vel.y;
|
|
299
|
+
* }
|
|
300
|
+
*
|
|
301
|
+
* // Cleanup
|
|
302
|
+
* ecs.destroyEntity(entity);
|
|
303
|
+
* ```
|
|
304
|
+
*
|
|
305
|
+
* @category Core
|
|
306
|
+
*/
|
|
53
307
|
export declare class Coordinator {
|
|
54
308
|
private _entityManager;
|
|
55
309
|
private _componentManager;
|
package/index.js
CHANGED
|
@@ -1,267 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
var MAX_ENTITIES = 1e4;
|
|
3
|
-
var MAX_COMPONENTS = 32;
|
|
1
|
+
var O=1e4,W=32;class G{_availableEntities=[];_signatures=[];_maxEntities;_livingEntityCount=0;constructor(b=1e4){this._maxEntities=b;for(let j=0;j<this._maxEntities;j++)this._availableEntities.push(j),this._signatures.push(0)}createEntity(){if(this._livingEntityCount>=this._maxEntities)throw Error("Max entities reached");let b=this._availableEntities.shift();if(b===void 0)throw Error("No available entities");return this._signatures[b]=0,this._livingEntityCount++,b}destroyEntity(b){if(b>=this._maxEntities||b<0)throw Error("Invalid entity out of range");this._signatures[b]=0,this._availableEntities.push(b),this._livingEntityCount--}setSignature(b,j){if(b>=this._maxEntities||b<0)throw Error("Invalid entity out of range");this._signatures[b]=j}getSignature(b){if(b>=this._maxEntities||b<0)return null;return this._signatures[b]}}class H{denseArray;sparse;reverse;_count;constructor(b){this._count=0,this.denseArray=Array(b),this.sparse=Array(b),this.reverse=Array(b)}insertData(b,j){if(this.getData(b)!==null)this.removeData(b);if(this.sparse.length<b)this.sparse=[...this.sparse,...Array(b-this.sparse.length).fill(null)];this.denseArray[this._count]=j,this.reverse[this._count]=b,this.sparse[b]=this._count,this._count++}getData(b){if(this.sparse.length<=b)return null;let j=this.sparse[b];if(j===void 0||j===null||j>=this._count)return null;if(this.reverse[j]!==b)return null;return this.denseArray[j]}removeData(b){let j=this.sparse[b];if(j===void 0||j===null||j>=this._count)return;let z=this.reverse[this._count-1];if(z===null)return;this.denseArray[j]=this.denseArray[this._count-1],this.reverse[j]=z,this.sparse[z]=j,this.sparse[b]=null,this._count--}entityDestroyed(b){this.removeData(b)}}class J{_componentNameToTypeMap=new Map;_nextAvailableComponentType=0;registerComponent(b){if(this._componentNameToTypeMap.has(b))console.warn(`Component ${b} already registered; registering with the given new type`);let j=this._nextAvailableComponentType;this._componentNameToTypeMap.set(b,{componentType:j,componentArray:new H(1e4)}),this._nextAvailableComponentType++}getComponentType(b){return this._componentNameToTypeMap.get(b)?.componentType??null}addComponentToEntity(b,j,z){let B=this._getComponentArray(b);if(B===null)return;B.insertData(j,z)}removeComponentFromEntity(b,j){let z=this._getComponentArray(b);if(z===null)return;z.removeData(j)}getComponentFromEntity(b,j){let z=this._getComponentArray(b);if(z===null)return null;return z.getData(j)}entityDestroyed(b){for(let j of this._componentNameToTypeMap.values())j.componentArray.entityDestroyed(b)}_getComponentArray(b){let j=this._componentNameToTypeMap.get(b);if(j===void 0)return console.warn(`Component ${b} not registered`),null;return j.componentArray}}class K{_systems=new Map;registerSystem(b,j){if(this._systems.has(b)){console.warn(`System ${b} already registered`);return}this._systems.set(b,{system:j,signature:0})}setSignature(b,j){if(!this._systems.has(b)){console.warn(`System ${b} not registered`);return}let z=this._systems.get(b);if(z===void 0){console.warn(`System ${b} not registered`);return}z.signature=j}entityDestroyed(b){for(let j of this._systems.values())j.system.entities.delete(b)}entitySignatureChanged(b,j){for(let z of this._systems.values()){let B=z.signature;if((B&j)===B)z.system.entities.add(b);else z.system.entities.delete(b)}}}class L{_entityManager;_componentManager;_systemManager;constructor(){this._entityManager=new G,this._componentManager=new J,this._systemManager=new K}createEntity(){return this._entityManager.createEntity()}destroyEntity(b){this._entityManager.destroyEntity(b),this._componentManager.entityDestroyed(b),this._systemManager.entityDestroyed(b)}registerComponent(b){this._componentManager.registerComponent(b)}addComponentToEntity(b,j,z){this._componentManager.addComponentToEntity(b,j,z);let B=this._entityManager.getSignature(j);if(B===null)B=0;let D=this._componentManager.getComponentType(b);if(D===null){console.warn(`Component ${b} not registered`);return}B|=1<<D,this._entityManager.setSignature(j,B),this._systemManager.entitySignatureChanged(j,B)}removeComponentFromEntity(b,j){this._componentManager.removeComponentFromEntity(b,j);let z=this._entityManager.getSignature(j);if(z===null)z=0;let B=this._componentManager.getComponentType(b);if(B===null)return;z&=~(1<<B),this._entityManager.setSignature(j,z),this._systemManager.entitySignatureChanged(j,z)}getComponentFromEntity(b,j){return this._componentManager.getComponentFromEntity(b,j)}getComponentType(b){return this._componentManager.getComponentType(b)??null}registerSystem(b,j){this._systemManager.registerSystem(b,j)}setSystemSignature(b,j){this._systemManager.setSignature(b,j)}}export{K as SystemManager,O as MAX_ENTITIES,W as MAX_COMPONENTS,G as EntityManager,L as Coordinator,J as ComponentManager,H as ComponentArray};
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
_availableEntities = [];
|
|
7
|
-
_signatures = [];
|
|
8
|
-
_maxEntities;
|
|
9
|
-
_livingEntityCount = 0;
|
|
10
|
-
constructor(maxEntities = MAX_ENTITIES) {
|
|
11
|
-
this._maxEntities = maxEntities;
|
|
12
|
-
for (let i = 0;i < this._maxEntities; i++) {
|
|
13
|
-
this._availableEntities.push(i);
|
|
14
|
-
this._signatures.push(0);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
createEntity() {
|
|
18
|
-
if (this._livingEntityCount >= this._maxEntities) {
|
|
19
|
-
throw new Error("Max entities reached");
|
|
20
|
-
}
|
|
21
|
-
const entity = this._availableEntities.shift();
|
|
22
|
-
if (entity === undefined) {
|
|
23
|
-
throw new Error("No available entities");
|
|
24
|
-
}
|
|
25
|
-
this._signatures[entity] = 0;
|
|
26
|
-
this._livingEntityCount++;
|
|
27
|
-
return entity;
|
|
28
|
-
}
|
|
29
|
-
destroyEntity(entity) {
|
|
30
|
-
if (entity >= this._maxEntities || entity < 0) {
|
|
31
|
-
throw new Error("Invalid entity out of range");
|
|
32
|
-
}
|
|
33
|
-
this._signatures[entity] = 0;
|
|
34
|
-
this._availableEntities.push(entity);
|
|
35
|
-
this._livingEntityCount--;
|
|
36
|
-
}
|
|
37
|
-
setSignature(entity, signature) {
|
|
38
|
-
if (entity >= this._maxEntities || entity < 0) {
|
|
39
|
-
throw new Error("Invalid entity out of range");
|
|
40
|
-
}
|
|
41
|
-
this._signatures[entity] = signature;
|
|
42
|
-
}
|
|
43
|
-
getSignature(entity) {
|
|
44
|
-
if (entity >= this._maxEntities || entity < 0) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
return this._signatures[entity];
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
class ComponentArray {
|
|
52
|
-
denseArray;
|
|
53
|
-
sparse;
|
|
54
|
-
reverse;
|
|
55
|
-
_count;
|
|
56
|
-
constructor(maxEntities) {
|
|
57
|
-
this._count = 0;
|
|
58
|
-
this.denseArray = new Array(maxEntities);
|
|
59
|
-
this.sparse = new Array(maxEntities);
|
|
60
|
-
this.reverse = new Array(maxEntities);
|
|
61
|
-
}
|
|
62
|
-
insertData(entity, data) {
|
|
63
|
-
if (this.getData(entity) !== null) {
|
|
64
|
-
this.removeData(entity);
|
|
65
|
-
}
|
|
66
|
-
if (this.sparse.length < entity) {
|
|
67
|
-
this.sparse = [...this.sparse, ...new Array(entity - this.sparse.length).fill(null)];
|
|
68
|
-
}
|
|
69
|
-
this.denseArray[this._count] = data;
|
|
70
|
-
this.reverse[this._count] = entity;
|
|
71
|
-
this.sparse[entity] = this._count;
|
|
72
|
-
this._count++;
|
|
73
|
-
}
|
|
74
|
-
getData(entity) {
|
|
75
|
-
if (this.sparse.length <= entity) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
const denseIndex = this.sparse[entity];
|
|
79
|
-
if (denseIndex === undefined || denseIndex === null || denseIndex >= this._count) {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
if (this.reverse[denseIndex] !== entity) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
return this.denseArray[denseIndex];
|
|
86
|
-
}
|
|
87
|
-
removeData(entity) {
|
|
88
|
-
const denseIndex = this.sparse[entity];
|
|
89
|
-
if (denseIndex === undefined || denseIndex === null || denseIndex >= this._count) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
const lastEntity = this.reverse[this._count - 1];
|
|
93
|
-
if (lastEntity === null) {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
this.denseArray[denseIndex] = this.denseArray[this._count - 1];
|
|
97
|
-
this.reverse[denseIndex] = lastEntity;
|
|
98
|
-
this.sparse[lastEntity] = denseIndex;
|
|
99
|
-
this.sparse[entity] = null;
|
|
100
|
-
this._count--;
|
|
101
|
-
}
|
|
102
|
-
entityDestroyed(entity) {
|
|
103
|
-
this.removeData(entity);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
class ComponentManager {
|
|
108
|
-
_componentNameToTypeMap = new Map;
|
|
109
|
-
_nextAvailableComponentType = 0;
|
|
110
|
-
registerComponent(componentName) {
|
|
111
|
-
if (this._componentNameToTypeMap.has(componentName)) {
|
|
112
|
-
console.warn(`Component ${componentName} already registered; registering with the given new type`);
|
|
113
|
-
}
|
|
114
|
-
const componentType = this._nextAvailableComponentType;
|
|
115
|
-
this._componentNameToTypeMap.set(componentName, { componentType, componentArray: new ComponentArray(MAX_ENTITIES) });
|
|
116
|
-
this._nextAvailableComponentType++;
|
|
117
|
-
}
|
|
118
|
-
getComponentType(componentName) {
|
|
119
|
-
return this._componentNameToTypeMap.get(componentName)?.componentType ?? null;
|
|
120
|
-
}
|
|
121
|
-
addComponentToEntity(componentName, entity, component) {
|
|
122
|
-
const componentArray = this._getComponentArray(componentName);
|
|
123
|
-
if (componentArray === null) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
componentArray.insertData(entity, component);
|
|
127
|
-
}
|
|
128
|
-
removeComponentFromEntity(componentName, entity) {
|
|
129
|
-
const componentArray = this._getComponentArray(componentName);
|
|
130
|
-
if (componentArray === null) {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
componentArray.removeData(entity);
|
|
134
|
-
}
|
|
135
|
-
getComponentFromEntity(componentName, entity) {
|
|
136
|
-
const componentArray = this._getComponentArray(componentName);
|
|
137
|
-
if (componentArray === null) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
return componentArray.getData(entity);
|
|
141
|
-
}
|
|
142
|
-
entityDestroyed(entity) {
|
|
143
|
-
for (const component of this._componentNameToTypeMap.values()) {
|
|
144
|
-
component.componentArray.entityDestroyed(entity);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
_getComponentArray(componentName) {
|
|
148
|
-
const component = this._componentNameToTypeMap.get(componentName);
|
|
149
|
-
if (component === undefined) {
|
|
150
|
-
console.warn(`Component ${componentName} not registered`);
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
return component.componentArray;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
class SystemManager {
|
|
158
|
-
_systems = new Map;
|
|
159
|
-
registerSystem(systemName, system) {
|
|
160
|
-
if (this._systems.has(systemName)) {
|
|
161
|
-
console.warn(`System ${systemName} already registered`);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
this._systems.set(systemName, { system, signature: 0 });
|
|
165
|
-
}
|
|
166
|
-
setSignature(systemName, signature) {
|
|
167
|
-
if (!this._systems.has(systemName)) {
|
|
168
|
-
console.warn(`System ${systemName} not registered`);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
const system = this._systems.get(systemName);
|
|
172
|
-
if (system === undefined) {
|
|
173
|
-
console.warn(`System ${systemName} not registered`);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
system.signature = signature;
|
|
177
|
-
}
|
|
178
|
-
entityDestroyed(entity) {
|
|
179
|
-
for (const system of this._systems.values()) {
|
|
180
|
-
system.system.entities.delete(entity);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
entitySignatureChanged(entity, signature) {
|
|
184
|
-
for (const system of this._systems.values()) {
|
|
185
|
-
const systemSignature = system.signature;
|
|
186
|
-
if ((systemSignature & signature) === systemSignature) {
|
|
187
|
-
system.system.entities.add(entity);
|
|
188
|
-
} else {
|
|
189
|
-
system.system.entities.delete(entity);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
class Coordinator {
|
|
196
|
-
_entityManager;
|
|
197
|
-
_componentManager;
|
|
198
|
-
_systemManager;
|
|
199
|
-
constructor() {
|
|
200
|
-
this._entityManager = new EntityManager;
|
|
201
|
-
this._componentManager = new ComponentManager;
|
|
202
|
-
this._systemManager = new SystemManager;
|
|
203
|
-
}
|
|
204
|
-
createEntity() {
|
|
205
|
-
return this._entityManager.createEntity();
|
|
206
|
-
}
|
|
207
|
-
destroyEntity(entity) {
|
|
208
|
-
this._entityManager.destroyEntity(entity);
|
|
209
|
-
this._componentManager.entityDestroyed(entity);
|
|
210
|
-
this._systemManager.entityDestroyed(entity);
|
|
211
|
-
}
|
|
212
|
-
registerComponent(componentName) {
|
|
213
|
-
this._componentManager.registerComponent(componentName);
|
|
214
|
-
}
|
|
215
|
-
addComponentToEntity(componentName, entity, component) {
|
|
216
|
-
this._componentManager.addComponentToEntity(componentName, entity, component);
|
|
217
|
-
let signature = this._entityManager.getSignature(entity);
|
|
218
|
-
if (signature === null) {
|
|
219
|
-
signature = 0;
|
|
220
|
-
}
|
|
221
|
-
const componentType = this._componentManager.getComponentType(componentName);
|
|
222
|
-
if (componentType === null) {
|
|
223
|
-
console.warn(`Component ${componentName} not registered`);
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
signature |= 1 << componentType;
|
|
227
|
-
this._entityManager.setSignature(entity, signature);
|
|
228
|
-
this._systemManager.entitySignatureChanged(entity, signature);
|
|
229
|
-
}
|
|
230
|
-
removeComponentFromEntity(componentName, entity) {
|
|
231
|
-
this._componentManager.removeComponentFromEntity(componentName, entity);
|
|
232
|
-
let signature = this._entityManager.getSignature(entity);
|
|
233
|
-
if (signature === null) {
|
|
234
|
-
signature = 0;
|
|
235
|
-
}
|
|
236
|
-
const componentType = this._componentManager.getComponentType(componentName);
|
|
237
|
-
if (componentType === null) {
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
signature &= ~(1 << componentType);
|
|
241
|
-
this._entityManager.setSignature(entity, signature);
|
|
242
|
-
this._systemManager.entitySignatureChanged(entity, signature);
|
|
243
|
-
}
|
|
244
|
-
getComponentFromEntity(componentName, entity) {
|
|
245
|
-
return this._componentManager.getComponentFromEntity(componentName, entity);
|
|
246
|
-
}
|
|
247
|
-
getComponentType(componentName) {
|
|
248
|
-
return this._componentManager.getComponentType(componentName) ?? null;
|
|
249
|
-
}
|
|
250
|
-
registerSystem(systemName, system) {
|
|
251
|
-
this._systemManager.registerSystem(systemName, system);
|
|
252
|
-
}
|
|
253
|
-
setSystemSignature(systemName, signature) {
|
|
254
|
-
this._systemManager.setSignature(systemName, signature);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
export {
|
|
258
|
-
SystemManager,
|
|
259
|
-
MAX_ENTITIES,
|
|
260
|
-
MAX_COMPONENTS,
|
|
261
|
-
EntityManager,
|
|
262
|
-
Coordinator,
|
|
263
|
-
ComponentManager,
|
|
264
|
-
ComponentArray
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
//# debugId=840788BF2D72B20064756E2164756E21
|
|
3
|
+
//# debugId=983008DE2E282BA464756E2164756E21
|
package/index.js.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/** This is a direct port of the tutorial from https://austinmorlan.com/posts/entity_component_system/ with slight modifications */\n\nexport const MAX_ENTITIES = 10000;\nexport const MAX_COMPONENTS = 32;\n\nexport type ComponentSignature = number;\nexport type ComponentType = number;\n\nexport type Entity = number;\n\n\nexport class EntityManager {\n\n private _availableEntities: Entity[] = [];\n private _signatures: ComponentSignature[] = [];\n private _maxEntities: number;\n\n private _livingEntityCount = 0;\n\n constructor(maxEntities: number = MAX_ENTITIES) {\n this._maxEntities = maxEntities;\n for (let i = 0; i < this._maxEntities; i++) {\n this._availableEntities.push(i);\n this._signatures.push(0);\n }\n }\n\n createEntity(): Entity {\n if(this._livingEntityCount >= this._maxEntities) {\n throw new Error('Max entities reached');\n }\n const entity = this._availableEntities.shift();\n if(entity === undefined) {\n throw new Error('No available entities');\n }\n this._signatures[entity] = 0;\n this._livingEntityCount++;\n return entity;\n }\n\n destroyEntity(entity: Entity): void {\n if(entity >= this._maxEntities || entity < 0) {\n throw new Error('Invalid entity out of range');\n }\n this._signatures[entity] = 0;\n this._availableEntities.push(entity);\n this._livingEntityCount--;\n }\n\n setSignature(entity: Entity, signature: ComponentSignature): void {\n if(entity >= this._maxEntities || entity < 0) {\n throw new Error('Invalid entity out of range');\n }\n this._signatures[entity] = signature;\n }\n\n getSignature(entity: Entity): ComponentSignature | null {\n if(entity >= this._maxEntities || entity < 0) {\n return null;\n }\n return this._signatures[entity];\n }\n}\n\ntype Tuple<T, N extends number> = N extends N\n ? number extends N\n ? T[]\n : _TupleOf<T, N, []>\n : never;\n\ntype _TupleOf<T, N extends number, R extends unknown[]> = R['length'] extends N\n ? R\n : _TupleOf<T, N, [...R, T]>;\n\n// Usage\n\nexport interface CArray {\n entityDestroyed(entity: Entity): void;\n}\n\nexport class ComponentArray<T> implements CArray {\n\n private denseArray: T[]; // packed array of data\n private sparse: (Entity | null)[]; // maps entity to index in dense array\n private reverse: (Entity | null)[]; // maps index in dense array to entity\n private _count: number;\n\n constructor(maxEntities: number) {\n this._count = 0;\n this.denseArray = new Array(maxEntities);\n this.sparse = new Array(maxEntities);\n this.reverse = new Array(maxEntities);\n }\n\n insertData(entity: Entity, data: T): void {\n if(this.getData(entity) !== null) {\n this.removeData(entity);\n }\n if(this.sparse.length < entity){\n // resize the array for the new entity but normally this should not happen\n this.sparse = [...this.sparse, ...new Array(entity - this.sparse.length).fill(null)];\n }\n\n this.denseArray[this._count] = data;\n this.reverse[this._count] = entity;\n this.sparse[entity] = this._count;\n this._count++;\n }\n\n getData(entity: Entity): T | null {\n if(this.sparse.length <= entity){\n return null;\n }\n\n const denseIndex = this.sparse[entity];\n if(denseIndex === undefined || denseIndex === null || denseIndex >= this._count){\n return null;\n }\n\n if(this.reverse[denseIndex] !== entity) {\n return null;\n }\n\n return this.denseArray[denseIndex];\n }\n\n removeData(entity: Entity): void {\n const denseIndex = this.sparse[entity];\n if(denseIndex === undefined || denseIndex === null || denseIndex >= this._count){\n return;\n }\n\n const lastEntity = this.reverse[this._count - 1];\n\n if(lastEntity === null) {\n return;\n }\n\n this.denseArray[denseIndex] = this.denseArray[this._count - 1];\n this.reverse[denseIndex] = lastEntity;\n this.sparse[lastEntity] = denseIndex;\n this.sparse[entity] = null;\n\n this._count--;\n }\n\n entityDestroyed(entity: Entity): void {\n this.removeData(entity);\n }\n}\n\nexport class ComponentManager {\n\n private _componentNameToTypeMap: Map<string, {componentType: ComponentType, componentArray: CArray}> = new Map();\n private _nextAvailableComponentType: ComponentType = 0;\n \n registerComponent<T>(componentName: string){\n if(this._componentNameToTypeMap.has(componentName)) {\n console.warn(`Component ${componentName} already registered; registering with the given new type`);\n }\n const componentType = this._nextAvailableComponentType;\n this._componentNameToTypeMap.set(componentName, {componentType, componentArray: new ComponentArray<T>(MAX_ENTITIES)});\n this._nextAvailableComponentType++;\n }\n\n getComponentType(componentName: string): ComponentType | null {\n return this._componentNameToTypeMap.get(componentName)?.componentType ?? null;\n }\n\n addComponentToEntity<T>(componentName: string, entity: Entity, component: T){\n const componentArray = this._getComponentArray<T>(componentName);\n if(componentArray === null) {\n return;\n }\n componentArray.insertData(entity, component);\n }\n\n removeComponentFromEntity<T>(componentName: string, entity: Entity){\n const componentArray = this._getComponentArray<T>(componentName);\n if(componentArray === null) {\n return;\n }\n componentArray.removeData(entity);\n }\n\n getComponentFromEntity<T>(componentName: string, entity: Entity): T | null {\n const componentArray = this._getComponentArray<T>(componentName);\n if(componentArray === null) {\n return null;\n }\n return componentArray.getData(entity);\n }\n\n entityDestroyed(entity: Entity){\n for(const component of this._componentNameToTypeMap.values()){\n component.componentArray.entityDestroyed(entity);\n }\n }\n\n private _getComponentArray<T>(componentName: string): ComponentArray<T> | null {\n const component = this._componentNameToTypeMap.get(componentName);\n if(component === undefined) {\n console.warn(`Component ${componentName} not registered`);\n return null;\n }\n return component.componentArray as ComponentArray<T>;\n }\n\n}\n\nexport interface System {\n entities: Set<Entity>;\n}\n\nexport class SystemManager {\n private _systems: Map<string, {system: System, signature: ComponentSignature}> = new Map();\n\n registerSystem(systemName: string, system: System){\n if(this._systems.has(systemName)) {\n console.warn(`System ${systemName} already registered`);\n return;\n }\n this._systems.set(systemName, {system, signature: 0});\n }\n\n setSignature(systemName: string, signature: ComponentSignature){\n if(!this._systems.has(systemName)) {\n console.warn(`System ${systemName} not registered`);\n return;\n }\n const system = this._systems.get(systemName);\n if(system === undefined) {\n console.warn(`System ${systemName} not registered`);\n return;\n }\n system.signature = signature;\n }\n\n entityDestroyed(entity: Entity){\n for(const system of this._systems.values()){\n system.system.entities.delete(entity);\n }\n }\n\n entitySignatureChanged(entity: Entity, signature: ComponentSignature){\n for(const system of this._systems.values()){\n const systemSignature = system.signature;\n if((systemSignature & signature) === systemSignature){\n system.system.entities.add(entity);\n } else {\n system.system.entities.delete(entity);\n }\n }\n }\n}\n\nexport class Coordinator {\n private _entityManager: EntityManager;\n private _componentManager: ComponentManager;\n private _systemManager: SystemManager;\n\n constructor(){\n this._entityManager = new EntityManager();\n this._componentManager = new ComponentManager();\n this._systemManager = new SystemManager();\n }\n\n createEntity(): Entity {\n return this._entityManager.createEntity();\n }\n\n destroyEntity(entity: Entity): void {\n this._entityManager.destroyEntity(entity);\n this._componentManager.entityDestroyed(entity);\n this._systemManager.entityDestroyed(entity);\n }\n\n registerComponent<T>(componentName: string): void {\n this._componentManager.registerComponent<T>(componentName);\n }\n\n addComponentToEntity<T>(componentName: string, entity: Entity, component: T): void {\n this._componentManager.addComponentToEntity<T>(componentName, entity, component);\n let signature = this._entityManager.getSignature(entity);\n if(signature === null) {\n signature = 0;\n }\n const componentType = this._componentManager.getComponentType(componentName);\n if(componentType === null) {\n console.warn(`Component ${componentName} not registered`);\n return;\n }\n signature |= 1 << componentType;\n this._entityManager.setSignature(entity, signature);\n this._systemManager.entitySignatureChanged(entity, signature);\n }\n\n removeComponentFromEntity<T>(componentName: string, entity: Entity): void {\n this._componentManager.removeComponentFromEntity<T>(componentName, entity);\n let signature = this._entityManager.getSignature(entity);\n if(signature === null) {\n signature = 0;\n }\n const componentType = this._componentManager.getComponentType(componentName);\n if(componentType === null) {\n return;\n }\n signature &= ~(1 << componentType);\n this._entityManager.setSignature(entity, signature);\n this._systemManager.entitySignatureChanged(entity, signature);\n }\n\n getComponentFromEntity<T>(componentName: string, entity: Entity): T | null {\n return this._componentManager.getComponentFromEntity<T>(componentName, entity);\n }\n\n getComponentType(componentName: string): ComponentType | null {\n return this._componentManager.getComponentType(componentName) ?? null;\n }\n\n registerSystem(systemName: string, system: System): void {\n this._systemManager.registerSystem(systemName, system);\n }\n\n setSystemSignature(systemName: string, signature: ComponentSignature): void {\n this._systemManager.setSignature(systemName, signature);\n }\n}\n"
|
|
5
|
+
"/**\n * @packageDocumentation\n * Entity Component System (ECS) implementation for TypeScript.\n *\n * @remarks\n * The `@ue-too/ecs` package provides a high-performance Entity Component System architecture\n * based on the tutorial from https://austinmorlan.com/posts/entity_component_system/\n *\n * ## ECS Architecture\n *\n * - **Entities**: Unique identifiers (numbers) representing game objects\n * - **Components**: Data containers attached to entities\n * - **Systems**: Logic that operates on entities with specific component combinations\n * - **Signatures**: Bit flags indicating which components an entity has\n *\n * ## Key Features\n *\n * - **Efficient Storage**: Component arrays using sparse-set data structure\n * - **Fast Iteration**: Dense packing for cache-friendly iteration\n * - **Type-Safe**: TypeScript generics for component type safety\n * - **Signature Matching**: Automatic system updates when entity signatures change\n * - **Pooling**: Entity ID recycling for memory efficiency\n *\n * ## Core Classes\n *\n * - {@link Coordinator}: Main ECS coordinator managing all subsystems\n * - {@link EntityManager}: Creates and destroys entities\n * - {@link ComponentManager}: Registers components and manages component data\n * - {@link SystemManager}: Registers systems and maintains entity sets\n * - {@link ComponentArray}: Efficient sparse-set storage for component data\n *\n * @example\n * Basic ECS usage\n * ```typescript\n * import { Coordinator } from '@ue-too/ecs';\n *\n * // Define component types\n * type Position = { x: number; y: number };\n * type Velocity = { x: number; y: number };\n *\n * // Create coordinator\n * const coordinator = new Coordinator();\n *\n * // Register components\n * coordinator.registerComponent<Position>('Position');\n * coordinator.registerComponent<Velocity>('Velocity');\n *\n * // Create entity with components\n * const entity = coordinator.createEntity();\n * coordinator.addComponentToEntity('Position', entity, { x: 0, y: 0 });\n * coordinator.addComponentToEntity('Velocity', entity, { x: 1, y: 1 });\n *\n * // Query components\n * const pos = coordinator.getComponentFromEntity<Position>('Position', entity);\n * console.log('Position:', pos);\n * ```\n *\n * @example\n * System registration\n * ```typescript\n * import { Coordinator, System } from '@ue-too/ecs';\n *\n * const coordinator = new Coordinator();\n * coordinator.registerComponent<Position>('Position');\n * coordinator.registerComponent<Velocity>('Velocity');\n *\n * // Create a movement system\n * const movementSystem: System = {\n * entities: new Set()\n * };\n *\n * coordinator.registerSystem('Movement', movementSystem);\n *\n * // Set signature (entities with Position AND Velocity)\n * const posType = coordinator.getComponentType('Position')!;\n * const velType = coordinator.getComponentType('Velocity')!;\n * const signature = (1 << posType) | (1 << velType);\n * coordinator.setSystemSignature('Movement', signature);\n *\n * // Update loop\n * function update(deltaTime: number) {\n * movementSystem.entities.forEach(entity => {\n * const pos = coordinator.getComponentFromEntity<Position>('Position', entity)!;\n * const vel = coordinator.getComponentFromEntity<Velocity>('Velocity', entity)!;\n * pos.x += vel.x * deltaTime;\n * pos.y += vel.y * deltaTime;\n * });\n * }\n * ```\n *\n * @see {@link Coordinator} for the main ECS API\n */\n\n/**\n * Maximum number of entities that can exist simultaneously.\n * @category Configuration\n */\nexport const MAX_ENTITIES = 10000;\n\n/**\n * Maximum number of component types that can be registered.\n * @category Configuration\n */\nexport const MAX_COMPONENTS = 32;\n\n/**\n * Component signature type (bit field indicating which components an entity has).\n * @category Types\n */\nexport type ComponentSignature = number;\n\n/**\n * Component type identifier.\n * @category Types\n */\nexport type ComponentType = number;\n\n/**\n * Entity identifier (unique number).\n * @category Types\n */\nexport type Entity = number;\n\n/**\n * Manages entity lifecycle and signatures.\n *\n * @remarks\n * The EntityManager handles:\n * - Creating new entities (recycling IDs from a pool)\n * - Destroying entities (returning IDs to the pool)\n * - Storing and updating component signatures for each entity\n *\n * Entities are represented as simple numbers (IDs) and the manager maintains\n * a signature (bit field) for each entity indicating which components it has.\n *\n * @category Managers\n */\nexport class EntityManager {\n\n private _availableEntities: Entity[] = [];\n private _signatures: ComponentSignature[] = [];\n private _maxEntities: number;\n\n private _livingEntityCount = 0;\n\n constructor(maxEntities: number = MAX_ENTITIES) {\n this._maxEntities = maxEntities;\n for (let i = 0; i < this._maxEntities; i++) {\n this._availableEntities.push(i);\n this._signatures.push(0);\n }\n }\n\n createEntity(): Entity {\n if(this._livingEntityCount >= this._maxEntities) {\n throw new Error('Max entities reached');\n }\n const entity = this._availableEntities.shift();\n if(entity === undefined) {\n throw new Error('No available entities');\n }\n this._signatures[entity] = 0;\n this._livingEntityCount++;\n return entity;\n }\n\n destroyEntity(entity: Entity): void {\n if(entity >= this._maxEntities || entity < 0) {\n throw new Error('Invalid entity out of range');\n }\n this._signatures[entity] = 0;\n this._availableEntities.push(entity);\n this._livingEntityCount--;\n }\n\n setSignature(entity: Entity, signature: ComponentSignature): void {\n if(entity >= this._maxEntities || entity < 0) {\n throw new Error('Invalid entity out of range');\n }\n this._signatures[entity] = signature;\n }\n\n getSignature(entity: Entity): ComponentSignature | null {\n if(entity >= this._maxEntities || entity < 0) {\n return null;\n }\n return this._signatures[entity];\n }\n}\n\ntype Tuple<T, N extends number> = N extends N\n ? number extends N\n ? T[]\n : _TupleOf<T, N, []>\n : never;\n\ntype _TupleOf<T, N extends number, R extends unknown[]> = R['length'] extends N\n ? R\n : _TupleOf<T, N, [...R, T]>;\n\n// Usage\n\n/**\n * Internal interface for component array lifecycle hooks.\n * @internal\n */\nexport interface CArray {\n entityDestroyed(entity: Entity): void;\n}\n\n/**\n * Efficient sparse-set data structure for storing component data.\n *\n * @remarks\n * ComponentArray uses a sparse-set implementation for O(1) insertion, deletion,\n * and lookup while maintaining dense packing for cache-efficient iteration.\n *\n * The sparse-set consists of:\n * - **Dense array**: Packed component data for iteration\n * - **Sparse array**: Maps entity ID to dense array index\n * - **Reverse array**: Maps dense array index back to entity ID\n *\n * This structure allows fast component access by entity ID and fast iteration\n * over all components without gaps.\n *\n * @typeParam T - The component data type\n *\n * @category Data Structures\n */\nexport class ComponentArray<T> implements CArray {\n\n private denseArray: T[]; // packed array of data\n private sparse: (Entity | null)[]; // maps entity to index in dense array\n private reverse: (Entity | null)[]; // maps index in dense array to entity\n private _count: number;\n\n constructor(maxEntities: number) {\n this._count = 0;\n this.denseArray = new Array(maxEntities);\n this.sparse = new Array(maxEntities);\n this.reverse = new Array(maxEntities);\n }\n\n insertData(entity: Entity, data: T): void {\n if(this.getData(entity) !== null) {\n this.removeData(entity);\n }\n if(this.sparse.length < entity){\n // resize the array for the new entity but normally this should not happen\n this.sparse = [...this.sparse, ...new Array(entity - this.sparse.length).fill(null)];\n }\n\n this.denseArray[this._count] = data;\n this.reverse[this._count] = entity;\n this.sparse[entity] = this._count;\n this._count++;\n }\n\n getData(entity: Entity): T | null {\n if(this.sparse.length <= entity){\n return null;\n }\n\n const denseIndex = this.sparse[entity];\n if(denseIndex === undefined || denseIndex === null || denseIndex >= this._count){\n return null;\n }\n\n if(this.reverse[denseIndex] !== entity) {\n return null;\n }\n\n return this.denseArray[denseIndex];\n }\n\n removeData(entity: Entity): void {\n const denseIndex = this.sparse[entity];\n if(denseIndex === undefined || denseIndex === null || denseIndex >= this._count){\n return;\n }\n\n const lastEntity = this.reverse[this._count - 1];\n\n if(lastEntity === null) {\n return;\n }\n\n this.denseArray[denseIndex] = this.denseArray[this._count - 1];\n this.reverse[denseIndex] = lastEntity;\n this.sparse[lastEntity] = denseIndex;\n this.sparse[entity] = null;\n\n this._count--;\n }\n\n entityDestroyed(entity: Entity): void {\n this.removeData(entity);\n }\n}\n\n/**\n * Manages component registration and component data storage.\n *\n * @remarks\n * The ComponentManager handles:\n * - Registering new component types and assigning unique type IDs\n * - Creating ComponentArray storage for each component type\n * - Adding, removing, and querying component data for entities\n * - Cleaning up component data when entities are destroyed\n *\n * Each component type gets a unique ID (0-31) and its own ComponentArray\n * for efficient storage and retrieval.\n *\n * @category Managers\n */\nexport class ComponentManager {\n\n private _componentNameToTypeMap: Map<string, {componentType: ComponentType, componentArray: CArray}> = new Map();\n private _nextAvailableComponentType: ComponentType = 0;\n \n registerComponent<T>(componentName: string){\n if(this._componentNameToTypeMap.has(componentName)) {\n console.warn(`Component ${componentName} already registered; registering with the given new type`);\n }\n const componentType = this._nextAvailableComponentType;\n this._componentNameToTypeMap.set(componentName, {componentType, componentArray: new ComponentArray<T>(MAX_ENTITIES)});\n this._nextAvailableComponentType++;\n }\n\n getComponentType(componentName: string): ComponentType | null {\n return this._componentNameToTypeMap.get(componentName)?.componentType ?? null;\n }\n\n addComponentToEntity<T>(componentName: string, entity: Entity, component: T){\n const componentArray = this._getComponentArray<T>(componentName);\n if(componentArray === null) {\n return;\n }\n componentArray.insertData(entity, component);\n }\n\n removeComponentFromEntity<T>(componentName: string, entity: Entity){\n const componentArray = this._getComponentArray<T>(componentName);\n if(componentArray === null) {\n return;\n }\n componentArray.removeData(entity);\n }\n\n getComponentFromEntity<T>(componentName: string, entity: Entity): T | null {\n const componentArray = this._getComponentArray<T>(componentName);\n if(componentArray === null) {\n return null;\n }\n return componentArray.getData(entity);\n }\n\n entityDestroyed(entity: Entity){\n for(const component of this._componentNameToTypeMap.values()){\n component.componentArray.entityDestroyed(entity);\n }\n }\n\n private _getComponentArray<T>(componentName: string): ComponentArray<T> | null {\n const component = this._componentNameToTypeMap.get(componentName);\n if(component === undefined) {\n console.warn(`Component ${componentName} not registered`);\n return null;\n }\n return component.componentArray as ComponentArray<T>;\n }\n\n}\n\n/**\n * System interface for processing entities with specific component combinations.\n *\n * @remarks\n * A System maintains a set of entities that match its component signature.\n * The ECS automatically updates this set when entities are created, destroyed,\n * or have their components modified.\n *\n * Systems contain only the logic for processing entities - the `entities` set\n * is automatically managed by the SystemManager.\n *\n * @example\n * ```typescript\n * const movementSystem: System = {\n * entities: new Set()\n * };\n *\n * // System logic (called in game loop)\n * function updateMovement(deltaTime: number) {\n * movementSystem.entities.forEach(entity => {\n * const pos = ecs.getComponentFromEntity<Position>('Position', entity);\n * const vel = ecs.getComponentFromEntity<Velocity>('Velocity', entity);\n * if (pos && vel) {\n * pos.x += vel.x * deltaTime;\n * pos.y += vel.y * deltaTime;\n * }\n * });\n * }\n * ```\n *\n * @category Types\n */\nexport interface System {\n entities: Set<Entity>;\n}\n\n/**\n * Manages system registration and entity-system matching.\n *\n * @remarks\n * The SystemManager handles:\n * - Registering systems with their component signature requirements\n * - Maintaining the set of entities that match each system's signature\n * - Automatically adding/removing entities from systems when signatures change\n * - Cleaning up system entity sets when entities are destroyed\n *\n * When an entity's component signature changes (components added/removed),\n * the SystemManager checks all registered systems and updates their entity sets.\n * An entity is added to a system's set if its signature contains all components\n * required by the system's signature.\n *\n * @category Managers\n */\nexport class SystemManager {\n private _systems: Map<string, {system: System, signature: ComponentSignature}> = new Map();\n\n registerSystem(systemName: string, system: System){\n if(this._systems.has(systemName)) {\n console.warn(`System ${systemName} already registered`);\n return;\n }\n this._systems.set(systemName, {system, signature: 0});\n }\n\n setSignature(systemName: string, signature: ComponentSignature){\n if(!this._systems.has(systemName)) {\n console.warn(`System ${systemName} not registered`);\n return;\n }\n const system = this._systems.get(systemName);\n if(system === undefined) {\n console.warn(`System ${systemName} not registered`);\n return;\n }\n system.signature = signature;\n }\n\n entityDestroyed(entity: Entity){\n for(const system of this._systems.values()){\n system.system.entities.delete(entity);\n }\n }\n\n entitySignatureChanged(entity: Entity, signature: ComponentSignature){\n for(const system of this._systems.values()){\n const systemSignature = system.signature;\n if((systemSignature & signature) === systemSignature){\n system.system.entities.add(entity);\n } else {\n system.system.entities.delete(entity);\n }\n }\n }\n}\n\n/**\n * Main ECS coordinator that manages entities, components, and systems.\n *\n * @remarks\n * The Coordinator is the central API for working with the ECS. It provides a unified\n * interface for:\n * - Creating and destroying entities\n * - Registering and managing components\n * - Registering and configuring systems\n * - Querying component data\n *\n * The Coordinator automatically keeps entity signatures up-to-date and notifies\n * systems when entities match their component requirements.\n *\n * @example\n * Complete ECS workflow\n * ```typescript\n * const ecs = new Coordinator();\n *\n * // Setup\n * ecs.registerComponent<Position>('Position');\n * ecs.registerComponent<Velocity>('Velocity');\n *\n * // Create entity\n * const entity = ecs.createEntity();\n * ecs.addComponentToEntity('Position', entity, { x: 0, y: 0 });\n * ecs.addComponentToEntity('Velocity', entity, { x: 1, y: 0 });\n *\n * // Update\n * const pos = ecs.getComponentFromEntity<Position>('Position', entity);\n * const vel = ecs.getComponentFromEntity<Velocity>('Velocity', entity);\n * if (pos && vel) {\n * pos.x += vel.x;\n * pos.y += vel.y;\n * }\n *\n * // Cleanup\n * ecs.destroyEntity(entity);\n * ```\n *\n * @category Core\n */\nexport class Coordinator {\n private _entityManager: EntityManager;\n private _componentManager: ComponentManager;\n private _systemManager: SystemManager;\n\n constructor(){\n this._entityManager = new EntityManager();\n this._componentManager = new ComponentManager();\n this._systemManager = new SystemManager();\n }\n\n createEntity(): Entity {\n return this._entityManager.createEntity();\n }\n\n destroyEntity(entity: Entity): void {\n this._entityManager.destroyEntity(entity);\n this._componentManager.entityDestroyed(entity);\n this._systemManager.entityDestroyed(entity);\n }\n\n registerComponent<T>(componentName: string): void {\n this._componentManager.registerComponent<T>(componentName);\n }\n\n addComponentToEntity<T>(componentName: string, entity: Entity, component: T): void {\n this._componentManager.addComponentToEntity<T>(componentName, entity, component);\n let signature = this._entityManager.getSignature(entity);\n if(signature === null) {\n signature = 0;\n }\n const componentType = this._componentManager.getComponentType(componentName);\n if(componentType === null) {\n console.warn(`Component ${componentName} not registered`);\n return;\n }\n signature |= 1 << componentType;\n this._entityManager.setSignature(entity, signature);\n this._systemManager.entitySignatureChanged(entity, signature);\n }\n\n removeComponentFromEntity<T>(componentName: string, entity: Entity): void {\n this._componentManager.removeComponentFromEntity<T>(componentName, entity);\n let signature = this._entityManager.getSignature(entity);\n if(signature === null) {\n signature = 0;\n }\n const componentType = this._componentManager.getComponentType(componentName);\n if(componentType === null) {\n return;\n }\n signature &= ~(1 << componentType);\n this._entityManager.setSignature(entity, signature);\n this._systemManager.entitySignatureChanged(entity, signature);\n }\n\n getComponentFromEntity<T>(componentName: string, entity: Entity): T | null {\n return this._componentManager.getComponentFromEntity<T>(componentName, entity);\n }\n\n getComponentType(componentName: string): ComponentType | null {\n return this._componentManager.getComponentType(componentName) ?? null;\n }\n\n registerSystem(systemName: string, system: System): void {\n this._systemManager.registerSystem(systemName, system);\n }\n\n setSystemSignature(systemName: string, signature: ComponentSignature): void {\n this._systemManager.setSignature(systemName, signature);\n }\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "983008DE2E282BA464756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|