@funderforge/ecs 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,412 @@
1
+ # Component Framework
2
+
3
+ A generic component framework, optimized for performance and simplicity.
4
+
5
+ ## Overview
6
+
7
+ This framework implements an Entity-Component System (ECS) pattern that allows you to build flexible, hierarchical structures where entities can contain multiple components. The framework is designed with performance in mind, featuring efficient traversal algorithms, cached property access, and method-based architecture.
8
+
9
+ ## How It Works
10
+
11
+ ### Architecture
12
+
13
+ The framework consists of two main classes:
14
+
15
+ - **Entity**: A node in a hierarchical tree structure that can have children and components
16
+ - **Component**: Functionality attached to entities that can respond to lifecycle events through virtual methods
17
+
18
+ ### Visual Structure
19
+
20
+ ```text
21
+ Entity Tree Structure:
22
+ ═══════════════════════════════════════════════════════════
23
+
24
+ [Root Entity]
25
+ ├─ Component A
26
+ ├─ Component B
27
+ └─ Component C
28
+
29
+ ┌───────┴───────┐
30
+ │ │
31
+ [Child Entity 1] [Child Entity 2]
32
+ ├─ Component X ├─ Component Y
33
+ └─ Component Z └─ Component W
34
+ │ │
35
+ [Grandchild] [Grandchild]
36
+ ├─ Component ├─ Component
37
+
38
+ ═══════════════════════════════════════════════════════════
39
+
40
+ Key Concepts:
41
+ • Entities form a tree hierarchy (parent-child relationships)
42
+ • Each entity can have multiple components
43
+ • Components belong to exactly one entity
44
+ • Enabled/disabled state cascades down the tree
45
+ • Components respond to lifecycle events through virtual methods
46
+ ```
47
+
48
+ ### Core Features
49
+
50
+ - **Hierarchical Entity Tree**: Entities can have parent-child relationships
51
+ - **Component System**: Attach multiple components to entities for modular functionality
52
+ - **Enabled/Disabled State**: Cascading state management with efficient caching
53
+ - **Method-Based Architecture**: Components can override `onTick` and `onEnabledChanged` virtual methods
54
+ - **Efficient Traversal**: Optimized methods to traverse entities and components
55
+ - **Performance Optimized**: Cached property access, minimal allocations
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ npm install @funderforge/ecs
61
+ # or
62
+ pnpm install @funderforge/ecs
63
+ # or
64
+ yarn add @funderforge/ecs
65
+ ```
66
+
67
+ ## Examples
68
+
69
+ ### Basic Entity Creation
70
+
71
+ ```typescript
72
+ import { Entity } from '@funderforge/ecs';
73
+
74
+ // Create a root entity
75
+ const root = new Entity({ id: 'root' });
76
+
77
+ // Create child entities
78
+ const child1 = new Entity({ id: 'child1' });
79
+ const child2 = new Entity({ id: 'child2' });
80
+
81
+ // Build the hierarchy
82
+ root.addChild(child1);
83
+ root.addChild(child2);
84
+
85
+ console.log(root.childrenLength()); // 2
86
+ console.log(child1.parent === root); // true
87
+ ```
88
+
89
+ ### Creating Components
90
+
91
+ ```typescript
92
+ import { Entity, Component } from '@funderforge/ecs';
93
+
94
+ // Define a custom component
95
+ class TransformComponent extends Component {
96
+ x: number = 0;
97
+ y: number = 0;
98
+
99
+ move(dx: number, dy: number) {
100
+ this.x += dx;
101
+ this.y += dy;
102
+ }
103
+ }
104
+
105
+ // Create an entity and attach components
106
+ const entity = new Entity({ id: 'player' });
107
+ const transform = new TransformComponent(entity);
108
+ const renderer = new RenderComponent(entity);
109
+
110
+ console.log(entity.componentsLength()); // 2
111
+
112
+ // Access components with type safety
113
+ const transformComp = entity.getComponent<TransformComponent>(
114
+ c => c instanceof TransformComponent
115
+ );
116
+ transformComp?.move(10, 20);
117
+ ```
118
+
119
+ ### Entity Hierarchy
120
+
121
+ ```typescript
122
+ // Create a game scene hierarchy
123
+ const scene = new Entity({ id: 'scene' });
124
+ const player = new Entity({ id: 'player', parent: scene });
125
+ const enemy1 = new Entity({ id: 'enemy1', parent: scene });
126
+ const enemy2 = new Entity({ id: 'enemy2', parent: scene });
127
+
128
+ // Nested hierarchy
129
+ const weapon = new Entity({ id: 'weapon', parent: player });
130
+ const bullet = new Entity({ id: 'bullet', parent: weapon });
131
+
132
+ // Get hierarchy level
133
+ console.log(bullet.hierarchyLevel()); // 3 (scene -> player -> weapon -> bullet)
134
+
135
+ // Remove from hierarchy
136
+ scene.removeChild(enemy1);
137
+ console.log(enemy1.parent); // null
138
+ ```
139
+
140
+ ### Enabled/Disabled State
141
+
142
+ ```typescript
143
+ // Create entity with initial state
144
+ const entity = new Entity({ id: 'entity', enabledSelf: true });
145
+ const child = new Entity({ id: 'child', parent: entity });
146
+
147
+ console.log(entity.enabled); // true
148
+ console.log(child.enabled); // true (inherits from parent)
149
+
150
+ // Disable parent - children are also disabled
151
+ entity.enabledSelf = false;
152
+ console.log(entity.enabled); // false
153
+ console.log(child.enabled); // false
154
+
155
+ // Re-enable
156
+ entity.enabledSelf = true;
157
+ console.log(child.enabled); // true
158
+
159
+ // Components respect entity enabled state
160
+ const component = new MyComponent(entity, { enabledSelf: true });
161
+ entity.enabledSelf = false;
162
+ console.log(component.enabled); // false (disabled because entity is disabled)
163
+ ```
164
+
165
+ ### Handling Tick Events
166
+
167
+ ```typescript
168
+ class MovementComponent extends Component {
169
+ velocity: number = 5;
170
+ position: number = 0;
171
+
172
+ // Override the onTick virtual method
173
+ protected onTick(deltaTime: number): void {
174
+ // Update position based on deltaTime (in milliseconds)
175
+ this.position += this.velocity * (deltaTime / 1000);
176
+ }
177
+ }
178
+
179
+ // Create entities and components
180
+ const root = new Entity({ id: 'root' });
181
+ const entity = new Entity({ id: 'moving-entity', parent: root });
182
+ const movement = new MovementComponent(entity);
183
+
184
+ // Tick the root entity (traverses the entire tree)
185
+ // This will call onTick on all enabled components
186
+ (root as any)._tick(16.67); // ~60fps deltaTime
187
+ ```
188
+
189
+ ### Handling Enabled State Changes
190
+
191
+ ```typescript
192
+ class AudioComponent extends Component {
193
+ // Override the onEnabledChanged virtual method
194
+ protected onEnabledChanged(newValue: boolean): void {
195
+ if (newValue) {
196
+ console.log('Component enabled, starting audio');
197
+ // Start audio playback
198
+ } else {
199
+ console.log('Component disabled, stopping audio');
200
+ // Stop audio playback
201
+ }
202
+ }
203
+ }
204
+
205
+ const entity = new Entity({ id: 'audio-entity' });
206
+ const audio = new AudioComponent(entity);
207
+
208
+ // Disable the component
209
+ audio.enabledSelf = false; // Calls onEnabledChanged(false)
210
+
211
+ // Disable the entity (also calls onEnabledChanged for all components)
212
+ entity.enabledSelf = false;
213
+ ```
214
+
215
+ ### Destroying Components
216
+
217
+ ```typescript
218
+ const entity = new Entity({ id: 'entity' });
219
+ const component = new MyComponent(entity);
220
+
221
+ // Use the component...
222
+ component.doSomething();
223
+
224
+ // Destroy the component when no longer needed
225
+ component.destroy();
226
+
227
+ // CRITICAL: After destroy(), the component is "dead" and must not be accessed
228
+ // Remove all references and never use it again
229
+ // component = null; // or let it go out of scope
230
+
231
+ // ❌ WRONG - Never do this after destroy():
232
+ // component.doSomething(); // Undefined behavior - component is dead
233
+ // console.log(component.entity); // Undefined behavior - component is dead
234
+ ```
235
+
236
+ **Important**: After calling `destroy()` on a component, you must remove all references to it and never access it again. The component is permanently removed from its entity and accessing it will result in undefined behavior.
237
+
238
+ ### Traversing Entities and Components
239
+
240
+ ```typescript
241
+ const root = new Entity({ id: 'root' });
242
+ // ... build tree structure ...
243
+
244
+ // Traverse all children
245
+ for (const child of root.traverseChildren()) {
246
+ console.log(`Child: ${child.id}`);
247
+ }
248
+
249
+ // Traverse children with a filter
250
+ for (const enabledChild of root.traverseChildren(child => child.enabledSelf)) {
251
+ console.log(`Enabled child: ${enabledChild.id}`);
252
+ }
253
+
254
+ // Traverse all components in the tree
255
+ for (const component of root.traverseComponents()) {
256
+ console.log(`Component: ${component.constructor.name}`);
257
+ }
258
+
259
+ // Traverse specific component types with type safety
260
+ for (const transform of root.traverseComponents<TransformComponent>(
261
+ c => c instanceof TransformComponent
262
+ )) {
263
+ console.log(`Transform at entity: ${transform.entity.id}`);
264
+ }
265
+
266
+ // Find specific entities/components with type safety
267
+ const player = root.getChild(e => e.id === 'player');
268
+ const transform = player?.getComponent<TransformComponent>(
269
+ c => c instanceof TransformComponent
270
+ );
271
+ ```
272
+
273
+ ### Complete Example: Simple Game Scene
274
+
275
+ ```typescript
276
+ import { Entity, Component } from '@funderforge/ecs';
277
+
278
+ // Define components
279
+ class TransformComponent extends Component {
280
+ x: number = 0;
281
+ y: number = 0;
282
+ rotation: number = 0;
283
+ }
284
+
285
+ class RenderComponent extends Component {
286
+ color: string = '#ffffff';
287
+
288
+ protected onTick(): void {
289
+ const transform = this.entity.getComponent<TransformComponent>(
290
+ c => c instanceof TransformComponent
291
+ );
292
+ if (transform) {
293
+ console.log(`Rendering at (${transform.x}, ${transform.y})`);
294
+ }
295
+ }
296
+ }
297
+
298
+ class PhysicsComponent extends Component {
299
+ velocityX: number = 0;
300
+ velocityY: number = 0;
301
+
302
+ protected onTick(deltaTime: number): void {
303
+ const transform = this.entity.getComponent<TransformComponent>(
304
+ c => c instanceof TransformComponent
305
+ );
306
+ if (transform) {
307
+ transform.x += this.velocityX * (deltaTime / 1000);
308
+ transform.y += this.velocityY * (deltaTime / 1000);
309
+ }
310
+ }
311
+ }
312
+
313
+ // Create game scene
314
+ const scene = new Entity({ id: 'scene' });
315
+
316
+ // Create player entity
317
+ const player = new Entity({ id: 'player', parent: scene });
318
+ new TransformComponent(player);
319
+ new PhysicsComponent(player);
320
+ new RenderComponent(player);
321
+
322
+ // Create enemy entities
323
+ for (let i = 0; i < 5; i++) {
324
+ const enemy = new Entity({ id: `enemy-${i}`, parent: scene });
325
+ new TransformComponent(enemy);
326
+ new RenderComponent(enemy);
327
+ }
328
+
329
+ // Game loop
330
+ function gameLoop() {
331
+ const deltaTime = 16.67; // ~60fps
332
+ (scene as any)._tick(deltaTime);
333
+ }
334
+
335
+ // Run game loop
336
+ setInterval(gameLoop, 16.67);
337
+ ```
338
+
339
+ ## API Reference
340
+
341
+ ### Entity
342
+
343
+ #### Entity Constructor
344
+
345
+ ```typescript
346
+ new Entity(preset?: {
347
+ id?: string | number;
348
+ enabledSelf?: boolean;
349
+ parent?: Entity;
350
+ })
351
+ ```
352
+
353
+ #### Entity Methods
354
+
355
+ - `addChild(child: Entity)`: Add a child entity
356
+ - `removeChild(child: Entity)`: Remove a child entity
357
+ - `setParent(parent: Entity | null)`: Set or clear the parent entity
358
+ - `childByIdx<T extends Entity = Entity>(index: number): T | undefined`: Get a child entity by index
359
+ - `getChild<T extends Entity = Entity>(predicate: (child: Entity) => boolean): T | undefined`: Find a child entity
360
+ - `getChildren<T extends Entity = Entity>(filter: (child: Entity) => boolean): T[]`: Get filtered children
361
+ - `traverseChildren<T extends Entity = Entity>(predicate?: (child: Entity) => boolean): Generator<T>`: Traverse all descendants
362
+ - `componentByIdx<T extends Component = Component>(index: number): T | undefined`: Get a component by index
363
+ - `getComponent<T extends Component = Component>(predicate: (component: Component) => boolean): T | undefined`: Find a component
364
+ - `getComponents<T extends Component = Component>(filter: (component: Component) => boolean): T[]`: Get filtered components
365
+ - `traverseComponents<T extends Component = Component>(predicate?: (component: Component) => boolean): Generator<T>`: Traverse all components in tree
366
+ - `hierarchyLevel()`: Get the depth level in the hierarchy
367
+
368
+ #### Entity Properties
369
+
370
+ - `id: string | number`: Unique identifier
371
+ - `enabledSelf: boolean`: Whether this entity is enabled (setter/getter)
372
+ - `enabled: boolean`: Whether this entity is enabled (considering parent state)
373
+ - `parent: Entity | null`: Parent entity reference
374
+
375
+ ### Component
376
+
377
+ #### Component Constructor
378
+
379
+ ```typescript
380
+ new Component(entity: Entity, preset?: {
381
+ enabledSelf?: boolean;
382
+ precacheTypeLookup?: boolean;
383
+ })
384
+ ```
385
+
386
+ #### Component Virtual Methods
387
+
388
+ - `protected onTick(deltaTime: number): void`: Override this method to handle tick events. Called during entity update cycles.
389
+ - `protected onEnabledChanged(newValue: boolean): void`: Override this method to respond to enabled state changes.
390
+
391
+ #### Component Methods
392
+
393
+ - `destroy(): void`: Destroys this component, removing it from its entity. **After calling `destroy()`, the component is considered "dead" and must not be accessed or used in any way. Remove all references to the component and never access its properties or methods again.**
394
+
395
+ #### Component Properties
396
+
397
+ - `entity: Entity`: The entity this component belongs to
398
+ - `enabledSelf: boolean`: Whether this component is enabled (setter/getter)
399
+ - `enabled: boolean`: Whether this component is enabled (considering entity state)
400
+
401
+ ## Performance
402
+
403
+ The framework is optimized for performance:
404
+
405
+ - **Cached Property Access**: The `enabled` property is cached and only recomputed when necessary
406
+ - **Efficient Traversal**: Uses stack-based traversal algorithms that minimize allocations
407
+ - **Selective Processing**: Disabled entities and components are skipped during traversal
408
+ - **Method-Based Architecture**: Direct method calls instead of event system overhead
409
+
410
+ ## License
411
+
412
+ MIT
@@ -0,0 +1,135 @@
1
+ import type Entity from "./entity";
2
+ /**
3
+ * Abstract base class for components that can be attached to entities.
4
+ * Components provide functionality to entities and can respond to lifecycle events through virtual methods.
5
+ *
6
+ * Key features:
7
+ * - Belongs to exactly one entity
8
+ * - Method-based architecture with `onTick` and `onEnabledChanged` virtual methods
9
+ * - Enabled/disabled state that respects entity state
10
+ * - Automatic registration with the entity upon construction
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * class TransformComponent extends Component {
15
+ * x = 0;
16
+ * y = 0;
17
+ *
18
+ * protected onTick(deltaTime: number): void {
19
+ * // Update position based on deltaTime
20
+ * }
21
+ * }
22
+ *
23
+ * const entity = new Entity({ id: 'player' });
24
+ * const transform = new TransformComponent(entity);
25
+ * ```
26
+ */
27
+ export default abstract class Component {
28
+ /** The entity this component belongs to. Set automatically during construction and cannot be changed. */
29
+ readonly entity: Entity;
30
+ private _enabledSelf;
31
+ /**
32
+ * Whether to add this component to the entity's type map for optimized lookups.
33
+ *
34
+ * - `false` (default): Component is NOT added to the type map. Lookups will fall back to iteration, which is slower
35
+ * but avoids the overhead of maintaining the map for components that are rarely looked up by type.
36
+ * - `true`: Component is added to the type map, enabling fast O(1) lookups via `entity.getComponentByType()`.
37
+ *
38
+ * @default false
39
+ */
40
+ readonly precacheTypeLookup: boolean;
41
+ /**
42
+ * Creates a new Component instance and attaches it to the specified entity.
43
+ * The component is automatically registered with the entity and cannot be moved to another entity.
44
+ *
45
+ * @param entity - The entity this component belongs to. Required and cannot be changed after construction.
46
+ * @param preset - Optional configuration object for the component.
47
+ * @param preset.enabledSelf - Whether the component is enabled by default. Defaults to `true`.
48
+ * @param preset.precacheTypeLookup - Whether to add this component to the entity's type map for optimized lookups.
49
+ * Defaults to `false` (component is NOT added to map, uses slower iteration-based lookups). Set to `true` to add
50
+ * the component to the map for fast O(1) lookups via `entity.getComponentByType()`.
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * class MyComponent extends Component {
55
+ * constructor(entity: Entity) {
56
+ * super(entity, { enabledSelf: true });
57
+ * }
58
+ * }
59
+ *
60
+ * const entity = new Entity({ id: 'entity' });
61
+ * const component = new MyComponent(entity);
62
+ *
63
+ * // Component is NOT added to type map by default (slower lookups)
64
+ * const found = entity.getComponentByType(MyComponent);
65
+ *
66
+ * // Add to type map for components frequently looked up by type (fast lookups)
67
+ * const transform = new TransformComponent(entity, { precacheTypeLookup: true });
68
+ * const foundTransform = entity.getComponentByType(TransformComponent); // Fast O(1) lookup
69
+ * ```
70
+ */
71
+ constructor(entity: Entity, preset?: {
72
+ enabledSelf?: boolean;
73
+ precacheTypeLookup?: boolean;
74
+ });
75
+ /**
76
+ * Gets whether this component itself is enabled, independent of its entity's state.
77
+ * @returns `true` if this component is enabled, `false` otherwise.
78
+ */
79
+ get enabledSelf(): boolean;
80
+ /**
81
+ * Sets whether this component itself is enabled. When the effective enabled state changes (considering both this value and the entity's enabled state),
82
+ * the `onEnabledChanged` method will be called.
83
+ * @param value - `true` to enable this component, `false` to disable it.
84
+ */
85
+ set enabledSelf(value: boolean);
86
+ /**
87
+ * Virtual method called when the component is ticked during the entity update cycle.
88
+ * Override this method in derived classes to implement tick-based logic.
89
+ * @param deltaTime - The time elapsed since the last tick in milliseconds.
90
+ */
91
+ protected onTick(_deltaTime: number): void;
92
+ /**
93
+ * Virtual method called when the component's effective enabled state changes.
94
+ * Override this method in derived classes to respond to enabled state changes.
95
+ * @param newValue - The new effective enabled state of the component.
96
+ */
97
+ protected onEnabledChanged(_newValue: boolean): void;
98
+ /**
99
+ * Gets the effective enabled state of this component, considering both its own `enabledSelf` value and its entity's enabled state.
100
+ * @returns `true` only if both this component's `enabledSelf` is `true` and its entity's `enabled` is `true`.
101
+ */
102
+ get enabled(): boolean;
103
+ /**
104
+ * Destroys this component, removing it from its entity and marking it as "dead".
105
+ *
106
+ * After calling `destroy()`, the component is permanently removed from the entity's component list
107
+ * and its entity reference is cleared. The component should be considered "dead" and must not be
108
+ * accessed or used in any way after destruction.
109
+ *
110
+ * **CRITICAL**: After calling `destroy()`, you must:
111
+ * - Remove all references to this component from your code
112
+ * - Never access the component's properties or methods again
113
+ * - Never call any methods on the component
114
+ *
115
+ * Accessing a destroyed component will result in undefined behavior. In non-production builds,
116
+ * the component object is frozen to help detect misuse.
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * const component = new MyComponent(entity);
121
+ *
122
+ * // Use the component...
123
+ * component.doSomething();
124
+ *
125
+ * // Destroy the component when no longer needed
126
+ * component.destroy();
127
+ *
128
+ * // IMPORTANT: Remove all references and never access it again
129
+ * // component = null; // or let it go out of scope
130
+ * // DO NOT: component.doSomething(); // ❌ WRONG - component is dead
131
+ * ```
132
+ */
133
+ destroy(): void;
134
+ }
135
+ //# sourceMappingURL=component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AAEnC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,OAAO,CAAC,QAAQ,OAAO,SAAS;IACtC,yGAAyG;IACzG,QAAQ,CAAC,MAAM,EAAG,MAAM,CAAC;IACzB,OAAO,CAAC,YAAY,CAAU;IAE9B;;;;;;;;OAQG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IAErC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;gBAEF,MAAM,EAAE,MAAM,EACd,MAAM,GAAE;QAAE,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;KAAO;IAoBrE;;;OAGG;IACH,IAAI,WAAW,IASQ,OAAO,CAP7B;IAED;;;;OAIG;IACH,IAAI,WAAW,CAAC,KAAK,EAAE,OAAO,EAQ7B;IAED;;;;OAIG;IACH,SAAS,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAE1C;;;;OAIG;IACH,SAAS,CAAC,gBAAgB,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI;IAEpD;;;OAGG;IACH,IAAI,OAAO,YAEV;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,OAAO,IAAI,IAAI;CAoBf"}