├── .eslintignore ├── .eslintrc.js ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.MD ├── LICENSE ├── README.md ├── build ├── core │ ├── aspect.d.ts │ ├── aspect.js │ ├── collection.d.ts │ ├── collection.js │ ├── component.d.ts │ ├── component.js │ ├── dispatcher.d.ts │ ├── dispatcher.js │ ├── engine.d.ts │ ├── engine.js │ ├── entity.collection.d.ts │ ├── entity.collection.js │ ├── entity.d.ts │ ├── entity.js │ ├── system.d.ts │ ├── system.js │ ├── types.d.ts │ └── types.js ├── index.d.ts └── index.js ├── examples └── rectangles │ ├── .gitignore │ ├── README.md │ ├── build │ └── index.html │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── components │ │ ├── position.ts │ │ ├── size.ts │ │ └── velocity.ts │ ├── entity.ts │ ├── index.ts │ └── systems │ │ ├── collision.ts │ │ ├── gravity.ts │ │ ├── movement.ts │ │ ├── renderer.ts │ │ └── resize.ts │ └── tsconfig.json ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── core │ ├── aspect.spec.ts │ ├── aspect.ts │ ├── collection.spec.ts │ ├── collection.ts │ ├── component.spec.ts │ ├── component.ts │ ├── dispatcher.spec.ts │ ├── dispatcher.ts │ ├── engine.spec.ts │ ├── engine.ts │ ├── entity.collection.spec.ts │ ├── entity.collection.ts │ ├── entity.spec.ts │ ├── entity.ts │ ├── system.spec.ts │ ├── system.ts │ └── types.ts └── index.ts ├── tsconfig.json └── tsconfig.test.json /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage 3 | node_modules 4 | webpack.config.js 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint'], 5 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 6 | env: { 7 | browser: true, 8 | amd: true, 9 | node: true, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Trixt0r 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: trixt0r 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: 13 | [ 14 | 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WDV9MU2GU35NN', 15 | 'https://www.buymeacoffee.com/Trixt0r', 16 | ] 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | !./build 2 | 3 | .github 4 | .vscode 5 | examples 6 | coverage 7 | src 8 | .eslintignore 9 | .eslintrc.js 10 | .prettierignore 11 | .prettierrc 12 | CHANGELOG.MD 13 | jest.config.js 14 | tsconfig.* 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | node_modules 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "bracketSpacing": true, 7 | "printWidth": 120, 8 | "endOfLine": "lf", 9 | "useTabs": false, 10 | "arrowParens": "avoid", 11 | "bracketSameLine": true 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "streetsidesoftware.code-spell-checker", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode", 6 | "orta.vscode-jest", 7 | "eg2.vscode-npm-script" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "name": "vscode-jest-tests", 10 | "request": "launch", 11 | "args": ["--runInBand"], 12 | "cwd": "${workspaceFolder}", 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen", 15 | "disableOptimisticBPs": true, 16 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "jest.coverageFormatter": "GutterFormatter", 3 | "jest.showCoverageOnLoad": true 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.MD: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 0.5.0 4 | 5 | Added `EntityCollection` which enables you to access entities directly by their `id`. 6 | 7 | ```ts 8 | engine.entities.get(1); 9 | ``` 10 | 11 | ### 0.4.6 12 | 13 | Type definition fix for `Class` and `ComponentClass`. 14 | 15 | ### 0.4.4 16 | 17 | 100% code coverage & code cleanup. 18 | 19 | ### 0.4.3 20 | 21 | Fixed component caching bug, not returning all components for a string type per component instance. 22 | 23 | ### 0.4.0 24 | 25 | Fixed aspect bugs. 26 | 27 | ### 0.3.0 28 | 29 | Components can now also have an optional `id`. 30 | Aspects support the id value for filtering certain entities. 31 | 32 | ### 0.2.2 33 | 34 | Target ES5. 35 | 36 | ### 0.2.1 37 | 38 | Fixed vulnerabilities in package.json. 39 | 40 | ## 0.2.0 41 | 42 | This version adds improvements to the `Aspect` and `AbstractEntitySystem` classes. 43 | 44 | ### Improvements 45 | 46 | - `Aspect` is now a dispatcher, which dispatches events like `onAddedEntities`, `onAddedComponents`, etc. 47 | - `AbstractEntitySystem` listens for the new dispatched events and adds stub methods 48 | for systems extending the abstraction. This way you can avoid boilerplate code. 49 | - Various minor fixes. 50 | 51 | ## 0.1.0 52 | 53 | This version includes breaking changes for filtering entities by components. 54 | 55 | ### Improvements 56 | 57 | - Upgrade to Typescript 3.6.3 58 | - Renamed `Filter` to `Aspect` 59 | - The API changed. 60 | - Instead of using `Filter.getFor(...)` use now `Aspect.for(engine|collection).one(...)` to get the old behavior. 61 | - `Aspect` provides 3 more methods for better filtering: `all`, `exclude` and `one`. 62 | - `Engine` has no `getFilter` method anymore. Use `Aspect.for(engine)` instead. 63 | - Options for `Engine.run` is now an optional argument. 64 | - New `AbstractEntitySystem` which can be used for systems which process entities. 65 | - Use this class to keep boilerplate code low. 66 | - Those systems can pass component type arguments to the super constructor, to auto filter entities within an engine. 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Heinrich Reich 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ECS-TS 2 | 3 | A simple to use entity component system library written in TypeScript. 4 | 5 | It is meant to be used for any use case. 6 | So you will not find any game specific logic in this library. 7 | 8 | ## Install 9 | 10 | ``` 11 | npm install @trixt0r/ecs 12 | ``` 13 | 14 | ## Examples 15 | 16 | Checkout the [examples](https://github.com/Trixt0r/ecsts/tree/master/examples). 17 | 18 | Check the [rectangles example](https://stackblitz.com/edit/ecs-example-rectangles) out, if you do not want to checkout the code. 19 | 20 | ## Usage 21 | 22 | The main parts of this library are 23 | 24 | - [`Component`](https://github.com/Trixt0r/ecsts/blob/master/src/core/component.ts) 25 | - [`Entity`](https://github.com/Trixt0r/ecsts/blob/master/src/core/entity.ts#L52) 26 | - [`System`](https://github.com/Trixt0r/ecsts/blob/master/src/core/system.ts#L77) 27 | - [`Engine`](https://github.com/Trixt0r/ecsts/blob/master/src/core/engine.ts#L96) 28 | 29 | ### Component 30 | 31 | A [`Component`](https://github.com/Trixt0r/ecsts/blob/master/src/core/component.ts) is defined as an interface with no specific methods or properties.
32 | I highly suggest you to implement your components as classes, since the systems will rely on those. 33 | 34 | For example, you could define a position like this: 35 | 36 | ```ts 37 | import { Component } from '@trixt0r/ecs'; 38 | 39 | class Position implements Component { 40 | constructor(public x = 0, public y = 0) {} 41 | } 42 | ``` 43 | 44 | ### Entity 45 | 46 | Entities are the elements, your systems will work with and have an unique identifier. 47 | 48 | Since this library doesn't want you to tell, how to generate your ids, the base class [`AbstractEntity`](https://github.com/Trixt0r/ecsts/blob/master/src/core/entity.ts#L52) is abstract.
49 | This means your entity implementation should extend [`AbstractEntity`](https://github.com/Trixt0r/ecsts/blob/master/src/core/entity.ts#L52). 50 | 51 | For example, you could do something like this: 52 | 53 | ```ts 54 | import { AbstractEntity } from '@trixt0r/ecs'; 55 | 56 | class MyEntity extends AbstractEntity { 57 | constructor() { 58 | super(makeId()); 59 | } 60 | } 61 | ``` 62 | 63 | Adding components is just as simple as: 64 | 65 | ```ts 66 | myComponent.components.add(new Position(10, 20)); 67 | ``` 68 | 69 | An entity, is a [`Dispatcher`](https://github.com/Trixt0r/ecsts/blob/master/src/core/dispatcher.ts), which means, you can register an [`EntityListener`](https://github.com/Trixt0r/ecsts/blob/master/src/core/entity.ts#L12) on it, to check whether a component has been added, removed, the components have been sorted or cleared. 70 | 71 | ### System 72 | 73 | Systems implement the actual behavior of your entities, based on which components they own. 74 | 75 | For programming your own systems, you should implement the abstract class [`System`](https://github.com/Trixt0r/ecsts/blob/master/src/core/system.ts#L77).
76 | This base class provides basic functionalities, such as 77 | 78 | - an `updating` flag, which indicates whether a system is still updating. 79 | - an `active` flag, which tells the engine to either run the system in the next update call or not. 80 | - an `engine` property, which will be set/unset, as soon as the system gets added/removed to/from an engine. 81 | 82 | A system is also a [`Dispatcher`](https://github.com/Trixt0r/ecsts/blob/master/src/core/dispatcher.ts), which means, you can react to any actions happening to a system, by registering a [`SystemListener`](https://github.com/Trixt0r/ecsts/blob/master/src/core/system.ts#L14). 83 | 84 | Here is a minimal example of a system, which obtains a list of entities with the component type `Position`. 85 | 86 | ```ts 87 | import { System, Aspect } from '@trixt0r/ecs'; 88 | 89 | class MySystem extends System { 90 | private aspect: Aspect; 91 | 92 | constructor() { 93 | super(/* optional priority here */); 94 | } 95 | 96 | onAddedToEngine(engine: Engine): void { 97 | // get entities by component 'Position' 98 | this.aspect = Aspect.for(engine).all(Position); 99 | } 100 | 101 | async process(): void { 102 | const entities = this.aspect.entities; 103 | entities.forEach(entity => { 104 | const position = entity.components.get(Position); 105 | //... do your logic here 106 | }); 107 | } 108 | } 109 | ``` 110 | 111 | Note that `process` can be `async`.
112 | If your systems need to do asynchronous tasks, you can implement them as those. 113 | Your engine can then run them as such.
114 | This might be useful, if you do not have data which needs to be processed every frame. 115 | 116 | In order to keep your focus on the actual system and not the boilerplate code around, 117 | you can use the [`AbstractEntitySystem`](https://github.com/Trixt0r/ecsts/blob/master/src/core/system.ts#L269). 118 | 119 | The class will help you by providing component types for directly defining an aspect for your system. 120 | The above code would become: 121 | 122 | ```ts 123 | import { System, Aspect } from '@trixt0r/ecs'; 124 | 125 | class MySystem extends AbstractEntitySystem { 126 | 127 | constructor() { 128 | super(/* optional priority here */, [Position]); 129 | } 130 | 131 | processEntity(entity: MyEntity): void { 132 | const position = entity.components.get(Position); 133 | //... do your logic here 134 | } 135 | } 136 | ``` 137 | 138 | ### Engine 139 | 140 | An [`Engine`](https://github.com/Trixt0r/ecsts/blob/master/src/core/engine.ts#L96) ties systems and entities together.
141 | It holds collections of both types, to which you can register listeners. But you could also register an [`EngineListener`](https://github.com/Trixt0r/ecsts/blob/master/src/core/engine.ts#L12), to listen for actions happening inside an engine. 142 | 143 | Here is a minimal example on how to initialize an engine and add systems and/or entities to it: 144 | 145 | ```ts 146 | import { Engine, EngineMode } from '@trixt0r/ecs'; 147 | 148 | // Init the engine 149 | const engine = new Engine(); 150 | 151 | engine.systems.add(new MySystem()); 152 | engine.entities.add(new MyEntity()); 153 | 154 | // anywhere in your business logic or main loop 155 | engine.run(delta); 156 | 157 | // if you want to perform your tasks asynchronously 158 | engine.run(delta, EngineMode.SUCCESSIVE); // Wait for a task to finish 159 | // or... 160 | engine.run(delta, EngineMode.PARALLEL); // Run all systems asynchronously in parallel 161 | ``` 162 | 163 | ## Support 164 | 165 | If you find any odd behavior or other improvements, feel free to create issues. 166 | Pull requests are also welcome! 167 | 168 | Otherwise you can help me out by buying me a coffee. 169 | 170 | [![paypal](https://www.paypalobjects.com/en_US/CH/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WDV9MU2GU35NN) 171 | 172 | Buy Me A Coffee 173 | -------------------------------------------------------------------------------- /build/core/aspect.d.ts: -------------------------------------------------------------------------------- 1 | import { Component } from './component'; 2 | import { Collection, CollectionListener } from './collection'; 3 | import { AbstractEntity, EntityListener } from './entity'; 4 | import { Engine } from './engine'; 5 | import { ComponentClass } from './types'; 6 | import { Dispatcher } from './dispatcher'; 7 | /** 8 | * Component or component class. 9 | */ 10 | declare type CompType = ComponentClass | Component; 11 | /** 12 | * A collection of entities. 13 | */ 14 | declare type EntityCollection = Collection; 15 | /** 16 | * Entity which is synced within an aspect. 17 | */ 18 | declare type SyncedEntity = AbstractEntity & { 19 | /** 20 | * Entity listener mapping for aspect specific caching purposes. 21 | */ 22 | __ecsEntityListener: Record; 23 | /** 24 | * The list of listeners for this entity. 25 | */ 26 | _lockedListeners: EntityListener[]; 27 | }; 28 | /** 29 | * Describes the constraints of an aspect. 30 | */ 31 | export interface AspectDescriptor { 32 | /** 33 | * Components which all have to be matched by an entity. 34 | */ 35 | all: CompType[]; 36 | /** 37 | * Components which are not allowed to be matched by an entity. 38 | */ 39 | exclude: CompType[]; 40 | /** 41 | * Components of which at least one has to be matched by an entity. 42 | */ 43 | one: CompType[]; 44 | } 45 | /** 46 | * Listener which listens to various aspect events. 47 | */ 48 | export interface AspectListener { 49 | /** 50 | * Called if new entities got added to the aspect. 51 | * 52 | * @param entities 53 | * 54 | */ 55 | onAddedEntities?(...entities: AbstractEntity[]): void; 56 | /** 57 | * Called if existing entities got removed from the aspect. 58 | * 59 | * @param entities 60 | * 61 | */ 62 | onRemovedEntities?(...entities: AbstractEntity[]): void; 63 | /** 64 | * Called if the source entities got cleared. 65 | * 66 | * 67 | */ 68 | onClearedEntities?(): void; 69 | /** 70 | * Called if the source entities got sorted. 71 | * 72 | * 73 | */ 74 | onSortedEntities?(): void; 75 | /** 76 | * Gets called if new components got added to the given entity. 77 | * 78 | * @param entity 79 | * @param components 80 | * 81 | */ 82 | onAddedComponents?(entity: AbstractEntity, ...components: Component[]): void; 83 | /** 84 | * Gets called if components got removed from the given entity. 85 | * 86 | * @param entity 87 | * @param components 88 | * 89 | */ 90 | onRemovedComponents?(entity: AbstractEntity, ...components: Component[]): void; 91 | /** 92 | * Gets called if the components of the given entity got cleared. 93 | * 94 | * @param entity 95 | * 96 | */ 97 | onClearedComponents?(entity: AbstractEntity): void; 98 | /** 99 | * Gets called if the components of the given entity got sorted. 100 | * 101 | * @param entity 102 | * 103 | */ 104 | onSortedComponents?(entity: AbstractEntity): void; 105 | /** 106 | * Gets called if the aspect got attached. 107 | * 108 | * 109 | */ 110 | onAttached?(): void; 111 | /** 112 | * Gets called if the aspect got detached. 113 | * 114 | * 115 | */ 116 | onDetached?(): void; 117 | } 118 | /** 119 | * An aspect is used to filter a collection of entities by component types. 120 | * 121 | * Use @see {Aspect#get} to obtain an aspect for a list of components to observe on an engine or a collection of entities. 122 | * The obtained aspect instance will take care of synchronizing with the source collection in an efficient way. 123 | * The user will always have snapshot of entities which meet the aspect criteria no matter when an entity got 124 | * added or removed. 125 | * 126 | */ 127 | export declare class Aspect extends Dispatcher { 128 | source: EntityCollection; 129 | /** 130 | * Internal index. 131 | */ 132 | protected static ID: number; 133 | /** 134 | * Internal unique id. 135 | */ 136 | protected readonly id: number; 137 | /** 138 | * Component types which all have to be matched by the entity source. 139 | */ 140 | protected allComponents: CompType[]; 141 | /** 142 | * Component types which all are not allowed to match. 143 | */ 144 | protected excludeComponents: CompType[]; 145 | /** 146 | * Component types of which at least one has to match. 147 | */ 148 | protected oneComponents: CompType[]; 149 | /** 150 | * The entities which meet the filter conditions. 151 | */ 152 | protected filteredEntities: SyncedEntity[]; 153 | /** 154 | * A frozen copy of the filtered entities for the public access. 155 | */ 156 | protected frozenEntities: AbstractEntity[]; 157 | /** 158 | * The collection listener for syncing data. 159 | */ 160 | protected listener: CollectionListener; 161 | /** 162 | * Whether this filter is currently attached to its collection as a listener or not. 163 | */ 164 | protected attached: boolean; 165 | /** 166 | * Creates an instance of an Aspect. 167 | * 168 | * @param source The collection of entities to filter. 169 | * @param all Optional component types which should all match. 170 | * @param exclude Optional component types which should not match. 171 | * @param one Optional component types of which at least one should match. 172 | */ 173 | protected constructor(source: EntityCollection, all?: CompType[], exclude?: CompType[], one?: CompType[]); 174 | /** 175 | * Performs the match on each entity in the source collection. 176 | * 177 | * 178 | */ 179 | protected matchAll(): void; 180 | /** 181 | * Checks whether the given entity matches the constraints on this aspect. 182 | * 183 | * @param entity The entity to check for. 184 | * @return Whether the given entity has at least one component which matches. 185 | */ 186 | matches(entity: AbstractEntity): boolean; 187 | /** 188 | * Updates the frozen entities. 189 | * 190 | * 191 | */ 192 | protected updateFrozen(): void; 193 | /** 194 | * Sets up the component sync logic. 195 | * 196 | * @param entities The entities to perform the setup for. 197 | * @return {void} 198 | */ 199 | protected setupComponentSync(entities: SyncedEntity[]): void; 200 | /** 201 | * Removes the component sync logic. 202 | * 203 | * @param entities The entities to remove the setup from. 204 | * @return {void} 205 | */ 206 | protected removeComponentSync(entities: Readonly): void; 207 | /** 208 | * Attaches this filter to its collection. 209 | * 210 | * 211 | */ 212 | attach(): void; 213 | /** 214 | * Detaches this filter from its collection. 215 | * 216 | * 217 | */ 218 | detach(): void; 219 | /** 220 | * Whether this filter is attached to its collection or not. 221 | */ 222 | get isAttached(): boolean; 223 | /** 224 | * The entities which match the criteria of this filter. 225 | */ 226 | get entities(): readonly AbstractEntity[]; 227 | /** 228 | * Includes all the given component types. 229 | * 230 | * Entities have to match every type. 231 | * 232 | * @param classes 233 | */ 234 | all(...classes: CompType[]): this; 235 | /** 236 | * @alias @see {Aspect#all} 237 | * @param classes 238 | */ 239 | every(...classes: CompType[]): this; 240 | /** 241 | * Excludes all of the given component types. 242 | * 243 | * Entities have to exclude all types. 244 | * 245 | * @param classes 246 | */ 247 | exclude(...classes: CompType[]): this; 248 | /** 249 | * @alias @see {Aspect#exclude} 250 | * @param classes 251 | */ 252 | without(...classes: CompType[]): this; 253 | /** 254 | * Includes one of the given component types. 255 | * 256 | * Entities have to match only one type. 257 | * 258 | * @param classes 259 | */ 260 | one(...classes: CompType[]): this; 261 | /** 262 | * @alias @see {Aspect#one} 263 | * @param classes 264 | */ 265 | some(...classes: CompType[]): this; 266 | /** 267 | * Collects information about this aspect and returns it. 268 | * 269 | * 270 | */ 271 | getDescriptor(): AspectDescriptor; 272 | /** 273 | * Returns an aspect for the given engine or collection of entities. 274 | * 275 | * @param collOrEngine 276 | * @param all Optional component types which should all match. 277 | * @param exclude Optional component types which should not match. 278 | * @param one Optional component types of which at least one should match. 279 | * 280 | */ 281 | static for(collOrEngine: EntityCollection | Engine, all?: CompType[], exclude?: CompType[], one?: CompType[]): Aspect; 282 | } 283 | export {}; 284 | -------------------------------------------------------------------------------- /build/core/collection.d.ts: -------------------------------------------------------------------------------- 1 | import { Dispatcher } from './dispatcher'; 2 | /** 3 | * The listener interface for a listener on an entity. 4 | */ 5 | export interface CollectionListener { 6 | /** 7 | * Called as soon as new elements have been added to the collection. 8 | * 9 | * @param elements 10 | */ 11 | onAdded?(...elements: T[]): void; 12 | /** 13 | * Called as soon as elements got removed from the collection. 14 | * 15 | * @param elements 16 | */ 17 | onRemoved?(...elements: T[]): void; 18 | /** 19 | * Called as soon as all elements got removed from the collection. 20 | */ 21 | onCleared?(): void; 22 | /** 23 | * Called as soon as the elements got sorted. 24 | */ 25 | onSorted?(): void; 26 | } 27 | /** 28 | * A collection holds a list of elements of a certain type 29 | * and allows to add, remove, sort and clear the list. 30 | * On each operation the internal list of elements gets frozen, 31 | * so a user of the collection will not be able to operate on the real reference, 32 | * but read the data without the need of copying the data on each read access. 33 | */ 34 | export declare class Collection extends Dispatcher> implements IterableIterator { 35 | /** 36 | * The internal list of elements. 37 | */ 38 | protected _elements: T[]; 39 | /** 40 | * The frozen list of elements which is used to expose the element list to the public. 41 | */ 42 | protected _frozenElements: T[]; 43 | /** 44 | * The internal iterator pointer. 45 | */ 46 | protected pointer: number; 47 | /** 48 | * Creates an instance of Collection. 49 | * 50 | * @param initial An optional initial list of elements. 51 | */ 52 | constructor(initial?: T[]); 53 | /** 54 | * @inheritdoc 55 | */ 56 | next(): IteratorResult; 57 | /** 58 | * @inheritdoc 59 | */ 60 | [Symbol.iterator](): IterableIterator; 61 | /** 62 | * A snapshot of all elements in this collection. 63 | */ 64 | get elements(): readonly T[]; 65 | /** 66 | * The length, of this collection, i.e. how many elements this collection contains. 67 | */ 68 | get length(): number; 69 | /** 70 | * Updates the internal frozen element list. 71 | */ 72 | protected updatedFrozenObjects(): void; 73 | /** 74 | * Adds the given element to this collection. 75 | * 76 | * @param element 77 | * @return Whether the element has been added or not. 78 | * It may not be added, if already present in the element list. 79 | */ 80 | protected addSingle(element: T): boolean; 81 | /** 82 | * Adds the given element(s) to this collection. 83 | * 84 | * @param elements 85 | * @return Whether elements have been added or not. 86 | * They may not have been added, if they were already present in the element list. 87 | */ 88 | add(...elements: T[]): boolean; 89 | /** 90 | * Removes the given element or the element at the given index. 91 | * 92 | * @param elOrIndex 93 | * @return Whether the element has been removed or not. 94 | * It may not have been removed, if it was not in the element list. 95 | */ 96 | protected removeSingle(elOrIndex: T | number): boolean; 97 | /** 98 | * Removes the given element(s) or the elements at the given indices. 99 | * 100 | * @param elementsOrIndices 101 | * @return Whether elements have been removed or not. 102 | * They may not have been removed, if every element was not in the element list. 103 | */ 104 | remove(...elementsOrIndices: (T | number)[]): boolean; 105 | /** 106 | * Clears this collection, i.e. removes all elements from the internal list. 107 | */ 108 | clear(): void; 109 | /** 110 | * Returns the index of the given element. 111 | * 112 | * @param element The element. 113 | * @return The index of the given element or id. 114 | */ 115 | indexOf(element: T): number; 116 | /** 117 | * Sorts this collection. 118 | * 119 | * @param [compareFn] 120 | * 121 | */ 122 | sort(compareFn?: (a: T, b: T) => number): this; 123 | /** 124 | * Returns the elements of this collection that meet the condition specified in a callback function. 125 | * 126 | * @param callbackfn 127 | * A function that accepts up to three arguments. 128 | * The filter method calls the `callbackfn` function one time for each element in the collection. 129 | * @param thisArg An object to which the this keyword can refer in the callbackfn function. 130 | * If `thisArg` is omitted, undefined is used as the this value. 131 | * @return An array with elements which met the condition. 132 | */ 133 | filter(callbackfn: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: U): T[]; 134 | /** 135 | * Performs the specified action for each element in the collection. 136 | * 137 | * @param callbackFn 138 | * A function that accepts up to three arguments. 139 | * forEach calls the callbackfn function one time for each element in the array. 140 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 141 | * If thisArg is omitted, undefined is used as the this value. 142 | */ 143 | forEach(callbackFn: (element: T, index: number, array: readonly T[]) => void, thisArg?: U): void; 144 | /** 145 | * Returns the value of the first element in the collection where predicate is true, and undefined 146 | * otherwise. 147 | * 148 | * @param predicate 149 | * Find calls predicate once for each element of the array, in ascending order, 150 | * until it finds one where predicate returns true. If such an element is found, find 151 | * immediately returns that element value. Otherwise, find returns undefined. 152 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 153 | * If thisArg is omitted, undefined is used as the this value. 154 | * 155 | */ 156 | find(predicate: (element: T, index: number, array: readonly T[]) => unknown, thisArg?: U): T | undefined; 157 | /** 158 | * Returns the index of the first element in the collection where predicate is true, and -1 159 | * otherwise. 160 | * 161 | * @param predicate 162 | * Find calls predicate once for each element of the array, in ascending order, 163 | * until it finds one where predicate returns true. If such an element is found, 164 | * findIndex immediately returns that element index. Otherwise, findIndex returns -1. 165 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 166 | * If thisArg is omitted, undefined is used as the this value. 167 | * 168 | */ 169 | findIndex(predicate: (element: T, index: number, array: readonly T[]) => unknown, thisArg?: U): number; 170 | /** 171 | * Calls a defined callback function on each element of an collection, and returns an array that contains the results. 172 | * 173 | * @param callbackFn 174 | * A function that accepts up to three arguments. 175 | * The map method calls the callbackfn function one time for each element in the array. 176 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 177 | * If thisArg is omitted, undefined is used as the this value. 178 | * 179 | */ 180 | map(callbackFn: (element: T, index: number, array: readonly T[]) => V, thisArg?: U): V[]; 181 | /** 182 | * 183 | * Determines whether all the members of the collection satisfy the specified test. 184 | * 185 | * @param callbackFn 186 | * A function that accepts up to three arguments. 187 | * The every method calls the callbackfn function for each element in array1 until the callbackfn 188 | * returns false, or until the end of the array. 189 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 190 | * If thisArg is omitted, undefined is used as the this value. 191 | * 192 | */ 193 | every(callbackFn: (element: T, index: number, array: readonly T[]) => unknown, thisArg?: U): boolean; 194 | /** 195 | * Determines whether the specified callback function returns true for any element of the collection. 196 | * 197 | * @param callbackFn 198 | * A function that accepts up to three arguments. 199 | * The some method calls the callbackfn function for each element in the collection until the callbackfn 200 | * returns true, or until the end of the collection. 201 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 202 | * If thisArg is omitted, undefined is used as the this value. 203 | * 204 | */ 205 | some(callbackFn: (element: T, index: number, array: readonly T[]) => unknown, thisArg?: U): boolean; 206 | /** 207 | * Calls the specified callback function for all the elements in the collection. 208 | * The return value of the callback function is the accumulated result, 209 | * and is provided as an argument in the next call to the callback function. 210 | * 211 | * @param callbackFn 212 | * A function that accepts up to four arguments. 213 | * The reduce method calls the callbackfn function one time for each element in the array. 214 | * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. 215 | * The first call to the callbackfn function provides this value as an argument instead of 216 | * an collection value. 217 | * 218 | */ 219 | reduce(callbackFn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U, initialValue: U): U; 220 | /** 221 | * Calls the specified callback function for all the elements in the collection, in descending order. 222 | * The return value of the callback function is the accumulated result, 223 | * and is provided as an argument in the next call to the callback function. 224 | * 225 | * @param callbackFn 226 | * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. 227 | * The first call to the callbackfn function provides this value as an argument instead of 228 | * an collection value. 229 | * 230 | */ 231 | reduceRight(callbackFn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U, initialValue: U): U; 232 | } 233 | -------------------------------------------------------------------------------- /build/core/collection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | extendStatics(d, b); 11 | function __() { this.constructor = d; } 12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 13 | }; 14 | })(); 15 | var __read = (this && this.__read) || function (o, n) { 16 | var m = typeof Symbol === "function" && o[Symbol.iterator]; 17 | if (!m) return o; 18 | var i = m.call(o), r, ar = [], e; 19 | try { 20 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); 21 | } 22 | catch (error) { e = { error: error }; } 23 | finally { 24 | try { 25 | if (r && !r.done && (m = i["return"])) m.call(i); 26 | } 27 | finally { if (e) throw e.error; } 28 | } 29 | return ar; 30 | }; 31 | var __spread = (this && this.__spread) || function () { 32 | for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); 33 | return ar; 34 | }; 35 | Object.defineProperty(exports, "__esModule", { value: true }); 36 | exports.Collection = void 0; 37 | var dispatcher_1 = require("./dispatcher"); 38 | /** 39 | * A collection holds a list of elements of a certain type 40 | * and allows to add, remove, sort and clear the list. 41 | * On each operation the internal list of elements gets frozen, 42 | * so a user of the collection will not be able to operate on the real reference, 43 | * but read the data without the need of copying the data on each read access. 44 | */ 45 | var Collection = /** @class */ (function (_super) { 46 | __extends(Collection, _super); 47 | /** 48 | * Creates an instance of Collection. 49 | * 50 | * @param initial An optional initial list of elements. 51 | */ 52 | function Collection(initial) { 53 | if (initial === void 0) { initial = []; } 54 | var _this = _super.call(this) || this; 55 | /** 56 | * The frozen list of elements which is used to expose the element list to the public. 57 | */ 58 | _this._frozenElements = []; 59 | /** 60 | * The internal iterator pointer. 61 | */ 62 | _this.pointer = 0; 63 | _this._elements = initial.slice(); 64 | _this.updatedFrozenObjects(); 65 | return _this; 66 | } 67 | /** 68 | * @inheritdoc 69 | */ 70 | Collection.prototype.next = function () { 71 | if (this.pointer < this._elements.length) { 72 | return { 73 | done: false, 74 | value: this._elements[this.pointer++], 75 | }; 76 | } 77 | else { 78 | return { 79 | done: true, 80 | value: null, 81 | }; 82 | } 83 | }; 84 | /** 85 | * @inheritdoc 86 | */ 87 | Collection.prototype[Symbol.iterator] = function () { 88 | this.pointer = 0; 89 | return this; 90 | }; 91 | Object.defineProperty(Collection.prototype, "elements", { 92 | /** 93 | * A snapshot of all elements in this collection. 94 | */ 95 | get: function () { 96 | return this._frozenElements; 97 | }, 98 | enumerable: false, 99 | configurable: true 100 | }); 101 | Object.defineProperty(Collection.prototype, "length", { 102 | /** 103 | * The length, of this collection, i.e. how many elements this collection contains. 104 | */ 105 | get: function () { 106 | return this._frozenElements.length; 107 | }, 108 | enumerable: false, 109 | configurable: true 110 | }); 111 | /** 112 | * Updates the internal frozen element list. 113 | */ 114 | Collection.prototype.updatedFrozenObjects = function () { 115 | this._frozenElements = this._elements.slice(); 116 | Object.freeze(this._frozenElements); 117 | }; 118 | /** 119 | * Adds the given element to this collection. 120 | * 121 | * @param element 122 | * @return Whether the element has been added or not. 123 | * It may not be added, if already present in the element list. 124 | */ 125 | Collection.prototype.addSingle = function (element) { 126 | if (this._elements.indexOf(element) >= 0) 127 | return false; 128 | this._elements.push(element); 129 | return true; 130 | }; 131 | /** 132 | * Adds the given element(s) to this collection. 133 | * 134 | * @param elements 135 | * @return Whether elements have been added or not. 136 | * They may not have been added, if they were already present in the element list. 137 | */ 138 | Collection.prototype.add = function () { 139 | var _this = this; 140 | var elements = []; 141 | for (var _i = 0; _i < arguments.length; _i++) { 142 | elements[_i] = arguments[_i]; 143 | } 144 | var added = elements.filter(function (element) { return _this.addSingle(element); }); 145 | if (added.length <= 0) 146 | return false; 147 | this.updatedFrozenObjects(); 148 | this.dispatch.apply(this, __spread(['onAdded'], added)); 149 | return true; 150 | }; 151 | /** 152 | * Removes the given element or the element at the given index. 153 | * 154 | * @param elOrIndex 155 | * @return Whether the element has been removed or not. 156 | * It may not have been removed, if it was not in the element list. 157 | */ 158 | Collection.prototype.removeSingle = function (elOrIndex) { 159 | if (typeof elOrIndex === 'number') 160 | elOrIndex = this.elements[elOrIndex]; 161 | var idx = this._elements.findIndex(function (_) { return _ === elOrIndex; }); 162 | if (idx < 0 || idx >= this._elements.length) 163 | return false; 164 | this._elements.splice(idx, 1); 165 | return true; 166 | }; 167 | /** 168 | * Removes the given element(s) or the elements at the given indices. 169 | * 170 | * @param elementsOrIndices 171 | * @return Whether elements have been removed or not. 172 | * They may not have been removed, if every element was not in the element list. 173 | */ 174 | Collection.prototype.remove = function () { 175 | var _this = this; 176 | var elementsOrIndices = []; 177 | for (var _i = 0; _i < arguments.length; _i++) { 178 | elementsOrIndices[_i] = arguments[_i]; 179 | } 180 | var removed = elementsOrIndices.filter(function (element) { return _this.removeSingle(element); }); 181 | if (removed.length <= 0) 182 | return false; 183 | var removedElements = removed.map(function (o) { return (typeof o === 'number' ? _this.elements[o] : o); }); 184 | this.updatedFrozenObjects(); 185 | this.dispatch.apply(this, __spread(['onRemoved'], removedElements)); 186 | return true; 187 | }; 188 | /** 189 | * Clears this collection, i.e. removes all elements from the internal list. 190 | */ 191 | Collection.prototype.clear = function () { 192 | if (!this._elements.length) 193 | return; 194 | this._elements = []; 195 | this.updatedFrozenObjects(); 196 | this.dispatch('onCleared'); 197 | }; 198 | /** 199 | * Returns the index of the given element. 200 | * 201 | * @param element The element. 202 | * @return The index of the given element or id. 203 | */ 204 | Collection.prototype.indexOf = function (element) { 205 | return this._elements.indexOf(element); 206 | }; 207 | /** 208 | * Sorts this collection. 209 | * 210 | * @param [compareFn] 211 | * 212 | */ 213 | Collection.prototype.sort = function (compareFn) { 214 | if (!this._elements.length) 215 | return this; 216 | this._elements.sort(compareFn); 217 | this.updatedFrozenObjects(); 218 | this.dispatch('onSorted'); 219 | return this; 220 | }; 221 | /** 222 | * Returns the elements of this collection that meet the condition specified in a callback function. 223 | * 224 | * @param callbackfn 225 | * A function that accepts up to three arguments. 226 | * The filter method calls the `callbackfn` function one time for each element in the collection. 227 | * @param thisArg An object to which the this keyword can refer in the callbackfn function. 228 | * If `thisArg` is omitted, undefined is used as the this value. 229 | * @return An array with elements which met the condition. 230 | */ 231 | Collection.prototype.filter = function (callbackfn, thisArg) { 232 | return this._elements.filter(callbackfn, thisArg); 233 | }; 234 | /** 235 | * Performs the specified action for each element in the collection. 236 | * 237 | * @param callbackFn 238 | * A function that accepts up to three arguments. 239 | * forEach calls the callbackfn function one time for each element in the array. 240 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 241 | * If thisArg is omitted, undefined is used as the this value. 242 | */ 243 | Collection.prototype.forEach = function (callbackFn, thisArg) { 244 | this._elements.forEach(callbackFn, thisArg); 245 | }; 246 | /** 247 | * Returns the value of the first element in the collection where predicate is true, and undefined 248 | * otherwise. 249 | * 250 | * @param predicate 251 | * Find calls predicate once for each element of the array, in ascending order, 252 | * until it finds one where predicate returns true. If such an element is found, find 253 | * immediately returns that element value. Otherwise, find returns undefined. 254 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 255 | * If thisArg is omitted, undefined is used as the this value. 256 | * 257 | */ 258 | Collection.prototype.find = function (predicate, thisArg) { 259 | return this._elements.find(predicate, thisArg); 260 | }; 261 | /** 262 | * Returns the index of the first element in the collection where predicate is true, and -1 263 | * otherwise. 264 | * 265 | * @param predicate 266 | * Find calls predicate once for each element of the array, in ascending order, 267 | * until it finds one where predicate returns true. If such an element is found, 268 | * findIndex immediately returns that element index. Otherwise, findIndex returns -1. 269 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 270 | * If thisArg is omitted, undefined is used as the this value. 271 | * 272 | */ 273 | Collection.prototype.findIndex = function (predicate, thisArg) { 274 | return this._elements.findIndex(predicate, thisArg); 275 | }; 276 | /** 277 | * Calls a defined callback function on each element of an collection, and returns an array that contains the results. 278 | * 279 | * @param callbackFn 280 | * A function that accepts up to three arguments. 281 | * The map method calls the callbackfn function one time for each element in the array. 282 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 283 | * If thisArg is omitted, undefined is used as the this value. 284 | * 285 | */ 286 | Collection.prototype.map = function (callbackFn, thisArg) { 287 | return this._elements.map(callbackFn, thisArg); 288 | }; 289 | /** 290 | * 291 | * Determines whether all the members of the collection satisfy the specified test. 292 | * 293 | * @param callbackFn 294 | * A function that accepts up to three arguments. 295 | * The every method calls the callbackfn function for each element in array1 until the callbackfn 296 | * returns false, or until the end of the array. 297 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 298 | * If thisArg is omitted, undefined is used as the this value. 299 | * 300 | */ 301 | Collection.prototype.every = function (callbackFn, thisArg) { 302 | return this._elements.every(callbackFn, thisArg); 303 | }; 304 | /** 305 | * Determines whether the specified callback function returns true for any element of the collection. 306 | * 307 | * @param callbackFn 308 | * A function that accepts up to three arguments. 309 | * The some method calls the callbackfn function for each element in the collection until the callbackfn 310 | * returns true, or until the end of the collection. 311 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 312 | * If thisArg is omitted, undefined is used as the this value. 313 | * 314 | */ 315 | Collection.prototype.some = function (callbackFn, thisArg) { 316 | return this._elements.some(callbackFn, thisArg); 317 | }; 318 | /** 319 | * Calls the specified callback function for all the elements in the collection. 320 | * The return value of the callback function is the accumulated result, 321 | * and is provided as an argument in the next call to the callback function. 322 | * 323 | * @param callbackFn 324 | * A function that accepts up to four arguments. 325 | * The reduce method calls the callbackfn function one time for each element in the array. 326 | * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. 327 | * The first call to the callbackfn function provides this value as an argument instead of 328 | * an collection value. 329 | * 330 | */ 331 | Collection.prototype.reduce = function (callbackFn, initialValue) { 332 | return this._elements.reduce(callbackFn, initialValue); 333 | }; 334 | /** 335 | * Calls the specified callback function for all the elements in the collection, in descending order. 336 | * The return value of the callback function is the accumulated result, 337 | * and is provided as an argument in the next call to the callback function. 338 | * 339 | * @param callbackFn 340 | * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. 341 | * The first call to the callbackfn function provides this value as an argument instead of 342 | * an collection value. 343 | * 344 | */ 345 | Collection.prototype.reduceRight = function (callbackFn, initialValue) { 346 | return this._elements.reduceRight(callbackFn, initialValue); 347 | }; 348 | return Collection; 349 | }(dispatcher_1.Dispatcher)); 350 | exports.Collection = Collection; 351 | -------------------------------------------------------------------------------- /build/core/component.d.ts: -------------------------------------------------------------------------------- 1 | import { Collection, CollectionListener } from './collection'; 2 | import { ComponentClass } from './types'; 3 | /** 4 | * The component interface, every component has to implement. 5 | * 6 | * If you want your system to treat different Components the same way, 7 | * you may define a static string variable named `type` in your components. 8 | */ 9 | export interface Component extends Record { 10 | /** 11 | * An optional id for the component. 12 | */ 13 | id?: string; 14 | /** 15 | * An optional type for the component. 16 | */ 17 | type?: string; 18 | } 19 | /** 20 | * A collection for components. 21 | * Supports accessing components by their class. 22 | * 23 | */ 24 | export declare class ComponentCollection extends Collection implements CollectionListener { 25 | /** 26 | * Internal map for faster component access, by class or type. 27 | */ 28 | protected cache: Map, readonly C[]>; 29 | /** 30 | * Internal state for updating the components access memory. 31 | * 32 | */ 33 | protected dirty: Map, boolean>; 34 | constructor(initial?: C[]); 35 | /** 36 | * @inheritdoc 37 | * Update the internal cache. 38 | */ 39 | onAdded(...elements: C[]): void; 40 | /** 41 | * @inheritdoc 42 | * Update the internal cache. 43 | */ 44 | onRemoved(...elements: C[]): void; 45 | /** 46 | * @inheritdoc 47 | * Update the internal cache. 48 | */ 49 | onCleared(): void; 50 | /** 51 | * Searches for the first component matching the given class or type. 52 | * 53 | * @param classOrType The class or type a component has to match. 54 | * @return The found component or `null`. 55 | */ 56 | get(classOrType: ComponentClass | string): T; 57 | /** 58 | * Searches for the all components matching the given class or type. 59 | * 60 | * @param classOrType The class or type components have to match. 61 | * @return A list of all components matching the given class. 62 | */ 63 | getAll(classOrType: ComponentClass | string): readonly T[]; 64 | /** 65 | * Updates the cache for the given class or type. 66 | * 67 | * @param classOrType The class or type to update the cache for. 68 | */ 69 | protected updateCache(classOrType: ComponentClass | string): void; 70 | /** 71 | * Marks the classes and types of the given elements as dirty, 72 | * so their cache gets updated on the next request. 73 | * 74 | * @param elements 75 | * 76 | */ 77 | protected markForCacheUpdate(...elements: C[]): void; 78 | } 79 | -------------------------------------------------------------------------------- /build/core/component.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | extendStatics(d, b); 11 | function __() { this.constructor = d; } 12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 13 | }; 14 | })(); 15 | var __read = (this && this.__read) || function (o, n) { 16 | var m = typeof Symbol === "function" && o[Symbol.iterator]; 17 | if (!m) return o; 18 | var i = m.call(o), r, ar = [], e; 19 | try { 20 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); 21 | } 22 | catch (error) { e = { error: error }; } 23 | finally { 24 | try { 25 | if (r && !r.done && (m = i["return"])) m.call(i); 26 | } 27 | finally { if (e) throw e.error; } 28 | } 29 | return ar; 30 | }; 31 | var __spread = (this && this.__spread) || function () { 32 | for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); 33 | return ar; 34 | }; 35 | var __values = (this && this.__values) || function(o) { 36 | var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; 37 | if (m) return m.call(o); 38 | if (o && typeof o.length === "number") return { 39 | next: function () { 40 | if (o && i >= o.length) o = void 0; 41 | return { value: o && o[i++], done: !o }; 42 | } 43 | }; 44 | throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); 45 | }; 46 | Object.defineProperty(exports, "__esModule", { value: true }); 47 | exports.ComponentCollection = void 0; 48 | var collection_1 = require("./collection"); 49 | /** 50 | * A collection for components. 51 | * Supports accessing components by their class. 52 | * 53 | */ 54 | var ComponentCollection = /** @class */ (function (_super) { 55 | __extends(ComponentCollection, _super); 56 | function ComponentCollection(initial) { 57 | if (initial === void 0) { initial = []; } 58 | var _this = _super.call(this, initial) || this; 59 | /** 60 | * Internal map for faster component access, by class or type. 61 | */ 62 | _this.cache = new Map(); 63 | /** 64 | * Internal state for updating the components access memory. 65 | * 66 | */ 67 | _this.dirty = new Map(); 68 | _this.addListener(_this, true); 69 | return _this; 70 | } 71 | /** 72 | * @inheritdoc 73 | * Update the internal cache. 74 | */ 75 | ComponentCollection.prototype.onAdded = function () { 76 | var elements = []; 77 | for (var _i = 0; _i < arguments.length; _i++) { 78 | elements[_i] = arguments[_i]; 79 | } 80 | this.markForCacheUpdate.apply(this, __spread(elements)); 81 | }; 82 | /** 83 | * @inheritdoc 84 | * Update the internal cache. 85 | */ 86 | ComponentCollection.prototype.onRemoved = function () { 87 | var elements = []; 88 | for (var _i = 0; _i < arguments.length; _i++) { 89 | elements[_i] = arguments[_i]; 90 | } 91 | this.markForCacheUpdate.apply(this, __spread(elements)); 92 | }; 93 | /** 94 | * @inheritdoc 95 | * Update the internal cache. 96 | */ 97 | ComponentCollection.prototype.onCleared = function () { 98 | this.dirty.clear(); 99 | this.cache.clear(); 100 | }; 101 | /** 102 | * Searches for the first component matching the given class or type. 103 | * 104 | * @param classOrType The class or type a component has to match. 105 | * @return The found component or `null`. 106 | */ 107 | ComponentCollection.prototype.get = function (classOrType) { 108 | return this.getAll(classOrType)[0]; 109 | }; 110 | /** 111 | * Searches for the all components matching the given class or type. 112 | * 113 | * @param classOrType The class or type components have to match. 114 | * @return A list of all components matching the given class. 115 | */ 116 | ComponentCollection.prototype.getAll = function (classOrType) { 117 | if (this.dirty.get(classOrType)) 118 | this.updateCache(classOrType); 119 | if (this.cache.has(classOrType)) 120 | return this.cache.get(classOrType); 121 | this.updateCache(classOrType); 122 | return this.cache.get(classOrType); 123 | }; 124 | /** 125 | * Updates the cache for the given class or type. 126 | * 127 | * @param classOrType The class or type to update the cache for. 128 | */ 129 | ComponentCollection.prototype.updateCache = function (classOrType) { 130 | var e_1, _a; 131 | var keys = this.cache.keys(); 132 | var type = typeof classOrType === 'string' ? classOrType : classOrType.type; 133 | var filtered = this.filter(function (element) { 134 | var _a; 135 | var clazz = element.constructor; 136 | var typeVal = (_a = element.type) !== null && _a !== void 0 ? _a : clazz.type; 137 | return type && typeVal ? type === typeVal : clazz === classOrType; 138 | }); 139 | if (typeof classOrType !== 'string' && classOrType.type) { 140 | this.cache.set(classOrType.type, filtered); 141 | this.dirty.delete(classOrType.type); 142 | } 143 | else if (typeof classOrType === 'string') { 144 | try { 145 | for (var keys_1 = __values(keys), keys_1_1 = keys_1.next(); !keys_1_1.done; keys_1_1 = keys_1.next()) { 146 | var key = keys_1_1.value; 147 | if (typeof key !== 'string' && key.type === classOrType) { 148 | this.cache.set(key, filtered); 149 | this.dirty.delete(key); 150 | } 151 | } 152 | } 153 | catch (e_1_1) { e_1 = { error: e_1_1 }; } 154 | finally { 155 | try { 156 | if (keys_1_1 && !keys_1_1.done && (_a = keys_1.return)) _a.call(keys_1); 157 | } 158 | finally { if (e_1) throw e_1.error; } 159 | } 160 | } 161 | this.cache.set(classOrType, filtered); 162 | this.dirty.delete(classOrType); 163 | }; 164 | /** 165 | * Marks the classes and types of the given elements as dirty, 166 | * so their cache gets updated on the next request. 167 | * 168 | * @param elements 169 | * 170 | */ 171 | ComponentCollection.prototype.markForCacheUpdate = function () { 172 | var _this = this; 173 | var elements = []; 174 | for (var _i = 0; _i < arguments.length; _i++) { 175 | elements[_i] = arguments[_i]; 176 | } 177 | var keys = this.cache.keys(); 178 | elements.forEach(function (element) { 179 | var e_2, _a; 180 | var _b, _c; 181 | var clazz = element.constructor; 182 | var classOrType = (_c = (_b = element.type) !== null && _b !== void 0 ? _b : clazz.type) !== null && _c !== void 0 ? _c : clazz; 183 | if (_this.dirty.get(classOrType)) 184 | return; 185 | if (typeof classOrType === 'string') { 186 | try { 187 | for (var keys_2 = __values(keys), keys_2_1 = keys_2.next(); !keys_2_1.done; keys_2_1 = keys_2.next()) { 188 | var key = keys_2_1.value; 189 | if (typeof key !== 'string' && key.type === classOrType) 190 | _this.dirty.set(key, true); 191 | } 192 | } 193 | catch (e_2_1) { e_2 = { error: e_2_1 }; } 194 | finally { 195 | try { 196 | if (keys_2_1 && !keys_2_1.done && (_a = keys_2.return)) _a.call(keys_2); 197 | } 198 | finally { if (e_2) throw e_2.error; } 199 | } 200 | } 201 | _this.dirty.set(classOrType, true); 202 | }); 203 | }; 204 | return ComponentCollection; 205 | }(collection_1.Collection)); 206 | exports.ComponentCollection = ComponentCollection; 207 | -------------------------------------------------------------------------------- /build/core/dispatcher.d.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentTypes } from './types'; 2 | /** 3 | * A dispatcher is an abstract object which holds a list of listeners 4 | * to which data during certain events can be dispatched, by calling functions implemented by listeners. 5 | * 6 | */ 7 | export declare abstract class Dispatcher { 8 | /** 9 | * The list of listeners for this dispatcher. 10 | */ 11 | protected _listeners: Partial[]; 12 | /** 13 | * Locked listeners. 14 | * Those listeners in this array can not be removed anymore. 15 | */ 16 | protected _lockedListeners: Partial[]; 17 | /** 18 | * Creates an instance of Dispatcher. 19 | */ 20 | constructor(); 21 | /** 22 | * The current listeners for this dispatcher. 23 | */ 24 | get listeners(): readonly Partial[]; 25 | /** 26 | * Adds the given listener to this entity. 27 | * 28 | * @param listener 29 | * @return Whether the listener has been added or not. 30 | * It may not be added, if already present in the listener list. 31 | */ 32 | addListener(listener: Partial, lock?: boolean): boolean; 33 | /** 34 | * Removes the given listener or the listener at the given index. 35 | * 36 | * @param listenerOrIndex 37 | * @return Whether the listener has been removed or not. 38 | * It may not have been removed, if it was not in the listener list. 39 | */ 40 | removeListener(listenerOrIndex: Partial | number): boolean; 41 | /** 42 | * Dispatches the given arguments by calling the given function name 43 | * on each listener, if implemented. 44 | * Note that the listener's scope will be used, when the listener's function gets called. 45 | * 46 | * @param name The function name to call. 47 | * @param args The arguments to pass to the function. 48 | */ 49 | dispatch(name: K, ...args: ArgumentTypes): void; 50 | } 51 | -------------------------------------------------------------------------------- /build/core/dispatcher.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.Dispatcher = void 0; 4 | /** 5 | * A dispatcher is an abstract object which holds a list of listeners 6 | * to which data during certain events can be dispatched, by calling functions implemented by listeners. 7 | * 8 | */ 9 | var Dispatcher = /** @class */ (function () { 10 | /** 11 | * Creates an instance of Dispatcher. 12 | */ 13 | function Dispatcher() { 14 | this._listeners = []; 15 | this._lockedListeners = []; 16 | } 17 | Object.defineProperty(Dispatcher.prototype, "listeners", { 18 | /** 19 | * The current listeners for this dispatcher. 20 | */ 21 | get: function () { 22 | return this._listeners.slice(); 23 | }, 24 | enumerable: false, 25 | configurable: true 26 | }); 27 | /** 28 | * Adds the given listener to this entity. 29 | * 30 | * @param listener 31 | * @return Whether the listener has been added or not. 32 | * It may not be added, if already present in the listener list. 33 | */ 34 | Dispatcher.prototype.addListener = function (listener, lock) { 35 | if (lock === void 0) { lock = false; } 36 | if (this._listeners.indexOf(listener) >= 0) 37 | return false; 38 | this._listeners.push(listener); 39 | if (lock) 40 | this._lockedListeners.push(listener); 41 | return true; 42 | }; 43 | /** 44 | * Removes the given listener or the listener at the given index. 45 | * 46 | * @param listenerOrIndex 47 | * @return Whether the listener has been removed or not. 48 | * It may not have been removed, if it was not in the listener list. 49 | */ 50 | Dispatcher.prototype.removeListener = function (listenerOrIndex) { 51 | var idx = typeof listenerOrIndex === 'number' ? listenerOrIndex : this._listeners.indexOf(listenerOrIndex); 52 | if (idx >= 0 && idx < this._listeners.length) { 53 | var listener = this._listeners[idx]; 54 | var isLocked = this._lockedListeners.indexOf(listener) >= 0; 55 | if (isLocked) 56 | throw new Error("Listener at index " + idx + " is locked."); 57 | this._listeners.splice(idx, 1); 58 | return true; 59 | } 60 | return false; 61 | }; 62 | /** 63 | * Dispatches the given arguments by calling the given function name 64 | * on each listener, if implemented. 65 | * Note that the listener's scope will be used, when the listener's function gets called. 66 | * 67 | * @param name The function name to call. 68 | * @param args The arguments to pass to the function. 69 | */ 70 | Dispatcher.prototype.dispatch = function (name) { 71 | var args = []; 72 | for (var _i = 1; _i < arguments.length; _i++) { 73 | args[_i - 1] = arguments[_i]; 74 | } 75 | // TODO: optimize this; cache the listeners with the given function name 76 | this._listeners.forEach(function (listener) { 77 | var fn = listener[name]; 78 | if (typeof fn !== 'function') 79 | return; 80 | fn.apply(listener, args); 81 | }); 82 | }; 83 | return Dispatcher; 84 | }()); 85 | exports.Dispatcher = Dispatcher; 86 | -------------------------------------------------------------------------------- /build/core/engine.d.ts: -------------------------------------------------------------------------------- 1 | import { System, SystemListener } from './system'; 2 | import { AbstractEntity } from './entity'; 3 | import { Dispatcher } from './dispatcher'; 4 | import { Collection } from './collection'; 5 | import { EntityCollection } from './entity.collection'; 6 | /** 7 | * The listener interface for a listener on an engine. 8 | */ 9 | export interface EngineListener { 10 | /** 11 | * Called as soon as the given system gets added to the engine. 12 | * 13 | * @param systems 14 | */ 15 | onAddedSystems?(...systems: System[]): void; 16 | /** 17 | * Called as soon as the given system gets removed from the engine. 18 | * 19 | * @param systems 20 | */ 21 | onRemovedSystems?(...systems: System[]): void; 22 | /** 23 | * Called as soon as all systems got cleared from the engine. 24 | */ 25 | onClearedSystems?(): void; 26 | /** 27 | * Called as soon as an error occurred on in an active system during update. 28 | * 29 | * @param error The error that occurred. 30 | * @param system The system on which the error occurred. 31 | */ 32 | onErrorBySystem?(error: Error, system: System): void; 33 | /** 34 | * Called as soon as the given entity gets added to the engine. 35 | * 36 | * @param entities 37 | */ 38 | onAddedEntities?(...entities: T[]): void; 39 | /** 40 | * Called as soon as the given entity gets removed from the engine. 41 | * 42 | * @param entities 43 | */ 44 | onRemovedEntities?(...entities: AbstractEntity[]): void; 45 | /** 46 | * Called as soon as all entities got cleared from the engine. 47 | */ 48 | onClearedEntities?(): void; 49 | } 50 | /** 51 | * Defines how an engine executes its active systems. 52 | */ 53 | export declare enum EngineMode { 54 | /** 55 | * Execute all systems by priority without waiting for them to resolve. 56 | */ 57 | DEFAULT = "runDefault", 58 | /** 59 | * Execute all systems by priority. Successive systems 60 | * will wait until the current executing system resolves or rejects. 61 | */ 62 | SUCCESSIVE = "runSuccessive", 63 | /** 64 | * Start all systems by priority, but run them all in parallel. 65 | */ 66 | PARALLEL = "runParallel" 67 | } 68 | /** 69 | * An engine puts entities and systems together. 70 | * It holds for each type a collection, which can be queried by each system. 71 | * 72 | * The @see {Engine#update} method has to be called in order to perform updates on each system in a certain order. 73 | * The engine takes care of updating only active systems in any point of time. 74 | * 75 | */ 76 | export declare class Engine extends Dispatcher { 77 | /** 78 | * The internal list of all systems in this engine. 79 | */ 80 | protected _systems: Collection>; 81 | /** 82 | * The frozen list of active systems which is used to iterate during the update. 83 | */ 84 | protected _activeSystems: System[]; 85 | /** 86 | * The internal list of all entities in this engine. 87 | */ 88 | protected _entities: EntityCollection; 89 | /** 90 | * Creates an instance of Engine. 91 | */ 92 | constructor(); 93 | /** 94 | * A snapshot of all entities in this engine. 95 | */ 96 | get entities(): EntityCollection; 97 | /** 98 | * A snapshot of all systems in this engine. 99 | */ 100 | get systems(): Collection; 101 | /** 102 | * A snapshot of all active systems in this engine. 103 | */ 104 | get activeSystems(): readonly System[]; 105 | /** 106 | * Updates the internal active system list. 107 | */ 108 | protected updatedActiveSystems(): void; 109 | /** 110 | * Updates all systems in this engine by the given delta value. 111 | * 112 | * @param [options] 113 | * @param [mode = EngineMode.DEFAULT] 114 | */ 115 | run(options?: T, mode?: EngineMode): void | Promise; 116 | /** 117 | * Updates all systems in this engine by the given delta value, 118 | * without waiting for a resolve or reject of each system. 119 | * 120 | * @param [options] 121 | */ 122 | protected runDefault(options?: T): void; 123 | /** 124 | * Updates all systems in this engine by the given delta value, 125 | * by waiting for a system to resolve or reject before continuing with the next one. 126 | * 127 | * @param [options] 128 | */ 129 | protected runSuccessive(options?: T): Promise; 130 | /** 131 | * Updates all systems in this engine by the given delta value, 132 | * by running all systems in parallel and waiting for all systems to resolve or reject. 133 | * 134 | * @param [options] 135 | */ 136 | protected runParallel(options?: T): Promise; 137 | } 138 | -------------------------------------------------------------------------------- /build/core/engine.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | extendStatics(d, b); 11 | function __() { this.constructor = d; } 12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 13 | }; 14 | })(); 15 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 16 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 17 | return new (P || (P = Promise))(function (resolve, reject) { 18 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 19 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 20 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 21 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 22 | }); 23 | }; 24 | var __generator = (this && this.__generator) || function (thisArg, body) { 25 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 26 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 27 | function verb(n) { return function (v) { return step([n, v]); }; } 28 | function step(op) { 29 | if (f) throw new TypeError("Generator is already executing."); 30 | while (_) try { 31 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 32 | if (y = 0, t) op = [op[0] & 2, t.value]; 33 | switch (op[0]) { 34 | case 0: case 1: t = op; break; 35 | case 4: _.label++; return { value: op[1], done: false }; 36 | case 5: _.label++; y = op[1]; op = [0]; continue; 37 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 38 | default: 39 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 40 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 41 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 42 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 43 | if (t[2]) _.ops.pop(); 44 | _.trys.pop(); continue; 45 | } 46 | op = body.call(thisArg, _); 47 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 48 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 49 | } 50 | }; 51 | var __read = (this && this.__read) || function (o, n) { 52 | var m = typeof Symbol === "function" && o[Symbol.iterator]; 53 | if (!m) return o; 54 | var i = m.call(o), r, ar = [], e; 55 | try { 56 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); 57 | } 58 | catch (error) { e = { error: error }; } 59 | finally { 60 | try { 61 | if (r && !r.done && (m = i["return"])) m.call(i); 62 | } 63 | finally { if (e) throw e.error; } 64 | } 65 | return ar; 66 | }; 67 | var __spread = (this && this.__spread) || function () { 68 | for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); 69 | return ar; 70 | }; 71 | Object.defineProperty(exports, "__esModule", { value: true }); 72 | exports.Engine = exports.EngineMode = void 0; 73 | var system_1 = require("./system"); 74 | var dispatcher_1 = require("./dispatcher"); 75 | var collection_1 = require("./collection"); 76 | var entity_collection_1 = require("./entity.collection"); 77 | /** 78 | * Defines how an engine executes its active systems. 79 | */ 80 | var EngineMode; 81 | (function (EngineMode) { 82 | /** 83 | * Execute all systems by priority without waiting for them to resolve. 84 | */ 85 | EngineMode["DEFAULT"] = "runDefault"; 86 | /** 87 | * Execute all systems by priority. Successive systems 88 | * will wait until the current executing system resolves or rejects. 89 | */ 90 | EngineMode["SUCCESSIVE"] = "runSuccessive"; 91 | /** 92 | * Start all systems by priority, but run them all in parallel. 93 | */ 94 | EngineMode["PARALLEL"] = "runParallel"; 95 | })(EngineMode = exports.EngineMode || (exports.EngineMode = {})); 96 | /** 97 | * An engine puts entities and systems together. 98 | * It holds for each type a collection, which can be queried by each system. 99 | * 100 | * The @see {Engine#update} method has to be called in order to perform updates on each system in a certain order. 101 | * The engine takes care of updating only active systems in any point of time. 102 | * 103 | */ 104 | var Engine = /** @class */ (function (_super) { 105 | __extends(Engine, _super); 106 | /** 107 | * Creates an instance of Engine. 108 | */ 109 | function Engine() { 110 | var _this = _super.call(this) || this; 111 | /** 112 | * The internal list of all systems in this engine. 113 | */ 114 | _this._systems = new collection_1.Collection(); 115 | /** 116 | * The frozen list of active systems which is used to iterate during the update. 117 | */ 118 | _this._activeSystems = []; 119 | /** 120 | * The internal list of all entities in this engine. 121 | */ 122 | _this._entities = new entity_collection_1.EntityCollection(); 123 | _this._systems.addListener({ 124 | onAdded: function () { 125 | var systems = []; 126 | for (var _i = 0; _i < arguments.length; _i++) { 127 | systems[_i] = arguments[_i]; 128 | } 129 | _this._systems.sort(function (a, b) { return a.priority - b.priority; }); 130 | systems.forEach(function (system) { 131 | system.engine = _this; 132 | _this.updatedActiveSystems(); 133 | var systemListener = { 134 | onActivated: function () { return _this.updatedActiveSystems(); }, 135 | onDeactivated: function () { return _this.updatedActiveSystems(); }, 136 | onError: function (error) { return _this.dispatch('onErrorBySystem', error, system); }, 137 | }; 138 | system.__ecsEngineListener = systemListener; 139 | system.addListener(systemListener, true); 140 | }); 141 | _this.dispatch.apply(_this, __spread(['onAddedSystems'], systems)); 142 | }, 143 | onRemoved: function () { 144 | var systems = []; 145 | for (var _i = 0; _i < arguments.length; _i++) { 146 | systems[_i] = arguments[_i]; 147 | } 148 | systems.forEach(function (system) { 149 | system.engine = null; 150 | _this.updatedActiveSystems(); 151 | var systemListener = system.__ecsEngineListener; 152 | var locked = system._lockedListeners; 153 | locked.splice(locked.indexOf(systemListener), 1); 154 | system.removeListener(systemListener); 155 | }); 156 | _this.dispatch.apply(_this, __spread(['onRemovedSystems'], systems)); 157 | }, 158 | onCleared: function () { return _this.dispatch('onClearedSystems'); }, 159 | }, true); 160 | _this._entities.addListener({ 161 | onAdded: function () { 162 | var entities = []; 163 | for (var _i = 0; _i < arguments.length; _i++) { 164 | entities[_i] = arguments[_i]; 165 | } 166 | return _this.dispatch.apply(_this, __spread(['onAddedEntities'], entities)); 167 | }, 168 | onRemoved: function () { 169 | var entities = []; 170 | for (var _i = 0; _i < arguments.length; _i++) { 171 | entities[_i] = arguments[_i]; 172 | } 173 | return _this.dispatch.apply(_this, __spread(['onRemovedEntities'], entities)); 174 | }, 175 | onCleared: function () { return _this.dispatch('onClearedEntities'); }, 176 | }, true); 177 | _this.updatedActiveSystems(); 178 | return _this; 179 | } 180 | Object.defineProperty(Engine.prototype, "entities", { 181 | /** 182 | * A snapshot of all entities in this engine. 183 | */ 184 | get: function () { 185 | return this._entities; 186 | }, 187 | enumerable: false, 188 | configurable: true 189 | }); 190 | Object.defineProperty(Engine.prototype, "systems", { 191 | /** 192 | * A snapshot of all systems in this engine. 193 | */ 194 | get: function () { 195 | return this._systems; 196 | }, 197 | enumerable: false, 198 | configurable: true 199 | }); 200 | Object.defineProperty(Engine.prototype, "activeSystems", { 201 | /** 202 | * A snapshot of all active systems in this engine. 203 | */ 204 | get: function () { 205 | return this._activeSystems; 206 | }, 207 | enumerable: false, 208 | configurable: true 209 | }); 210 | /** 211 | * Updates the internal active system list. 212 | */ 213 | Engine.prototype.updatedActiveSystems = function () { 214 | this._activeSystems = this.systems.filter(function (system) { return system.active; }); 215 | Object.freeze(this._activeSystems); 216 | }; 217 | /** 218 | * Updates all systems in this engine by the given delta value. 219 | * 220 | * @param [options] 221 | * @param [mode = EngineMode.DEFAULT] 222 | */ 223 | Engine.prototype.run = function (options, mode) { 224 | if (mode === void 0) { mode = EngineMode.DEFAULT; } 225 | return this[mode].call(this, options); 226 | }; 227 | /** 228 | * Updates all systems in this engine by the given delta value, 229 | * without waiting for a resolve or reject of each system. 230 | * 231 | * @param [options] 232 | */ 233 | Engine.prototype.runDefault = function (options) { 234 | var length = this._activeSystems.length; 235 | for (var i = 0; i < length; i++) 236 | this._activeSystems[i].run(options, system_1.SystemMode.SYNC); 237 | }; 238 | /** 239 | * Updates all systems in this engine by the given delta value, 240 | * by waiting for a system to resolve or reject before continuing with the next one. 241 | * 242 | * @param [options] 243 | */ 244 | Engine.prototype.runSuccessive = function (options) { 245 | return __awaiter(this, void 0, void 0, function () { 246 | var length, i; 247 | return __generator(this, function (_a) { 248 | switch (_a.label) { 249 | case 0: 250 | length = this._activeSystems.length; 251 | i = 0; 252 | _a.label = 1; 253 | case 1: 254 | if (!(i < length)) return [3 /*break*/, 4]; 255 | return [4 /*yield*/, this._activeSystems[i].run(options, system_1.SystemMode.SYNC)]; 256 | case 2: 257 | _a.sent(); 258 | _a.label = 3; 259 | case 3: 260 | i++; 261 | return [3 /*break*/, 1]; 262 | case 4: return [2 /*return*/]; 263 | } 264 | }); 265 | }); 266 | }; 267 | /** 268 | * Updates all systems in this engine by the given delta value, 269 | * by running all systems in parallel and waiting for all systems to resolve or reject. 270 | * 271 | * @param [options] 272 | */ 273 | Engine.prototype.runParallel = function (options) { 274 | return __awaiter(this, void 0, void 0, function () { 275 | var mapped; 276 | return __generator(this, function (_a) { 277 | switch (_a.label) { 278 | case 0: 279 | mapped = this._activeSystems.map(function (system) { return system.run(options, system_1.SystemMode.ASYNC); }); 280 | return [4 /*yield*/, Promise.all(mapped)]; 281 | case 1: 282 | _a.sent(); 283 | return [2 /*return*/]; 284 | } 285 | }); 286 | }); 287 | }; 288 | return Engine; 289 | }(dispatcher_1.Dispatcher)); 290 | exports.Engine = Engine; 291 | -------------------------------------------------------------------------------- /build/core/entity.collection.d.ts: -------------------------------------------------------------------------------- 1 | import { Collection, CollectionListener } from './collection'; 2 | import { AbstractEntity } from './entity'; 3 | export declare class EntityCollection extends Collection implements CollectionListener { 4 | /** 5 | * Internal map for faster entity access, by id. 6 | */ 7 | protected cache: Map; 8 | constructor(...args: any[]); 9 | /** 10 | * Returns the entity for the given id in this collection. 11 | * 12 | * @param id The id to search for. 13 | * @return The found entity or `undefined` if not found. 14 | */ 15 | get(id: string | number): T | undefined; 16 | /** 17 | * @inheritdoc 18 | */ 19 | onAdded(...entities: T[]): void; 20 | /** 21 | * @inheritdoc 22 | */ 23 | onRemoved(...entities: T[]): void; 24 | /** 25 | * @inheritdoc 26 | */ 27 | onCleared(): void; 28 | } 29 | -------------------------------------------------------------------------------- /build/core/entity.collection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | extendStatics(d, b); 11 | function __() { this.constructor = d; } 12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 13 | }; 14 | })(); 15 | var __read = (this && this.__read) || function (o, n) { 16 | var m = typeof Symbol === "function" && o[Symbol.iterator]; 17 | if (!m) return o; 18 | var i = m.call(o), r, ar = [], e; 19 | try { 20 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); 21 | } 22 | catch (error) { e = { error: error }; } 23 | finally { 24 | try { 25 | if (r && !r.done && (m = i["return"])) m.call(i); 26 | } 27 | finally { if (e) throw e.error; } 28 | } 29 | return ar; 30 | }; 31 | var __spread = (this && this.__spread) || function () { 32 | for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); 33 | return ar; 34 | }; 35 | Object.defineProperty(exports, "__esModule", { value: true }); 36 | exports.EntityCollection = void 0; 37 | var collection_1 = require("./collection"); 38 | var EntityCollection = /** @class */ (function (_super) { 39 | __extends(EntityCollection, _super); 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 41 | function EntityCollection() { 42 | var args = []; 43 | for (var _i = 0; _i < arguments.length; _i++) { 44 | args[_i] = arguments[_i]; 45 | } 46 | var _this = _super.apply(this, __spread(args)) || this; 47 | /** 48 | * Internal map for faster entity access, by id. 49 | */ 50 | _this.cache = new Map(); 51 | _this.addListener(_this, true); 52 | return _this; 53 | } 54 | /** 55 | * Returns the entity for the given id in this collection. 56 | * 57 | * @param id The id to search for. 58 | * @return The found entity or `undefined` if not found. 59 | */ 60 | EntityCollection.prototype.get = function (id) { 61 | return this.cache.get(id); 62 | }; 63 | /** 64 | * @inheritdoc 65 | */ 66 | EntityCollection.prototype.onAdded = function () { 67 | var entities = []; 68 | for (var _i = 0; _i < arguments.length; _i++) { 69 | entities[_i] = arguments[_i]; 70 | } 71 | for (var i = 0, l = entities.length; i < l; i++) 72 | this.cache.set(entities[i].id, entities[i]); 73 | }; 74 | /** 75 | * @inheritdoc 76 | */ 77 | EntityCollection.prototype.onRemoved = function () { 78 | var entities = []; 79 | for (var _i = 0; _i < arguments.length; _i++) { 80 | entities[_i] = arguments[_i]; 81 | } 82 | for (var i = 0, l = entities.length; i < l; i++) 83 | this.cache.delete(entities[i].id); 84 | }; 85 | /** 86 | * @inheritdoc 87 | */ 88 | EntityCollection.prototype.onCleared = function () { 89 | this.cache.clear(); 90 | }; 91 | return EntityCollection; 92 | }(collection_1.Collection)); 93 | exports.EntityCollection = EntityCollection; 94 | -------------------------------------------------------------------------------- /build/core/entity.d.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentCollection } from './component'; 2 | import { Dispatcher } from './dispatcher'; 3 | import { CollectionListener } from './collection'; 4 | /** 5 | * The listener interface for a listener on an entity. 6 | */ 7 | export interface EntityListener { 8 | /** 9 | * Called as soon as a new component as been added to the entity. 10 | * 11 | * @param components The new added components. 12 | */ 13 | onAddedComponents?(...components: C[]): void; 14 | /** 15 | * Called as soon as a component got removed from the entity. 16 | * 17 | * @param components The removed components 18 | */ 19 | onRemovedComponents?(...components: C[]): void; 20 | /** 21 | * Called as soon as all components got removed from the entity. 22 | */ 23 | onClearedComponents?(): void; 24 | /** 25 | * Called as soon as the components got sorted. 26 | */ 27 | onSortedComponents?(): void; 28 | } 29 | /** 30 | * 31 | * An entity holds an id and a list of components attached to it. 32 | * You can add or remove components from the entity. 33 | */ 34 | export declare abstract class AbstractEntity> extends Dispatcher implements CollectionListener { 35 | readonly id: number | string; 36 | /** 37 | * The internal list of components. 38 | */ 39 | protected _components: ComponentCollection; 40 | /** 41 | * Creates an instance of Entity. 42 | * 43 | * @param id The id, you should provide by yourself. Maybe an uuid or a simple number. 44 | */ 45 | constructor(id: number | string); 46 | /** 47 | * A snapshot of all components of this entity. 48 | */ 49 | get components(): ComponentCollection; 50 | /** 51 | * Dispatches the `onAdded` event to all listeners as `onAddedComponents`. 52 | * 53 | * @param components 54 | */ 55 | onAdded(...components: C[]): void; 56 | /** 57 | * Dispatches the `onRemoved` event to all listeners as `onRemovedComponents`. 58 | * 59 | * @param components 60 | * 61 | */ 62 | onRemoved(...components: C[]): void; 63 | /** 64 | * Dispatches the `onCleared` event to all listeners as `onClearedComponents`. 65 | * 66 | * 67 | */ 68 | onCleared(): void; 69 | /** 70 | * Dispatches the `onSorted` event to all listeners as `onSortedComponents`. 71 | * 72 | * 73 | */ 74 | onSorted(): void; 75 | } 76 | -------------------------------------------------------------------------------- /build/core/entity.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | extendStatics(d, b); 11 | function __() { this.constructor = d; } 12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 13 | }; 14 | })(); 15 | var __read = (this && this.__read) || function (o, n) { 16 | var m = typeof Symbol === "function" && o[Symbol.iterator]; 17 | if (!m) return o; 18 | var i = m.call(o), r, ar = [], e; 19 | try { 20 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); 21 | } 22 | catch (error) { e = { error: error }; } 23 | finally { 24 | try { 25 | if (r && !r.done && (m = i["return"])) m.call(i); 26 | } 27 | finally { if (e) throw e.error; } 28 | } 29 | return ar; 30 | }; 31 | var __spread = (this && this.__spread) || function () { 32 | for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); 33 | return ar; 34 | }; 35 | Object.defineProperty(exports, "__esModule", { value: true }); 36 | exports.AbstractEntity = void 0; 37 | var component_1 = require("./component"); 38 | var dispatcher_1 = require("./dispatcher"); 39 | /** 40 | * 41 | * An entity holds an id and a list of components attached to it. 42 | * You can add or remove components from the entity. 43 | */ 44 | var AbstractEntity = /** @class */ (function (_super) { 45 | __extends(AbstractEntity, _super); 46 | /** 47 | * Creates an instance of Entity. 48 | * 49 | * @param id The id, you should provide by yourself. Maybe an uuid or a simple number. 50 | */ 51 | function AbstractEntity(id) { 52 | var _this = _super.call(this) || this; 53 | _this.id = id; 54 | _this._components = new component_1.ComponentCollection(); 55 | _this._components.addListener(_this, true); 56 | return _this; 57 | } 58 | Object.defineProperty(AbstractEntity.prototype, "components", { 59 | /** 60 | * A snapshot of all components of this entity. 61 | */ 62 | get: function () { 63 | return this._components; 64 | }, 65 | enumerable: false, 66 | configurable: true 67 | }); 68 | /** 69 | * Dispatches the `onAdded` event to all listeners as `onAddedComponents`. 70 | * 71 | * @param components 72 | */ 73 | AbstractEntity.prototype.onAdded = function () { 74 | var _a; 75 | var components = []; 76 | for (var _i = 0; _i < arguments.length; _i++) { 77 | components[_i] = arguments[_i]; 78 | } 79 | return (_a = this).dispatch.apply(_a, __spread(['onAddedComponents'], components)); 80 | }; 81 | /** 82 | * Dispatches the `onRemoved` event to all listeners as `onRemovedComponents`. 83 | * 84 | * @param components 85 | * 86 | */ 87 | AbstractEntity.prototype.onRemoved = function () { 88 | var _a; 89 | var components = []; 90 | for (var _i = 0; _i < arguments.length; _i++) { 91 | components[_i] = arguments[_i]; 92 | } 93 | return (_a = this).dispatch.apply(_a, __spread(['onRemovedComponents'], components)); 94 | }; 95 | /** 96 | * Dispatches the `onCleared` event to all listeners as `onClearedComponents`. 97 | * 98 | * 99 | */ 100 | AbstractEntity.prototype.onCleared = function () { 101 | return this.dispatch('onClearedComponents'); 102 | }; 103 | /** 104 | * Dispatches the `onSorted` event to all listeners as `onSortedComponents`. 105 | * 106 | * 107 | */ 108 | AbstractEntity.prototype.onSorted = function () { 109 | return this.dispatch('onSortedComponents'); 110 | }; 111 | return AbstractEntity; 112 | }(dispatcher_1.Dispatcher)); 113 | exports.AbstractEntity = AbstractEntity; 114 | -------------------------------------------------------------------------------- /build/core/system.d.ts: -------------------------------------------------------------------------------- 1 | import { Engine } from './engine'; 2 | import { Dispatcher } from './dispatcher'; 3 | import { AbstractEntity } from './entity'; 4 | import { ComponentClass } from './types'; 5 | import { Component } from './component'; 6 | import { Aspect, AspectListener } from './aspect'; 7 | /** 8 | * The listener interface for a listener added to a system. 9 | */ 10 | export interface SystemListener { 11 | /** 12 | * Called as soon as the `active` switched to `true`. 13 | */ 14 | onActivated?(): void; 15 | /** 16 | * Called as soon as the `active` switched to `false`. 17 | */ 18 | onDeactivated?(): void; 19 | /** 20 | * Called as soon as the system got removed from an engine. 21 | * 22 | * @param engine The engine this system got removed from. 23 | */ 24 | onRemovedFromEngine?(engine: Engine): void; 25 | /** 26 | * Called as soon as the system got added to an engine. 27 | * Note that this will be called after @see {SystemListener#onRemovedFromEngine}. 28 | * 29 | * @param engine The engine this system got added to. 30 | */ 31 | onAddedToEngine?(engine: Engine): void; 32 | /** 33 | * Called as soon an error occurred during update. 34 | * 35 | * @param error The error which occurred. 36 | */ 37 | onError?(error: Error): void; 38 | } 39 | /** 40 | * Defines how a system executes its task. 41 | * 42 | * @enum {number} 43 | */ 44 | export declare enum SystemMode { 45 | /** 46 | * Do work and resolve immediately. 47 | */ 48 | SYNC = "runSync", 49 | /** 50 | * Do async work. E.g. do work in a worker, make requests to another server, etc. 51 | */ 52 | ASYNC = "runAsync" 53 | } 54 | /** 55 | * A system processes a list of entities which belong to an engine. 56 | * Entities can only be accessed via the assigned engine. @see {Engine}. 57 | * The implementation of the specific system has to choose on which components of an entity to operate. 58 | * 59 | */ 60 | export declare abstract class System extends Dispatcher { 61 | priority: number; 62 | /** 63 | * Determines whether this system is active or not. 64 | * 65 | */ 66 | protected _active: boolean; 67 | /** 68 | * Determines whether this system is currently updating or not. 69 | * 70 | */ 71 | protected _updating: boolean; 72 | /** 73 | * The reference to the current engine. 74 | * 75 | * @memberof System 76 | */ 77 | protected _engine: Engine | null; 78 | /** 79 | * Creates an instance of System. 80 | * 81 | * @param [priority=0] The priority of this system. The lower the value the earlier it will process. 82 | */ 83 | constructor(priority?: number); 84 | /** 85 | * The active state of this system. 86 | * If the flag is set to `false`, this system will not be able to process. 87 | * 88 | */ 89 | get active(): boolean; 90 | set active(active: boolean); 91 | /** 92 | * The engine this system is assigned to. 93 | * 94 | */ 95 | get engine(): Engine | null; 96 | set engine(engine: Engine | null); 97 | /** 98 | * Determines whether this system is currently updating or not. 99 | * The value will stay `true` until @see {System#process} resolves or rejects. 100 | * 101 | * @readonly 102 | */ 103 | get updating(): boolean; 104 | /** 105 | * Runs the system process with the given delta time. 106 | * 107 | * @param options 108 | * @param mode The system mode to run in. 109 | * 110 | */ 111 | run(options: T, mode?: SystemMode): void | Promise; 112 | /** 113 | * Processes data synchronously. 114 | * 115 | * @param options 116 | * 117 | */ 118 | protected runSync(options: T): void; 119 | /** 120 | * Processes data asynchronously. 121 | * 122 | * @param options 123 | * 124 | */ 125 | protected runAsync(options: T): Promise; 126 | /** 127 | * Processes the entities of the current engine. 128 | * To be implemented by any concrete system. 129 | * 130 | * @param options 131 | * 132 | */ 133 | abstract process(options: T): void | Promise; 134 | /** 135 | * Called as soon as the `active` switched to `true`. 136 | * 137 | * 138 | */ 139 | onActivated(): void; 140 | /** 141 | * Called as soon as the `active` switched to `false`. 142 | * 143 | * 144 | */ 145 | onDeactivated(): void; 146 | /** 147 | * Called as soon as the system got removed from an engine. 148 | * 149 | * @param engine The engine this system got added to. 150 | * 151 | * 152 | */ 153 | onRemovedFromEngine(engine: Engine): void; 154 | /** 155 | * Called as soon as the system got added to an engine. 156 | * Note that this will be called after @see {SystemListener#onRemovedFromEngine}. 157 | * 158 | * @param engine The engine this system got added to. 159 | * 160 | * 161 | */ 162 | onAddedToEngine(engine: Engine): void; 163 | /** 164 | * Called as soon an error occurred during update. 165 | * 166 | * @param error The error which occurred. 167 | * 168 | * 169 | */ 170 | onError(error: Error): void; 171 | } 172 | /** 173 | * An abstract entity system is a system which processes each entity. 174 | * 175 | * Optionally it accepts component types for auto filtering the entities before processing. 176 | * This class abstracts away the initialization of aspects and detaches them properly, if needed. 177 | * 178 | */ 179 | export declare abstract class AbstractEntitySystem extends System implements AspectListener { 180 | priority: number; 181 | protected all?: (Component | ComponentClass)[] | undefined; 182 | protected exclude?: (Component | ComponentClass)[] | undefined; 183 | protected one?: (Component | ComponentClass)[] | undefined; 184 | /** 185 | * The optional aspect, if any. 186 | * 187 | */ 188 | protected aspect: Aspect | null; 189 | /** 190 | * Creates an instance of AbstractEntitySystem. 191 | * 192 | * @param priority The priority of this system. The lower the value the earlier it will process. 193 | * @param all Optional component types which should all match. 194 | * @param exclude Optional component types which should not match. 195 | * @param one Optional component types of which at least one should match. 196 | */ 197 | constructor(priority?: number, all?: (Component | ComponentClass)[] | undefined, exclude?: (Component | ComponentClass)[] | undefined, one?: (Component | ComponentClass)[] | undefined); 198 | /** @inheritdoc */ 199 | onAddedToEngine(engine: Engine): void; 200 | /** @inheritdoc */ 201 | onRemovedFromEngine(): void; 202 | /** 203 | * Called if new entities got added to the system. 204 | * 205 | * @param entities 206 | * 207 | */ 208 | onAddedEntities(...entities: AbstractEntity[]): void; 209 | /** 210 | * Called if existing entities got removed from the system. 211 | * 212 | * @param entities 213 | * 214 | */ 215 | onRemovedEntities?(...entities: AbstractEntity[]): void; 216 | /** 217 | * Called if the entities got cleared. 218 | * 219 | * 220 | */ 221 | onClearedEntities?(): void; 222 | /** 223 | * Called if the entities got sorted. 224 | * 225 | * 226 | */ 227 | onSortedEntities?(): void; 228 | /** 229 | * Gets called if new components got added to the given entity. 230 | * 231 | * @param entity 232 | * @param components 233 | * 234 | */ 235 | onAddedComponents?(entity: AbstractEntity, ...components: Component[]): void; 236 | /** 237 | * Gets called if components got removed from the given entity. 238 | * 239 | * @param entity 240 | * @param components 241 | * 242 | */ 243 | onRemovedComponents?(entity: AbstractEntity, ...components: Component[]): void; 244 | /** 245 | * Gets called if the components of the given entity got cleared. 246 | * 247 | * @param entity 248 | * 249 | */ 250 | onClearedComponents?(entity: AbstractEntity): void; 251 | /** 252 | * Gets called if the components of the given entity got sorted. 253 | * 254 | * @param entity 255 | * 256 | */ 257 | onSortedComponents?(entity: AbstractEntity): void; 258 | /** @inheritdoc */ 259 | process(options?: U): void; 260 | /** 261 | * Processes the given entity. 262 | * 263 | * @param entity 264 | * @param index 265 | * @param entities 266 | * @param options 267 | * 268 | */ 269 | abstract processEntity(entity: T, index?: number, entities?: T[], options?: U): void; 270 | } 271 | -------------------------------------------------------------------------------- /build/core/system.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | extendStatics(d, b); 11 | function __() { this.constructor = d; } 12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 13 | }; 14 | })(); 15 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 16 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 17 | return new (P || (P = Promise))(function (resolve, reject) { 18 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 19 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 20 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 21 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 22 | }); 23 | }; 24 | var __generator = (this && this.__generator) || function (thisArg, body) { 25 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 26 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 27 | function verb(n) { return function (v) { return step([n, v]); }; } 28 | function step(op) { 29 | if (f) throw new TypeError("Generator is already executing."); 30 | while (_) try { 31 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 32 | if (y = 0, t) op = [op[0] & 2, t.value]; 33 | switch (op[0]) { 34 | case 0: case 1: t = op; break; 35 | case 4: _.label++; return { value: op[1], done: false }; 36 | case 5: _.label++; y = op[1]; op = [0]; continue; 37 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 38 | default: 39 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 40 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 41 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 42 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 43 | if (t[2]) _.ops.pop(); 44 | _.trys.pop(); continue; 45 | } 46 | op = body.call(thisArg, _); 47 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 48 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 49 | } 50 | }; 51 | Object.defineProperty(exports, "__esModule", { value: true }); 52 | exports.AbstractEntitySystem = exports.System = exports.SystemMode = void 0; 53 | /* eslint-disable @typescript-eslint/no-empty-function */ 54 | var engine_1 = require("./engine"); 55 | var dispatcher_1 = require("./dispatcher"); 56 | var aspect_1 = require("./aspect"); 57 | /** 58 | * Defines how a system executes its task. 59 | * 60 | * @enum {number} 61 | */ 62 | var SystemMode; 63 | (function (SystemMode) { 64 | /** 65 | * Do work and resolve immediately. 66 | */ 67 | SystemMode["SYNC"] = "runSync"; 68 | /** 69 | * Do async work. E.g. do work in a worker, make requests to another server, etc. 70 | */ 71 | SystemMode["ASYNC"] = "runAsync"; 72 | })(SystemMode = exports.SystemMode || (exports.SystemMode = {})); 73 | /** 74 | * A system processes a list of entities which belong to an engine. 75 | * Entities can only be accessed via the assigned engine. @see {Engine}. 76 | * The implementation of the specific system has to choose on which components of an entity to operate. 77 | * 78 | */ 79 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 80 | var System = /** @class */ (function (_super) { 81 | __extends(System, _super); 82 | /** 83 | * Creates an instance of System. 84 | * 85 | * @param [priority=0] The priority of this system. The lower the value the earlier it will process. 86 | */ 87 | function System(priority) { 88 | if (priority === void 0) { priority = 0; } 89 | var _this = _super.call(this) || this; 90 | _this.priority = priority; 91 | _this._active = true; 92 | _this._updating = false; 93 | _this._engine = null; 94 | return _this; 95 | } 96 | Object.defineProperty(System.prototype, "active", { 97 | /** 98 | * The active state of this system. 99 | * If the flag is set to `false`, this system will not be able to process. 100 | * 101 | */ 102 | get: function () { 103 | return this._active; 104 | }, 105 | set: function (active) { 106 | if (active === this._active) 107 | return; 108 | this._active = active; 109 | if (active) { 110 | this.onActivated(); 111 | } 112 | else { 113 | this.onDeactivated(); 114 | } 115 | this.dispatch(active ? 'onActivated' : 'onDeactivated'); 116 | }, 117 | enumerable: false, 118 | configurable: true 119 | }); 120 | Object.defineProperty(System.prototype, "engine", { 121 | /** 122 | * The engine this system is assigned to. 123 | * 124 | */ 125 | get: function () { 126 | return this._engine; 127 | }, 128 | set: function (engine) { 129 | if (engine === this._engine) 130 | return; 131 | var oldEngine = this._engine; 132 | this._engine = engine; 133 | if (oldEngine instanceof engine_1.Engine) { 134 | this.onRemovedFromEngine(oldEngine); 135 | this.dispatch('onRemovedFromEngine', oldEngine); 136 | } 137 | if (engine instanceof engine_1.Engine) { 138 | this.onAddedToEngine(engine); 139 | this.dispatch('onAddedToEngine', engine); 140 | } 141 | }, 142 | enumerable: false, 143 | configurable: true 144 | }); 145 | Object.defineProperty(System.prototype, "updating", { 146 | /** 147 | * Determines whether this system is currently updating or not. 148 | * The value will stay `true` until @see {System#process} resolves or rejects. 149 | * 150 | * @readonly 151 | */ 152 | get: function () { 153 | return this._updating; 154 | }, 155 | enumerable: false, 156 | configurable: true 157 | }); 158 | /** 159 | * Runs the system process with the given delta time. 160 | * 161 | * @param options 162 | * @param mode The system mode to run in. 163 | * 164 | */ 165 | System.prototype.run = function (options, mode) { 166 | if (mode === void 0) { mode = SystemMode.SYNC; } 167 | return this[mode].call(this, options); 168 | }; 169 | /** 170 | * Processes data synchronously. 171 | * 172 | * @param options 173 | * 174 | */ 175 | System.prototype.runSync = function (options) { 176 | try { 177 | this.process(options); 178 | } 179 | catch (e) { 180 | this.onError(e); 181 | this.dispatch('onError', e); 182 | } 183 | }; 184 | /** 185 | * Processes data asynchronously. 186 | * 187 | * @param options 188 | * 189 | */ 190 | System.prototype.runAsync = function (options) { 191 | return __awaiter(this, void 0, void 0, function () { 192 | var e_1; 193 | return __generator(this, function (_a) { 194 | switch (_a.label) { 195 | case 0: 196 | this._updating = true; 197 | _a.label = 1; 198 | case 1: 199 | _a.trys.push([1, 3, 4, 5]); 200 | return [4 /*yield*/, this.process(options)]; 201 | case 2: 202 | _a.sent(); 203 | return [3 /*break*/, 5]; 204 | case 3: 205 | e_1 = _a.sent(); 206 | this.onError(e_1); 207 | this.dispatch('onError', e_1); 208 | return [3 /*break*/, 5]; 209 | case 4: 210 | this._updating = false; 211 | return [7 /*endfinally*/]; 212 | case 5: return [2 /*return*/]; 213 | } 214 | }); 215 | }); 216 | }; 217 | /** 218 | * Called as soon as the `active` switched to `true`. 219 | * 220 | * 221 | */ 222 | System.prototype.onActivated = function () { 223 | /* NOOP */ 224 | }; 225 | /** 226 | * Called as soon as the `active` switched to `false`. 227 | * 228 | * 229 | */ 230 | System.prototype.onDeactivated = function () { 231 | /* NOOP */ 232 | }; 233 | /** 234 | * Called as soon as the system got removed from an engine. 235 | * 236 | * @param engine The engine this system got added to. 237 | * 238 | * 239 | */ 240 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 241 | System.prototype.onRemovedFromEngine = function (engine) { 242 | /* NOOP */ 243 | }; 244 | /** 245 | * Called as soon as the system got added to an engine. 246 | * Note that this will be called after @see {SystemListener#onRemovedFromEngine}. 247 | * 248 | * @param engine The engine this system got added to. 249 | * 250 | * 251 | */ 252 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 253 | System.prototype.onAddedToEngine = function (engine) { 254 | /* NOOP */ 255 | }; 256 | /** 257 | * Called as soon an error occurred during update. 258 | * 259 | * @param error The error which occurred. 260 | * 261 | * 262 | */ 263 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 264 | System.prototype.onError = function (error) { 265 | /* NOOP */ 266 | }; 267 | return System; 268 | }(dispatcher_1.Dispatcher)); 269 | exports.System = System; 270 | /** 271 | * An abstract entity system is a system which processes each entity. 272 | * 273 | * Optionally it accepts component types for auto filtering the entities before processing. 274 | * This class abstracts away the initialization of aspects and detaches them properly, if needed. 275 | * 276 | */ 277 | var AbstractEntitySystem = /** @class */ (function (_super) { 278 | __extends(AbstractEntitySystem, _super); 279 | /** 280 | * Creates an instance of AbstractEntitySystem. 281 | * 282 | * @param priority The priority of this system. The lower the value the earlier it will process. 283 | * @param all Optional component types which should all match. 284 | * @param exclude Optional component types which should not match. 285 | * @param one Optional component types of which at least one should match. 286 | */ 287 | function AbstractEntitySystem(priority, all, exclude, one) { 288 | if (priority === void 0) { priority = 0; } 289 | var _this = _super.call(this, priority) || this; 290 | _this.priority = priority; 291 | _this.all = all; 292 | _this.exclude = exclude; 293 | _this.one = one; 294 | /** 295 | * The optional aspect, if any. 296 | * 297 | */ 298 | _this.aspect = null; 299 | return _this; 300 | } 301 | /** @inheritdoc */ 302 | AbstractEntitySystem.prototype.onAddedToEngine = function (engine) { 303 | this.aspect = aspect_1.Aspect.for(engine, this.all, this.exclude, this.one); 304 | this.aspect.addListener(this); 305 | }; 306 | /** @inheritdoc */ 307 | AbstractEntitySystem.prototype.onRemovedFromEngine = function () { 308 | if (!this.aspect) 309 | return; 310 | this.aspect.removeListener(this); 311 | this.aspect.detach(); 312 | }; 313 | /** 314 | * Called if new entities got added to the system. 315 | * 316 | * @param entities 317 | * 318 | */ 319 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 320 | AbstractEntitySystem.prototype.onAddedEntities = function () { 321 | var entities = []; 322 | for (var _i = 0; _i < arguments.length; _i++) { 323 | entities[_i] = arguments[_i]; 324 | } 325 | }; 326 | /** 327 | * Called if existing entities got removed from the system. 328 | * 329 | * @param entities 330 | * 331 | */ 332 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 333 | AbstractEntitySystem.prototype.onRemovedEntities = function () { 334 | var entities = []; 335 | for (var _i = 0; _i < arguments.length; _i++) { 336 | entities[_i] = arguments[_i]; 337 | } 338 | }; 339 | /** 340 | * Called if the entities got cleared. 341 | * 342 | * 343 | */ 344 | AbstractEntitySystem.prototype.onClearedEntities = function () { }; 345 | /** 346 | * Called if the entities got sorted. 347 | * 348 | * 349 | */ 350 | AbstractEntitySystem.prototype.onSortedEntities = function () { }; 351 | /** 352 | * Gets called if new components got added to the given entity. 353 | * 354 | * @param entity 355 | * @param components 356 | * 357 | */ 358 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 359 | AbstractEntitySystem.prototype.onAddedComponents = function (entity) { 360 | var components = []; 361 | for (var _i = 1; _i < arguments.length; _i++) { 362 | components[_i - 1] = arguments[_i]; 363 | } 364 | }; 365 | /** 366 | * Gets called if components got removed from the given entity. 367 | * 368 | * @param entity 369 | * @param components 370 | * 371 | */ 372 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 373 | AbstractEntitySystem.prototype.onRemovedComponents = function (entity) { 374 | var components = []; 375 | for (var _i = 1; _i < arguments.length; _i++) { 376 | components[_i - 1] = arguments[_i]; 377 | } 378 | }; 379 | /** 380 | * Gets called if the components of the given entity got cleared. 381 | * 382 | * @param entity 383 | * 384 | */ 385 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 386 | AbstractEntitySystem.prototype.onClearedComponents = function (entity) { }; 387 | /** 388 | * Gets called if the components of the given entity got sorted. 389 | * 390 | * @param entity 391 | * 392 | */ 393 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 394 | AbstractEntitySystem.prototype.onSortedComponents = function (entity) { }; 395 | /** @inheritdoc */ 396 | AbstractEntitySystem.prototype.process = function (options) { 397 | var _a; 398 | var entities = this.aspect ? this.aspect.entities : (_a = this._engine) === null || _a === void 0 ? void 0 : _a.entities.elements; 399 | if (!(entities === null || entities === void 0 ? void 0 : entities.length)) 400 | return; 401 | for (var i = 0, l = entities.length; i < l; i++) { 402 | this.processEntity(entities[i], i, entities, options); 403 | } 404 | }; 405 | return AbstractEntitySystem; 406 | }(System)); 407 | exports.AbstractEntitySystem = AbstractEntitySystem; 408 | -------------------------------------------------------------------------------- /build/core/types.d.ts: -------------------------------------------------------------------------------- 1 | import { Component } from './component'; 2 | /** 3 | * A type for the arguments of `F`. 4 | */ 5 | export declare type ArgumentTypes = F extends (...args: infer A) => unknown ? A : never; 6 | /** 7 | * Class definition for type `T`. 8 | */ 9 | export interface Class extends Function { 10 | new (...args: never[]): T; 11 | } 12 | /** 13 | * Class definition for a component type `T`. 14 | */ 15 | export interface ComponentClass extends Class { 16 | /** 17 | * The static id of the component. 18 | * 19 | */ 20 | readonly id?: string; 21 | /** 22 | * The static type of the component. 23 | * 24 | */ 25 | readonly type?: string; 26 | } 27 | -------------------------------------------------------------------------------- /build/core/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /build/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './core/collection'; 2 | export * from './core/component'; 3 | export * from './core/dispatcher'; 4 | export * from './core/engine'; 5 | export * from './core/entity'; 6 | export * from './core/entity.collection'; 7 | export * from './core/aspect'; 8 | export * from './core/system'; 9 | export * from './core/types'; 10 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); 5 | }) : (function(o, m, k, k2) { 6 | if (k2 === undefined) k2 = k; 7 | o[k2] = m[k]; 8 | })); 9 | var __exportStar = (this && this.__exportStar) || function(m, exports) { 10 | for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) __createBinding(exports, m, p); 11 | }; 12 | Object.defineProperty(exports, "__esModule", { value: true }); 13 | __exportStar(require("./core/collection"), exports); 14 | __exportStar(require("./core/component"), exports); 15 | __exportStar(require("./core/dispatcher"), exports); 16 | __exportStar(require("./core/engine"), exports); 17 | __exportStar(require("./core/entity"), exports); 18 | __exportStar(require("./core/entity.collection"), exports); 19 | __exportStar(require("./core/aspect"), exports); 20 | __exportStar(require("./core/system"), exports); 21 | __exportStar(require("./core/types"), exports); 22 | -------------------------------------------------------------------------------- /examples/rectangles/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build/main.js 3 | -------------------------------------------------------------------------------- /examples/rectangles/README.md: -------------------------------------------------------------------------------- 1 | # ECTS - Bouncing Rectangles 2 | 3 | A minimal example, built with pixi.js. 4 | 5 | The example shows how to implement the following systems: 6 | 7 | - Rendering (with pixi.js) 8 | - Movement 9 | - Gravity 10 | - Collision 11 | - Window Resizing 12 | 13 | # Run 14 | 15 | Install all dependencies and run the dev script: 16 | 17 | ``` 18 | npm install 19 | npm run dev 20 | ``` 21 | 22 | This will start a server on [http://localhost:8000](http://localhost:8000/). 23 | -------------------------------------------------------------------------------- /examples/rectangles/build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 25 | ECS - PIXI 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/rectangles/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@trixt0r/ecs-pixi", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "esbuild ./src/index.ts --bundle --outfile=build/main.js --servedir=build" 7 | }, 8 | "devDependencies": { 9 | "@trixt0r/ecs": "^0.5.1", 10 | "esbuild": "^0.14.8", 11 | "pixi.js": "^6.2.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/rectangles/src/components/position.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@trixt0r/ecs'; 2 | 3 | export class Position implements Component { 4 | constructor(public x = 0, public y = 0) {} 5 | } 6 | -------------------------------------------------------------------------------- /examples/rectangles/src/components/size.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@trixt0r/ecs'; 2 | 3 | export class Size implements Component { 4 | constructor(public width = 10, public height = 10) {} 5 | } 6 | -------------------------------------------------------------------------------- /examples/rectangles/src/components/velocity.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@trixt0r/ecs'; 2 | 3 | export class Velocity implements Component { 4 | constructor(public x = 0, public y = 0) {} 5 | } 6 | -------------------------------------------------------------------------------- /examples/rectangles/src/entity.ts: -------------------------------------------------------------------------------- 1 | import { AbstractEntity } from '@trixt0r/ecs'; 2 | 3 | let id = 1; 4 | 5 | export class MyEntity extends AbstractEntity { 6 | constructor() { 7 | super(id++); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/rectangles/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Engine } from '@trixt0r/ecs'; 2 | import { RenderingSystem } from './systems/renderer'; 3 | import { MovementSystem } from './systems/movement'; 4 | import { CollisionSystem } from './systems/collision'; 5 | import { GravitySystem } from './systems/gravity'; 6 | import { MyEntity } from './entity'; 7 | import { Position } from './components/position'; 8 | import { Velocity } from './components/velocity'; 9 | import { Size } from './components/size'; 10 | import { ResizeSystem } from './systems/resize'; 11 | 12 | // Setup the engine 13 | const engine = new Engine(); 14 | 15 | // Add all systems 16 | engine.systems.add(new MovementSystem(0)); 17 | engine.systems.add(new ResizeSystem(0)); 18 | engine.systems.add(new GravitySystem(0.25, 1)); 19 | engine.systems.add(new CollisionSystem(2)); 20 | engine.systems.add(new RenderingSystem(3)); 21 | 22 | const canvas = document.getElementById('canvas'); 23 | const canvasWidth = canvas.clientWidth; 24 | const canvasHeight = canvas.clientHeight; 25 | 26 | /** 27 | * Adds new entities to the engine, with a position, size and velocity. 28 | * 29 | * @param {number} times Indicates how many entities to add. 30 | */ 31 | function addEntity(times: number) { 32 | const toAdd = []; 33 | for (let i = 0; i < times; i++) { 34 | const entity = new MyEntity(); 35 | const size = new Size(5 + Math.random() * 10, 5 + Math.random() * 10); 36 | const position = new Position(Math.random() * canvasWidth, Math.random() * canvasHeight); 37 | 38 | entity.components.add(position); 39 | entity.components.add(new Velocity(-1 + Math.random() * 2, -1 + Math.random() * 2)); 40 | entity.components.add(size); 41 | toAdd.push(entity); 42 | } 43 | engine.entities.add(...toAdd); 44 | } 45 | 46 | /** 47 | * Resolves after the given time. 48 | */ 49 | async function wait(ms: number): Promise { 50 | return new Promise(resolve => setTimeout(resolve, ms)); 51 | } 52 | 53 | let t = Date.now(); 54 | 55 | function run() { 56 | requestAnimationFrame(() => { 57 | const now = Date.now(); 58 | const delta = now - t; 59 | t = now; 60 | engine.run(delta); 61 | run(); 62 | }); 63 | } 64 | 65 | run(); 66 | 67 | async function setUp() { 68 | for (let i = 0; i < 5; i++) { 69 | addEntity(200); 70 | await wait(10); 71 | } 72 | } 73 | 74 | setUp(); 75 | -------------------------------------------------------------------------------- /examples/rectangles/src/systems/collision.ts: -------------------------------------------------------------------------------- 1 | import { Engine, AbstractEntitySystem } from '@trixt0r/ecs'; 2 | import { Position } from '../components/position'; 3 | import { Velocity } from '../components/velocity'; 4 | import { Size } from '../components/size'; 5 | import { MyEntity } from 'entity'; 6 | 7 | /** 8 | * A system which handles collisions with the bounds of the scene. 9 | * 10 | * @export 11 | * @class CollisionSystem 12 | * @extends {System} 13 | */ 14 | export class CollisionSystem extends AbstractEntitySystem { 15 | canvas: HTMLCanvasElement; 16 | 17 | constructor(priority = 0) { 18 | super(priority, [Position, Velocity, Size]); 19 | } 20 | 21 | /** 22 | * Caches the filter and canvas. 23 | * 24 | * @inheritdoc 25 | * @param {Engine} engine 26 | */ 27 | onAddedToEngine(engine: Engine) { 28 | super.onAddedToEngine(engine); 29 | this.canvas = document.getElementById('canvas'); 30 | } 31 | 32 | /** 33 | * Performs the collision detection, 34 | * i.e. makes sure each entity stays in the scene. 35 | */ 36 | processEntity(entity: MyEntity) { 37 | const position = entity.components.get(Position); 38 | const velocity = entity.components.get(Velocity); 39 | const size = entity.components.get(Size); 40 | if (position.x + size.width > this.canvas.width || position.x < 0) velocity.x *= -1; 41 | if (position.y < 0 && velocity.y < 0) velocity.y = 0; 42 | else if (position.y + size.height >= this.canvas.height && velocity.y > 0) velocity.y *= -1; 43 | position.x = Math.min(this.canvas.width - size.width, Math.max(0, position.x)); 44 | position.y = Math.min(this.canvas.height - size.height, Math.max(0, position.y)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/rectangles/src/systems/gravity.ts: -------------------------------------------------------------------------------- 1 | import { Engine, AbstractEntitySystem } from '@trixt0r/ecs'; 2 | import { Position } from '../components/position'; 3 | import { Velocity } from '../components/velocity'; 4 | import { Size } from '../components/size'; 5 | import { MyEntity } from 'entity'; 6 | 7 | /** 8 | * The gravity system is responsible for increasing the vertical velocity 9 | * of each entity if not grounded. 10 | * 11 | * @export 12 | * @class GravitySystem 13 | * @extends {System} 14 | */ 15 | export class GravitySystem extends AbstractEntitySystem { 16 | canvas: HTMLCanvasElement; 17 | 18 | constructor(public speed: number, priority?: number) { 19 | super(priority, [Position, Velocity, Size]); 20 | } 21 | 22 | /** 23 | * Caches the filter and canvas. 24 | * 25 | * @inheritdoc 26 | * @param {Engine} engine 27 | */ 28 | onAddedToEngine(engine: Engine) { 29 | super.onAddedToEngine(engine); 30 | this.canvas = document.getElementById('canvas'); 31 | } 32 | 33 | /** 34 | * Makes each entity moves down, if not grounded. 35 | * The velocity will be increased based on current size component. 36 | */ 37 | processEntity(entity: MyEntity) { 38 | const position = entity.components.get(Position); 39 | const velocity = entity.components.get(Velocity); 40 | const size = entity.components.get(Size); 41 | if (position.y + size.height < this.canvas.height) velocity.y += this.speed * (size.width * size.height) * 0.01; 42 | else if (velocity.y !== 0) { 43 | velocity.y *= 0.5; 44 | if (Math.floor(Math.abs(velocity.y)) === 0) { 45 | position.y = this.canvas.height - size.height; 46 | velocity.y = 0; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/rectangles/src/systems/movement.ts: -------------------------------------------------------------------------------- 1 | import { AbstractEntitySystem } from '@trixt0r/ecs'; 2 | import { Position } from '../components/position'; 3 | import { Velocity } from '../components/velocity'; 4 | import { MyEntity } from 'entity'; 5 | 6 | /** 7 | * The movement system is responsible to update the position of each entity 8 | * based on its current velocity component. 9 | * 10 | * @export 11 | * @class MovementSystem 12 | * @extends {System} 13 | */ 14 | export class MovementSystem extends AbstractEntitySystem { 15 | constructor(priority = 0) { 16 | super(priority, [Position, Velocity]); 17 | } 18 | 19 | /** 20 | * Updates the position. 21 | */ 22 | processEntity(entity: MyEntity) { 23 | const position = entity.components.get(Position); 24 | const velocity = entity.components.get(Velocity); 25 | position.x += velocity.x; 26 | position.y += velocity.y; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/rectangles/src/systems/renderer.ts: -------------------------------------------------------------------------------- 1 | import { Application, Graphics, Text } from 'pixi.js'; 2 | import { System, Aspect, Engine } from '@trixt0r/ecs'; 3 | import { Position } from '../components/position'; 4 | import { Size } from '../components/size'; 5 | 6 | /** 7 | * Renders each entity, based its position and size component. 8 | * 9 | * @export 10 | * @class RenderingSystem 11 | * @extends {System} 12 | */ 13 | export class RenderingSystem extends System { 14 | aspect: Aspect; 15 | pixiApp: Application; 16 | graphics: Graphics; 17 | fps: Text; 18 | timePassed: number; 19 | 20 | constructor(priority = 0) { 21 | super(priority); 22 | this.pixiApp = new Application({ 23 | view: document.getElementById('canvas'), 24 | backgroundColor: 0x1155aa, 25 | }); 26 | this.timePassed = 350; 27 | this.fps = new Text('FPS: ', { fill: 0xffffff }); 28 | this.graphics = new Graphics(); 29 | this.pixiApp.stage.addChild(this.graphics); 30 | this.pixiApp.stage.addChild(this.fps); 31 | this.pixiApp.renderer.resize(window.innerWidth, window.innerHeight); 32 | this.pixiApp.stop(); 33 | window.addEventListener('resize', () => { 34 | this.pixiApp.renderer.resize(window.innerWidth, window.innerHeight); 35 | }); 36 | } 37 | 38 | /** 39 | * Caches the filter. 40 | * 41 | * @inheritdoc 42 | * @param {Engine} engine 43 | */ 44 | onAddedToEngine(engine: Engine) { 45 | this.aspect = Aspect.for(engine).all(Position, Size); 46 | } 47 | 48 | /** 49 | * Renders each entity as a red rectangle. 50 | * 51 | * @param {number} delta 52 | */ 53 | process(delta: number) { 54 | this.timePassed += delta; 55 | if (this.timePassed > 300) { 56 | this.fps.text = 57 | 'FPS: ' + Math.floor(1000 / delta) + '\n' + 'ms: ' + delta + '\n' + 'entities: ' + this.engine.entities.length; 58 | this.timePassed = 0; 59 | } 60 | const entities = this.aspect.entities; 61 | this.graphics.clear(); 62 | this.graphics.beginFill(0xaa1100); 63 | this.graphics.lineStyle(1, 0xffffff); 64 | for (const entity of entities) { 65 | const position = entity.components.get(Position); 66 | const size = entity.components.get(Size); 67 | this.graphics.drawRect(position.x, position.y, size.width, size.height); 68 | } 69 | this.graphics.endFill(); 70 | this.pixiApp.render(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/rectangles/src/systems/resize.ts: -------------------------------------------------------------------------------- 1 | import { System, Aspect, Engine } from '@trixt0r/ecs'; 2 | import { Velocity } from '../components/velocity'; 3 | 4 | /** 5 | * The resize system is responsible for making the rectangles bounce up 6 | * if the user shrinks the window height. 7 | * 8 | * @export 9 | * @class ResizeSystem 10 | * @extends {System} 11 | */ 12 | export class ResizeSystem extends System { 13 | aspect: Aspect; 14 | canvas: HTMLCanvasElement; 15 | dirty: boolean; 16 | oldHeight: number; 17 | 18 | constructor(priority?: number) { 19 | super(priority); 20 | this.dirty = false; 21 | this.oldHeight = window.innerHeight; 22 | window.addEventListener('resize', () => { 23 | if (window.innerHeight > this.oldHeight) this.oldHeight = window.innerHeight; 24 | else this.dirty = true; 25 | }); 26 | } 27 | 28 | /** 29 | * Caches the filter and canvas. 30 | * 31 | * @inheritdoc 32 | * @param {Engine} engine 33 | */ 34 | onAddedToEngine(engine: Engine) { 35 | this.aspect = Aspect.for(engine).one(Velocity); 36 | this.canvas = document.getElementById('canvas'); 37 | } 38 | 39 | /** 40 | * Runs the actual bounce logic on each entity, 41 | * if previously resized. 42 | */ 43 | process() { 44 | if (!this.dirty) return; 45 | const diff = Math.min(20, this.oldHeight - window.innerHeight); 46 | this.aspect.entities.forEach(entity => { 47 | const velocity = entity.components.get(Velocity); 48 | if (velocity.y === 0) { 49 | velocity.y -= diff; 50 | } 51 | }); 52 | this.oldHeight = window.innerHeight; 53 | this.dirty = false; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/rectangles/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "outDir": "./build", 5 | "baseUrl": "./src", 6 | "rootDir": "./src", 7 | "moduleResolution": "node", 8 | "noImplicitAny": true, 9 | "module": "commonjs", 10 | "target": "es6", 11 | "lib": ["dom", "es2015"] 12 | }, 13 | "exclude": ["**/*.spec.ts", "**/build/*"] 14 | } 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: 'tsconfig.test.json', 7 | }, 8 | }, 9 | transform: { 10 | '^.+\\.(t|j)sx?$': ['@swc/jest'], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@trixt0r/ecs", 3 | "version": "0.5.1", 4 | "description": "Entity component system for any JS environment written in TypeScript.", 5 | "main": "build/index.js", 6 | "keywords": [ 7 | "ecs", 8 | "entity", 9 | "component", 10 | "system", 11 | "entity component system", 12 | "typescript", 13 | "ts", 14 | "tsc" 15 | ], 16 | "types": "build/index.d.ts", 17 | "scripts": { 18 | "lint": "eslint ./src --ext .js,.ts", 19 | "lint.fix": "eslint ./src --ext .js,.ts --fix", 20 | "prettier": "prettier --write ./src", 21 | "test": "jest -i --coverage", 22 | "build": "tsc && tsc --declaration --emitDeclarationOnly" 23 | }, 24 | "author": "", 25 | "license": "ISC", 26 | "devDependencies": { 27 | "@swc/jest": "^0.2.20", 28 | "@types/jest": "^27.0.3", 29 | "@typescript-eslint/eslint-plugin": "^5.7.0", 30 | "@typescript-eslint/parser": "^5.7.0", 31 | "eslint": "^8.5.0", 32 | "eslint-config-prettier": "^8.3.0", 33 | "jest": "^27.5.1", 34 | "prettier": "2.5.1", 35 | "ts-jest": "^27.1.2", 36 | "typescript": "^3.9.10" 37 | }, 38 | "homepage": "https://github.com/Trixt0r/ecsts" 39 | } 40 | -------------------------------------------------------------------------------- /src/core/collection.spec.ts: -------------------------------------------------------------------------------- 1 | import { Collection, CollectionListener } from './collection'; 2 | import { Dispatcher } from './dispatcher'; 3 | 4 | class MyType {} 5 | class MySortableType extends MyType { 6 | constructor(public position: number) { 7 | super(); 8 | } 9 | } 10 | 11 | describe('Collection', () => { 12 | let collection: Collection; 13 | beforeEach(() => (collection = new Collection())); 14 | 15 | describe('initial', () => { 16 | it('should be a dispatcher', () => { 17 | expect(collection instanceof Dispatcher).toBe(true); 18 | }); 19 | 20 | it('should have no elements', () => { 21 | expect(collection.length).toBe(0); 22 | }); 23 | 24 | it('should accept an initial list of elements', () => { 25 | const source = [new MyType(), new MyType(), new MyType()]; 26 | const c = new Collection(source); 27 | expect(c.length).toBe(source.length); 28 | c.forEach((el, i) => expect(el).toBe(source[i])); 29 | }); 30 | }); 31 | 32 | describe('elements', () => { 33 | it('should be frozen initially', () => { 34 | expect(() => (collection.elements as unknown[]).push(new MyType())).toThrow(); 35 | }); 36 | 37 | it('should be frozen after an element got added', () => { 38 | collection.add(new MyType()); 39 | expect(() => (collection.elements as unknown[]).push(new MyType())).toThrow(); 40 | }); 41 | 42 | it('should be frozen after an element got removed', () => { 43 | const comp = new MyType(); 44 | collection.add(comp); 45 | collection.remove(comp); 46 | expect(() => (collection.elements as unknown[]).push(new MyType())).toThrow(); 47 | }); 48 | 49 | it('should be frozen after the collection got cleared', () => { 50 | const comp = new MyType(); 51 | collection.add(comp); 52 | collection.clear(); 53 | expect(() => (collection.elements as unknown[]).push(new MyType())).toThrow(); 54 | }); 55 | 56 | it('should be frozen after the collection got sorted', () => { 57 | const comp = new MyType(); 58 | collection.add(comp); 59 | collection.sort(); 60 | expect(() => (collection.elements as unknown[]).push(new MyType())).toThrow(); 61 | }); 62 | }); 63 | 64 | describe('add', () => { 65 | let element: MyType; 66 | 67 | beforeEach(() => (element = new MyType())); 68 | 69 | it('should add an element', () => { 70 | const re = collection.add(element); 71 | expect(re).toBe(true); 72 | expect(collection.length).toBe(1); 73 | expect(collection.elements).toContain(element); 74 | expect(collection.elements[0]).toBe(element); 75 | }); 76 | 77 | it('should add multiple elements', () => { 78 | const o1 = new MyType(); 79 | const o2 = new MyType(); 80 | const re = collection.add(element, o1, o2); 81 | expect(re).toBe(true); 82 | expect(collection.length).toBe(3); 83 | expect(collection.elements).toContain(element); 84 | expect(collection.elements).toContain(o1); 85 | expect(collection.elements).toContain(o2); 86 | expect(collection.elements[0]).toBe(element); 87 | expect(collection.elements[1]).toBe(o1); 88 | expect(collection.elements[2]).toBe(o2); 89 | }); 90 | 91 | it('should not add the same element twice (2 calls)', () => { 92 | collection.add(element); 93 | expect(collection.length).toBe(1); 94 | expect(collection.elements).toContain(element); 95 | 96 | const re = collection.add(element); 97 | expect(re).toBe(false); 98 | expect(collection.length).toBe(1); 99 | }); 100 | 101 | it('should not add the same element multiple times (1 call)', () => { 102 | collection.add(element, element, element, element); 103 | expect(collection.length).toBe(1); 104 | expect(collection.elements).toContain(element); 105 | }); 106 | 107 | it('should notify all listeners that an element got added', () => { 108 | let added: MyType = null; 109 | const listener: CollectionListener = { 110 | onAdded: (element: MyType) => (added = element), 111 | }; 112 | 113 | collection.addListener(listener); 114 | collection.add(element); 115 | expect(added).toBe(element); 116 | }); 117 | 118 | it('should notify all listeners that elements got added', () => { 119 | let added: MyType[] = []; 120 | const listener: CollectionListener = { 121 | onAdded: (...elements: MyType[]) => (added = elements), 122 | }; 123 | 124 | collection.addListener(listener); 125 | const o1 = new MyType(); 126 | const o2 = new MyType(); 127 | collection.add(element, o1, o2); 128 | expect(added.length).toBe(3); 129 | const objs = [element, o1, o2]; 130 | objs.forEach((obj, i) => expect(obj).toBe(added[i])); 131 | }); 132 | 133 | it('should not notify any listener that an element has been removed', () => { 134 | let removed: MyType = null; 135 | const listener: CollectionListener = { 136 | onRemoved: (element: MyType) => (removed = element), 137 | }; 138 | 139 | collection.addListener(listener); 140 | collection.add(element); 141 | expect(removed).toBe(null); 142 | }); 143 | }); 144 | 145 | describe('remove', () => { 146 | let element: MyType; 147 | 148 | beforeEach(() => { 149 | element = new MyType(); 150 | collection.add(element); 151 | }); 152 | 153 | it('should remove a previously added element', () => { 154 | const re = collection.remove(element); 155 | expect(re).toBe(true); 156 | expect(collection.length).toBe(0); 157 | expect(collection.elements).not.toContain(element); 158 | }); 159 | 160 | it('should remove multiple previously added elements', () => { 161 | const o1 = new MyType(); 162 | const o2 = new MyType(); 163 | collection.add(o1, o2); 164 | const re = collection.remove(element, o1, o2); 165 | expect(re).toBe(true); 166 | expect(collection.length).toBe(0); 167 | expect(collection.elements).not.toContain(element); 168 | expect(collection.elements).not.toContain(o1); 169 | expect(collection.elements).not.toContain(o2); 170 | }); 171 | 172 | it('should remove an element at the specified index (0)', () => { 173 | const re = collection.remove(0); 174 | expect(re).toBe(true); 175 | expect(collection.length).toBe(0); 176 | expect(collection.elements).not.toContain(element); 177 | }); 178 | 179 | it('should remove elements at the specified indices (0, 1, 2)', () => { 180 | const o1 = new MyType(); 181 | const o2 = new MyType(); 182 | collection.add(o1, o2); 183 | const re = collection.remove(0, 1, 2); 184 | expect(re).toBe(true); 185 | expect(collection.length).toBe(0); 186 | expect(collection.elements).not.toContain(element); 187 | expect(collection.elements).not.toContain(o1); 188 | expect(collection.elements).not.toContain(o2); 189 | }); 190 | 191 | it('should not remove an element which is not part of the collection', () => { 192 | const re = collection.remove(new MyType()); 193 | expect(re).toBe(false); 194 | expect(collection.length).toBe(1); 195 | }); 196 | 197 | it('should not remove elements which are not part of the collection', () => { 198 | const re = collection.remove(new MyType(), new MyType(), new MyType()); 199 | expect(re).toBe(false); 200 | expect(collection.length).toBe(1); 201 | }); 202 | 203 | it('should not remove an element at an out of bounds index', () => { 204 | const re = collection.remove(collection.length); 205 | expect(re).toBe(false); 206 | }); 207 | 208 | it('should not remove multiple elements at out of bounds indices', () => { 209 | const re = collection.remove(-1, collection.length, collection.length + 1); 210 | expect(re).toBe(false); 211 | }); 212 | 213 | it('should notify all listeners that an element got removed', () => { 214 | let removed: MyType = null; 215 | const listener: CollectionListener = { 216 | onRemoved: (element: MyType) => (removed = element), 217 | }; 218 | 219 | collection.addListener(listener); 220 | collection.remove(element); 221 | expect(removed).toBe(element); 222 | }); 223 | 224 | it('should notify all listeners that multiple elements got removed', () => { 225 | const o1 = new MyType(); 226 | const o2 = new MyType(); 227 | collection.add(o1, o2); 228 | let removed: MyType[] = []; 229 | const listener: CollectionListener = { 230 | onRemoved: (...elements: MyType[]) => (removed = elements), 231 | }; 232 | 233 | collection.addListener(listener); 234 | collection.remove(element, o1, o2); 235 | expect(removed.length).toBe(3); 236 | const objs = [element, o1, o2]; 237 | objs.forEach((obj, i) => expect(obj).toBe(removed[i])); 238 | }); 239 | 240 | it('should not notify any listener that a element has been added', () => { 241 | let added: MyType = null; 242 | const listener: CollectionListener = { 243 | onAdded: (element: MyType) => (added = element), 244 | }; 245 | 246 | collection.addListener(listener); 247 | collection.remove(element); 248 | expect(added).toBe(null); 249 | }); 250 | }); 251 | 252 | describe('clear', () => { 253 | let listener; 254 | beforeEach(() => { 255 | collection.add(new MyType(), new MyType(), new MyType()); 256 | listener = { onCleared: jest.fn() }; 257 | collection.addListener(listener); 258 | }); 259 | 260 | it('should remove all elements from the collection', () => { 261 | collection.clear(); 262 | expect(collection.length).toBe(0); 263 | }); 264 | 265 | it('should notify all listeners that the collection has been cleared', () => { 266 | collection.add(new MyType()); 267 | collection.clear(); 268 | expect(listener.onCleared).toHaveBeenCalled(); 269 | }); 270 | 271 | it('should not notify any listener that the collection has been cleared if there were no elements', () => { 272 | collection.remove(0, 1, 2); 273 | collection.clear(); 274 | expect(listener.onCleared).not.toHaveBeenCalled(); 275 | }); 276 | }); 277 | 278 | describe('sort', () => { 279 | let listener; 280 | beforeEach(() => { 281 | collection.add(new MySortableType(3), new MySortableType(2), new MySortableType(1)); 282 | listener = { onSorted: jest.fn() }; 283 | collection.addListener(listener); 284 | }); 285 | 286 | it('should sort the collection', () => { 287 | collection.sort((a, b) => (a as MySortableType).position - (b as MySortableType).position); 288 | 289 | const elements = collection.elements; 290 | expect(elements[0].position).toBe(1); 291 | expect(elements[1].position).toBe(2); 292 | expect(elements[2].position).toBe(3); 293 | }); 294 | 295 | it('should notify all listeners that the collection has been sorted', () => { 296 | collection.add(new MySortableType(1)); 297 | collection.sort(); 298 | expect(listener.onSorted).toHaveBeenCalled(); 299 | }); 300 | 301 | it('should not notify any listener that the collection has been sorted if there were no elements', () => { 302 | collection.remove(0, 1, 2); 303 | collection.sort(); 304 | expect(listener.onSorted).not.toHaveBeenCalled(); 305 | }); 306 | }); 307 | 308 | describe('iterable', () => { 309 | const amount = 10; 310 | beforeEach(() => { 311 | for (let i = 0; i < amount; i++) collection.add(new MyType()); 312 | }); 313 | 314 | it('should be iterable', () => { 315 | expect(typeof collection[Symbol.iterator] === 'function').toBe(true); 316 | }); 317 | 318 | it('should iterate over all elements in the collection', () => { 319 | let iterations = 0; 320 | for (const element of collection) { 321 | expect(element).toBe(collection.elements[iterations]); 322 | iterations++; 323 | } 324 | expect(iterations).toBe(collection.length); 325 | }); 326 | 327 | it('should iterate over all elements in the collection multiple times', () => { 328 | let iterations = 0; 329 | const times = 5; 330 | for (let i = 0; i < 5; i++) { 331 | let iteration = 0; 332 | for (const element of collection) { 333 | expect(element).toBe(collection.elements[iteration++]); 334 | iterations++; 335 | } 336 | } 337 | expect(iterations).toBe(collection.length * times); 338 | }); 339 | 340 | it('should iterate over all remaining elements in the collection', () => { 341 | let iterations = 0; 342 | collection.remove(amount - 1, amount - 2, amount - 3, amount - 4, amount - 5); 343 | for (const element of collection) { 344 | expect(element).toBe(collection.elements[iterations]); 345 | iterations++; 346 | } 347 | expect(iterations).toBe(collection.length); 348 | }); 349 | }); 350 | 351 | describe('array-like', () => { 352 | const amount = 10; 353 | beforeEach(() => { 354 | for (let i = 0; i < amount; i++) collection.add(new MyType()); 355 | }); 356 | 357 | it.each([ 358 | { name: 'filter', args: [(_: MyType, index: number) => index === 1, {}] }, 359 | { name: 'forEach', args: [(_: MyType, index: number) => index === 1, {}] }, 360 | { name: 'find', args: [(_: MyType, index: number) => index === 1, {}] }, 361 | { name: 'findIndex', args: [(_: MyType, index: number) => index === 1, {}] }, 362 | { name: 'indexOf', args: [0] }, 363 | { name: 'map', args: [(_: MyType, index: number) => index, {}] }, 364 | { name: 'every', args: [(_: MyType, index: number) => index === 1, {}] }, 365 | { name: 'some', args: [(_: MyType, index: number) => index === 1, {}] }, 366 | { name: 'reduce', args: [(prev, _elmnt, idx) => prev + idx, 0] }, 367 | { name: 'reduceRight', args: [(prev, _elmnt, idx) => prev + idx, 0] }, 368 | ])('should execute $name with $args', ({ name, args }) => { 369 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 370 | const spy = jest.spyOn((collection as any)._elements, name); 371 | 372 | collection[name](...args); 373 | 374 | expect(spy).toHaveBeenCalledWith(...args); 375 | }); 376 | }); 377 | }); 378 | -------------------------------------------------------------------------------- /src/core/collection.ts: -------------------------------------------------------------------------------- 1 | import { Dispatcher } from './dispatcher'; 2 | 3 | /** 4 | * The listener interface for a listener on an entity. 5 | */ 6 | export interface CollectionListener { 7 | /** 8 | * Called as soon as new elements have been added to the collection. 9 | * 10 | * @param elements 11 | */ 12 | onAdded?(...elements: T[]): void; 13 | 14 | /** 15 | * Called as soon as elements got removed from the collection. 16 | * 17 | * @param elements 18 | */ 19 | onRemoved?(...elements: T[]): void; 20 | 21 | /** 22 | * Called as soon as all elements got removed from the collection. 23 | */ 24 | onCleared?(): void; 25 | 26 | /** 27 | * Called as soon as the elements got sorted. 28 | */ 29 | onSorted?(): void; 30 | } 31 | 32 | /** 33 | * A collection holds a list of elements of a certain type 34 | * and allows to add, remove, sort and clear the list. 35 | * On each operation the internal list of elements gets frozen, 36 | * so a user of the collection will not be able to operate on the real reference, 37 | * but read the data without the need of copying the data on each read access. 38 | */ 39 | export class Collection extends Dispatcher> implements IterableIterator { 40 | /** 41 | * The internal list of elements. 42 | */ 43 | protected _elements: T[]; 44 | 45 | /** 46 | * The frozen list of elements which is used to expose the element list to the public. 47 | */ 48 | protected _frozenElements: T[] = []; 49 | 50 | /** 51 | * The internal iterator pointer. 52 | */ 53 | protected pointer = 0; 54 | 55 | /** 56 | * Creates an instance of Collection. 57 | * 58 | * @param initial An optional initial list of elements. 59 | */ 60 | constructor(initial: T[] = []) { 61 | super(); 62 | this._elements = initial.slice(); 63 | this.updatedFrozenObjects(); 64 | } 65 | 66 | /** 67 | * @inheritdoc 68 | */ 69 | public next(): IteratorResult { 70 | if (this.pointer < this._elements.length) { 71 | return { 72 | done: false, 73 | value: this._elements[this.pointer++], 74 | }; 75 | } else { 76 | return { 77 | done: true, 78 | value: null, 79 | }; 80 | } 81 | } 82 | 83 | /** 84 | * @inheritdoc 85 | */ 86 | [Symbol.iterator](): IterableIterator { 87 | this.pointer = 0; 88 | return this; 89 | } 90 | 91 | /** 92 | * A snapshot of all elements in this collection. 93 | */ 94 | get elements(): readonly T[] { 95 | return this._frozenElements; 96 | } 97 | 98 | /** 99 | * The length, of this collection, i.e. how many elements this collection contains. 100 | */ 101 | get length(): number { 102 | return this._frozenElements.length; 103 | } 104 | 105 | /** 106 | * Updates the internal frozen element list. 107 | */ 108 | protected updatedFrozenObjects(): void { 109 | this._frozenElements = this._elements.slice(); 110 | Object.freeze(this._frozenElements); 111 | } 112 | 113 | /** 114 | * Adds the given element to this collection. 115 | * 116 | * @param element 117 | * @return Whether the element has been added or not. 118 | * It may not be added, if already present in the element list. 119 | */ 120 | protected addSingle(element: T): boolean { 121 | if (this._elements.indexOf(element) >= 0) return false; 122 | this._elements.push(element); 123 | return true; 124 | } 125 | 126 | /** 127 | * Adds the given element(s) to this collection. 128 | * 129 | * @param elements 130 | * @return Whether elements have been added or not. 131 | * They may not have been added, if they were already present in the element list. 132 | */ 133 | add(...elements: T[]): boolean { 134 | const added: T[] = elements.filter(element => this.addSingle(element)); 135 | if (added.length <= 0) return false; 136 | this.updatedFrozenObjects(); 137 | this.dispatch('onAdded', ...added); 138 | return true; 139 | } 140 | 141 | /** 142 | * Removes the given element or the element at the given index. 143 | * 144 | * @param elOrIndex 145 | * @return Whether the element has been removed or not. 146 | * It may not have been removed, if it was not in the element list. 147 | */ 148 | protected removeSingle(elOrIndex: T | number): boolean { 149 | if (typeof elOrIndex === 'number') elOrIndex = this.elements[elOrIndex]; 150 | const idx = this._elements.findIndex(_ => _ === elOrIndex); 151 | if (idx < 0 || idx >= this._elements.length) return false; 152 | this._elements.splice(idx, 1); 153 | return true; 154 | } 155 | 156 | /** 157 | * Removes the given element(s) or the elements at the given indices. 158 | * 159 | * @param elementsOrIndices 160 | * @return Whether elements have been removed or not. 161 | * They may not have been removed, if every element was not in the element list. 162 | */ 163 | remove(...elementsOrIndices: (T | number)[]): boolean { 164 | const removed = elementsOrIndices.filter(element => this.removeSingle(element)); 165 | if (removed.length <= 0) return false; 166 | const removedElements = removed.map(o => (typeof o === 'number' ? this.elements[o] : o)); 167 | this.updatedFrozenObjects(); 168 | this.dispatch('onRemoved', ...removedElements); 169 | return true; 170 | } 171 | 172 | /** 173 | * Clears this collection, i.e. removes all elements from the internal list. 174 | */ 175 | clear(): void { 176 | if (!this._elements.length) return; 177 | this._elements = []; 178 | this.updatedFrozenObjects(); 179 | this.dispatch('onCleared'); 180 | } 181 | 182 | /** 183 | * Returns the index of the given element. 184 | * 185 | * @param element The element. 186 | * @return The index of the given element or id. 187 | */ 188 | indexOf(element: T): number { 189 | return this._elements.indexOf(element); 190 | } 191 | 192 | /** 193 | * Sorts this collection. 194 | * 195 | * @param [compareFn] 196 | * 197 | */ 198 | sort(compareFn?: (a: T, b: T) => number): this { 199 | if (!this._elements.length) return this; 200 | this._elements.sort(compareFn); 201 | this.updatedFrozenObjects(); 202 | this.dispatch('onSorted'); 203 | return this; 204 | } 205 | 206 | /** 207 | * Returns the elements of this collection that meet the condition specified in a callback function. 208 | * 209 | * @param callbackfn 210 | * A function that accepts up to three arguments. 211 | * The filter method calls the `callbackfn` function one time for each element in the collection. 212 | * @param thisArg An object to which the this keyword can refer in the callbackfn function. 213 | * If `thisArg` is omitted, undefined is used as the this value. 214 | * @return An array with elements which met the condition. 215 | */ 216 | filter(callbackfn: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: U): T[] { 217 | return this._elements.filter(callbackfn, thisArg); 218 | } 219 | 220 | /** 221 | * Performs the specified action for each element in the collection. 222 | * 223 | * @param callbackFn 224 | * A function that accepts up to three arguments. 225 | * forEach calls the callbackfn function one time for each element in the array. 226 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 227 | * If thisArg is omitted, undefined is used as the this value. 228 | */ 229 | forEach(callbackFn: (element: T, index: number, array: readonly T[]) => void, thisArg?: U): void { 230 | this._elements.forEach(callbackFn, thisArg); 231 | } 232 | 233 | /** 234 | * Returns the value of the first element in the collection where predicate is true, and undefined 235 | * otherwise. 236 | * 237 | * @param predicate 238 | * Find calls predicate once for each element of the array, in ascending order, 239 | * until it finds one where predicate returns true. If such an element is found, find 240 | * immediately returns that element value. Otherwise, find returns undefined. 241 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 242 | * If thisArg is omitted, undefined is used as the this value. 243 | * 244 | */ 245 | find(predicate: (element: T, index: number, array: readonly T[]) => unknown, thisArg?: U): T | undefined { 246 | return this._elements.find(predicate, thisArg); 247 | } 248 | 249 | /** 250 | * Returns the index of the first element in the collection where predicate is true, and -1 251 | * otherwise. 252 | * 253 | * @param predicate 254 | * Find calls predicate once for each element of the array, in ascending order, 255 | * until it finds one where predicate returns true. If such an element is found, 256 | * findIndex immediately returns that element index. Otherwise, findIndex returns -1. 257 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 258 | * If thisArg is omitted, undefined is used as the this value. 259 | * 260 | */ 261 | findIndex(predicate: (element: T, index: number, array: readonly T[]) => unknown, thisArg?: U): number { 262 | return this._elements.findIndex(predicate, thisArg); 263 | } 264 | 265 | /** 266 | * Calls a defined callback function on each element of an collection, and returns an array that contains the results. 267 | * 268 | * @param callbackFn 269 | * A function that accepts up to three arguments. 270 | * The map method calls the callbackfn function one time for each element in the array. 271 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 272 | * If thisArg is omitted, undefined is used as the this value. 273 | * 274 | */ 275 | map(callbackFn: (element: T, index: number, array: readonly T[]) => V, thisArg?: U): V[] { 276 | return this._elements.map(callbackFn, thisArg); 277 | } 278 | 279 | /** 280 | * 281 | * Determines whether all the members of the collection satisfy the specified test. 282 | * 283 | * @param callbackFn 284 | * A function that accepts up to three arguments. 285 | * The every method calls the callbackfn function for each element in array1 until the callbackfn 286 | * returns false, or until the end of the array. 287 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 288 | * If thisArg is omitted, undefined is used as the this value. 289 | * 290 | */ 291 | every(callbackFn: (element: T, index: number, array: readonly T[]) => unknown, thisArg?: U): boolean { 292 | return this._elements.every(callbackFn, thisArg); 293 | } 294 | 295 | /** 296 | * Determines whether the specified callback function returns true for any element of the collection. 297 | * 298 | * @param callbackFn 299 | * A function that accepts up to three arguments. 300 | * The some method calls the callbackfn function for each element in the collection until the callbackfn 301 | * returns true, or until the end of the collection. 302 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function. 303 | * If thisArg is omitted, undefined is used as the this value. 304 | * 305 | */ 306 | some(callbackFn: (element: T, index: number, array: readonly T[]) => unknown, thisArg?: U): boolean { 307 | return this._elements.some(callbackFn, thisArg); 308 | } 309 | 310 | /** 311 | * Calls the specified callback function for all the elements in the collection. 312 | * The return value of the callback function is the accumulated result, 313 | * and is provided as an argument in the next call to the callback function. 314 | * 315 | * @param callbackFn 316 | * A function that accepts up to four arguments. 317 | * The reduce method calls the callbackfn function one time for each element in the array. 318 | * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. 319 | * The first call to the callbackfn function provides this value as an argument instead of 320 | * an collection value. 321 | * 322 | */ 323 | reduce( 324 | callbackFn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U, 325 | initialValue: U 326 | ): U { 327 | return this._elements.reduce(callbackFn, initialValue); 328 | } 329 | 330 | /** 331 | * Calls the specified callback function for all the elements in the collection, in descending order. 332 | * The return value of the callback function is the accumulated result, 333 | * and is provided as an argument in the next call to the callback function. 334 | * 335 | * @param callbackFn 336 | * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. 337 | * The first call to the callbackfn function provides this value as an argument instead of 338 | * an collection value. 339 | * 340 | */ 341 | reduceRight( 342 | callbackFn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U, 343 | initialValue: U 344 | ): U { 345 | return this._elements.reduceRight(callbackFn, initialValue); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/core/component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentCollection, Component } from './component'; 2 | 3 | class MyComponent1 implements Component {} 4 | 5 | class MyComponentWithType implements Component { 6 | static type = 'myType'; 7 | } 8 | 9 | describe('ComponentCollection', () => { 10 | let collection: ComponentCollection; 11 | 12 | beforeEach(() => (collection = new ComponentCollection())); 13 | 14 | describe('initial', () => { 15 | it('should throw if someone tries to remove the first component listener', () => { 16 | expect(() => collection.removeListener(0)).toThrowError('Listener at index 0 is locked.'); 17 | }); 18 | }); 19 | 20 | describe('get', () => { 21 | it.each([MyComponent1, MyComponentWithType, 'myType'])('should not return any component for %p if empty', _ => 22 | expect(collection.get(_)).toBeUndefined() 23 | ); 24 | 25 | it.each([ 26 | { query: MyComponent1, comp: new MyComponent1() }, 27 | { query: MyComponentWithType, comp: new MyComponentWithType() }, 28 | { query: MyComponentWithType, comp: { type: 'myType' } }, 29 | { query: MyComponentWithType.type, comp: new MyComponentWithType() }, 30 | { query: 'testType', comp: { type: 'testType' } }, 31 | ])('should return $comp for $query', ({ query, comp }) => { 32 | collection.add(comp); 33 | expect(collection.get(query)).toBe(comp); 34 | }); 35 | 36 | it.each([ 37 | { query: MyComponent1, comp: new MyComponent1() }, 38 | { query: MyComponentWithType, comp: new MyComponentWithType() }, 39 | { query: MyComponentWithType, comp: { type: 'myType' } }, 40 | { query: MyComponentWithType.type, comp: new MyComponentWithType() }, 41 | { query: 'testType', comp: { type: 'testType' } }, 42 | ])('should return $comp for previously added class, after the first access', ({ query, comp }) => { 43 | collection.get(query); 44 | collection.add(comp); 45 | expect(collection.get(query)).toBe(comp); 46 | }); 47 | 48 | it.each([ 49 | { count: 10, query: MyComponent1, type: 'class' }, 50 | { count: 10, query: MyComponentWithType, type: 'type' }, 51 | ])('should return the first component of the previously added $query', ({ count, query }) => { 52 | const comps = []; 53 | for (let i = 0; i < count; i++) { 54 | comps.push(new query()); 55 | collection.add(comps[comps.length - 1]); 56 | } 57 | expect(collection.get(query)).toBe(comps[0]); 58 | expect(collection.get(query)).toBe(collection.elements[0]); 59 | }); 60 | }); 61 | 62 | describe('getAll', () => { 63 | it.each([MyComponent1, MyComponentWithType, 'myType'])('should not return any component for %p if empty', _ => { 64 | expect(collection.getAll(_)).toEqual([]); 65 | }); 66 | 67 | it.each([ 68 | { query: MyComponent1, comps: [new MyComponent1(), new MyComponent1(), new MyComponent1()] }, 69 | { query: MyComponentWithType, comps: [new MyComponentWithType(), { type: 'myType' }] }, 70 | { query: MyComponentWithType.type, comps: [new MyComponentWithType(), { type: 'myType' }] }, 71 | { query: 'testType', comps: [{ type: 'testType' }, { type: 'testType' }, { type: 'testType' }] }, 72 | ])('should return components for $query', ({ query, comps }) => { 73 | collection.add(...comps); 74 | expect(collection.getAll(query)).toEqual(comps); 75 | }); 76 | 77 | it.each([ 78 | { query: MyComponent1, comps: [new MyComponent1(), new MyComponent1(), new MyComponent1()] }, 79 | { query: MyComponentWithType, comps: [new MyComponentWithType(), { type: 'myType' }] }, 80 | { query: MyComponentWithType.type, comps: [new MyComponentWithType(), { type: 'myType' }] }, 81 | { query: 'testType', comps: [{ type: 'testType' }, { type: 'testType' }, { type: 'testType' }] }, 82 | ])('should return components for $query, after the first access', ({ query, comps }) => { 83 | collection.getAll(query); 84 | collection.add(...comps); 85 | expect(collection.getAll(query)).toEqual(comps); 86 | }); 87 | 88 | it('should return same type components with different prototypes', () => { 89 | collection.getAll(MyComponentWithType); 90 | collection.getAll(MyComponentWithType.type); 91 | 92 | const comp1 = { type: MyComponentWithType.type }; 93 | const comp2 = new MyComponentWithType(); 94 | collection.add(comp1, comp2); 95 | 96 | expect(collection.getAll(MyComponentWithType.type)).toEqual([comp1, comp2]); 97 | expect(collection.getAll(MyComponentWithType)).toEqual([comp1, comp2]); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /src/core/component.ts: -------------------------------------------------------------------------------- 1 | import { Collection, CollectionListener } from './collection'; 2 | import { ComponentClass } from './types'; 3 | 4 | /** 5 | * The component interface, every component has to implement. 6 | * 7 | * If you want your system to treat different Components the same way, 8 | * you may define a static string variable named `type` in your components. 9 | */ 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | export interface Component extends Record { 12 | /** 13 | * An optional id for the component. 14 | */ 15 | id?: string; 16 | 17 | /** 18 | * An optional type for the component. 19 | */ 20 | type?: string; 21 | } 22 | 23 | /** 24 | * A collection for components. 25 | * Supports accessing components by their class. 26 | * 27 | */ 28 | export class ComponentCollection 29 | extends Collection 30 | implements CollectionListener 31 | { 32 | /** 33 | * Internal map for faster component access, by class or type. 34 | */ 35 | protected cache = new Map | string, readonly C[]>(); 36 | 37 | /** 38 | * Internal state for updating the components access memory. 39 | * 40 | */ 41 | protected dirty = new Map | string, boolean>(); 42 | 43 | constructor(initial: C[] = []) { 44 | super(initial); 45 | this.addListener(this, true); 46 | } 47 | 48 | /** 49 | * @inheritdoc 50 | * Update the internal cache. 51 | */ 52 | onAdded(...elements: C[]): void { 53 | this.markForCacheUpdate(...elements); 54 | } 55 | 56 | /** 57 | * @inheritdoc 58 | * Update the internal cache. 59 | */ 60 | onRemoved(...elements: C[]): void { 61 | this.markForCacheUpdate(...elements); 62 | } 63 | 64 | /** 65 | * @inheritdoc 66 | * Update the internal cache. 67 | */ 68 | onCleared() { 69 | this.dirty.clear(); 70 | this.cache.clear(); 71 | } 72 | 73 | /** 74 | * Searches for the first component matching the given class or type. 75 | * 76 | * @param classOrType The class or type a component has to match. 77 | * @return The found component or `null`. 78 | */ 79 | get(classOrType: ComponentClass | string): T { 80 | return this.getAll(classOrType)[0]; 81 | } 82 | 83 | /** 84 | * Searches for the all components matching the given class or type. 85 | * 86 | * @param classOrType The class or type components have to match. 87 | * @return A list of all components matching the given class. 88 | */ 89 | getAll(classOrType: ComponentClass | string): readonly T[] { 90 | if (this.dirty.get(classOrType)) this.updateCache(classOrType); 91 | if (this.cache.has(classOrType)) return this.cache.get(classOrType) as T[]; 92 | this.updateCache(classOrType); 93 | return this.cache.get(classOrType) as T[]; 94 | } 95 | 96 | /** 97 | * Updates the cache for the given class or type. 98 | * 99 | * @param classOrType The class or type to update the cache for. 100 | */ 101 | protected updateCache(classOrType: ComponentClass | string): void { 102 | const keys = this.cache.keys(); 103 | const type = typeof classOrType === 'string' ? classOrType : classOrType.type; 104 | const filtered = this.filter(element => { 105 | const clazz = element.constructor as ComponentClass; 106 | const typeVal = element.type ?? clazz.type; 107 | return type && typeVal ? type === typeVal : clazz === classOrType; 108 | }); 109 | if (typeof classOrType !== 'string' && classOrType.type) { 110 | this.cache.set(classOrType.type, filtered); 111 | this.dirty.delete(classOrType.type); 112 | } else if (typeof classOrType === 'string') { 113 | for (const key of keys) { 114 | if (typeof key !== 'string' && key.type === classOrType) { 115 | this.cache.set(key, filtered); 116 | this.dirty.delete(key); 117 | } 118 | } 119 | } 120 | this.cache.set(classOrType, filtered); 121 | this.dirty.delete(classOrType); 122 | } 123 | 124 | /** 125 | * Marks the classes and types of the given elements as dirty, 126 | * so their cache gets updated on the next request. 127 | * 128 | * @param elements 129 | * 130 | */ 131 | protected markForCacheUpdate(...elements: C[]): void { 132 | const keys = this.cache.keys(); 133 | elements.forEach(element => { 134 | const clazz = element.constructor as ComponentClass; 135 | const classOrType = element.type ?? clazz.type ?? clazz; 136 | if (this.dirty.get(classOrType)) return; 137 | if (typeof classOrType === 'string') { 138 | for (const key of keys) { 139 | if (typeof key !== 'string' && key.type === classOrType) this.dirty.set(key, true); 140 | } 141 | } 142 | this.dirty.set(classOrType, true); 143 | }); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/core/dispatcher.spec.ts: -------------------------------------------------------------------------------- 1 | import { Dispatcher } from './dispatcher'; 2 | 3 | type MyListener = Record & { process?(...args: T[]): void }; 4 | class MyDispatcher extends Dispatcher {} 5 | 6 | describe('Dispatcher', () => { 7 | let dispatcher: MyDispatcher; 8 | 9 | beforeEach(() => { 10 | dispatcher = new MyDispatcher(); 11 | }); 12 | 13 | describe('initial', () => { 14 | it('should have no listeners', () => { 15 | expect(dispatcher.listeners.length).toBe(0); 16 | }); 17 | }); 18 | 19 | describe('listeners', () => { 20 | it('should not care about directly pushed listeners', () => { 21 | (dispatcher.listeners as unknown[]).push({}); 22 | expect(dispatcher.listeners.length).toBe(0); 23 | }); 24 | }); 25 | 26 | describe('addListener', () => { 27 | it('should add a listener to the dispatcher', () => { 28 | const listener: MyListener = {}; 29 | const re = dispatcher.addListener(listener); 30 | expect(dispatcher.listeners.length).toBe(1); 31 | expect(re).toBe(true); 32 | }); 33 | 34 | it('should not add the same listener twice', () => { 35 | const listener: MyListener = {}; 36 | dispatcher.addListener(listener); 37 | const re = dispatcher.addListener(listener); 38 | expect(dispatcher.listeners.length).toBe(1); 39 | expect(re).toBe(false); 40 | }); 41 | }); 42 | 43 | describe('removeListener', () => { 44 | const listener: MyListener = {}; 45 | 46 | beforeEach(() => dispatcher.addListener(listener)); 47 | 48 | it('should remove the previously added event listener', () => { 49 | const re = dispatcher.removeListener(listener); 50 | expect(dispatcher.listeners.length).toBe(0); 51 | expect(re).toBe(true); 52 | }); 53 | 54 | it('should remove the listener at an index in bounds', () => { 55 | const re = dispatcher.removeListener(0); 56 | expect(dispatcher.listeners.length).toBe(0); 57 | expect(re).toBe(true); 58 | }); 59 | 60 | it('should not remove a listener which was not added', () => { 61 | const re = dispatcher.removeListener({}); 62 | expect(dispatcher.listeners.length).toBe(1); 63 | expect(re).toBe(false); 64 | }); 65 | 66 | it('should not remove anything if the index is out of bounds', () => { 67 | const re = dispatcher.removeListener(1); 68 | expect(dispatcher.listeners.length).toBe(1); 69 | expect(re).toBe(false); 70 | }); 71 | 72 | it('should not remove previoulsy removed listener', () => { 73 | dispatcher.removeListener(listener); 74 | const re = dispatcher.removeListener(listener); 75 | expect(dispatcher.listeners.length).toBe(0); 76 | expect(re).toBe(false); 77 | }); 78 | 79 | it('should throw if a locked listener gets removed', () => { 80 | const listener = {}; 81 | dispatcher.addListener(listener, true); 82 | expect(() => dispatcher.removeListener(listener)).toThrowError('Listener at index 1 is locked.'); 83 | }); 84 | }); 85 | 86 | describe('dispatch', () => { 87 | it('should call the given function name on each listener', () => { 88 | let calls = 0; 89 | const listeners = 10; 90 | for (let i = 0; i < listeners; i++) dispatcher.addListener({ process: () => calls++ }); 91 | dispatcher.dispatch('process'); 92 | expect(calls).toBe(listeners); 93 | }); 94 | 95 | it('should pass all arguments to the listeners', () => { 96 | let args = 0; 97 | const listeners = 10; 98 | for (let i = 0; i < listeners; i++) 99 | dispatcher.addListener({ 100 | process: function (number, string, bool, nil, array) { 101 | args += arguments.length; 102 | expect(number).toBe(1); 103 | expect(string).toBe('abc'); 104 | expect(bool).toBe(true); 105 | expect(nil).toBe(null); 106 | expect(array).toEqual([]); 107 | }, 108 | }); 109 | dispatcher.dispatch('process', 1, 'abc', true, null, []); 110 | expect(args).toBe(listeners * 5); 111 | }); 112 | 113 | it('should not call the given function name on listeners which have been removed', () => { 114 | let calls = 0; 115 | const listeners = 10; 116 | const toRemove = 5; 117 | for (let i = 0; i < listeners; i++) dispatcher.addListener({ process: () => calls++ }); 118 | for (let i = 0; i < toRemove; i++) dispatcher.removeListener(listeners - 1 - i); 119 | dispatcher.dispatch('process'); 120 | expect(calls).toBe(listeners - toRemove); 121 | }); 122 | 123 | it('should only call the given function name on listeners which implemented the function', () => { 124 | let calls = 0; 125 | const listeners = 5; 126 | const wrongListeners = 5; 127 | for (let i = 0; i < listeners; i++) dispatcher.addListener({ process: () => calls++ }); 128 | for (let i = 0; i < wrongListeners; i++) dispatcher.addListener({ processOther: () => calls++ }); 129 | dispatcher.dispatch('process'); 130 | expect(calls).toBe(listeners); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /src/core/dispatcher.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentTypes } from './types'; 2 | 3 | /** 4 | * A dispatcher is an abstract object which holds a list of listeners 5 | * to which data during certain events can be dispatched, by calling functions implemented by listeners. 6 | * 7 | */ 8 | export abstract class Dispatcher { 9 | /** 10 | * The list of listeners for this dispatcher. 11 | */ 12 | protected _listeners: Partial[]; 13 | 14 | /** 15 | * Locked listeners. 16 | * Those listeners in this array can not be removed anymore. 17 | */ 18 | protected _lockedListeners: Partial[]; 19 | 20 | /** 21 | * Creates an instance of Dispatcher. 22 | */ 23 | constructor() { 24 | this._listeners = []; 25 | this._lockedListeners = []; 26 | } 27 | 28 | /** 29 | * The current listeners for this dispatcher. 30 | */ 31 | get listeners(): readonly Partial[] { 32 | return this._listeners.slice(); 33 | } 34 | 35 | /** 36 | * Adds the given listener to this entity. 37 | * 38 | * @param listener 39 | * @return Whether the listener has been added or not. 40 | * It may not be added, if already present in the listener list. 41 | */ 42 | addListener(listener: Partial, lock = false): boolean { 43 | if (this._listeners.indexOf(listener) >= 0) return false; 44 | this._listeners.push(listener); 45 | if (lock) this._lockedListeners.push(listener); 46 | return true; 47 | } 48 | 49 | /** 50 | * Removes the given listener or the listener at the given index. 51 | * 52 | * @param listenerOrIndex 53 | * @return Whether the listener has been removed or not. 54 | * It may not have been removed, if it was not in the listener list. 55 | */ 56 | removeListener(listenerOrIndex: Partial | number): boolean { 57 | const idx = typeof listenerOrIndex === 'number' ? listenerOrIndex : this._listeners.indexOf(listenerOrIndex); 58 | if (idx >= 0 && idx < this._listeners.length) { 59 | const listener = this._listeners[idx]; 60 | const isLocked = this._lockedListeners.indexOf(listener) >= 0; 61 | if (isLocked) throw new Error(`Listener at index ${idx} is locked.`); 62 | this._listeners.splice(idx, 1); 63 | return true; 64 | } 65 | return false; 66 | } 67 | 68 | /** 69 | * Dispatches the given arguments by calling the given function name 70 | * on each listener, if implemented. 71 | * Note that the listener's scope will be used, when the listener's function gets called. 72 | * 73 | * @param name The function name to call. 74 | * @param args The arguments to pass to the function. 75 | */ 76 | dispatch(name: K, ...args: ArgumentTypes): void { 77 | // TODO: optimize this; cache the listeners with the given function name 78 | this._listeners.forEach(listener => { 79 | const fn = listener[name]; 80 | if (typeof fn !== 'function') return; 81 | fn.apply(listener, args); 82 | }); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/core/engine.ts: -------------------------------------------------------------------------------- 1 | import { System, SystemListener, SystemMode } from './system'; 2 | import { AbstractEntity } from './entity'; 3 | import { Dispatcher } from './dispatcher'; 4 | import { Collection } from './collection'; 5 | import { EntityCollection } from './entity.collection'; 6 | 7 | /** 8 | * System which is synced within an engine. 9 | */ 10 | type SyncedSystem = System & { 11 | /** 12 | * System listener mapping for engine specific caching purposes. 13 | */ 14 | __ecsEngineListener: SystemListener; 15 | 16 | /** 17 | * The list of listeners for this system. 18 | */ 19 | _lockedListeners: SystemListener[]; 20 | }; 21 | 22 | /** 23 | * The listener interface for a listener on an engine. 24 | */ 25 | export interface EngineListener { 26 | /** 27 | * Called as soon as the given system gets added to the engine. 28 | * 29 | * @param systems 30 | */ 31 | onAddedSystems?(...systems: System[]): void; 32 | 33 | /** 34 | * Called as soon as the given system gets removed from the engine. 35 | * 36 | * @param systems 37 | */ 38 | onRemovedSystems?(...systems: System[]): void; 39 | 40 | /** 41 | * Called as soon as all systems got cleared from the engine. 42 | */ 43 | onClearedSystems?(): void; 44 | 45 | /** 46 | * Called as soon as an error occurred on in an active system during update. 47 | * 48 | * @param error The error that occurred. 49 | * @param system The system on which the error occurred. 50 | */ 51 | onErrorBySystem?(error: Error, system: System): void; 52 | 53 | /** 54 | * Called as soon as the given entity gets added to the engine. 55 | * 56 | * @param entities 57 | */ 58 | onAddedEntities?(...entities: T[]): void; 59 | 60 | /** 61 | * Called as soon as the given entity gets removed from the engine. 62 | * 63 | * @param entities 64 | */ 65 | onRemovedEntities?(...entities: AbstractEntity[]): void; 66 | 67 | /** 68 | * Called as soon as all entities got cleared from the engine. 69 | */ 70 | onClearedEntities?(): void; 71 | } 72 | 73 | /** 74 | * Defines how an engine executes its active systems. 75 | */ 76 | export enum EngineMode { 77 | /** 78 | * Execute all systems by priority without waiting for them to resolve. 79 | */ 80 | DEFAULT = 'runDefault', 81 | 82 | /** 83 | * Execute all systems by priority. Successive systems 84 | * will wait until the current executing system resolves or rejects. 85 | */ 86 | SUCCESSIVE = 'runSuccessive', 87 | 88 | /** 89 | * Start all systems by priority, but run them all in parallel. 90 | */ 91 | PARALLEL = 'runParallel', 92 | } 93 | 94 | /** 95 | * An engine puts entities and systems together. 96 | * It holds for each type a collection, which can be queried by each system. 97 | * 98 | * The @see {Engine#update} method has to be called in order to perform updates on each system in a certain order. 99 | * The engine takes care of updating only active systems in any point of time. 100 | * 101 | */ 102 | export class Engine extends Dispatcher { 103 | /** 104 | * The internal list of all systems in this engine. 105 | */ 106 | protected _systems = new Collection(); 107 | 108 | /** 109 | * The frozen list of active systems which is used to iterate during the update. 110 | */ 111 | protected _activeSystems: System[] = []; 112 | 113 | /** 114 | * The internal list of all entities in this engine. 115 | */ 116 | protected _entities = new EntityCollection(); 117 | 118 | /** 119 | * Creates an instance of Engine. 120 | */ 121 | constructor() { 122 | super(); 123 | this._systems.addListener( 124 | { 125 | onAdded: (...systems: SyncedSystem[]) => { 126 | this._systems.sort((a, b) => a.priority - b.priority); 127 | systems.forEach(system => { 128 | system.engine = this; 129 | this.updatedActiveSystems(); 130 | 131 | const systemListener: SystemListener = { 132 | onActivated: () => this.updatedActiveSystems(), 133 | onDeactivated: () => this.updatedActiveSystems(), 134 | onError: error => this.dispatch('onErrorBySystem', error, system), 135 | }; 136 | system.__ecsEngineListener = systemListener; 137 | system.addListener(systemListener, true); 138 | }); 139 | this.dispatch('onAddedSystems', ...systems); 140 | }, 141 | onRemoved: (...systems: SyncedSystem[]) => { 142 | systems.forEach(system => { 143 | system.engine = null; 144 | this.updatedActiveSystems(); 145 | const systemListener = system.__ecsEngineListener; 146 | const locked = system._lockedListeners; 147 | locked.splice(locked.indexOf(systemListener), 1); 148 | system.removeListener(systemListener); 149 | }); 150 | this.dispatch('onRemovedSystems', ...systems); 151 | }, 152 | onCleared: () => this.dispatch('onClearedSystems'), 153 | }, 154 | true 155 | ); 156 | 157 | this._entities.addListener( 158 | { 159 | onAdded: (...entities: T[]) => this.dispatch('onAddedEntities', ...entities), 160 | onRemoved: (...entities: T[]) => this.dispatch('onRemovedEntities', ...entities), 161 | onCleared: () => this.dispatch('onClearedEntities'), 162 | }, 163 | true 164 | ); 165 | 166 | this.updatedActiveSystems(); 167 | } 168 | 169 | /** 170 | * A snapshot of all entities in this engine. 171 | */ 172 | get entities(): EntityCollection { 173 | return this._entities; 174 | } 175 | 176 | /** 177 | * A snapshot of all systems in this engine. 178 | */ 179 | get systems(): Collection { 180 | return this._systems; 181 | } 182 | 183 | /** 184 | * A snapshot of all active systems in this engine. 185 | */ 186 | get activeSystems(): readonly System[] { 187 | return this._activeSystems; 188 | } 189 | 190 | /** 191 | * Updates the internal active system list. 192 | */ 193 | protected updatedActiveSystems(): void { 194 | this._activeSystems = this.systems.filter(system => system.active); 195 | Object.freeze(this._activeSystems); 196 | } 197 | 198 | /** 199 | * Updates all systems in this engine by the given delta value. 200 | * 201 | * @param [options] 202 | * @param [mode = EngineMode.DEFAULT] 203 | */ 204 | run(options?: T, mode: EngineMode = EngineMode.DEFAULT): void | Promise { 205 | return this[mode].call(this, options); 206 | } 207 | 208 | /** 209 | * Updates all systems in this engine by the given delta value, 210 | * without waiting for a resolve or reject of each system. 211 | * 212 | * @param [options] 213 | */ 214 | protected runDefault(options?: T): void { 215 | const length = this._activeSystems.length; 216 | for (let i = 0; i < length; i++) this._activeSystems[i].run(options, SystemMode.SYNC); 217 | } 218 | 219 | /** 220 | * Updates all systems in this engine by the given delta value, 221 | * by waiting for a system to resolve or reject before continuing with the next one. 222 | * 223 | * @param [options] 224 | */ 225 | protected async runSuccessive(options?: T): Promise { 226 | const length = this._activeSystems.length; 227 | for (let i = 0; i < length; i++) await this._activeSystems[i].run(options, SystemMode.SYNC); 228 | } 229 | 230 | /** 231 | * Updates all systems in this engine by the given delta value, 232 | * by running all systems in parallel and waiting for all systems to resolve or reject. 233 | * 234 | * @param [options] 235 | */ 236 | protected async runParallel(options?: T): Promise { 237 | const mapped = this._activeSystems.map(system => system.run(options, SystemMode.ASYNC)); 238 | await Promise.all(mapped); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/core/entity.collection.spec.ts: -------------------------------------------------------------------------------- 1 | import { AbstractEntity } from './entity'; 2 | import { EntityCollection } from './entity.collection'; 3 | 4 | class Entity extends AbstractEntity {} 5 | 6 | describe('EntityCollection', () => { 7 | let collection: EntityCollection; 8 | 9 | beforeEach(() => (collection = new EntityCollection())); 10 | 11 | describe('get', () => { 12 | describe('onAdded', () => { 13 | beforeEach(() => { 14 | collection.add(new Entity(3), new Entity('1'), new Entity(null)); 15 | }); 16 | it('should return entities for previously added entities', () => { 17 | expect(collection.get(3)).toBeInstanceOf(Entity); 18 | expect(collection.get('1')).toBeInstanceOf(Entity); 19 | expect(collection.get(null)).toBeInstanceOf(Entity); 20 | 21 | expect(collection.get(3).id).toBe(3); 22 | expect(collection.get('1').id).toBe('1'); 23 | expect(collection.get(null).id).toBe(null); 24 | }); 25 | it('should not return entities which were not added', () => { 26 | expect(collection.get(1)).toBeUndefined(); 27 | expect(collection.get('3')).toBeUndefined(); 28 | expect(collection.get(undefined)).toBeUndefined(); 29 | }); 30 | }); 31 | describe('onRemoved', () => { 32 | beforeEach(() => { 33 | collection.add(new Entity(3), new Entity('1'), new Entity(null)); 34 | collection.remove(0, 2); 35 | }); 36 | it('should not return entities for previously removed entities', () => { 37 | expect(collection.get(3)).toBeUndefined(); 38 | expect(collection.get(null)).toBeUndefined(); 39 | }); 40 | it('should return entities which were not removed', () => { 41 | expect(collection.get('1')).toBeDefined(); 42 | }); 43 | }); 44 | describe('onCleared', () => { 45 | beforeEach(() => { 46 | collection.add(new Entity(3), new Entity('1'), new Entity(null)); 47 | collection.clear(); 48 | }); 49 | it('should not return any entities after clear', () => { 50 | expect(collection.get(3)).toBeUndefined(); 51 | expect(collection.get('1')).toBeUndefined(); 52 | expect(collection.get(null)).toBeUndefined(); 53 | }); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/core/entity.collection.ts: -------------------------------------------------------------------------------- 1 | import { Collection, CollectionListener } from './collection'; 2 | import { AbstractEntity } from './entity'; 3 | 4 | export class EntityCollection 5 | extends Collection 6 | implements CollectionListener 7 | { 8 | /** 9 | * Internal map for faster entity access, by id. 10 | */ 11 | protected cache = new Map(); 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | constructor(...args: any[]) { 15 | super(...args); 16 | this.addListener(this, true); 17 | } 18 | 19 | /** 20 | * Returns the entity for the given id in this collection. 21 | * 22 | * @param id The id to search for. 23 | * @return The found entity or `undefined` if not found. 24 | */ 25 | get(id: string | number): T | undefined { 26 | return this.cache.get(id); 27 | } 28 | 29 | /** 30 | * @inheritdoc 31 | */ 32 | onAdded(...entities: T[]): void { 33 | for (let i = 0, l = entities.length; i < l; i++) this.cache.set(entities[i].id, entities[i]); 34 | } 35 | 36 | /** 37 | * @inheritdoc 38 | */ 39 | onRemoved(...entities: T[]): void { 40 | for (let i = 0, l = entities.length; i < l; i++) this.cache.delete(entities[i].id); 41 | } 42 | 43 | /** 44 | * @inheritdoc 45 | */ 46 | onCleared(): void { 47 | this.cache.clear(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/core/entity.spec.ts: -------------------------------------------------------------------------------- 1 | import { AbstractEntity } from './entity'; 2 | import { Dispatcher } from './dispatcher'; 3 | import { ComponentCollection } from './component'; 4 | 5 | class MyEntity extends AbstractEntity {} 6 | 7 | describe('Entity', () => { 8 | let myEntity: AbstractEntity; 9 | beforeEach(() => (myEntity = new MyEntity('randomId'))); 10 | 11 | describe('initial', () => { 12 | it('should have an id', () => { 13 | expect(myEntity.id).toBe('randomId'); 14 | }); 15 | 16 | it('should be a dispatcher', () => { 17 | expect(myEntity instanceof Dispatcher).toBe(true); 18 | }); 19 | 20 | it('should have a component collection', () => { 21 | expect(myEntity.components instanceof ComponentCollection).toBe(true); 22 | }); 23 | 24 | it('should have no components', () => { 25 | expect(myEntity.components.length).toBe(0); 26 | }); 27 | 28 | it('should have the entity as the first listener on the components collection', () => { 29 | expect(myEntity.components.listeners.length).toBe(2); 30 | expect(myEntity.components.listeners[1]).toBe(myEntity); 31 | }); 32 | 33 | it('should throw if someone tries to remove the entity from the components listeners', () => { 34 | expect(() => myEntity.components.removeListener(myEntity)).toThrowError('Listener at index 1 is locked.'); 35 | }); 36 | }); 37 | 38 | describe('dispatch', () => { 39 | it('should dispatch onAddedComponents if a component got added', () => { 40 | let called = false; 41 | myEntity.addListener({ onAddedComponents: () => (called = true) }); 42 | myEntity.components.add({}); 43 | expect(called).toBe(true); 44 | }); 45 | 46 | it('should dispatch onRemovedComponents if a component got removed', () => { 47 | let called = false; 48 | myEntity.addListener({ onRemovedComponents: () => (called = true) }); 49 | myEntity.components.add({}); 50 | myEntity.components.remove(0); 51 | expect(called).toBe(true); 52 | }); 53 | 54 | it('should dispatch onClearedComponents if the components got cleared', () => { 55 | let called = false; 56 | myEntity.addListener({ onClearedComponents: () => (called = true) }); 57 | myEntity.components.add({}); 58 | myEntity.components.clear(); 59 | expect(called).toBe(true); 60 | }); 61 | 62 | it('should dispatch onSortedComponents if the components got sorted', () => { 63 | let called = false; 64 | myEntity.addListener({ onSortedComponents: () => (called = true) }); 65 | myEntity.components.add({}); 66 | myEntity.components.sort(); 67 | expect(called).toBe(true); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/core/entity.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentCollection } from './component'; 2 | import { Dispatcher } from './dispatcher'; 3 | import { CollectionListener } from './collection'; 4 | 5 | /** 6 | * The listener interface for a listener on an entity. 7 | */ 8 | export interface EntityListener { 9 | /** 10 | * Called as soon as a new component as been added to the entity. 11 | * 12 | * @param components The new added components. 13 | */ 14 | onAddedComponents?(...components: C[]): void; 15 | 16 | /** 17 | * Called as soon as a component got removed from the entity. 18 | * 19 | * @param components The removed components 20 | */ 21 | onRemovedComponents?(...components: C[]): void; 22 | 23 | /** 24 | * Called as soon as all components got removed from the entity. 25 | */ 26 | onClearedComponents?(): void; 27 | 28 | /** 29 | * Called as soon as the components got sorted. 30 | */ 31 | onSortedComponents?(): void; 32 | } 33 | 34 | /** 35 | * 36 | * An entity holds an id and a list of components attached to it. 37 | * You can add or remove components from the entity. 38 | */ 39 | export abstract class AbstractEntity> 40 | extends Dispatcher 41 | implements CollectionListener 42 | { 43 | /** 44 | * The internal list of components. 45 | */ 46 | protected _components: ComponentCollection; 47 | 48 | /** 49 | * Creates an instance of Entity. 50 | * 51 | * @param id The id, you should provide by yourself. Maybe an uuid or a simple number. 52 | */ 53 | constructor(public readonly id: number | string) { 54 | super(); 55 | this._components = new ComponentCollection(); 56 | this._components.addListener(this, true); 57 | } 58 | 59 | /** 60 | * A snapshot of all components of this entity. 61 | */ 62 | get components(): ComponentCollection { 63 | return this._components; 64 | } 65 | 66 | /** 67 | * Dispatches the `onAdded` event to all listeners as `onAddedComponents`. 68 | * 69 | * @param components 70 | */ 71 | onAdded(...components: C[]): void { 72 | return (>this).dispatch('onAddedComponents', ...components); 73 | } 74 | 75 | /** 76 | * Dispatches the `onRemoved` event to all listeners as `onRemovedComponents`. 77 | * 78 | * @param components 79 | * 80 | */ 81 | onRemoved(...components: C[]): void { 82 | return (>this).dispatch('onRemovedComponents', ...components); 83 | } 84 | 85 | /** 86 | * Dispatches the `onCleared` event to all listeners as `onClearedComponents`. 87 | * 88 | * 89 | */ 90 | onCleared(): void { 91 | return (>this).dispatch('onClearedComponents'); 92 | } 93 | 94 | /** 95 | * Dispatches the `onSorted` event to all listeners as `onSortedComponents`. 96 | * 97 | * 98 | */ 99 | onSorted(): void { 100 | return (>this).dispatch('onSortedComponents'); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/core/system.spec.ts: -------------------------------------------------------------------------------- 1 | import { System, SystemMode, AbstractEntitySystem, SystemListener } from './system'; 2 | import { Engine } from './engine'; 3 | import { Dispatcher } from './dispatcher'; 4 | import { AbstractEntity } from './entity'; 5 | import { Component } from './component'; 6 | import { Aspect } from './aspect'; 7 | 8 | class MySyncSystem extends System { 9 | public throw: string; 10 | 11 | process(): void { 12 | if (this.throw) throw new Error(this.throw); 13 | } 14 | } 15 | 16 | class MyAsyncSystem extends System { 17 | public throw: string; 18 | public timer = 10; 19 | 20 | async process(): Promise { 21 | if (this.throw) throw new Error(this.throw); 22 | else return new Promise(resolve => setTimeout(resolve, this.timer)); 23 | } 24 | } 25 | 26 | describe('System', () => { 27 | let system: MySyncSystem; 28 | let listener: SystemListener; 29 | 30 | beforeEach(() => { 31 | system = new MySyncSystem(); 32 | listener = { 33 | onActivated: jest.fn(), 34 | onDeactivated: jest.fn(), 35 | onAddedToEngine: jest.fn(), 36 | onRemovedFromEngine: jest.fn(), 37 | onError: jest.fn(), 38 | }; 39 | system.addListener(listener); 40 | }); 41 | 42 | describe('initial', () => { 43 | it('should be a dispatcher', () => { 44 | expect(system instanceof Dispatcher).toBe(true); 45 | }); 46 | 47 | it('should be active', () => { 48 | expect(system.active).toBe(true); 49 | }); 50 | 51 | it('should not be updating', () => { 52 | expect(system.updating).toBe(false); 53 | }); 54 | }); 55 | 56 | describe('active', () => { 57 | it('should not notify any listener if the value did not change', () => { 58 | system.active = true; 59 | expect(listener.onActivated).not.toHaveBeenCalled(); 60 | expect(listener.onDeactivated).not.toHaveBeenCalled(); 61 | }); 62 | 63 | it('should call onActivated on each listener if the value changes to "true"', () => { 64 | system.active = false; 65 | system.active = true; 66 | expect(listener.onActivated).toHaveBeenCalled(); 67 | expect(listener.onDeactivated).toHaveBeenCalledTimes(1); 68 | }); 69 | 70 | it('should call onDeactivated on each listener if the value changes to "false"', () => { 71 | system.active = false; 72 | expect(listener.onActivated).not.toHaveBeenCalled(); 73 | expect(listener.onDeactivated).toHaveBeenCalled(); 74 | }); 75 | }); 76 | 77 | describe('engine', () => { 78 | it('should not notify any listener if the value did not change', () => { 79 | const engine = system.engine; 80 | system.engine = engine; 81 | expect(listener.onAddedToEngine).not.toHaveBeenCalled(); 82 | expect(listener.onRemovedFromEngine).not.toHaveBeenCalled(); 83 | }); 84 | 85 | it('should call onAddedToEngine on each listener if the value changes', () => { 86 | system.engine = new Engine(); 87 | expect(listener.onAddedToEngine).toHaveBeenCalledWith(system.engine); 88 | expect(listener.onRemovedFromEngine).not.toHaveBeenCalled(); 89 | }); 90 | 91 | it('should not call onRemovedFromEngine on any listener if there was no engine before assigned"', () => { 92 | system.engine = new Engine(); 93 | expect(listener.onRemovedFromEngine).not.toHaveBeenCalled(); 94 | }); 95 | 96 | it('should not call onAddedToEngine on any listener if there is no new engine assigned"', () => { 97 | const before = new Engine(); 98 | system.engine = before; 99 | system.engine = null; 100 | expect(listener.onAddedToEngine).toHaveBeenLastCalledWith(before); 101 | }); 102 | 103 | it.each([new Engine(), null])( 104 | 'should call onRemovedFromEngine on each listener if the value changes to %p and an engine was assigned', 105 | newEngine => { 106 | const oldEngine = new Engine(); 107 | system.engine = oldEngine; 108 | system.engine = newEngine; 109 | expect(listener.onRemovedFromEngine).toHaveBeenCalledWith(oldEngine); 110 | } 111 | ); 112 | }); 113 | 114 | describe('run (sync)', () => { 115 | it('should call the process method with the correct delta', () => { 116 | let called = false; 117 | const dlt = 5; 118 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 119 | (system as any).process = function (delta) { 120 | called = delta; 121 | }; 122 | system.run(5); 123 | expect(called).toBe(dlt); 124 | }); 125 | 126 | it('should notify all listeners if an exception occurred', () => { 127 | system.throw = 'Error system.spec'; 128 | let called: Error = null; 129 | system.addListener({ onError: error => (called = error) }); 130 | try { 131 | system.run(0); 132 | } finally { 133 | expect(called).not.toBe(null); 134 | expect(called.message).toBe(system.throw); 135 | } 136 | }); 137 | 138 | it('should convert the result of the system into a promise, if forced', () => { 139 | expect(system.run(5, SystemMode.ASYNC) instanceof Promise).toBe(true); 140 | }); 141 | }); 142 | 143 | describe('run (async)', () => { 144 | beforeEach(() => (system = new MyAsyncSystem())); 145 | 146 | it('should turn the system into the updating state', async () => { 147 | const re = system.run(0, SystemMode.ASYNC); 148 | expect(system.updating).toBe(true); 149 | await re; 150 | }); 151 | 152 | it('should call the process method with the correct delta', async () => { 153 | let called = false; 154 | const dlt = 5; 155 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 156 | (system).process = function (delta) { 157 | called = delta; 158 | }; 159 | const re = system.run(5, SystemMode.ASYNC); 160 | await re; 161 | expect(called).toBe(dlt); 162 | }); 163 | 164 | it('should return the system from the updating state', async () => { 165 | await system.run(0, SystemMode.ASYNC); 166 | expect(system.updating).toBe(false); 167 | }); 168 | 169 | it('should return the system from the updating state if an exception occurred', async () => { 170 | system.throw = 'Error system.spec'; 171 | try { 172 | await system.run(0, SystemMode.ASYNC); 173 | } finally { 174 | expect(system.updating).toBe(false); 175 | } 176 | }); 177 | 178 | it('should notify all listeners if an exception occurred', async () => { 179 | system.throw = 'Error system.spec'; 180 | let called: Error = null; 181 | system.addListener({ onError: error => (called = error) }); 182 | try { 183 | await system.run(0, SystemMode.ASYNC); 184 | } finally { 185 | expect(called).not.toBe(null); 186 | expect(called.message).toBe(system.throw); 187 | } 188 | }); 189 | }); 190 | }); 191 | 192 | class MyEntity extends AbstractEntity {} 193 | class MyComponent1 implements Component {} 194 | class MyComponent2 implements Component {} 195 | class MyComponent3 implements Component {} 196 | class MyComponent4 implements Component {} 197 | 198 | class MyEntitySystem extends AbstractEntitySystem { 199 | entities: MyEntity[] = []; 200 | 201 | processEntity(entity: MyEntity): void { 202 | this.entities.push(entity); 203 | } 204 | 205 | getAspect(): Aspect { 206 | return this.aspect; 207 | } 208 | 209 | setEngine(engine: Engine): void { 210 | this._engine = engine; 211 | } 212 | } 213 | 214 | describe('AbstractEntitySystem', () => { 215 | it('should process each entity in the engine', () => { 216 | const engine = new Engine(); 217 | engine.entities.add(new MyEntity('1'), new MyEntity('2'), new MyEntity('3')); 218 | const system = new MyEntitySystem(); 219 | engine.systems.add(system); 220 | engine.run(); 221 | expect(system.entities.length).toBe(engine.entities.length); 222 | engine.entities.forEach((entity, i) => { 223 | expect(system.entities[i]).toBe(entity); 224 | }); 225 | }); 226 | 227 | it('should process each entity fitting the provided aspects', () => { 228 | const engine = new Engine(); 229 | engine.entities.add(new MyEntity('1'), new MyEntity('2'), new MyEntity('3')); 230 | engine.entities.elements[0].components.add(new MyComponent1(), new MyComponent2(), new MyComponent4()); 231 | engine.entities.elements[1].components.add(new MyComponent1(), new MyComponent2(), new MyComponent3()); 232 | engine.entities.elements[2].components.add(new MyComponent1(), new MyComponent2()); 233 | 234 | const system = new MyEntitySystem(0, [MyComponent1], [MyComponent3], [MyComponent4, MyComponent2]); 235 | engine.systems.add(system); 236 | engine.run(); 237 | expect(system.entities.length).toBe(2); 238 | }); 239 | 240 | it('should not do anything if onRemovedFromEngine is being called without have been added to an engine', () => { 241 | const system = new MyEntitySystem(0); 242 | expect(() => system.onRemovedFromEngine()).not.toThrow(); 243 | }); 244 | 245 | it('should detach the aspect after removing the system from the engine', () => { 246 | const engine = new Engine(); 247 | 248 | const system = new MyEntitySystem(0, [MyComponent1], [MyComponent3], [MyComponent4, MyComponent2]); 249 | engine.systems.add(system); 250 | expect(system.getAspect().isAttached).toBe(true); 251 | engine.systems.remove(system); 252 | expect(system.getAspect().isAttached).toBe(false); 253 | }); 254 | 255 | it.each([ 256 | { name: 'onAddedEntities', args: [new MyEntity('1'), new MyEntity('2'), new MyEntity('3')] }, 257 | { name: 'onRemovedEntities', args: [new MyEntity('1'), new MyEntity('2'), new MyEntity('3')] }, 258 | { name: 'onClearedEntities', args: [] }, 259 | { name: 'onSortedEntities', args: [] }, 260 | { name: 'onAddedComponents', args: [new MyEntity('1'), [new MyComponent1(), new MyComponent2()]] }, 261 | { name: 'onRemovedComponents', args: [new MyEntity('2'), [new MyComponent1(), new MyComponent2()]] }, 262 | { name: 'onClearedComponents', args: [new MyEntity('3')] }, 263 | { name: 'onSortedComponents', args: [new MyEntity('4')] }, 264 | ])('should call $name with $args when dispatching via the aspect', ({ name, args }) => { 265 | const system = new MyEntitySystem(); 266 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 267 | const spy = jest.spyOn(system, name as any); 268 | system.engine = new Engine(); 269 | const aspect = system.getAspect(); 270 | 271 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 272 | aspect.dispatch(name as any, ...args); 273 | 274 | expect(spy).toHaveBeenCalledWith(...args); 275 | }); 276 | 277 | it.each(['engine', 'engine-no-aspect', 'aspect'])( 278 | 'should not process entities, if there are no %s entities', 279 | type => { 280 | const system = new MyEntitySystem(0); 281 | if (type === 'aspect') system.engine = new Engine(); 282 | else if (type === 'engine-no-aspect') system.setEngine(new Engine()); 283 | const spy = jest.spyOn(system, 'processEntity'); 284 | 285 | system.process(); 286 | 287 | expect(spy).not.toHaveBeenCalled(); 288 | } 289 | ); 290 | }); 291 | -------------------------------------------------------------------------------- /src/core/system.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | import { Engine } from './engine'; 3 | import { Dispatcher } from './dispatcher'; 4 | import { AbstractEntity } from './entity'; 5 | import { ComponentClass } from './types'; 6 | import { Component } from './component'; 7 | import { Aspect, AspectListener } from './aspect'; 8 | 9 | /** 10 | * The listener interface for a listener added to a system. 11 | */ 12 | export interface SystemListener { 13 | /** 14 | * Called as soon as the `active` switched to `true`. 15 | */ 16 | onActivated?(): void; 17 | 18 | /** 19 | * Called as soon as the `active` switched to `false`. 20 | */ 21 | onDeactivated?(): void; 22 | 23 | /** 24 | * Called as soon as the system got removed from an engine. 25 | * 26 | * @param engine The engine this system got removed from. 27 | */ 28 | onRemovedFromEngine?(engine: Engine): void; 29 | 30 | /** 31 | * Called as soon as the system got added to an engine. 32 | * Note that this will be called after @see {SystemListener#onRemovedFromEngine}. 33 | * 34 | * @param engine The engine this system got added to. 35 | */ 36 | onAddedToEngine?(engine: Engine): void; 37 | 38 | /** 39 | * Called as soon an error occurred during update. 40 | * 41 | * @param error The error which occurred. 42 | */ 43 | onError?(error: Error): void; 44 | } 45 | 46 | /** 47 | * Defines how a system executes its task. 48 | * 49 | * @enum {number} 50 | */ 51 | export enum SystemMode { 52 | /** 53 | * Do work and resolve immediately. 54 | */ 55 | SYNC = 'runSync', 56 | 57 | /** 58 | * Do async work. E.g. do work in a worker, make requests to another server, etc. 59 | */ 60 | ASYNC = 'runAsync', 61 | } 62 | 63 | /** 64 | * A system processes a list of entities which belong to an engine. 65 | * Entities can only be accessed via the assigned engine. @see {Engine}. 66 | * The implementation of the specific system has to choose on which components of an entity to operate. 67 | * 68 | */ 69 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 70 | export abstract class System extends Dispatcher { 71 | /** 72 | * Determines whether this system is active or not. 73 | * 74 | */ 75 | protected _active: boolean; 76 | 77 | /** 78 | * Determines whether this system is currently updating or not. 79 | * 80 | */ 81 | protected _updating: boolean; 82 | 83 | /** 84 | * The reference to the current engine. 85 | * 86 | * @memberof System 87 | */ 88 | protected _engine: Engine | null; 89 | 90 | /** 91 | * Creates an instance of System. 92 | * 93 | * @param [priority=0] The priority of this system. The lower the value the earlier it will process. 94 | */ 95 | constructor(public priority: number = 0) { 96 | super(); 97 | this._active = true; 98 | this._updating = false; 99 | this._engine = null; 100 | } 101 | 102 | /** 103 | * The active state of this system. 104 | * If the flag is set to `false`, this system will not be able to process. 105 | * 106 | */ 107 | get active(): boolean { 108 | return this._active; 109 | } 110 | 111 | set active(active: boolean) { 112 | if (active === this._active) return; 113 | this._active = active; 114 | if (active) { 115 | this.onActivated(); 116 | } else { 117 | this.onDeactivated(); 118 | } 119 | (>this).dispatch(active ? 'onActivated' : 'onDeactivated'); 120 | } 121 | 122 | /** 123 | * The engine this system is assigned to. 124 | * 125 | */ 126 | get engine(): Engine | null { 127 | return this._engine; 128 | } 129 | 130 | set engine(engine: Engine | null) { 131 | if (engine === this._engine) return; 132 | const oldEngine = this._engine; 133 | this._engine = engine; 134 | if (oldEngine instanceof Engine) { 135 | this.onRemovedFromEngine(oldEngine); 136 | (>this).dispatch('onRemovedFromEngine', oldEngine); 137 | } 138 | if (engine instanceof Engine) { 139 | this.onAddedToEngine(engine); 140 | (>this).dispatch('onAddedToEngine', engine); 141 | } 142 | } 143 | 144 | /** 145 | * Determines whether this system is currently updating or not. 146 | * The value will stay `true` until @see {System#process} resolves or rejects. 147 | * 148 | * @readonly 149 | */ 150 | get updating(): boolean { 151 | return this._updating; 152 | } 153 | 154 | /** 155 | * Runs the system process with the given delta time. 156 | * 157 | * @param options 158 | * @param mode The system mode to run in. 159 | * 160 | */ 161 | run(options: T, mode: SystemMode = SystemMode.SYNC): void | Promise { 162 | return this[mode].call(this, options); 163 | } 164 | 165 | /** 166 | * Processes data synchronously. 167 | * 168 | * @param options 169 | * 170 | */ 171 | protected runSync(options: T): void { 172 | try { 173 | this.process(options); 174 | } catch (e) { 175 | this.onError(e as Error); 176 | (>this).dispatch('onError', e as Error); 177 | } 178 | } 179 | 180 | /** 181 | * Processes data asynchronously. 182 | * 183 | * @param options 184 | * 185 | */ 186 | protected async runAsync(options: T): Promise { 187 | this._updating = true; 188 | try { 189 | await this.process(options); 190 | } catch (e) { 191 | this.onError(e as Error); 192 | (>this).dispatch('onError', e as Error); 193 | } finally { 194 | this._updating = false; 195 | } 196 | } 197 | 198 | /** 199 | * Processes the entities of the current engine. 200 | * To be implemented by any concrete system. 201 | * 202 | * @param options 203 | * 204 | */ 205 | abstract process(options: T): void | Promise; 206 | 207 | /** 208 | * Called as soon as the `active` switched to `true`. 209 | * 210 | * 211 | */ 212 | onActivated(): void { 213 | /* NOOP */ 214 | } 215 | 216 | /** 217 | * Called as soon as the `active` switched to `false`. 218 | * 219 | * 220 | */ 221 | onDeactivated(): void { 222 | /* NOOP */ 223 | } 224 | 225 | /** 226 | * Called as soon as the system got removed from an engine. 227 | * 228 | * @param engine The engine this system got added to. 229 | * 230 | * 231 | */ 232 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 233 | onRemovedFromEngine(engine: Engine): void { 234 | /* NOOP */ 235 | } 236 | 237 | /** 238 | * Called as soon as the system got added to an engine. 239 | * Note that this will be called after @see {SystemListener#onRemovedFromEngine}. 240 | * 241 | * @param engine The engine this system got added to. 242 | * 243 | * 244 | */ 245 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 246 | onAddedToEngine(engine: Engine): void { 247 | /* NOOP */ 248 | } 249 | 250 | /** 251 | * Called as soon an error occurred during update. 252 | * 253 | * @param error The error which occurred. 254 | * 255 | * 256 | */ 257 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 258 | onError(error: Error): void { 259 | /* NOOP */ 260 | } 261 | } 262 | 263 | type CompType = ComponentClass | Component; 264 | 265 | /** 266 | * An abstract entity system is a system which processes each entity. 267 | * 268 | * Optionally it accepts component types for auto filtering the entities before processing. 269 | * This class abstracts away the initialization of aspects and detaches them properly, if needed. 270 | * 271 | */ 272 | export abstract class AbstractEntitySystem 273 | extends System 274 | implements AspectListener 275 | { 276 | /** 277 | * The optional aspect, if any. 278 | * 279 | */ 280 | protected aspect: Aspect | null = null; 281 | 282 | /** 283 | * Creates an instance of AbstractEntitySystem. 284 | * 285 | * @param priority The priority of this system. The lower the value the earlier it will process. 286 | * @param all Optional component types which should all match. 287 | * @param exclude Optional component types which should not match. 288 | * @param one Optional component types of which at least one should match. 289 | */ 290 | constructor( 291 | public priority = 0, 292 | protected all?: CompType[], 293 | protected exclude?: CompType[], 294 | protected one?: CompType[] 295 | ) { 296 | super(priority); 297 | } 298 | 299 | /** @inheritdoc */ 300 | onAddedToEngine(engine: Engine): void { 301 | this.aspect = Aspect.for(engine, this.all, this.exclude, this.one); 302 | this.aspect.addListener(this); 303 | } 304 | 305 | /** @inheritdoc */ 306 | onRemovedFromEngine(): void { 307 | if (!this.aspect) return; 308 | this.aspect.removeListener(this); 309 | this.aspect.detach(); 310 | } 311 | 312 | /** 313 | * Called if new entities got added to the system. 314 | * 315 | * @param entities 316 | * 317 | */ 318 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 319 | onAddedEntities(...entities: AbstractEntity[]): void {} 320 | 321 | /** 322 | * Called if existing entities got removed from the system. 323 | * 324 | * @param entities 325 | * 326 | */ 327 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 328 | onRemovedEntities?(...entities: AbstractEntity[]): void {} 329 | 330 | /** 331 | * Called if the entities got cleared. 332 | * 333 | * 334 | */ 335 | onClearedEntities?(): void {} 336 | 337 | /** 338 | * Called if the entities got sorted. 339 | * 340 | * 341 | */ 342 | onSortedEntities?(): void {} 343 | 344 | /** 345 | * Gets called if new components got added to the given entity. 346 | * 347 | * @param entity 348 | * @param components 349 | * 350 | */ 351 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 352 | onAddedComponents?(entity: AbstractEntity, ...components: Component[]): void {} 353 | 354 | /** 355 | * Gets called if components got removed from the given entity. 356 | * 357 | * @param entity 358 | * @param components 359 | * 360 | */ 361 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 362 | onRemovedComponents?(entity: AbstractEntity, ...components: Component[]): void {} 363 | 364 | /** 365 | * Gets called if the components of the given entity got cleared. 366 | * 367 | * @param entity 368 | * 369 | */ 370 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 371 | onClearedComponents?(entity: AbstractEntity): void {} 372 | 373 | /** 374 | * Gets called if the components of the given entity got sorted. 375 | * 376 | * @param entity 377 | * 378 | */ 379 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 380 | onSortedComponents?(entity: AbstractEntity): void {} 381 | 382 | /** @inheritdoc */ 383 | process(options?: U): void { 384 | const entities = this.aspect ? this.aspect.entities : this._engine?.entities.elements; 385 | if (!entities?.length) return; 386 | for (let i = 0, l = entities.length; i < l; i++) { 387 | this.processEntity(entities[i], i, entities, options); 388 | } 389 | } 390 | 391 | /** 392 | * Processes the given entity. 393 | * 394 | * @param entity 395 | * @param index 396 | * @param entities 397 | * @param options 398 | * 399 | */ 400 | abstract processEntity(entity: T, index?: number, entities?: T[], options?: U): void; 401 | } 402 | -------------------------------------------------------------------------------- /src/core/types.ts: -------------------------------------------------------------------------------- 1 | import { Component } from './component'; 2 | 3 | /** 4 | * A type for the arguments of `F`. 5 | */ 6 | export type ArgumentTypes = F extends (...args: infer A) => unknown ? A : never; 7 | 8 | /** 9 | * Class definition for type `T`. 10 | */ 11 | export interface Class extends Function { 12 | new (...args: never[]): T; 13 | } 14 | 15 | /** 16 | * Class definition for a component type `T`. 17 | */ 18 | export interface ComponentClass extends Class { 19 | /** 20 | * The static id of the component. 21 | * 22 | */ 23 | readonly id?: string; 24 | 25 | /** 26 | * The static type of the component. 27 | * 28 | */ 29 | readonly type?: string; 30 | } 31 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core/collection'; 2 | export * from './core/component'; 3 | export * from './core/dispatcher'; 4 | export * from './core/engine'; 5 | export * from './core/entity'; 6 | export * from './core/entity.collection'; 7 | export * from './core/aspect'; 8 | export * from './core/system'; 9 | export * from './core/types'; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "target": "es5", 5 | "outDir": "./build", 6 | "baseUrl": "./src", 7 | "rootDir": "./src", 8 | "skipLibCheck": true, 9 | "strict": true, 10 | "alwaysStrict": true, 11 | "lib": ["ES2015"], 12 | "downlevelIteration": true 13 | }, 14 | "exclude": ["**/*.spec.ts", "**/build/*", "**/examples/*"] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "outDir": "./build", 7 | "baseUrl": "./src", 8 | "rootDir": "./src", 9 | "skipLibCheck": true 10 | }, 11 | "exclude": ["**/*.spec.ts", "**/build/*", "**/examples/*"] 12 | } 13 | --------------------------------------------------------------------------------