├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── example ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── circle_05.png │ ├── envmap │ │ ├── bridge │ │ │ ├── nx.jpg │ │ │ ├── ny.jpg │ │ │ ├── nz.jpg │ │ │ ├── px.jpg │ │ │ ├── py.jpg │ │ │ └── pz.jpg │ │ ├── lake │ │ │ ├── nx.jpg │ │ │ ├── ny.jpg │ │ │ ├── nz.jpg │ │ │ ├── px.jpg │ │ │ ├── py.jpg │ │ │ ├── pz.jpg │ │ │ ├── readme.txt │ │ │ └── sor_lake1.shader │ │ └── miramar │ │ │ ├── README.TXT │ │ │ ├── miramar_bk.tga │ │ │ ├── miramar_dn.tga │ │ │ ├── miramar_ft.tga │ │ │ ├── miramar_lf.tga │ │ │ ├── miramar_rt.tga │ │ │ ├── miramar_up.tga │ │ │ ├── nx.jpg │ │ │ ├── ny.jpg │ │ │ ├── nz.jpg │ │ │ ├── px.jpg │ │ │ ├── py.jpg │ │ │ └── pz.jpg │ └── index.html ├── src │ ├── Constants.ts │ ├── component │ │ ├── BoxComponent.ts │ │ ├── ColorComponent.ts │ │ ├── Object3DComponent.ts │ │ ├── ParticleComponent.ts │ │ ├── PongComponent.ts │ │ └── SphereComponent.ts │ ├── entity │ │ ├── AnimatedEntity.ts │ │ ├── CubeEntity.ts │ │ └── SphereEntity.ts │ ├── main.tsx │ ├── pages │ │ ├── IndexPage.tsx │ │ ├── KeyboardPage.tsx │ │ └── TimescalePage.tsx │ ├── system │ │ ├── CubeFactorySystem.ts │ │ ├── KeyboardSystem.ts │ │ ├── LogSystem.ts │ │ ├── NPCSystem.ts │ │ ├── ParticleSystem.ts │ │ ├── PongSystem.ts │ │ ├── SceneObjectSystem.ts │ │ └── SphereFactorySystem.ts │ └── utils │ │ ├── GUISession.ts │ │ ├── KeyboardState.ts │ │ └── cubeEnv.ts ├── tsconfig.json └── webpack.config.js ├── logo.jpg ├── package-lock.json ├── package.json ├── publish.js ├── repository-open-graph.png ├── repository-open-graph.psd ├── src └── index.ts ├── test ├── ECS.test.d.ts ├── ECS.test.ts ├── Iterator.test.d.ts ├── Iterator.test.ts ├── mocha.opts └── setup.js ├── tsconfig.json ├── v-release.js └── v-snapshot.js /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea 3 | 4 | index.js 5 | index.d.ts 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | .env.test 80 | 81 | # parcel-bundler cache (https://parceljs.org/) 82 | .cache 83 | 84 | # Next.js build output 85 | .next 86 | 87 | # Nuxt.js build / generate output 88 | .nuxt 89 | dist 90 | 91 | # Gatsby files 92 | .cache/ 93 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 94 | # https://nextjs.org/blog/next-9-1#public-directory-support 95 | # public 96 | 97 | # vuepress build output 98 | .vuepress/dist 99 | 100 | # Serverless directories 101 | .serverless/ 102 | 103 | # FuseBox cache 104 | .fusebox/ 105 | 106 | # DynamoDB Local files 107 | .dynamodb/ 108 | 109 | # TernJS port file 110 | .tern-port 111 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6.0" 4 | after_success: npm run coverage 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alex Rodin 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 |
2 | 3 | npm package 4 | 5 | 6 | Build Status 7 | 8 | 9 | Coverage Status 10 | 11 |
12 | 13 |
14 | 15 | **ecs-lib** is a tiny and easy to use [ECS _(Entity Component System)_](https://en.wikipedia.org/wiki/Entity_component_system) library for game programming. It's written in Typescript but you can use on node.js and web browser too. 16 | 17 | 18 | **TLDR;** Take a look at the [example](https://nidorx.github.io/ecs-lib/) and its [source code](https://github.com/nidorx/ecs-lib/tree/master/example) 19 | 20 | 21 | ```bash 22 | npm install --save ecs-lib 23 | ``` 24 | 25 | 26 | ## Table of contents 27 | * [Documentation](#documentation) 28 | * [World](#world) 29 | * [Component](#component) 30 | * [Raw data access](#raw-data-access) 31 | * [Secondary attributes](#secondary-attributes) 32 | * [Entity](#entity) 33 | * [Adding and removing from the world](#adding-and-removing-from-the-world) 34 | * [Adding and removing components](#adding-and-removing-components) 35 | * [Subscribing to changes](#subscribing-to-changes) 36 | * [Accessing components](#accessing-components) 37 | * [System](#system) 38 | * [Adding and removing from the world](#adding-and-removing-from-the-world-1) 39 | * [Limiting frequency (FPS)](#limiting-frequency-fps) 40 | * [Time Scaling - Slow motion effect](#time-scaling---slow-motion-effect) 41 | * [Pausing](#pausing) 42 | * [Global systems - all entities](#global-systems---all-entities) 43 | * [Before and After update](#before-and-after-update) 44 | * [Enter - When adding new entities](#enter---when-adding-new-entities) 45 | * [Change - When you add or remove components](#change---when-you-add-or-remove-components) 46 | * [Exit - When removing entities](#exit---when-removing-entities) 47 | * [API](#api) 48 | * [ECS](#ecs) 49 | * [Component](#component-1) 50 | * [Component<T>](#componentt) 51 | * [Entity](#entity-1) 52 | * [System](#system-1) 53 | * [Feedback, Requests and Roadmap](#feedback-requests-and-roadmap) 54 | * [Contributing](#contributing) 55 | * [Translating and documenting](#translating-and-documenting) 56 | * [Reporting Issues](#reporting-issues) 57 | * [Fixing defects and adding improvements](#fixing-defects-and-adding-improvements) 58 | * [License](#license) 59 | 60 | 61 | ## Documentation 62 | 63 | Entity-Component-System (ECS) is a distributed and compositional architectural design pattern that is mostly used in game development. It enables flexible decoupling of domain-specific behaviour, which overcomes many of the drawbacks of traditional object-oriented inheritance. 64 | 65 | For further details: 66 | 67 | - [Entity Systems Wiki](http://entity-systems.wikidot.com/) 68 | - [Evolve Your Hierarchy](http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/) 69 | - [ECS on Wikipedia](https://en.wikipedia.org/wiki/Entity_component_system) 70 | - [Entity Component Systems in Elixir](https://yos.io/2016/09/17/entity-component-systems/) 71 | 72 | ### World 73 | 74 | A ECS instance is used to describe you game world or **Entity System** if you will. The World is a container for Entities, Components, and Systems. 75 | 76 | ```typescript 77 | import ECS from "ecs-lib"; 78 | 79 | const world = new ECS(); 80 | ``` 81 | 82 | 83 | ### Component 84 | 85 | Represents the different facets of an entity, such as position, velocity, geometry, physics, and hit points for example. Components store only raw data for one aspect of the object, and how it interacts with the world. 86 | 87 | In other words, the component labels the entity as having this particular aspect. 88 | 89 | 90 | ```typescript 91 | import {Component} from "ecs-lib"; 92 | 93 | export type Box = { 94 | width: number; 95 | height: number; 96 | depth: number; 97 | } 98 | 99 | export const BoxComponent = Component.register(); 100 | ``` 101 | 102 | The register method generates a new class that represents this type of component, which has a unique identifier. You also have access to the type id from the created instances. 103 | 104 | ```typescript 105 | const boxCmp = new BoxComponent({ width:10, height:10, depth:10 }); 106 | 107 | // prints true, in this case type = 1 108 | console.log(BoxComponent.type === boxCmp.type); 109 | ``` 110 | 111 | > You can also have access to the `Component` class from ECS (`ECS.Component.register`) 112 | 113 | 114 | #### Raw data access 115 | 116 | Component instance displays raw data by property `data` 117 | 118 | ```typescript 119 | boxCmp.data.width = 33; 120 | console.log(boxCmp.data.width); 121 | ``` 122 | 123 | #### Secondary attributes 124 | A component can have attributes. Attributes are secondary values used to save miscellaneous data required by some specialized systems. 125 | 126 | ```typescript 127 | boxCmp.attr.specializedSystemMetadata = 33; 128 | console.log(boxCmp.attr.specializedSystemMetadata); 129 | ``` 130 | 131 | ### Entity 132 | 133 | The entity is a general purpose object. An entity is what you use to describe an object in your game. e.g. a player, a gun, etc. It consists only of a unique ID and the list of components that make up this entity. 134 | 135 | ```typescript 136 | import {Entity} from "ecs-lib"; 137 | import {ColorComponent} from "../component/ColorComponent"; 138 | import {Box, BoxComponent} from "../component/BoxComponent"; 139 | 140 | export default class CubeEntity extends Entity { 141 | 142 | constructor(cube: Box, color: string) { 143 | super(); 144 | 145 | this.add(new BoxComponent(cube)); 146 | this.add(new ColorComponent(color)); 147 | } 148 | } 149 | ``` 150 | 151 | > You can also have access to the `Entity` class from ECS (`ECS.Entity`) 152 | 153 | 154 | #### Adding and removing from the world 155 | 156 | You can add multiple instances of the same entity in the world. Each entity is given a **unique identifier** at creation time. 157 | 158 | ```typescript 159 | const cubeEnt = new CubeEntity({ 160 | width: 10, 161 | height: 10, 162 | depth: 10 163 | }, '#FF0000'); 164 | 165 | console.log(cubeEnt, cubeEnt.id); 166 | 167 | world.addEntity(cubeEnt); 168 | 169 | world.removeEntity(cubeEnt); 170 | world.removeEntity(cubeEnt.id); 171 | ``` 172 | 173 | #### Adding and removing components 174 | 175 | At any point in the entity's life cycle, you can add or remove components, using `add` and `remove` methods. 176 | 177 | ```typescript 178 | cubeEnt.add(boxCmp); 179 | cubeEnt.remove(boxCmp); 180 | ``` 181 | 182 | **ecs-lib** entities can have more than one component per type, it is up to the programmer to control the addition and removal of entity components. 183 | 184 | ```typescript 185 | cubeEnt.add(new BoxComponent({ width:10, height:10, depth:10 })); 186 | cubeEnt.add(new BoxComponent({ width:20, height:20, depth:20 })); 187 | ``` 188 | 189 | #### Subscribing to changes 190 | 191 | In **ecs-lib** you can be informed when a component is added or removed from an entity by simply subscribing to the entity. 192 | 193 | To unsubscribe, simply invoke the function you received at the time of subscription. 194 | 195 | ```typescript 196 | const cancel = cubeEnt.subscribe((entity)=>{ 197 | console.log(entity === cubeEnt); 198 | }); 199 | 200 | cancel(); 201 | ``` 202 | 203 | #### Accessing components 204 | 205 | To gain access to the components of an entity, simply use the `allFrom` and `oneFrom` methods of the `Component` class to get all or the first instance of this component respectively. 206 | 207 | ```typescript 208 | BoxComponent.allFrom(cubeEnt) 209 | .forEach((boxCmp)=>{ 210 | console.log(boxCmp.data.height); 211 | }); 212 | 213 | const boxCmp = BoxComponent.oneFrom(cubeEnt); 214 | console.log(boxCmp.data.height); 215 | ``` 216 | 217 | 218 | ### System 219 | 220 | Represents the logic that transforms component data of an entity from its current state to its next state. A system runs on entities that have a specific set of component types. 221 | 222 | Each system runs continuously (as if each system had its own thread). 223 | 224 | In **ecs-lib**, a system has a strong connection with component types. You must define which components this system works on in the `System` abstract class constructor. 225 | 226 | If the `update` method is implemented, it will be invoked for every update in the world. Whenever an entity with the characteristics expected by this system is added or removed on world, or it components is changed, the system is informed via the `enter`, `change` and `exit` methods. 227 | 228 | ```typescript 229 | import {Entity, System} from "ecs-lib"; 230 | import KeyboardState from "../utils/KeyboardState"; 231 | import {BoxComponent} from "../component/BoxComponent"; 232 | import {Object3DComponent} from "../component/Object3DComponent"; 233 | 234 | export default class KeyboardSystem extends System { 235 | 236 | constructor() { 237 | super([ 238 | Object3DComponent.type, 239 | BoxComponent.type 240 | ]); 241 | } 242 | 243 | update(time: number, delta: number, entity: Entity): void { 244 | let object3D = Object3DComponent.oneFrom(entity).data; 245 | if (KeyboardState.pressed("right")) { 246 | object3D.translateX(0.3); 247 | } else if (KeyboardState.pressed("left")) { 248 | object3D.translateX(-0.3); 249 | } else if (KeyboardState.pressed("up")) { 250 | object3D.translateZ(-0.3); 251 | } else if (KeyboardState.pressed("down")) { 252 | object3D.translateZ(0.3); 253 | } 254 | } 255 | } 256 | ``` 257 | 258 | #### Adding and removing from the world 259 | 260 | To add or remove a system to the world, simply use the `addSystem` and `removeSystem` methods. 261 | 262 | ```typescript 263 | const keyboardSys = new KeyboardSystem(); 264 | world.addSystem(keyboardSys); 265 | world.removeSystem(keyboardSys); 266 | ``` 267 | 268 | #### Limiting frequency (FPS) 269 | 270 | It is possible to limit the maximum number of invocations that the `update` method can perform per second (FPS) by simply entering the `frequency` parameter in the class constructor. This control is useful for example to limit the processing of physics systems to a specific frequency in order to decrease the processing cost. 271 | 272 | ```typescript 273 | export default class PhysicsSystem extends System { 274 | 275 | constructor() { 276 | super([ 277 | Object3DComponent.type, 278 | VelocityComponent.type, 279 | PositionComponent.type, 280 | DirectionComponent.type 281 | ], 25); // <- LIMIT FPS 282 | } 283 | 284 | // Will run at 25 FPS 285 | update(time: number, delta: number, entity: Entity): void { 286 | //... physics stuff 287 | } 288 | } 289 | ``` 290 | 291 | #### Time Scaling - Slow motion effect 292 | 293 | A very interesting feature in **ecs-lib** is the TIMESCALE. This allows you to change the rate that time passes in the game, also known as the timescale. You can set the timescale by changing the `timeScale` property of the world. 294 | 295 | A time scale of 1 means normal speed. 0.5 means half the speed and 2.0 means twice the speed. If you set the game's timescale to 0.1, it will be ten times slower but still smooth - a good slow motion effect! 296 | 297 | The timescale works by changing the value returned in the `time` and `delta` properties of the system update method. This means that the behaviors are affected and any movement using delta. If you do not use delta in your motion calculations, motion will not be affected by the timescale! Therefore, to use the timescale, simply use the delta correctly in all movements. 298 | 299 | > **ATTENTION!** The systems continue to be invoked obeying their normal frequencies, what changes is only the values received in the time and delta parameters. 300 | 301 | ```typescript 302 | export default class PhysicsSystem extends System { 303 | 304 | constructor() { 305 | super([ 306 | Object3DComponent.type, 307 | VelocityComponent.type 308 | ], 25); 309 | } 310 | 311 | update(time: number, delta: number, entity: Entity): void { 312 | let object = Object3DComponent.oneFrom(entity).data; 313 | let velocity = VelocityComponent.oneFrom(entity).data; 314 | object.position.y += velocity.y * delta; 315 | } 316 | } 317 | 318 | world.timeScale = 1; // Normal speed 319 | world.timeScale = 0.5; // Slow motion 320 | ``` 321 | 322 | ##### Pausing 323 | 324 | You can set the timescale to 0. This stops all movement. It is an easy way to pause the game. Go back to 1 and the game will resume. 325 | 326 | You may find that you can still do things like shoot using the game controls. You can get around this by placing your main game events in a group and activating / deactivating that group while pausing and not pausing. 327 | 328 | It's also a good way to test if you used delta correctly. If you used it correctly, setting the timescale to 0 will stop everything in the game. If you have not used it correctly, some objects may keep moving even if the game should be paused! In this case, you can check how these objects are moved and make sure you are using delta correctly. 329 | 330 | #### Global systems - all entities 331 | 332 | You can also create systems that receive updates from all entities, regardless of existing components. To do this, simply enter `[-1]` in the system builder. This functionality may be useful for debugging and other rating mechanisms for your game. 333 | 334 | ```typescript 335 | import {Entity, System} from "ecs-lib"; 336 | 337 | export default class LogSystem extends System { 338 | 339 | constructor() { 340 | super([-1], 0.5); // Logs all entities every 2 seconds (0.5 FPS) 341 | } 342 | 343 | update(time: number, delta: number, entity: Entity): void { 344 | console.log(entity); 345 | } 346 | } 347 | ``` 348 | 349 | #### Before and After update 350 | 351 | If necessary, the system can be informed before and after executing the update of its entities in this interaction (respecting the execution frequency defined for that system). 352 | 353 | ```typescript 354 | import {Entity, System} from "ecs-lib"; 355 | 356 | export default class LogSystem extends System { 357 | 358 | constructor() { 359 | super([-1], 0.5); // Logs all entities every 2 seconds (0.5 FPS) 360 | } 361 | 362 | beforeUpdateAll(time: number): void { 363 | console.log('Before update'); 364 | } 365 | 366 | update(time: number, delta: number, entity: Entity): void { 367 | console.log(entity); 368 | } 369 | 370 | afterUpdateAll(time: number, entities: Entity[]): void { 371 | console.log('After update'); 372 | } 373 | } 374 | ``` 375 | 376 | 377 | #### Enter - When adding new entities 378 | 379 | Invoked when: 380 | 381 | 1. An entity with the characteristics (components) expected by this system is added in the world; 382 | 2. This system is added in the world and this world has one or more entities with the characteristics expected by this system; 383 | 3. An existing entity in the same world receives a new component at runtime and all of its new components match the standard expected by this system. 384 | 385 | It can be used for initialization of new components in this entity, or even registration of this entity in a more complex management system. 386 | 387 | ```typescript 388 | import {Entity, System} from "ecs-lib"; 389 | import {BoxGeometry, Mesh, MeshBasicMaterial} from "three"; 390 | import {BoxComponent} from "../component/BoxComponent"; 391 | import {ColorComponent} from "../component/ColorComponent"; 392 | import {Object3DComponent} from "../component/Object3DComponent"; 393 | 394 | export default class CubeFactorySystem extends System { 395 | 396 | constructor() { 397 | super([ 398 | ColorComponent.type, 399 | BoxComponent.type 400 | ]); 401 | } 402 | 403 | enter(entity: Entity): void { 404 | let object = Object3DComponent.oneFrom(entity); 405 | if (!object) { 406 | const box = BoxComponent.oneFrom(entity).data; 407 | const color = ColorComponent.oneFrom(entity).data; 408 | 409 | const geometry = new BoxGeometry(box.width, box.height, box.depth); 410 | const material = new MeshBasicMaterial({color: color}); 411 | const cube = new Mesh(geometry, material); 412 | 413 | // Append new component to entity 414 | entity.add(new Object3DComponent(cube)); 415 | } 416 | } 417 | } 418 | ``` 419 | 420 | #### Change - When you add or remove components 421 | 422 | A system can also be informed when adding or removing components of an entity by simply implementing the "change" method. 423 | 424 | ```typescript 425 | import {Entity, System, Component} from "ecs-lib"; 426 | 427 | export default class LogSystem extends System { 428 | 429 | constructor() { 430 | super([-1], 0.5); // Logs all entities every 2 seconds (0.5 FPS) 431 | } 432 | 433 | change(entity: Entity, added?: Component, removed?: Component): void { 434 | console.log(entity, added, removed); 435 | } 436 | } 437 | ``` 438 | 439 | 440 | #### Exit - When removing entities 441 | 442 | Invoked when: 443 | 444 | 1. An entity with the characteristics (components) expected by this system is removed from the world; 445 | 2. This system is removed from the world and this world has one or more entities with the characteristics expected by this system; 446 | 3. An existing entity in the same world loses a component at runtime and its new component set no longer matches the standard expected by this system; 447 | 448 | Can be used to clean memory and references. 449 | 450 | ```typescript 451 | import {Scene} from "three"; 452 | import {Entity, System} from "ecs-lib"; 453 | import {Object3DComponent} from "../component/Object3DComponent"; 454 | 455 | export default class SceneObjectSystem extends System { 456 | 457 | private scene: Scene; 458 | 459 | constructor(scene: Scene) { 460 | super([ 461 | Object3DComponent.type 462 | ]); 463 | 464 | this.scene = scene; 465 | } 466 | 467 | exit(entity: Entity): void { 468 | let model = Object3DComponent.oneFrom(entity); 469 | this.scene.remove(model.data); 470 | } 471 | } 472 | ``` 473 | 474 | ## API 475 | 476 | | name | type | description | 477 | |---|---- |:---- | 478 | |

ECS

| 479 | | `System` | `System` | _`static`_ reference to `System` class. _(`ECS.System`)_ | 480 | | `Entity` | `Entity` | _`static`_ reference to `Entity` class. _(`ECS.Entity`)_ | 481 | | `Component` | `Component` | _`static`_ reference to `Component` class. _(`ECS.Component`)_ | 482 | | `constructor` | `(systems?: System[])` | | 483 | | `getEntity(id: number)` | Entity | undefined | Get an entity by id | 484 | | `addEntity(entity: Entity)` | | Add an entity to this world | 485 | | removeEntity(entity: number | Entity) | | Remove an entity from this world | 486 | | `addSystem(system: System)` | | Add a system in this world | 487 | | `removeSystem(system: System)` | | Remove a system from this world | 488 | | `update()` | | Invokes the `update` method of the systems in this world. | 489 | |

Component

| 490 | | `register()` | `Class>` | _`static`_ Register a new component class | 491 | |

Component<T>

| 492 | | `type` | `number` | _`static`_ reference to type id | 493 | | `allFrom(entity: Entity)` | `Component[]` | _`static`_ Get all instances of this component from entity | 494 | | `oneFrom(entity: Entity)` | `Component` | _`static`_ Get one instance of this component from entity | 495 | | `constructor` | `(data: T)` | Create a new instance of this custom component | 496 | | `type` | `number` | reference to type id from instance | 497 | | `data` | `T` | reference to raw data from instance | 498 | |

Entity

| 499 | | `id` | `number` | Instance unique id | 500 | | `active` | `boolean` | Informs if the entity is active | 501 | | `add(component: Component)` | | Add a component to this entity | 502 | | `remove(component: Component)` | | Removes a component's reference from this entity | 503 | | `subscribe(handler: Susbcription)` | `cancel = () => Entity` | Allows interested parties to receive information when this entity's component list is updated
`Susbcription = (entity: Entity, added: Component[], removed: Component[]) => void` | 504 | |

System

| 505 | | `constructor` | `(components: number[], frequence: number = 0)` | | 506 | | `id` | `number` | Unique identifier of an instance of this system | 507 | | `frequence` | `number` | The maximum times per second this system should be updated | 508 | | `beforeUpdateAll(time: number)` | | Invoked before updating entities available for this system. It is only invoked when there are entities with the characteristics expected by this system. | 509 | | `update(time: number, delta: number, entity: Entity)` | | Invoked in updates, limited to the value set in the "frequency" attribute | 510 | | `afterUpdateAll(time: number, entities: Entity[])` | | Invoked after performing update of entities available for this system. It is only invoked when there are entities with the characteristics expected by this system. | 511 | | `change(entity: Entity, added?: Component, removed?: Component)` | | Invoked when an expected feature of this system is added or removed from the entity | 512 | | `enter(entity: Entity)` | | Invoked when:
**A)** An entity with the characteristics (components) expected by this system is added in the world;
**B)** This system is added in the world and this world has one or more entities with the characteristics expected by this system;
**C)** An existing entity in the same world receives a new component at runtime and all of its new components match the standard expected by this system. | 513 | | `exit(entity: Entity)` | | Invoked when:
**A)** An entity with the characteristics (components) expected by this system is removed from the world;
**B)** This system is removed from the world and this world has one or more entities with the characteristics expected by this system;
**C)** An existing entity in the same world loses a component at runtime and its new component set no longer matches the standard expected by this system | 514 | 515 | 516 | ## Feedback, Requests and Roadmap 517 | 518 | Please use [GitHub issues] for feedback, questions or comments. 519 | 520 | If you have specific feature requests or would like to vote on what others are recommending, please go to the [GitHub issues] section as well. I would love to see what you are thinking. 521 | 522 | ## Contributing 523 | 524 | You can contribute in many ways to this project. 525 | 526 | ### Translating and documenting 527 | 528 | I'm not a native speaker of the English language, so you may have noticed a lot of grammar errors in this documentation. 529 | 530 | You can FORK this project and suggest improvements to this document (https://github.com/nidorx/ecs-lib/edit/master/README.md). 531 | 532 | If you find it more convenient, report a issue with the details on [GitHub issues]. 533 | 534 | ### Reporting Issues 535 | 536 | If you have encountered a problem with this component please file a defect on [GitHub issues]. 537 | 538 | Describe as much detail as possible to get the problem reproduced and eventually corrected. 539 | 540 | ### Fixing defects and adding improvements 541 | 542 | 1. Fork it () 543 | 2. Commit your changes (`git commit -am 'Add some fooBar'`) 544 | 3. Push to your master branch (`git push`) 545 | 4. Create a new Pull Request 546 | 547 | ## License 548 | 549 | This code is distributed under the terms and conditions of the [MIT license](LICENSE). 550 | 551 | 552 | [GitHub issues]: https://github.com/nidorx/ecs-lib/issues 553 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # ecs-lib Example 2 | 3 | 4 | This is an [example](https://nidorx.github.io/ecs-lib/) of using ecs-lib (using Typescript and Threejs, you can use pure js too). 5 | 6 | 7 | ```bash 8 | npm install 9 | npm start 10 | 11 | # Navigate to http://localhost:8080 12 | ``` 13 | 14 | 15 | ## Components 16 | 17 | - **ColorComponent** - Simple component that determines entity color 18 | - **BoxComponent** - Configuration data of a box geometry. 19 | - **SphereComponent** - Configuration data of a sphere geometry. 20 | - **Object3DComponent** - Configuration data of a 3D Object. In this game, all 3D objects are inserted into the scene automatically (See SceneObjectSystem.ts) 21 | 22 | ## Entities 23 | 24 | - **CubeEntity** - An entity that has the **color** and **box** components. In our game, after this component receives a 3D Object (see Object3DComponent.ts and SceneObjectSystem.ts) this entity is eligible to be managed by the keyboard (see KeyboardSystem.ts). 25 | - **SphereEntity** - An entity that has the **color** and **sphere** components. In our game, after this component receives a 3D Object (see Object3DComponent.ts and SceneObjectSystem.ts) this entity is eligible to be considered an artificial intelligence (NPC) (see AISystem.ts) 26 | 27 | ## Systems 28 | 29 | - **CubeFactorySystem** - Whenever an entity with **color** and **box** components is added, this system performs the creation of the 3D object of that entity. 30 | - **SphereFactorySystem** - Whenever an entity with **color** and **sphere** components is added, this system performs the creation of the 3D object of that entity. 31 | - **SceneObjectSystem** - Whenever an entity with the **3D Object** component is added, or when an existing entity receives a **3D Object**, this system adds the 3D Object of that entity to the scene. 32 | - **KeyboardSystem** - For any entity that has the **box** and **3D Object** components, this system moves the 3D Object of that entity based on the keys pressed by the user. **This system therefore represents the player's actions in the game**. 33 | - **AISystem** - For any entity that has the **sphere** and **3D Object** components, this system randomly moves the 3D Object from that entity. This system therefore represents the actions of NPC and artificial intelligence in the game. 34 | 35 | 36 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecs-lib-example-web-ts", 3 | "version": "1.0.0", 4 | "description": "ecs-lib web example with Typescript", 5 | "main": "index.js", 6 | "private": true, 7 | "scripts": { 8 | "start": "webpack-dev-server", 9 | "build": "webpack --mode=production" 10 | }, 11 | "keywords": [], 12 | "author": "Alex Rodin ", 13 | "license": "MIT", 14 | "dependencies": { 15 | "dat.gui": "0.7.6", 16 | "ecs-lib": "0.8.0-pre.0", 17 | "three": "0.137.0", 18 | "react": "^16.12.0", 19 | "react-dom": "^16.12.0" 20 | }, 21 | "devDependencies": { 22 | "@types/dat.gui": "0.7.5", 23 | "@types/react": "^16.9.11", 24 | "@types/react-dom": "^16.9.4", 25 | "copy-webpack-plugin": "5.1.0", 26 | "ts-loader": "6.2.1", 27 | "typescript": "3.7.2", 28 | "webpack": "4.41.2", 29 | "webpack-cli": "3.3.10", 30 | "webpack-dev-server": "3.9.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/public/circle_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/circle_05.png -------------------------------------------------------------------------------- /example/public/envmap/bridge/nx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/bridge/nx.jpg -------------------------------------------------------------------------------- /example/public/envmap/bridge/ny.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/bridge/ny.jpg -------------------------------------------------------------------------------- /example/public/envmap/bridge/nz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/bridge/nz.jpg -------------------------------------------------------------------------------- /example/public/envmap/bridge/px.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/bridge/px.jpg -------------------------------------------------------------------------------- /example/public/envmap/bridge/py.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/bridge/py.jpg -------------------------------------------------------------------------------- /example/public/envmap/bridge/pz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/bridge/pz.jpg -------------------------------------------------------------------------------- /example/public/envmap/lake/nx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/lake/nx.jpg -------------------------------------------------------------------------------- /example/public/envmap/lake/ny.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/lake/ny.jpg -------------------------------------------------------------------------------- /example/public/envmap/lake/nz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/lake/nz.jpg -------------------------------------------------------------------------------- /example/public/envmap/lake/px.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/lake/px.jpg -------------------------------------------------------------------------------- /example/public/envmap/lake/py.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/lake/py.jpg -------------------------------------------------------------------------------- /example/public/envmap/lake/pz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/lake/pz.jpg -------------------------------------------------------------------------------- /example/public/envmap/lake/readme.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/lake/readme.txt -------------------------------------------------------------------------------- /example/public/envmap/lake/sor_lake1.shader: -------------------------------------------------------------------------------- 1 | // Direction & elevation checked and adjusted - Speaker 2 | 3 | textures/skies/sor_lake1 4 | { 5 | qer_editorimage env/sor_lake1/lake1_ft.tga 6 | surfaceparm noimpact 7 | surfaceparm nolightmap 8 | q3map_globaltexture 9 | q3map_lightsubdivide 256 10 | surfaceparm sky 11 | q3map_surfacelight 200 12 | q3map_sun 1 1 1 250 50 30 13 | skyparms env/sor_lake1/lake1 - - 14 | } 15 | -------------------------------------------------------------------------------- /example/public/envmap/miramar/README.TXT: -------------------------------------------------------------------------------- 1 | THIS SKY WAS UPDATED AT THE 27TH 2 | THE ORIG HAD SOME ERRORS 3 | 4 | MIRAMAR 5 | high res 1024^2 environment map 6 | ships as TGA. 7 | 8 | 9 | By Jockum Skoglund aka hipshot 10 | hipshot@zfight.com 11 | www.zfight.com 12 | Stockholm, 2005 08 25 13 | 14 | 15 | Modify however you like, just cred me for my work, maybe link to my page. 16 | -------------------------------------------------------------------------------- /example/public/envmap/miramar/miramar_bk.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/miramar/miramar_bk.tga -------------------------------------------------------------------------------- /example/public/envmap/miramar/miramar_ft.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/miramar/miramar_ft.tga -------------------------------------------------------------------------------- /example/public/envmap/miramar/miramar_lf.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/miramar/miramar_lf.tga -------------------------------------------------------------------------------- /example/public/envmap/miramar/miramar_rt.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/miramar/miramar_rt.tga -------------------------------------------------------------------------------- /example/public/envmap/miramar/miramar_up.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/miramar/miramar_up.tga -------------------------------------------------------------------------------- /example/public/envmap/miramar/nx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/miramar/nx.jpg -------------------------------------------------------------------------------- /example/public/envmap/miramar/ny.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/miramar/ny.jpg -------------------------------------------------------------------------------- /example/public/envmap/miramar/nz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/miramar/nz.jpg -------------------------------------------------------------------------------- /example/public/envmap/miramar/px.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/miramar/px.jpg -------------------------------------------------------------------------------- /example/public/envmap/miramar/py.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/miramar/py.jpg -------------------------------------------------------------------------------- /example/public/envmap/miramar/pz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/example/public/envmap/miramar/pz.jpg -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | lib-ecs - Examples 7 | 125 | 126 | 127 | 128 |
129 |
130 |   138 | 139 |   147 | 148 |   156 | 157 |   165 |
166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /example/src/Constants.ts: -------------------------------------------------------------------------------- 1 | import {KeyboardPage} from "./pages/KeyboardPage"; 2 | import {TimescalePage} from "./pages/TimescalePage"; 3 | 4 | /** 5 | * Todas as páginas usadas para experimentos e ferramentas de desenvolvimento 6 | * 7 | * A classe deve possuir uma propriedade estática `title`, manter o padrão " - Name" 8 | */ 9 | export const PAGES = [ 10 | KeyboardPage, 11 | TimescalePage 12 | ]; 13 | 14 | // Faz a ordenação dos itens 15 | PAGES.sort((a: any, b: any) => { 16 | return a.title.localeCompare(b.title); 17 | }); 18 | -------------------------------------------------------------------------------- /example/src/component/BoxComponent.ts: -------------------------------------------------------------------------------- 1 | import {Component} from "ecs-lib"; 2 | 3 | export type Box = { 4 | width: number; 5 | height: number; 6 | depth: number; 7 | } 8 | 9 | export const BoxComponent = Component.register(); 10 | -------------------------------------------------------------------------------- /example/src/component/ColorComponent.ts: -------------------------------------------------------------------------------- 1 | import {Component} from "ecs-lib"; 2 | 3 | export const ColorComponent = Component.register(); 4 | -------------------------------------------------------------------------------- /example/src/component/Object3DComponent.ts: -------------------------------------------------------------------------------- 1 | import {Object3D} from "three"; 2 | import {Component} from "ecs-lib"; 3 | 4 | export const Object3DComponent = Component.register(); 5 | -------------------------------------------------------------------------------- /example/src/component/ParticleComponent.ts: -------------------------------------------------------------------------------- 1 | import {Component} from "ecs-lib"; 2 | 3 | export type Config = { 4 | particles: number; 5 | size: number; 6 | } 7 | 8 | export const ParticleComponent = Component.register(); 9 | -------------------------------------------------------------------------------- /example/src/component/PongComponent.ts: -------------------------------------------------------------------------------- 1 | import {Component} from "ecs-lib"; 2 | 3 | export type Config = { 4 | mass: number; 5 | impulse: number; 6 | } 7 | 8 | export const PongComponent = Component.register(); 9 | -------------------------------------------------------------------------------- /example/src/component/SphereComponent.ts: -------------------------------------------------------------------------------- 1 | import {Component} from "ecs-lib"; 2 | 3 | export type Sphere = { 4 | radius: number; 5 | widthSegments: number; 6 | heightSegments: number; 7 | x?:number; 8 | z?:number; 9 | } 10 | 11 | export const SphereComponent = Component.register(); 12 | -------------------------------------------------------------------------------- /example/src/entity/AnimatedEntity.ts: -------------------------------------------------------------------------------- 1 | import {Entity} from "ecs-lib"; 2 | import {ParticleComponent} from "../component/ParticleComponent"; 3 | 4 | export default class AnimatedEntity extends Entity { 5 | 6 | constructor() { 7 | super(); 8 | 9 | this.add(new ParticleComponent({ 10 | particles: 800, 11 | size: 5 12 | })); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/src/entity/CubeEntity.ts: -------------------------------------------------------------------------------- 1 | import {Entity} from "ecs-lib"; 2 | import {ColorComponent} from "../component/ColorComponent"; 3 | import {Box, BoxComponent} from "../component/BoxComponent"; 4 | 5 | export default class CubeEntity extends Entity { 6 | 7 | constructor(cube: Box, color: string) { 8 | super(); 9 | 10 | this.add(new BoxComponent(cube)); 11 | this.add(new ColorComponent(color)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/src/entity/SphereEntity.ts: -------------------------------------------------------------------------------- 1 | import {Entity} from "ecs-lib"; 2 | import {ColorComponent} from "../component/ColorComponent"; 3 | import {Sphere, SphereComponent} from "../component/SphereComponent"; 4 | 5 | export default class SphereEntity extends Entity { 6 | 7 | constructor(sphere: Sphere, color: string) { 8 | super(); 9 | 10 | this.add(new SphereComponent(sphere)); 11 | this.add(new ColorComponent(color)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {IndexPage} from "./pages/IndexPage"; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /example/src/pages/IndexPage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {GUI} from "dat.gui"; 3 | import { 4 | AxesHelper, 5 | Camera, 6 | Clock, 7 | Color, 8 | GridHelper, 9 | Material, 10 | OrthographicCamera, 11 | PerspectiveCamera, 12 | Scene, 13 | WebGLRenderer 14 | } from "three"; 15 | import {OrbitControls} from "three/examples/jsm/controls/OrbitControls"; 16 | import {PAGES} from "../Constants"; 17 | import GUISession from "../utils/GUISession"; 18 | import {createCubeEnv} from "../utils/cubeEnv"; 19 | import ECS from "ecs-lib"; 20 | import SceneObjectSystem from "../system/SceneObjectSystem"; 21 | import LogSystem from "../system/LogSystem"; 22 | 23 | const ENVMAPS = [ 24 | { 25 | texture: 'lake', 26 | title: 'Lake' 27 | }, 28 | { 29 | texture: 'bridge', 30 | title: 'Bridge' 31 | }, 32 | { 33 | texture: 'miramar', 34 | title: 'Miramar' 35 | } 36 | ]; 37 | 38 | type Props = {}; 39 | type State = { 40 | world?: ECS; 41 | scene?: Scene; 42 | camera?: Camera; 43 | renderer?: WebGLRenderer; 44 | gui: GUI, 45 | page?: typeof React.Component, 46 | // A Sessão é o experimento ativo no momento 47 | session?: GUISession 48 | }; 49 | 50 | export class IndexPage extends React.PureComponent { 51 | 52 | state: State = { 53 | gui: new GUI() 54 | }; 55 | 56 | private pageRef = React.createRef(); 57 | 58 | componentDidMount(): void { 59 | 60 | const gui = this.state.gui; 61 | gui.width = 300; 62 | 63 | const APPKEY = 'ecs-lib-examples-'; 64 | 65 | class StorageProxy { 66 | constructor(private key: string, private type: 'number' | 'bool' | 'string' = 'number') { 67 | 68 | } 69 | 70 | get(def?: any): any { 71 | switch (this.type) { 72 | case 'number': 73 | return Number.parseInt(window.localStorage.getItem(APPKEY + this.key) || (def ? '' + def : undefined) || '0'); 74 | break; 75 | case 'bool': 76 | let value = window.localStorage.getItem(APPKEY + this.key); 77 | if (value === 'true') { 78 | return true; 79 | } 80 | if (value === 'false') { 81 | return false; 82 | } 83 | if (def === undefined) { 84 | return true; 85 | } 86 | return def; 87 | break; 88 | case 'string': 89 | return window.localStorage.getItem(APPKEY + this.key) || def; 90 | break; 91 | } 92 | } 93 | 94 | set(value: any) { 95 | window.localStorage.setItem(APPKEY + this.key, value); 96 | } 97 | } 98 | 99 | // Funções a serem executadas sempre que alterar a página 100 | const onPageChange: Array = []; 101 | 102 | var scene: Scene, 103 | camera: Camera, 104 | controls: OrbitControls, 105 | HEIGHT = window.innerHeight, 106 | WIDTH = window.innerWidth, 107 | windowHalfX = WIDTH / 2, 108 | windowHalfY = HEIGHT / 2, 109 | cubeEnv: any; 110 | 111 | const renderer = new WebGLRenderer({ 112 | canvas: document.getElementById('canvas') as HTMLCanvasElement, 113 | // alpha: true, 114 | antialias: true 115 | }); 116 | renderer.setSize(WIDTH, HEIGHT); 117 | renderer.setPixelRatio(window.devicePixelRatio); 118 | 119 | let aspect = WIDTH / HEIGHT; 120 | var frustumSize = 120; 121 | const perspectiveCamera = new PerspectiveCamera(60, aspect, 1, 2000); 122 | const orthographicCamera = new OrthographicCamera( 123 | frustumSize * aspect / -2, 124 | frustumSize * aspect / 2, 125 | frustumSize / 2, 126 | frustumSize / -2, 127 | -120, 128 | 2000 129 | ); 130 | 131 | perspectiveCamera.position.y = 60; 132 | perspectiveCamera.position.x = 60; 133 | perspectiveCamera.position.z = 60; 134 | 135 | orthographicCamera.position.y = 60; 136 | orthographicCamera.position.x = 60; 137 | orthographicCamera.position.z = 60; 138 | 139 | const perspectiveControls = new OrbitControls(perspectiveCamera, renderer.domElement); 140 | const orthographicControls = new OrbitControls(orthographicCamera, renderer.domElement); 141 | 142 | perspectiveControls.enableKeys = false; 143 | orthographicControls.enableKeys = false; 144 | 145 | // ---------------------------------------------------------------- 146 | // CONTROLE DE RENDERIZAÇÃO 147 | // ---------------------------------------------------------------- 148 | (() => { 149 | window.addEventListener('resize', function () { 150 | HEIGHT = window.innerHeight; 151 | WIDTH = window.innerWidth; 152 | windowHalfX = WIDTH / 2; 153 | windowHalfY = HEIGHT / 2; 154 | 155 | if (renderer) { 156 | renderer.setSize(WIDTH, HEIGHT); 157 | } 158 | 159 | let aspect = WIDTH / HEIGHT; 160 | 161 | perspectiveCamera.aspect = aspect; 162 | perspectiveCamera.updateProjectionMatrix(); 163 | 164 | orthographicCamera.left = frustumSize * aspect / -2; 165 | orthographicCamera.right = frustumSize * aspect / 2; 166 | orthographicCamera.top = frustumSize / 2; 167 | orthographicCamera.bottom = frustumSize / -2; 168 | orthographicCamera.updateProjectionMatrix(); 169 | 170 | if (cubeEnv) { 171 | cubeEnv.onResize(aspect); 172 | } 173 | }, false); 174 | 175 | 176 | const render = () => { 177 | if (scene && camera) { 178 | 179 | // Render page 180 | if (this.pageRef.current && this.pageRef.current.render3D) { 181 | this.pageRef.current.render3D(); 182 | } 183 | 184 | renderer.render(scene, camera); 185 | } 186 | }; 187 | 188 | let clock = new Clock(); 189 | let delta = 0; 190 | // 60 fps 191 | let interval = 1 / 60; 192 | 193 | 194 | const animate = () => { 195 | requestAnimationFrame(animate); 196 | 197 | // Update ECS 198 | 199 | if (this.state.world) { 200 | this.state.world.update(); 201 | } 202 | 203 | perspectiveControls.update(); 204 | orthographicControls.update(); 205 | 206 | delta += clock.getDelta(); 207 | 208 | // Animate page 209 | if (this.pageRef.current && this.pageRef.current.animate3D) { 210 | this.pageRef.current.animate3D(); 211 | } 212 | 213 | if (delta > interval) { 214 | // The draw or time dependent code are here 215 | render(); 216 | 217 | delta = delta % interval; 218 | } 219 | }; 220 | 221 | animate(); 222 | })(); 223 | 224 | 225 | // ---------------------------------------------------------------- 226 | // SELEÇÃO DE PÁGINA (Experimento/Ferramenta) 227 | // ---------------------------------------------------------------- 228 | (() => { 229 | const pages: { 230 | [key: string]: any 231 | } = {}; 232 | 233 | PAGES.map((page, i) => { 234 | pages[page.title] = i; 235 | }); 236 | 237 | const pageStorage = new StorageProxy('page'); 238 | const pageParams = { 239 | page: pageStorage.get() 240 | }; 241 | const pageController = gui.add(pageParams, 'page', pages) 242 | .onChange((index) => { 243 | 244 | pageStorage.set(pageParams.page); 245 | 246 | const page = PAGES[pageParams.page]; 247 | const oldSession = this.state.session; 248 | 249 | // Realiza a limpeza da página anerior 250 | this.setState({ 251 | page: undefined, 252 | session: new GUISession(this.state.gui, page.title) 253 | }, () => { 254 | 255 | // Remove os controles criados pelo experimento anterior 256 | if (oldSession) { 257 | oldSession.destroy(); 258 | } 259 | 260 | if (scene) { 261 | // Remove os elementos inseridos na página atual, incluindo a própria cena 262 | scene.dispose(); 263 | } 264 | scene = new Scene(); 265 | scene.background = new Color(0x888888); 266 | // scene.fog = new Fog(scene.background, 10, 20); 267 | 268 | // scene.add(new Mesh(new BoxBufferGeometry(20, 20, 20), new MeshLambertMaterial({color: Math.random() * 0xffffff}))); 269 | 270 | 271 | onPageChange.forEach(fn => { 272 | fn(); 273 | }); 274 | 275 | if (this.state.world) { 276 | this.state.world.destroy(); 277 | } 278 | 279 | let world = new ECS([ 280 | new SceneObjectSystem(scene), 281 | new LogSystem() 282 | ]); 283 | 284 | // Renderiza a nova página 285 | this.setState({ 286 | scene: scene, 287 | camera: camera, 288 | renderer: renderer, 289 | page: page, 290 | world: world 291 | }); 292 | }); 293 | }); 294 | 295 | setTimeout(function () { 296 | pageController.setValue(pageParams.page) 297 | }, 10) 298 | })(); 299 | 300 | // ---------------------------------------------------------------- 301 | // AMBIENTE 302 | // ---------------------------------------------------------------- 303 | var environment = gui.addFolder('Environment'); 304 | 305 | 306 | setTimeout(() => { 307 | 308 | // ---------------------------------------------------------------- 309 | // CAMERA 310 | // ---------------------------------------------------------------- 311 | const cameras = { 312 | Perspective: 'P', 313 | Orthographic: 'O', 314 | }; 315 | const cameraStorage = new StorageProxy('camera', 'string'); 316 | const cameraParams = { 317 | camera: cameraStorage.get('P'), 318 | }; 319 | let cameraUpdate = () => { 320 | 321 | cameraStorage.set(cameraParams.camera); 322 | 323 | if (cameraParams.camera === 'P') { 324 | // Perspective 325 | camera = perspectiveCamera; 326 | } else { 327 | // Orthographic 328 | camera = orthographicCamera; 329 | } 330 | 331 | this.setState({ 332 | camera: camera, 333 | }); 334 | }; 335 | onPageChange.push(cameraUpdate); 336 | environment.add(cameraParams, 'camera', cameras) 337 | .onChange(cameraUpdate) 338 | .setValue(cameraParams.camera); 339 | 340 | // ---------------------------------------------------------------- 341 | // ENVMAP 342 | // ---------------------------------------------------------------- 343 | const envmaps: { 344 | [key: string]: any 345 | } = {}; 346 | 347 | ENVMAPS.forEach((item, i) => { 348 | envmaps[item.title] = i; 349 | }); 350 | 351 | const envmapStorage = new StorageProxy('envmap'); 352 | const envmapParams = { 353 | envmap: envmapStorage.get(), 354 | }; 355 | let envmapUpdate = () => { 356 | 357 | envmapStorage.set(envmapParams.envmap); 358 | 359 | const envmap = ENVMAPS[envmapParams.envmap].texture; 360 | 361 | if (cubeEnv) { 362 | cubeEnv.destroy(); 363 | } 364 | 365 | if (renderer) { 366 | cubeEnv = createCubeEnv(envmap, WIDTH / HEIGHT, renderer); 367 | } 368 | }; 369 | onPageChange.push(envmapUpdate); 370 | environment.add(envmapParams, 'envmap', envmaps) 371 | .onChange(envmapUpdate) 372 | .setValue(envmapParams.envmap); 373 | 374 | 375 | // ---------------------------------------------------------------- 376 | // GRID 377 | // ---------------------------------------------------------------- 378 | const grid = environment.addFolder('Grid'); 379 | const gridStorageShow = new StorageProxy('grid-show', 'bool'); 380 | const gridStorageSize = new StorageProxy('grid-size'); 381 | const gridStorageDivisions = new StorageProxy('grid-divisions'); 382 | const gridStorageColor1 = new StorageProxy('grid-color1', 'string'); 383 | const gridStorageColor2 = new StorageProxy('grid-color2', 'string'); 384 | const gridParams = { 385 | show: gridStorageShow.get(), 386 | size: gridStorageSize.get(200), 387 | divisions: gridStorageDivisions.get(20), 388 | color1: gridStorageColor1.get('#9923D2'), 389 | color2: gridStorageColor2.get('#F5D0FE'), 390 | }; 391 | var gridHelper: GridHelper; 392 | let gridUpdate = function () { 393 | 394 | gridStorageShow.set(gridParams.show); 395 | gridStorageSize.set(gridParams.size); 396 | gridStorageDivisions.set(gridParams.divisions); 397 | gridStorageColor1.set(gridParams.color1); 398 | gridStorageColor2.set(gridParams.color2); 399 | 400 | if (!scene) { 401 | return; 402 | } 403 | if (gridHelper) { 404 | scene.remove(gridHelper); 405 | gridHelper = undefined; 406 | } 407 | 408 | if (gridParams.show) { 409 | gridHelper = new GridHelper(gridParams.size, gridParams.divisions, gridParams.color1, gridParams.color2); 410 | scene.add(gridHelper); 411 | } 412 | }; 413 | onPageChange.push(gridUpdate); 414 | grid.add(gridParams, 'show').onChange(gridUpdate).setValue(gridParams.show); 415 | grid.add(gridParams, 'size', 10, 500, 5).onChange(gridUpdate).setValue(gridParams.size); 416 | grid.add(gridParams, 'divisions', 5, 50, 5).onChange(gridUpdate).setValue(gridParams.divisions); 417 | grid.addColor(gridParams, 'color1').onChange(gridUpdate).setValue(gridParams.color1); 418 | grid.addColor(gridParams, 'color2').onChange(gridUpdate).setValue(gridParams.color2); 419 | 420 | 421 | // ---------------------------------------------------------------- 422 | // AXIS 423 | // ---------------------------------------------------------------- 424 | const axes = environment.addFolder('Axes'); 425 | const axesStorageShow = new StorageProxy('axes-show', 'bool'); 426 | const axesStorageSize = new StorageProxy('axes-size'); 427 | const axesStorageDepthTest = new StorageProxy('axes-depthTest', 'bool'); 428 | const axesParams = { 429 | show: axesStorageShow.get(), 430 | size: axesStorageSize.get(100), 431 | depthTest: axesStorageDepthTest.get(false), 432 | }; 433 | var axesHelper: AxesHelper; 434 | let axesUpdate = function () { 435 | 436 | axesStorageShow.set(axesParams.show); 437 | axesStorageDepthTest.set(axesParams.depthTest); 438 | axesStorageSize.set(axesParams.size); 439 | 440 | if (!scene) { 441 | return; 442 | } 443 | 444 | if (axesHelper) { 445 | scene.remove(axesHelper); 446 | axesHelper = undefined; 447 | } 448 | 449 | if (axesParams.show) { 450 | axesHelper = new AxesHelper(axesParams.size); 451 | if (!axesParams.depthTest) { 452 | (axesHelper.material as Material).depthTest = false; 453 | axesHelper.renderOrder = 1; 454 | } 455 | scene.add(axesHelper); 456 | } 457 | }; 458 | onPageChange.push(axesUpdate); 459 | axes.add(axesParams, 'show').onChange(axesUpdate).setValue(axesParams.show); 460 | axes.add(axesParams, 'size', 10, 260, 5).onChange(axesUpdate).setValue(axesParams.size); 461 | axes.add(axesParams, 'depthTest').onChange(axesUpdate).setValue(axesParams.depthTest); 462 | }, 100); 463 | } 464 | 465 | render() { 466 | const PageComponent = this.state.page; 467 | return ( 468 |
469 | { 470 | PageComponent 471 | ? ( 472 |
473 |
474 |

ECS (Entity Component System) library for game programming

475 | https://github.com/nidorx/ecs-lib/tree/master/example 476 |

{(PageComponent as any).title}

477 | {(PageComponent as any).help} 478 |
479 | 487 |
488 | ) 489 | : null 490 | } 491 |
492 | ); 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /example/src/pages/KeyboardPage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {PerspectiveCamera, Scene, WebGLRenderer} from "three"; 3 | import KeyboardSystem from "../system/KeyboardSystem"; 4 | import NPCSystem from "../system/NPCSystem"; 5 | import ECS from "ecs-lib"; 6 | import SphereFactorySystem from "../system/SphereFactorySystem"; 7 | import CubeFactorySystem from "../system/CubeFactorySystem"; 8 | import CubeEntity from "../entity/CubeEntity"; 9 | import SphereEntity from "../entity/SphereEntity"; 10 | import GUISession from "../utils/GUISession"; 11 | 12 | type Props = { 13 | gui: GUISession, 14 | world: ECS, 15 | scene: Scene; 16 | camera: PerspectiveCamera; 17 | renderer: WebGLRenderer; 18 | }; 19 | 20 | type State = {}; 21 | 22 | export class KeyboardPage extends React.PureComponent { 23 | 24 | static title = 'Keyboard & Npc'; 25 | 26 | static help = ( 27 |
28 |

KeyboardSystem: Use directional keys to move character (Cube)

29 |

NPCSystem: System Controlled Character (Sphere)

30 |
31 | ); 32 | 33 | state: State = {}; 34 | 35 | componentDidMount(): void { 36 | const gui = this.props.gui; 37 | const world = this.props.world; 38 | 39 | // Player and NPC 40 | world.addSystem(new SphereFactorySystem()); 41 | world.addSystem(new CubeFactorySystem()); 42 | 43 | // Add our player (CUBE) 44 | world.addEntity(new CubeEntity({ 45 | width: 10, 46 | height: 10, 47 | depth: 10 48 | }, '#FF0000')); 49 | 50 | // Add AI player (Sphere) 51 | world.addEntity(new SphereEntity({ 52 | radius: 5, 53 | heightSegments: 8, 54 | widthSegments: 8 55 | }, '#0000FF')); 56 | 57 | const keyboardSystem = new KeyboardSystem(); 58 | const aiSystem = new NPCSystem(); 59 | 60 | const guiOptions = { 61 | KeyboardSystem: false, 62 | NPCSystem: false, 63 | }; 64 | 65 | gui.add(guiOptions, 'KeyboardSystem').onChange(() => { 66 | if (guiOptions.KeyboardSystem) { 67 | world.addSystem(keyboardSystem); 68 | } else { 69 | world.removeSystem(keyboardSystem); 70 | } 71 | }); 72 | 73 | gui.add(guiOptions, 'NPCSystem').onChange(() => { 74 | if (guiOptions.NPCSystem) { 75 | world.addSystem(aiSystem); 76 | } else { 77 | world.removeSystem(aiSystem); 78 | } 79 | }); 80 | } 81 | 82 | /** 83 | * Permite animar algum elemento 3D antes de renderizar o canvas 84 | */ 85 | animate3D() { 86 | 87 | } 88 | 89 | /** 90 | * Permite executar ações ao renderizar o canvas 91 | */ 92 | render3D() { 93 | } 94 | 95 | render() { 96 | // Esse experimento não possui conteúdo html 97 | return (
); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /example/src/pages/TimescalePage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {PerspectiveCamera, Scene, WebGLRenderer} from "three"; 3 | import ECS from "ecs-lib"; 4 | import GUISession from "../utils/GUISession"; 5 | import AnimatedEntity from "../entity/AnimatedEntity"; 6 | import ParticleSystem from "../system/ParticleSystem"; 7 | import PongSystem from "../system/PongSystem"; 8 | import SphereEntity from "../entity/SphereEntity"; 9 | import SphereFactorySystem from "../system/SphereFactorySystem"; 10 | import {PongComponent} from "../component/PongComponent"; 11 | 12 | type Props = { 13 | gui: GUISession, 14 | world: ECS, 15 | scene: Scene; 16 | camera: PerspectiveCamera; 17 | renderer: WebGLRenderer; 18 | }; 19 | 20 | type State = {}; 21 | 22 | export class TimescalePage extends React.PureComponent { 23 | 24 | static title = 'Timescale (Slow Motion)'; 25 | 26 | static help = ( 27 |
28 |

A time scale of 1 means normal speed. 0.5 means half the speed and 2.0 means twice the speed. If you set 29 | the game's timescale to 0.1, it will be ten times slower but still smooth - a good slow motion effect! 30 |

31 |
32 | ); 33 | 34 | state: State = {}; 35 | 36 | componentDidMount(): void { 37 | const gui = this.props.gui; 38 | const world = this.props.world; 39 | 40 | world.addSystem(new SphereFactorySystem()); 41 | world.addSystem(new PongSystem()); 42 | world.addSystem(new ParticleSystem()); 43 | 44 | // Add animated entity 45 | world.addEntity(new AnimatedEntity()); 46 | 47 | let sphereA = new SphereEntity({ 48 | radius: 5, 49 | heightSegments: 8, 50 | widthSegments: 8, 51 | x: 25, 52 | z: -25 53 | }, '#0000FF'); 54 | 55 | sphereA.add(new PongComponent({ 56 | mass: 2.0, 57 | impulse: 0.3 58 | })); 59 | world.addEntity(sphereA); 60 | 61 | 62 | let sphereB = new SphereEntity({ 63 | radius: 5, 64 | heightSegments: 8, 65 | widthSegments: 8, 66 | x: -25, 67 | z: 25 68 | }, '#FF0000'); 69 | 70 | sphereB.add(new PongComponent({ 71 | mass: 0.5, 72 | impulse: 0.2 73 | })); 74 | world.addEntity(sphereB); 75 | 76 | const guiOptions = { 77 | timescale: 1.0, 78 | pause: false 79 | }; 80 | 81 | gui.add(guiOptions, 'timescale', 0.1, 2.0).onChange(() => { 82 | if (guiOptions.pause) { 83 | world.timeScale = 0; 84 | } else { 85 | world.timeScale = guiOptions.timescale; 86 | } 87 | }); 88 | 89 | gui.add(guiOptions, 'pause').onChange(() => { 90 | if (guiOptions.pause) { 91 | world.timeScale = 0; 92 | } else { 93 | world.timeScale = guiOptions.timescale; 94 | } 95 | }); 96 | } 97 | 98 | /** 99 | * Permite animar algum elemento 3D antes de renderizar o canvas 100 | */ 101 | animate3D() { 102 | 103 | } 104 | 105 | /** 106 | * Permite executar ações ao renderizar o canvas 107 | */ 108 | render3D() { 109 | } 110 | 111 | render() { 112 | // Esse experimento não possui conteúdo html 113 | return (
); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /example/src/system/CubeFactorySystem.ts: -------------------------------------------------------------------------------- 1 | import {BoxGeometry, Mesh, MeshBasicMaterial} from "three"; 2 | import {Component, Entity, System} from "ecs-lib"; 3 | import {Object3DComponent} from "../component/Object3DComponent"; 4 | import {ColorComponent} from "../component/ColorComponent"; 5 | import {BoxComponent} from "../component/BoxComponent"; 6 | 7 | /** 8 | * Responsible for creating boxes, when an entity has the color and cube components. 9 | */ 10 | export default class CubeFactorySystem extends System { 11 | 12 | constructor() { 13 | super([ 14 | ColorComponent.type, 15 | BoxComponent.type 16 | ]); 17 | } 18 | 19 | enter(entity: Entity): void { 20 | let object = Object3DComponent.oneFrom(entity); 21 | if (!object) { 22 | const box = BoxComponent.oneFrom(entity).data; 23 | const color = ColorComponent.oneFrom(entity).data; 24 | 25 | const geometry = new BoxGeometry(box.width, box.height, box.depth); 26 | const material = new MeshBasicMaterial({color: color}); 27 | const cube = new Mesh(geometry, material); 28 | 29 | // Append new component to entity 30 | entity.add(new Object3DComponent(cube)); 31 | } 32 | } 33 | 34 | change(entity: Entity, added?: Component, removed?: Component): void { 35 | console.log('CubeFactorySystem::change', entity, added, removed); 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /example/src/system/KeyboardSystem.ts: -------------------------------------------------------------------------------- 1 | import {Entity, System} from "ecs-lib"; 2 | import {Object3DComponent} from "../component/Object3DComponent"; 3 | import {BoxComponent} from "../component/BoxComponent"; 4 | import KeyboardState from "../utils/KeyboardState"; 5 | 6 | /** 7 | * Represents the player, lets you control the cube by keyboard 8 | */ 9 | export default class KeyboardSystem extends System { 10 | 11 | constructor() { 12 | super([ 13 | Object3DComponent.type, 14 | BoxComponent.type 15 | ]); 16 | } 17 | 18 | update(time: number, delta: number, entity: Entity): void { 19 | let object3D = Object3DComponent.oneFrom(entity).data; 20 | if (KeyboardState.pressed("right")) { 21 | object3D.translateX(0.3); 22 | } else if (KeyboardState.pressed("left")) { 23 | object3D.translateX(-0.3); 24 | } else if (KeyboardState.pressed("up")) { 25 | object3D.translateZ(-0.3); 26 | } else if (KeyboardState.pressed("down")) { 27 | object3D.translateZ(0.3); 28 | } 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /example/src/system/LogSystem.ts: -------------------------------------------------------------------------------- 1 | import {Component, Entity, System} from "ecs-lib"; 2 | 3 | export default class LogSystem extends System { 4 | 5 | constructor() { 6 | super([-1], (1 / 5)); // Logs all entities every 5 seconds (1/5 = 0.2 FPS) 7 | } 8 | 9 | beforeUpdateAll(time: number): void { 10 | console.log('LogSystem: Before update', time); 11 | } 12 | 13 | update(time: number, delta: number, entity: Entity): void { 14 | console.log('LogSystem', entity); 15 | } 16 | 17 | afterUpdateAll(time: number, entities: Entity[]): void { 18 | console.log('LogSystem: After update', time, entities); 19 | } 20 | 21 | change(entity: Entity, added?: Component, removed?: Component): void { 22 | console.log('LogSystem::change', entity, added, removed); 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /example/src/system/NPCSystem.ts: -------------------------------------------------------------------------------- 1 | import {Entity, System} from "ecs-lib"; 2 | import {Object3DComponent} from "../component/Object3DComponent"; 3 | import {SphereComponent} from "../component/SphereComponent"; 4 | 5 | /** 6 | * Represents artificial intelligence, controls sphere animation on map 7 | */ 8 | export default class NPCSystem extends System { 9 | 10 | constructor() { 11 | super([ 12 | Object3DComponent.type, 13 | SphereComponent.type 14 | ]); 15 | } 16 | 17 | update(time: number, delta: number, entity: Entity): void { 18 | let object3D = Object3DComponent.oneFrom(entity).data; 19 | if (Math.random() > 0.7) { 20 | object3D.translateX(0.3); 21 | if (object3D.position.x > 50) { 22 | object3D.position.x = 50; 23 | } 24 | } else if (Math.random() > 0.7) { 25 | object3D.translateX(-0.3); 26 | if (object3D.position.x < -50) { 27 | object3D.position.x = -50; 28 | } 29 | } else if (Math.random() > 0.7) { 30 | object3D.translateZ(-0.3); 31 | if (object3D.position.z > 50) { 32 | object3D.position.z = 50; 33 | } 34 | } else if (Math.random() > 0.7) { 35 | object3D.translateZ(0.3); 36 | if (object3D.position.z < -50) { 37 | object3D.position.z = -50; 38 | } 39 | } 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /example/src/system/ParticleSystem.ts: -------------------------------------------------------------------------------- 1 | import {Entity, System} from "ecs-lib"; 2 | import {Object3DComponent} from "../component/Object3DComponent"; 3 | import {ParticleComponent} from "../component/ParticleComponent"; 4 | import {AdditiveBlending, Geometry, Mesh, Points, PointsMaterial, TextureLoader, Vector3} from "three"; 5 | 6 | const textureLoader = new TextureLoader(); 7 | 8 | export default class ParticleSystem extends System { 9 | 10 | constructor() { 11 | super([ 12 | ParticleComponent.type 13 | ]); 14 | } 15 | 16 | update(time: number, delta: number, entity: Entity): void { 17 | let object = Object3DComponent.oneFrom(entity); 18 | if (object) { 19 | let particleSystem = object.data; 20 | let config = ParticleComponent.oneFrom(entity); 21 | let data = config.data; 22 | 23 | particleSystem.rotateY(delta * 0.00005); 24 | 25 | const geometry = ((particleSystem as Mesh).geometry as Geometry); 26 | 27 | let count = data.particles; 28 | while (count--) { 29 | 30 | // get the particle 31 | let particle = geometry.vertices[count]; 32 | let velocity = config.attr.velocity[count]; 33 | 34 | // check if we need to reset 35 | if (particle.y < 0) { 36 | particle.y = 100; 37 | velocity.y = -Math.random(); 38 | } 39 | 40 | // update the velocity with a splat of randomniz 41 | // velocity.y -= Math.random() * delta * 0.00005; 42 | // particle.add(velocity); 43 | particle.y += velocity.y * delta * 0.005; 44 | } 45 | 46 | // flag to the particle system that we've changed its vertices. 47 | geometry.verticesNeedUpdate = true; 48 | } 49 | } 50 | 51 | enter(entity: Entity): void { 52 | let object = Object3DComponent.oneFrom(entity); 53 | if (!object) { 54 | let config = ParticleComponent.oneFrom(entity); 55 | let data = config.data; 56 | 57 | // Saves particle velocity 58 | config.attr.velocity = []; 59 | 60 | // create the particle variables 61 | let particles = new Geometry(); 62 | let pMaterial = new PointsMaterial({ 63 | color: 0xFFFFFF, 64 | size: data.size, 65 | map: textureLoader.load("circle_05.png"), 66 | blending: AdditiveBlending, 67 | transparent: true, 68 | depthTest: false, 69 | sizeAttenuation: true 70 | }); 71 | 72 | // now create the individual particles 73 | for (var p = 0; p < data.particles; p++) { 74 | 75 | // create a particle with random 76 | // position values, -100 -> 100 77 | let pX = Math.random() * 200 - 100; 78 | let pY = Math.random() * 100 ; 79 | let pZ = Math.random() * 200 - 100; 80 | let particle = new Vector3(pX, pY, pZ); 81 | 82 | config.attr.velocity.push(new Vector3(0, -Math.random(), 0)); 83 | 84 | // add it to the geometry 85 | particles.vertices.push(particle); 86 | } 87 | 88 | // create the particle system 89 | var particleSystem = new Points(particles, pMaterial); 90 | 91 | // add it to the scene 92 | entity.add(new Object3DComponent(particleSystem)); 93 | } 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /example/src/system/PongSystem.ts: -------------------------------------------------------------------------------- 1 | import {Entity, System} from "ecs-lib"; 2 | import {Object3DComponent} from "../component/Object3DComponent"; 3 | import {Vector3} from "three"; 4 | import {PongComponent} from "../component/PongComponent"; 5 | import {SphereComponent} from "../component/SphereComponent"; 6 | 7 | const GRAVITY = 0.0005; 8 | 9 | export default class PongSystem extends System { 10 | 11 | constructor() { 12 | super([ 13 | PongComponent.type, 14 | SphereComponent.type, 15 | Object3DComponent.type, 16 | ], 60); 17 | } 18 | 19 | update(time: number, delta: number, entity: Entity): void { 20 | let object = Object3DComponent.oneFrom(entity); 21 | let sphere = SphereComponent.oneFrom(entity); 22 | let config = PongComponent.oneFrom(entity); 23 | 24 | 25 | config.attr.velocity.y -= (GRAVITY * config.data.mass) * delta; 26 | object.data.position.y += config.attr.velocity.y * delta; 27 | 28 | // Ground, impulse up 29 | let ground = sphere.data.radius; 30 | if (object.data.position.y <= ground) { 31 | object.data.position.y = ground; 32 | config.attr.velocity.y = config.data.impulse; 33 | } 34 | } 35 | 36 | enter(entity: Entity): void { 37 | let config = PongComponent.oneFrom(entity); 38 | if (!config.attr.velocity) { 39 | config.attr.velocity = new Vector3(0, config.data.impulse, 0); 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /example/src/system/SceneObjectSystem.ts: -------------------------------------------------------------------------------- 1 | import {Scene} from "three"; 2 | import {Entity, System} from "ecs-lib"; 3 | import {Object3DComponent} from "../component/Object3DComponent"; 4 | 5 | /** 6 | * All Entity with Object3DComponent will be added and removed from scene 7 | */ 8 | export default class SceneObjectSystem extends System { 9 | 10 | private scene: Scene; 11 | 12 | constructor(scene: Scene) { 13 | super([ 14 | Object3DComponent.type 15 | ]); 16 | 17 | this.scene = scene; 18 | } 19 | 20 | enter(entity: Entity): void { 21 | let model = Object3DComponent.oneFrom(entity); 22 | 23 | if (this.scene.children.indexOf(model.data) < 0) { 24 | this.scene.add(model.data); 25 | } 26 | } 27 | 28 | exit(entity: Entity): void { 29 | let model = Object3DComponent.oneFrom(entity); 30 | this.scene.remove(model.data); 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /example/src/system/SphereFactorySystem.ts: -------------------------------------------------------------------------------- 1 | import {Mesh, MeshBasicMaterial, SphereGeometry} from "three"; 2 | import {Entity, System} from "ecs-lib"; 3 | import {Object3DComponent} from "../component/Object3DComponent"; 4 | import {ColorComponent} from "../component/ColorComponent"; 5 | import {SphereComponent} from "../component/SphereComponent"; 6 | 7 | /** 8 | * Responsible for creating Sphere, when an entity has the color and sphere components. 9 | */ 10 | export default class SphereFactorySystem extends System { 11 | 12 | constructor() { 13 | super([ 14 | ColorComponent.type, 15 | SphereComponent.type 16 | ]); 17 | } 18 | 19 | enter(entity: Entity): void { 20 | let object = Object3DComponent.oneFrom(entity); 21 | if (!object) { 22 | const sphere = SphereComponent.oneFrom(entity).data; 23 | const color = ColorComponent.oneFrom(entity).data; 24 | 25 | const geometry = new SphereGeometry(sphere.radius, sphere.widthSegments, sphere.heightSegments); 26 | const material = new MeshBasicMaterial({color: color}); 27 | const object3d = new Mesh(geometry, material); 28 | 29 | if (sphere.x) { 30 | object3d.position.x = sphere.x; 31 | } 32 | 33 | if (sphere.z) { 34 | object3d.position.z = sphere.z; 35 | } 36 | 37 | // Append new component to entity 38 | entity.add(new Object3DComponent(object3d)); 39 | } 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /example/src/utils/GUISession.ts: -------------------------------------------------------------------------------- 1 | import {GUI, GUIController} from "dat.gui"; 2 | 3 | 4 | export class GUISessionController { 5 | 6 | initSession: (name: string) => void = () => { 7 | } 8 | } 9 | 10 | export default class GUISession { 11 | 12 | private gui: GUI; 13 | 14 | private sessions: Array = []; 15 | 16 | constructor(gui: GUI, group: string) { 17 | // Cria um grupo específico para essa sessão 18 | this.gui = gui.addFolder(group); 19 | 20 | // Já exibe o seu conteúdo 21 | this.gui.open(); 22 | } 23 | 24 | /** 25 | * Salva uma controller na sessão atual 26 | * 27 | * @param instance 28 | */ 29 | private save(instance: GUIController | GUI): GUIController | GUI { 30 | if (this.sessions.indexOf(instance) < 0) { 31 | this.sessions.push(instance); 32 | } 33 | return instance; 34 | } 35 | 36 | destroy() { 37 | this.sessions.forEach(comp => { 38 | if ((comp as GUI).__folders || (comp as GUI).__controllers) { 39 | this.removeFolder(comp as GUI); 40 | } else { 41 | this.remove(comp as GUIController); 42 | } 43 | }); 44 | this.sessions = []; 45 | 46 | // Remove o próprio grupo 47 | this.gui.parent.removeFolder(this.gui); 48 | }; 49 | 50 | add(target: Object, propName: string, itemsOrMin?: number | boolean | string[] | Object, max?: number, step?: number): GUIController { 51 | return this.save(this.gui.add(target, propName, itemsOrMin as any, max, step)) as GUIController; 52 | }; 53 | 54 | addColor(target: Object, propName: string): GUIController { 55 | return this.save(this.gui.addColor(target, propName)) as GUIController; 56 | }; 57 | 58 | addFolder(propName: string): GUI { 59 | return this.save(this.gui.addFolder(propName)) as GUI; 60 | }; 61 | 62 | removeFolder(subFolder: GUI): void { 63 | this.gui.removeFolder(subFolder); 64 | var idx = this.sessions.indexOf(subFolder); 65 | if (idx >= 0) { 66 | this.sessions.splice(idx, 1); 67 | } 68 | }; 69 | 70 | remove(controller: GUIController): void { 71 | this.gui.remove(controller); 72 | var idx = this.sessions.indexOf(controller); 73 | if (idx >= 0) { 74 | this.sessions.splice(idx, 1); 75 | } 76 | }; 77 | 78 | 79 | // remember(target: Object, ...additionalTargets: Object[]): void; 80 | 81 | // listen(controller: GUIController): void; 82 | 83 | // updateDisplay(): void; 84 | } 85 | -------------------------------------------------------------------------------- /example/src/utils/KeyboardState.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * From: http://learningthreejs.com/data/THREEx/THREEx.KeyboardState.js 3 | */ 4 | class KeyboardState { 5 | 6 | static MODIFIERS = ['shift', 'ctrl', 'alt', 'meta']; 7 | 8 | static ALIAS: { 9 | [key: string]: number 10 | } = { 11 | 'left': 37, 12 | 'up': 38, 13 | 'right': 39, 14 | 'down': 40, 15 | 'space': 32, 16 | 'pageup': 33, 17 | 'pagedown': 34, 18 | 'tab': 9 19 | }; 20 | 21 | private keyCodes: { 22 | [key: number]: boolean 23 | } = {}; 24 | 25 | private modifiers: { 26 | [key: string]: boolean 27 | } = {}; 28 | 29 | private onKeyDown = (event: KeyboardEvent) => { 30 | this.onKeyChange(event, true) 31 | }; 32 | 33 | private onKeyUp = (event: KeyboardEvent) => { 34 | this.onKeyChange(event, false); 35 | }; 36 | 37 | private onKeyChange = (event: KeyboardEvent, pressed: boolean) => { 38 | var keyCode = event.keyCode; 39 | 40 | this.keyCodes[keyCode] = pressed; 41 | 42 | // update this.modifiers 43 | this.modifiers['alt'] = event.altKey; 44 | this.modifiers['meta'] = event.metaKey; 45 | this.modifiers['ctrl'] = event.ctrlKey; 46 | this.modifiers['shift'] = event.shiftKey; 47 | }; 48 | 49 | constructor() { 50 | // bind keyEvents 51 | document.addEventListener("keydown", this.onKeyDown, false); 52 | document.addEventListener("keyup", this.onKeyUp, false); 53 | 54 | (window as any).joypadKeyDown = (key: number) => { 55 | this.onKeyDown({ 56 | keyCode: KeyboardState.ALIAS[key], 57 | altKey: false, 58 | metaKey: false, 59 | ctrlKey: false, 60 | shiftKey: false 61 | } as any); 62 | }; 63 | 64 | (window as any).joypadKeyUp = (key: string) => { 65 | this.onKeyUp({ 66 | keyCode: KeyboardState.ALIAS[key], 67 | altKey: false, 68 | metaKey: false, 69 | ctrlKey: false, 70 | shiftKey: false 71 | } as any); 72 | }; 73 | } 74 | 75 | pressed(keyDesc: string) { 76 | 77 | var keys = keyDesc.split("+"); 78 | for (var i = 0; i < keys.length; i++) { 79 | var key = keys[i]; 80 | var pressed; 81 | if (KeyboardState.MODIFIERS.indexOf(key) !== -1) { 82 | pressed = this.modifiers[key]; 83 | } else if (Object.keys(KeyboardState.ALIAS).indexOf(key) != -1) { 84 | pressed = this.keyCodes[KeyboardState.ALIAS[key]]; 85 | } else { 86 | pressed = this.keyCodes[key.toUpperCase().charCodeAt(0)] 87 | } 88 | if (!pressed) return false; 89 | } 90 | 91 | return true; 92 | } 93 | } 94 | 95 | export default new KeyboardState(); 96 | -------------------------------------------------------------------------------- /example/src/utils/cubeEnv.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Obtém um cubo com envmap 3 | * 4 | * @returns {Mesh} 5 | */ 6 | import { 7 | BackSide, 8 | BoxBufferGeometry, 9 | CubeTextureLoader, 10 | Mesh, 11 | PerspectiveCamera, 12 | RGBFormat, 13 | Scene, 14 | ShaderLib, 15 | ShaderMaterial, 16 | WebGLRenderer 17 | } from "three"; 18 | 19 | const cubeTextureLoader = new CubeTextureLoader(); 20 | 21 | /** 22 | * http://www.humus.name/index.php?page=Textures&start=0 23 | * https://www.cleanpng.com/free/skybox.html 24 | * http://www.custommapmakers.org/skyboxes.php 25 | * https://opengameart.org/art-search-advanced?field_art_tags_tid=skybox 26 | * 27 | * @param name 28 | */ 29 | function loadCubemap(name: string) { 30 | var format = '.jpg'; 31 | var parts = name.split('.'); 32 | if (parts.length > 1) { 33 | name = parts[0]; 34 | format = '.' + parts[1]; 35 | } 36 | 37 | var path = './envmap/' + name + '/'; 38 | // px = positive x 39 | // nx = negative x 40 | // py = positive y 41 | // ny = negative y 42 | // pz = positive z 43 | // nz = negative z 44 | var urls = [ 45 | path + 'px' + format, path + 'nx' + format, 46 | path + 'py' + format, path + 'ny' + format, 47 | path + 'pz' + format, path + 'nz' + format 48 | ]; 49 | 50 | var textureCube = cubeTextureLoader.load(urls); 51 | textureCube.format = RGBFormat; 52 | // textureCube.mapping = CubeReflectionMapping; 53 | // textureCube.encoding = sRGBEncoding; 54 | return textureCube; 55 | } 56 | 57 | export function createCubeEnv(texture: string, aspect: number, renderer: WebGLRenderer) { 58 | 59 | var textureCube = loadCubemap(texture); 60 | 61 | var shader = ShaderLib.cube; 62 | var material = new ShaderMaterial({ 63 | fragmentShader: shader.fragmentShader, 64 | vertexShader: shader.vertexShader, 65 | uniforms: shader.uniforms, 66 | depthWrite: false, 67 | side: BackSide 68 | }); 69 | material.uniforms.tCube.value = textureCube; 70 | 71 | 72 | Object.defineProperty(material, 'map', { 73 | get: function () { 74 | return this.uniforms.tCube.value; 75 | } 76 | }); 77 | 78 | let geometry = new BoxBufferGeometry(100, 100, 100); 79 | var mesh = new Mesh(geometry, material); 80 | 81 | var cubeScene = new Scene(); 82 | var cubeCamera = new PerspectiveCamera(70, aspect, 1, 100000); 83 | 84 | cubeScene.add(mesh); 85 | 86 | renderer.autoClearColor = false; 87 | 88 | // Sobrescrever o método antigo para renderizar o cubemap antes 89 | var oldrender = renderer.render; 90 | var render = oldrender.bind(renderer); 91 | renderer.render = function (scene, camera) { 92 | cubeCamera.rotation.copy(camera.rotation); 93 | render(cubeScene, cubeCamera); 94 | render(scene, camera); 95 | }; 96 | 97 | return { 98 | scene: cubeScene, 99 | camera: cubeCamera, 100 | destroy: function () { 101 | cubeScene.remove(mesh); 102 | geometry.dispose(); 103 | material.dispose(); 104 | textureCube.dispose(); 105 | 106 | renderer.render = oldrender; 107 | }, 108 | onResize: function (aspect: number) { 109 | cubeCamera.aspect = aspect; 110 | cubeCamera.updateProjectionMatrix(); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./web-ts", 4 | "outDir": "./dist/assets/js", 5 | "noImplicitAny": true, 6 | "sourceMap": true, 7 | "module": "es6", 8 | "target": "es5", 9 | "jsx": "react", 10 | "allowJs": true, 11 | "allowSyntheticDefaultImports": true, 12 | "moduleResolution": "node", 13 | "lib": [ 14 | "dom", 15 | "es2015" 16 | ], 17 | "types": [ 18 | "three" 19 | ] 20 | }, 21 | "exclude": [ 22 | "node_modules" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const output = path.resolve(__dirname, 'dist', 'assets', 'js'); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | let config = { 5 | mode: 'development', 6 | entry: { 7 | game: './src/main.tsx' 8 | }, 9 | output: { 10 | path: output, 11 | filename: '[name].bundle.js', 12 | publicPath: 'assets/js' 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.ts(x?)$/, 18 | use: 'ts-loader', 19 | exclude: /node_modules/, 20 | } 21 | ], 22 | }, 23 | resolve: { 24 | extensions: ['.tsx', '.ts', '.js'], 25 | }, 26 | devServer: { 27 | contentBase: [path.join(__dirname, 'public')] 28 | }, 29 | plugins: [ 30 | new CopyPlugin([ 31 | { 32 | from: path.resolve(__dirname, 'public'), 33 | to: path.resolve(__dirname, 'dist') 34 | } 35 | ]), 36 | ], 37 | }; 38 | 39 | module.exports = (env, argv) => { 40 | config.devtool = 'source-map'; 41 | return config; 42 | }; 43 | -------------------------------------------------------------------------------- /logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/logo.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecs-lib", 3 | "version": "0.8.0-pre.2", 4 | "description": "Tiny and easy to use ECS (Entity Component System) library for game programming and much more", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "nyc mocha", 8 | "coverage": "nyc report --reporter=text-lcov | coveralls", 9 | "build": "npm run-script build-ts && npm run-script build-js", 10 | "build-js": "browserify src/index.ts -p [ tsify --emitDeclarationOnly=false ] -s ecs-lib > index.js", 11 | "build-ts": "tsc -p tsconfig.json", 12 | "publish-lib": "node v-release.js && node publish.js", 13 | "publish-lib-snapshot": "node v-snapshot.js && node publish.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/nidorx/ecs-lib.git" 18 | }, 19 | "author": "Alex Rodin ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/nidorx/ecs-lib/issues" 23 | }, 24 | "homepage": "https://github.com/nidorx/ecs-lib#readme", 25 | "devDependencies": { 26 | "@istanbuljs/nyc-config-typescript": "0.1.3", 27 | "@types/chai": "4.2.7", 28 | "@types/mocha": "5.2.7", 29 | "@types/node": "12.12.17", 30 | "browserify": "16.5.0", 31 | "chai": "4.2.0", 32 | "coveralls": "3.0.9", 33 | "mocha": "6.2.2", 34 | "nyc": "14.1.1", 35 | "semver": "6.3.0", 36 | "source-map-support": "0.5.16", 37 | "ts-node": "8.5.4", 38 | "tsify": "4.0.1", 39 | "typescript": "3.7.3" 40 | }, 41 | "files": [ 42 | "index.js", 43 | "index.d.ts", 44 | "LICENSE", 45 | "README.md" 46 | ], 47 | "keywords": [ 48 | "ecs", 49 | "ecs-framework", 50 | "entity-component", 51 | "entity-component-system", 52 | "entity", 53 | "game", 54 | "game-engine", 55 | "gamedev", 56 | "game-development", 57 | "game-ecs", 58 | "gameloop", 59 | "threejs" 60 | ], 61 | "nyc": { 62 | "extension": [ 63 | ".ts", 64 | ".tsx" 65 | ], 66 | "exclude": [ 67 | "*.*", 68 | "**/*.d.ts", 69 | "example/**/*.*", 70 | "coverage/**/*.*", 71 | "test/**/*.*" 72 | ], 73 | "reporter": [ 74 | "html", 75 | "lcov", 76 | "text-summary" 77 | ], 78 | "all": true 79 | } 80 | } -------------------------------------------------------------------------------- /publish.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Faz o build publicação no repositorio e tag do github. 3 | * 4 | * O versionamento do package.json não é feito automaticamente, afim de permitir um maior controle sobre o deploy. 5 | * 6 | * Os passos para usar esses script são: 7 | * 8 | * 1 - Após fazer alterações de código, conduzir normalmente com os commits no git 9 | * 2 - No momento de fazer a publicação de uma versão, no terminal: 10 | * a) git add --all 11 | * b) git commit -m "Mensagem das alterações feitas" 12 | * c) node ./publish.js 13 | */ 14 | 15 | const fs = require('fs'); 16 | const cpExec = require('child_process').exec; 17 | 18 | function exec(command, callback) { 19 | callback = callback || function () { 20 | }; 21 | 22 | return new Promise(function (accept, reject) { 23 | console.log('[' + command + ']'); 24 | const com = cpExec(command); 25 | 26 | com.stdout.on('data', function (data) { 27 | console.log(data.toString()); 28 | }); 29 | 30 | com.stderr.on('data', function (data) { 31 | console.error(data.toString()); 32 | }); 33 | 34 | com.on('exit', function (code, signal) { 35 | if (signal) { 36 | reject({ 37 | code: code, 38 | signal: signal 39 | }); 40 | callback(code); 41 | } else { 42 | accept({ 43 | code: code, 44 | signal: signal 45 | }); 46 | callback(null, signal); 47 | } 48 | }); 49 | }); 50 | } 51 | 52 | if (fs.existsSync('./index.js')) { 53 | fs.unlinkSync('./index.js'); 54 | } 55 | 56 | if (fs.existsSync('./index.d.ts')) { 57 | fs.unlinkSync('./index.d.ts'); 58 | } 59 | 60 | var package = JSON.parse(fs.readFileSync(__dirname + '/package.json')); 61 | 62 | exec('npm run-script build') 63 | .then(exec.bind(undefined, 'npm publish', null)) 64 | .then(exec.bind(undefined, 'git add --all', null)) 65 | .then(exec.bind(undefined, 'git commit -m "Publicação da versão v' + package.version + '"', null)) 66 | .then(exec.bind(undefined, 'git push', null)) 67 | .then(exec.bind(undefined, 'git tag v' + package.version, null)) 68 | .then(exec.bind(undefined, 'git push --tags', null)) 69 | .catch(err => { 70 | console.error(err); 71 | }); 72 | -------------------------------------------------------------------------------- /repository-open-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/repository-open-graph.png -------------------------------------------------------------------------------- /repository-open-graph.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidorx/ecs-lib/e4738e088e4653b6ae37ad5d45cac0d2e00be6d9/repository-open-graph.psd -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | let NOW: () => number; 2 | 3 | // Include a performance.now polyfill. 4 | // In node.js, use process.hrtime. 5 | // @ts-ignore 6 | if (typeof (self) === 'undefined' && typeof (process) !== 'undefined' && process.hrtime) { 7 | NOW = function () { 8 | // @ts-ignore 9 | var time = process.hrtime(); 10 | 11 | // Convert [seconds, nanoseconds] to milliseconds. 12 | return time[0] * 1000 + time[1] / 1000000; 13 | }; 14 | } 15 | // In a browser, use self.performance.now if it is available. 16 | else if (typeof (self) !== 'undefined' && self.performance !== undefined && self.performance.now !== undefined) { 17 | // This must be bound, because directly assigning this function 18 | // leads to an invocation exception in Chrome. 19 | NOW = self.performance.now.bind(self.performance); 20 | } 21 | // Use Date.now if it is available. 22 | else if (Date.now !== undefined) { 23 | NOW = Date.now; 24 | } 25 | // Otherwise, use 'new Date().getTime()'. 26 | else { 27 | NOW = function () { 28 | return new Date().getTime(); 29 | }; 30 | } 31 | 32 | let SEQ_SYSTEM = 1; 33 | 34 | let SEQ_ENTITY = 1; 35 | 36 | let SEQ_COMPONENT = 1; 37 | 38 | /** 39 | * Utility class for asynchronous access to a list 40 | */ 41 | export class Iterator { 42 | 43 | private end = false; 44 | 45 | private cache: T[] = []; 46 | 47 | private next: (i: number) => T | void; 48 | 49 | constructor(next: (i: number) => T | void) { 50 | this.next = next; 51 | } 52 | 53 | /** 54 | * Allows iterate across all items 55 | * 56 | * @param cb 57 | */ 58 | each(cb: (item: T) => boolean | void) { 59 | let index = 0; 60 | while (true) { 61 | let value; 62 | if (this.cache.length <= index) { 63 | if (this.end) { 64 | break; 65 | } 66 | 67 | value = this.next(index++); 68 | if (value === undefined) { 69 | this.end = true; 70 | break; 71 | } 72 | this.cache.push(value); 73 | } else { 74 | value = this.cache[index++]; 75 | } 76 | 77 | if (cb(value) === false) { 78 | break; 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * returns the value of the first element that satisfies the provided testing function. 85 | * 86 | * @param test 87 | */ 88 | find(test: (item: T) => boolean): T | undefined { 89 | let out = undefined; 90 | this.each((item) => { 91 | if (test(item)) { 92 | out = item; 93 | // break 94 | return false 95 | } 96 | }); 97 | return out; 98 | } 99 | 100 | /** 101 | * creates a array with all elements that pass the test implemented by the provided function. 102 | * 103 | * @param test 104 | */ 105 | filter(test: (item: T) => boolean): T[] { 106 | let list: T[] = []; 107 | this.each((item) => { 108 | if (test(item)) { 109 | list.push(item); 110 | } 111 | }); 112 | return list; 113 | } 114 | 115 | /** 116 | * creates a new array with the results of calling a provided function on every element in this iterator. 117 | * 118 | * @param cb 119 | */ 120 | map

(cb: (item: T) => P): P[] { 121 | let list: P[] = []; 122 | this.each((item) => { 123 | list.push(cb(item)); 124 | }); 125 | return list; 126 | } 127 | } 128 | 129 | export type Susbcription = (entity: Entity, added?: Component, removed?: Component) => void; 130 | 131 | /** 132 | * Representation of an entity in ECS 133 | */ 134 | export abstract class Entity { 135 | 136 | /** 137 | * Lista de interessados sobre a atualiação dos componentes 138 | */ 139 | private subscriptions: Array = []; 140 | 141 | /** 142 | * Components by type 143 | */ 144 | private components: { 145 | [key: number]: Component[] 146 | } = {}; 147 | 148 | public id: number; 149 | 150 | /** 151 | * Informs if the entity is active 152 | */ 153 | public active: boolean = true; 154 | 155 | constructor() { 156 | this.id = SEQ_ENTITY++; 157 | } 158 | 159 | /** 160 | * Allows interested parties to receive information when this entity's component list is updated 161 | * 162 | * @param handler 163 | */ 164 | public subscribe(handler: Susbcription): () => Entity { 165 | this.subscriptions.push(handler); 166 | 167 | return () => { 168 | const idx = this.subscriptions.indexOf(handler); 169 | if (idx >= 0) { 170 | this.subscriptions.splice(idx, 1); 171 | } 172 | return this; 173 | } 174 | } 175 | 176 | /** 177 | * Add a component to this entity 178 | * 179 | * @param component 180 | */ 181 | public add(component: Component) { 182 | const type = component.type; 183 | if (!this.components[type]) { 184 | this.components[type] = []; 185 | } 186 | 187 | if (this.components[type].indexOf(component) >= 0) { 188 | return 189 | } 190 | 191 | this.components[type].push(component); 192 | 193 | // Informa aos interessados sobre a atualização 194 | this.subscriptions.forEach(cb => cb(this, component, undefined)); 195 | } 196 | 197 | /** 198 | * Removes a component's reference from this entity 199 | * 200 | * @param component 201 | */ 202 | public remove(component: Component) { 203 | const type = component.type; 204 | if (!this.components[type]) { 205 | return; 206 | } 207 | 208 | const idx = this.components[type].indexOf(component); 209 | if (idx >= 0) { 210 | this.components[type].splice(idx, 1); 211 | 212 | if (this.components[type].length < 1) { 213 | delete this.components[type]; 214 | } 215 | 216 | // Informa aos interessados sobre a atualização 217 | this.subscriptions.forEach(cb => cb(this, undefined, component)); 218 | } 219 | } 220 | } 221 | 222 | /** 223 | * Force typing 224 | */ 225 | export type ComponentClassType

= (new (data: P) => Component

) & { 226 | 227 | /** 228 | * Static reference to type id 229 | */ 230 | type: number; 231 | 232 | /** 233 | * Get all instances of this component from entity 234 | * 235 | * @param entity 236 | */ 237 | allFrom(entity: Entity): Component

[]; 238 | 239 | /** 240 | * Get one instance of this component from entity 241 | * 242 | * @param entity 243 | */ 244 | oneFrom(entity: Entity): Component

; 245 | } 246 | 247 | /** 248 | * Representation of a component in ECS 249 | */ 250 | export abstract class Component { 251 | 252 | /** 253 | * Register a new component class 254 | */ 255 | public static register

(): ComponentClassType

{ 256 | const typeID = SEQ_COMPONENT++; 257 | 258 | class ComponentImpl extends Component

{ 259 | 260 | static type = typeID; 261 | 262 | static allFrom(entity: Entity): ComponentImpl[] { 263 | let components: ComponentImpl[] = (entity as any).components[typeID]; 264 | return components || []; 265 | } 266 | 267 | static oneFrom(entity: Entity): ComponentImpl | undefined { 268 | let components = ComponentImpl.allFrom(entity); 269 | if (components && components.length > 0) { 270 | return components[0]; 271 | } 272 | } 273 | 274 | /** 275 | * Create a new instance of this custom component 276 | * 277 | * @param data 278 | */ 279 | constructor(data: P) { 280 | super(typeID, data); 281 | } 282 | } 283 | 284 | return (ComponentImpl as any) as ComponentClassType

; 285 | } 286 | 287 | public type: number; 288 | 289 | public data: T; 290 | 291 | /** 292 | * A component can have attributes. Attributes are secondary values used to save miscellaneous data required by some 293 | * specialized systems. 294 | */ 295 | public attr: { 296 | [key: string]: any 297 | } = {}; 298 | 299 | constructor(type: number, data: T) { 300 | this.type = type; 301 | this.data = data; 302 | } 303 | } 304 | 305 | /** 306 | * System callback 307 | */ 308 | export type EventCallback = (data: any, entities: Iterator) => void; 309 | 310 | /** 311 | * Represents the logic that transforms component data of an entity from its current state to its next state. A system 312 | * runs on entities that have a specific set of component types. 313 | */ 314 | export abstract class System { 315 | 316 | /** 317 | * IDs of the types of components this system expects the entity to have before it can act on. If you want to 318 | * create a system that acts on all entities, enter [-1] 319 | */ 320 | private readonly componentTypes: number[] = []; 321 | 322 | private readonly callbacks: { [key: string]: Array } = {}; 323 | 324 | /** 325 | * Unique identifier of an instance of this system 326 | */ 327 | public readonly id: number; 328 | 329 | /** 330 | * The maximum times per second this system should be updated 331 | */ 332 | public frequence: number; 333 | 334 | /** 335 | * Reference to the world, changed at runtime during interactions. 336 | */ 337 | protected world: ECS = undefined as any; 338 | 339 | /** 340 | * Allows to trigger any event. Systems interested in this event will be notified immediately 341 | * 342 | * Injected by ECS at runtime 343 | * 344 | * @param event 345 | * @param data 346 | */ 347 | protected trigger: (event: string, data: any) => void = undefined as any; 348 | 349 | /** 350 | * Invoked before updating entities available for this system. It is only invoked when there are entities with the 351 | * characteristics expected by this system. 352 | * 353 | * @param time 354 | */ 355 | public beforeUpdateAll?(time: number): void; 356 | 357 | /** 358 | * Invoked in updates, limited to the value set in the "frequency" attribute 359 | * 360 | * @param time 361 | * @param delta 362 | * @param entity 363 | */ 364 | public update?(time: number, delta: number, entity: Entity): void; 365 | 366 | /** 367 | * Invoked after performing update of entities available for this system. It is only invoked when there are entities 368 | * with the characteristics expected by this system. 369 | * 370 | * @param time 371 | */ 372 | public afterUpdateAll?(time: number, entities: Entity[]): void; 373 | 374 | /** 375 | * Invoked when an expected feature of this system is added or removed from the entity 376 | * 377 | * @param entity 378 | * @param added 379 | * @param removed 380 | */ 381 | public change?(entity: Entity, added?: Component, removed?: Component): void; 382 | 383 | /** 384 | * Invoked when: 385 | * a) An entity with the characteristics (components) expected by this system is added in the world; 386 | * b) This system is added in the world and this world has one or more entities with the characteristics expected by 387 | * this system; 388 | * c) An existing entity in the same world receives a new component at runtime and all of its new components match 389 | * the standard expected by this system. 390 | * 391 | * @param entity 392 | */ 393 | public enter?(entity: Entity): void; 394 | 395 | /** 396 | * Invoked when: 397 | * a) An entity with the characteristics (components) expected by this system is removed from the world; 398 | * b) This system is removed from the world and this world has one or more entities with the characteristics 399 | * expected by this system; 400 | * c) An existing entity in the same world loses a component at runtime and its new component set no longer matches 401 | * the standard expected by this system 402 | * 403 | * @param entity 404 | */ 405 | public exit?(entity: Entity): void; 406 | 407 | /** 408 | * @param componentTypes IDs of the types of components this system expects the entity to have before it can act on. 409 | * If you want to create a system that acts on all entities, enter [-1] 410 | * @param frequence The maximum times per second this system should be updated. Defaults 0 411 | */ 412 | constructor(componentTypes: number[], frequence: number = 0) { 413 | this.id = SEQ_SYSTEM++; 414 | this.componentTypes = componentTypes; 415 | this.frequence = frequence; 416 | } 417 | 418 | /** 419 | * Allows you to search in the world for all entities that have a specific set of components. 420 | * 421 | * @param componentTypes Enter [-1] to list all entities 422 | */ 423 | protected query(componentTypes: number[]): Iterator { 424 | return this.world.query(componentTypes); 425 | } 426 | 427 | /** 428 | * Allows the system to listen for a specific event that occurred during any update. 429 | * 430 | * In callback, the system has access to the existing entities in the world that are processed by this system, in 431 | * the form of an Iterator, and the raw data sent by the event trigger. 432 | * 433 | * ATTENTION! The callback method will be invoked immediately after the event fires, avoid heavy processing. 434 | * 435 | * @param event 436 | * @param callback 437 | * @param once Allows you to perform the callback only once 438 | */ 439 | protected listenTo(event: string, callback: EventCallback, once?: boolean) { 440 | if (!this.callbacks.hasOwnProperty(event)) { 441 | this.callbacks[event] = []; 442 | } 443 | 444 | if (once) { 445 | let tmp = callback.bind(this); 446 | 447 | callback = (data: any, entities: Iterator) => { 448 | 449 | tmp(data, entities); 450 | 451 | let idx = this.callbacks[event].indexOf(callback); 452 | if (idx >= 0) { 453 | this.callbacks[event].splice(idx, 1); 454 | } 455 | if (this.callbacks[event].length === 0) { 456 | delete this.callbacks[event]; 457 | } 458 | } 459 | } 460 | 461 | this.callbacks[event].push(callback); 462 | } 463 | } 464 | 465 | /** 466 | * The very definition of the ECS. Also called Admin or Manager in other implementations. 467 | */ 468 | export default class ECS { 469 | 470 | public static System = System; 471 | 472 | public static Entity = Entity; 473 | 474 | public static Component = Component; 475 | 476 | /** 477 | * All systems in this world 478 | */ 479 | private systems: System[] = []; 480 | 481 | /** 482 | * All entities in this world 483 | */ 484 | private entities: Entity[] = []; 485 | 486 | /** 487 | * Indexes the systems that must be run for each entity 488 | */ 489 | private entitySystems: { [key: number]: System[] } = {}; 490 | 491 | /** 492 | * Records the last instant a system was run in this world for an entity, using real time 493 | */ 494 | private entitySystemLastUpdate: { [key: number]: { [key: number]: number } } = {}; 495 | 496 | /** 497 | * Records the last instant a system was run in this world for an entity, using game time 498 | */ 499 | private entitySystemLastUpdateGame: { [key: number]: { [key: number]: number } } = {}; 500 | 501 | /** 502 | * Saves subscriptions made to entities 503 | */ 504 | private entitySubscription: { [key: number]: () => void } = {}; 505 | 506 | /** 507 | * Injection for the system trigger method 508 | * 509 | * @param event 510 | * @param data 511 | */ 512 | private systemTrigger = (event: string, data: any) => { 513 | this.systems.forEach(system => { 514 | 515 | let callbacks: { 516 | [key: string]: Array 517 | } = (system as any).callbacks; 518 | 519 | if (callbacks.hasOwnProperty(event) && callbacks[event].length > 0) { 520 | this.inject(system); 521 | let entitiesIterator = this.query((system as any).componentTypes); 522 | callbacks[event].forEach(callback => { 523 | callback(data, entitiesIterator); 524 | }); 525 | } 526 | }) 527 | }; 528 | 529 | /** 530 | * Allows you to apply slow motion effect on systems execution. When timeScale is 1, the timestamp and delta 531 | * parameters received by the systems are consistent with the actual timestamp. When timeScale is 0.5, the values 532 | * received by systems will be half of the actual value. 533 | * 534 | * ATTENTION! The systems continue to be invoked obeying their normal frequencies, what changes is only the values 535 | * received in the timestamp and delta parameters. 536 | */ 537 | public timeScale: number = 1; 538 | 539 | /** 540 | * Last execution of update method 541 | */ 542 | private lastUpdate: number = NOW(); 543 | 544 | /** 545 | * The timestamp of the game, different from the real world, is updated according to timeScale. If at no time does 546 | * the timeScale change, the value is the same as the current timestamp. 547 | * 548 | * This value is sent to the systems update method. 549 | */ 550 | private gameTime: number = 0; 551 | 552 | constructor(systems?: System[]) { 553 | if (systems) { 554 | systems.forEach(system => { 555 | this.addSystem(system); 556 | }); 557 | } 558 | } 559 | 560 | /** 561 | * Remove all entities and systems 562 | */ 563 | public destroy() { 564 | this.entities.forEach((entity) => { 565 | this.removeEntity(entity); 566 | }); 567 | 568 | this.systems.forEach(system => { 569 | this.removeSystem(system); 570 | }); 571 | } 572 | 573 | /** 574 | * Get an entity by id 575 | * 576 | * @param id 577 | */ 578 | public getEntity(id: number): Entity | undefined { 579 | return this.entities.find(entity => entity.id === id); 580 | } 581 | 582 | /** 583 | * Add an entity to this world 584 | * 585 | * @param entity 586 | */ 587 | public addEntity(entity: Entity) { 588 | if (!entity || this.entities.indexOf(entity) >= 0) { 589 | return; 590 | } 591 | 592 | this.entities.push(entity); 593 | this.entitySystemLastUpdate[entity.id] = {}; 594 | this.entitySystemLastUpdateGame[entity.id] = {}; 595 | 596 | // Remove subscription 597 | if (this.entitySubscription[entity.id]) { 598 | this.entitySubscription[entity.id](); 599 | } 600 | 601 | // Add new subscription 602 | this.entitySubscription[entity.id] = entity 603 | .subscribe((entity, added, removed) => { 604 | this.onEntityUpdate(entity, added, removed); 605 | this.indexEntity(entity); 606 | }); 607 | 608 | this.indexEntity(entity); 609 | } 610 | 611 | /** 612 | * Remove an entity from this world 613 | * 614 | * @param idOrInstance 615 | */ 616 | public removeEntity(idOrInstance: number | Entity) { 617 | let entity: Entity = idOrInstance as Entity; 618 | if (typeof idOrInstance === 'number') { 619 | entity = this.getEntity(idOrInstance) as Entity; 620 | } 621 | 622 | if (!entity) { 623 | return; 624 | } 625 | 626 | const idx = this.entities.indexOf(entity); 627 | if (idx >= 0) { 628 | this.entities.splice(idx, 1); 629 | } 630 | 631 | // Remove subscription, if any 632 | if (this.entitySubscription[entity.id]) { 633 | this.entitySubscription[entity.id](); 634 | } 635 | 636 | // Invoke system exit 637 | let systems = this.entitySystems[entity.id]; 638 | if (systems) { 639 | systems.forEach(system => { 640 | if (system.exit) { 641 | this.inject(system); 642 | system.exit(entity as Entity); 643 | } 644 | }); 645 | } 646 | 647 | // Remove associative indexes 648 | delete this.entitySystems[entity.id]; 649 | delete this.entitySystemLastUpdate[entity.id]; 650 | delete this.entitySystemLastUpdateGame[entity.id]; 651 | } 652 | 653 | /** 654 | * Add a system in this world 655 | * 656 | * @param system 657 | */ 658 | public addSystem(system: System) { 659 | if (!system) { 660 | return; 661 | } 662 | 663 | if (this.systems.indexOf(system) >= 0) { 664 | return; 665 | } 666 | 667 | this.systems.push(system); 668 | 669 | // Indexes entities 670 | this.entities.forEach(entity => { 671 | this.indexEntity(entity, system); 672 | }); 673 | 674 | // Invokes system enter 675 | this.entities.forEach(entity => { 676 | if (entity.active) { 677 | let systems = this.entitySystems[entity.id]; 678 | if (systems && systems.indexOf(system) >= 0) { 679 | if (system.enter) { 680 | this.inject(system); 681 | system.enter(entity); 682 | } 683 | } 684 | } 685 | }); 686 | } 687 | 688 | /** 689 | * Remove a system from this world 690 | * 691 | * @param system 692 | */ 693 | public removeSystem(system: System) { 694 | if (!system) { 695 | return; 696 | } 697 | 698 | const idx = this.systems.indexOf(system); 699 | if (idx >= 0) { 700 | // Invoke system exit 701 | this.entities.forEach(entity => { 702 | if (entity.active) { 703 | let systems = this.entitySystems[entity.id]; 704 | if (systems && systems.indexOf(system) >= 0) { 705 | if (system.exit) { 706 | this.inject(system); 707 | system.exit(entity); 708 | } 709 | } 710 | } 711 | }); 712 | 713 | this.systems.splice(idx, 1); 714 | 715 | if ((system as any).world === this) { 716 | (system as any).world = undefined; 717 | (system as any).trigger = undefined; 718 | } 719 | 720 | // Indexes entities 721 | this.entities.forEach(entity => { 722 | this.indexEntity(entity, system); 723 | }); 724 | } 725 | } 726 | 727 | /** 728 | * Allows you to search for all entities that have a specific set of components. 729 | * 730 | * @param componentTypes Enter [-1] to list all entities 731 | */ 732 | public query(componentTypes: number[]): Iterator { 733 | let index = 0; 734 | let listAll = componentTypes.indexOf(-1) >= 0; 735 | 736 | return new Iterator(() => { 737 | outside: 738 | for (let l = this.entities.length; index < l; index++) { 739 | let entity = this.entities[index]; 740 | if (listAll) { 741 | // Prevents unnecessary processing 742 | return entity; 743 | } 744 | 745 | // -1 = All components. Allows to query for all entities in the world. 746 | const entityComponentIDs: number[] = [-1].concat( 747 | Object.keys((entity as any).components).map(v => Number.parseInt(v, 10)) 748 | ); 749 | 750 | for (var a = 0, j = componentTypes.length; a < j; a++) { 751 | if (entityComponentIDs.indexOf(componentTypes[a]) < 0) { 752 | continue outside; 753 | } 754 | } 755 | 756 | // Entity has all the components 757 | return entity; 758 | } 759 | }); 760 | } 761 | 762 | 763 | /** 764 | * Invokes the "update" method of the systems in this world. 765 | */ 766 | public update() { 767 | let now = NOW(); 768 | 769 | // adds scaledDelta 770 | this.gameTime += (now - this.lastUpdate) * this.timeScale; 771 | this.lastUpdate = now; 772 | 773 | let toCallAfterUpdateAll: { 774 | [key: string]: { 775 | system: System; 776 | entities: Entity[]; 777 | } 778 | } = {}; 779 | 780 | 781 | this.entities.forEach(entity => { 782 | if (!entity.active) { 783 | // Entidade inativa 784 | return this.removeEntity(entity); 785 | } 786 | 787 | let systems = this.entitySystems[entity.id]; 788 | if (!systems) { 789 | return; 790 | } 791 | 792 | const entityLastUpdates = this.entitySystemLastUpdate[entity.id]; 793 | const entityLastUpdatesGame = this.entitySystemLastUpdateGame[entity.id]; 794 | let elapsed, elapsedScaled, interval; 795 | 796 | systems.forEach(system => { 797 | if (system.update) { 798 | this.inject(system); 799 | 800 | elapsed = now - entityLastUpdates[system.id]; 801 | elapsedScaled = this.gameTime - entityLastUpdatesGame[system.id]; 802 | 803 | 804 | // Limit FPS 805 | if (system.frequence > 0) { 806 | interval = 1000 / system.frequence; 807 | if (elapsed < interval) { 808 | return; 809 | } 810 | 811 | // adjust for fpsInterval not being a multiple of RAF's interval (16.7ms) 812 | entityLastUpdates[system.id] = now - (elapsed % interval); 813 | entityLastUpdatesGame[system.id] = this.gameTime; 814 | } else { 815 | entityLastUpdates[system.id] = now; 816 | entityLastUpdatesGame[system.id] = this.gameTime; 817 | } 818 | 819 | let id = `_` + system.id; 820 | if (!toCallAfterUpdateAll[id]) { 821 | // Call afterUpdateAll 822 | if (system.beforeUpdateAll) { 823 | system.beforeUpdateAll(this.gameTime); 824 | } 825 | 826 | // Save for afterUpdateAll 827 | toCallAfterUpdateAll[id] = { 828 | system: system, 829 | entities: [] 830 | }; 831 | } 832 | toCallAfterUpdateAll[id].entities.push(entity); 833 | 834 | // Call update 835 | system.update(this.gameTime, elapsedScaled, entity); 836 | } 837 | }); 838 | }); 839 | 840 | 841 | // Call afterUpdateAll 842 | for (var attr in toCallAfterUpdateAll) { 843 | if (!toCallAfterUpdateAll.hasOwnProperty(attr)) { 844 | continue; 845 | } 846 | 847 | let system = toCallAfterUpdateAll[attr].system; 848 | if (system.afterUpdateAll) { 849 | this.inject(system); 850 | system.afterUpdateAll(this.gameTime, toCallAfterUpdateAll[attr].entities); 851 | } 852 | } 853 | toCallAfterUpdateAll = {}; 854 | } 855 | 856 | /** 857 | * Injects the execution context into the system. 858 | * 859 | * A system can exist on several worlds at the same time, ECS ensures that global methods will always reference the 860 | * currently running world. 861 | * 862 | * @param system 863 | */ 864 | private inject(system: System): System { 865 | (system as any).world = this; 866 | (system as any).trigger = this.systemTrigger; 867 | return system; 868 | } 869 | 870 | /** 871 | * When an entity receives or loses components, invoking the change method of the systems 872 | * 873 | * @param entity 874 | */ 875 | private onEntityUpdate(entity: Entity, added?: Component, removed?: Component) { 876 | if (!this.entitySystems[entity.id]) { 877 | return; 878 | } 879 | 880 | const toNotify: System[] = this.entitySystems[entity.id].slice(0); 881 | 882 | outside: 883 | for (var idx = toNotify.length - 1; idx >= 0; idx--) { 884 | let system = toNotify[idx]; 885 | 886 | // System is listening to updates on entity? 887 | if (system.change) { 888 | 889 | let systemComponentTypes = (system as any).componentTypes; 890 | 891 | // Listen to all component type 892 | if (systemComponentTypes.indexOf(-1) >= 0) { 893 | continue; 894 | } 895 | 896 | if (added && systemComponentTypes.indexOf(added.type) >= 0) { 897 | continue outside; 898 | } 899 | 900 | if (removed && systemComponentTypes.indexOf(removed.type) >= 0) { 901 | continue outside; 902 | } 903 | } 904 | 905 | // dont match 906 | toNotify.splice(idx, 1); 907 | } 908 | 909 | // Notify systems 910 | toNotify.forEach(system => { 911 | system = this.inject(system); 912 | const systemComponentTypes = (system as any).componentTypes; 913 | const all = systemComponentTypes.indexOf(-1) >= 0; 914 | (system.change as any)( 915 | entity, 916 | // Send only the list of components this system expects 917 | all 918 | ? added 919 | : ( 920 | added && systemComponentTypes.indexOf(added.type) >= 0 921 | ? added 922 | : undefined 923 | ), 924 | all 925 | ? removed 926 | : ( 927 | removed && systemComponentTypes.indexOf(removed.type) >= 0 928 | ? removed 929 | : undefined 930 | ) 931 | ); 932 | }); 933 | } 934 | 935 | private indexEntitySystem = (entity: Entity, system: System) => { 936 | const idx = this.entitySystems[entity.id].indexOf(system); 937 | 938 | // Sistema não existe neste mundo, remove indexação 939 | if (this.systems.indexOf(system) < 0) { 940 | if (idx >= 0) { 941 | this.entitySystems[entity.id].splice(idx, 1); 942 | delete this.entitySystemLastUpdate[entity.id][system.id]; 943 | delete this.entitySystemLastUpdateGame[entity.id][system.id]; 944 | } 945 | return; 946 | } 947 | 948 | const systemComponentTypes = (system as any).componentTypes; 949 | 950 | for (var a = 0, l = systemComponentTypes.length; a < l; a++) { 951 | // -1 = All components. Allows a system to receive updates from all entities in the world. 952 | let entityComponentIDs: number[] = [-1].concat( 953 | Object.keys((entity as any).components).map(v => Number.parseInt(v, 10)) 954 | ); 955 | if (entityComponentIDs.indexOf(systemComponentTypes[a]) < 0) { 956 | // remove 957 | if (idx >= 0) { 958 | // Informs the system of relationship removal 959 | if (system.exit) { 960 | this.inject(system); 961 | system.exit(entity); 962 | } 963 | 964 | this.entitySystems[entity.id].splice(idx, 1); 965 | delete this.entitySystemLastUpdate[entity.id][system.id]; 966 | delete this.entitySystemLastUpdateGame[entity.id][system.id]; 967 | } 968 | return 969 | } 970 | } 971 | 972 | // Entity has all the components this system needs 973 | if (idx < 0) { 974 | this.entitySystems[entity.id].push(system); 975 | this.entitySystemLastUpdate[entity.id][system.id] = NOW(); 976 | this.entitySystemLastUpdateGame[entity.id][system.id] = this.gameTime; 977 | 978 | // Informs the system about the new relationship 979 | if (system.enter) { 980 | this.inject(system); 981 | system.enter(entity); 982 | } 983 | } 984 | }; 985 | 986 | /** 987 | * Indexes an entity 988 | * 989 | * @param entity 990 | */ 991 | private indexEntity(entity: Entity, system?: System) { 992 | 993 | if (!this.entitySystems[entity.id]) { 994 | this.entitySystems[entity.id] = []; 995 | } 996 | 997 | if (system) { 998 | // Index entity for a specific system 999 | this.indexEntitySystem(entity, system); 1000 | 1001 | } else { 1002 | // Indexes the entire entity 1003 | this.systems.forEach((system) => { 1004 | this.indexEntitySystem(entity, system); 1005 | }); 1006 | } 1007 | } 1008 | } 1009 | 1010 | -------------------------------------------------------------------------------- /test/ECS.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /test/ECS.test.ts: -------------------------------------------------------------------------------- 1 | import {describe} from "mocha"; 2 | import ECS, {Component, Entity, System} from "../src"; 3 | import {expect} from "chai"; 4 | 5 | 6 | const ComponentA = Component.register(); 7 | 8 | const ComponentB = Component.register(); 9 | 10 | const ComponentC = Component.register(); 11 | 12 | class Entt extends Entity { 13 | 14 | } 15 | 16 | class SysCompA extends System { 17 | constructor() { 18 | super([ComponentA.type]); 19 | } 20 | } 21 | 22 | 23 | describe("ECS", () => { 24 | 25 | describe("Components", () => { 26 | 27 | it("must be unique", () => { 28 | let world = new ECS(); 29 | 30 | expect(ComponentA.type).to.not.equal(ComponentB.type); 31 | 32 | let compA = new ComponentA(100); 33 | let compA2 = new ComponentA(100); 34 | let compB = new ComponentB(200); 35 | 36 | expect(ComponentA.type).to.eql(compA.type); 37 | expect(ComponentA.type).to.eql(compA2.type); 38 | expect(compA2.type).to.eql(compA2.type); 39 | 40 | expect(ComponentB.type).to.eql(compB.type); 41 | 42 | 43 | expect(compA.data).to.eql(100); 44 | expect(compB.data).to.eql(200); 45 | }); 46 | }); 47 | 48 | describe("Entity", () => { 49 | 50 | it("must have unique identifiers", () => { 51 | let world = new ECS(); 52 | 53 | let enttA = new Entt(); 54 | let enttB = new Entt(); 55 | expect(enttA.id).to.not.equal(enttB.id); 56 | expect(enttA.id).to.eql(enttB.id - 1); 57 | }); 58 | }); 59 | 60 | 61 | describe("System", () => { 62 | 63 | it("must add systems at runtime", () => { 64 | 65 | let sys1 = new SysCompA(); 66 | 67 | let world = new ECS([sys1]); 68 | 69 | expect((world as any).systems.length).to.eql(1); 70 | 71 | let sys2 = new SysCompA(); 72 | world.addSystem(sys2); 73 | expect((world as any).systems).to.eql([sys1, sys2]); 74 | 75 | // Must ignore undefined 76 | world.addSystem(undefined as any); 77 | expect((world as any).systems).to.eql([sys1, sys2]); 78 | 79 | // Must ignore if exists 80 | world.addSystem(sys2); 81 | expect((world as any).systems).to.eql([sys1, sys2]); 82 | }); 83 | 84 | it("must remove systems at runtime", () => { 85 | 86 | let sys1 = new SysCompA(); 87 | 88 | let world = new ECS([sys1]); 89 | 90 | expect((world as any).systems.length).to.eql(1); 91 | 92 | let sys2 = new SysCompA(); 93 | world.addSystem(sys2); 94 | 95 | expect((world as any).systems).to.eql([sys1, sys2]); 96 | 97 | // Must ignore undefined 98 | world.removeSystem(undefined as any); 99 | expect((world as any).systems).to.eql([sys1, sys2]); 100 | 101 | world.removeSystem(sys1); 102 | expect((world as any).systems).to.eql([sys2]); 103 | 104 | world.removeSystem(sys2); 105 | expect((world as any).systems).to.eql([]); 106 | }); 107 | 108 | 109 | it("must be invoked in the expected order", () => { 110 | 111 | let callOrder = 1; 112 | 113 | let enterCalled = 0; 114 | let changedCalled = 0; 115 | let beforeUpdateAllCalled = 0; 116 | let updateCalled = 0; 117 | let afterUpdateAllCalled = 0; 118 | let exitCalled = 0; 119 | 120 | function clear() { 121 | callOrder = 1; 122 | enterCalled = 0; 123 | changedCalled = 0; 124 | beforeUpdateAllCalled = 0; 125 | updateCalled = 0; 126 | afterUpdateAllCalled = 0; 127 | exitCalled = 0; 128 | } 129 | 130 | class Sys extends System { 131 | constructor(type: number) { 132 | super([type]); 133 | } 134 | 135 | beforeUpdateAll(time: number): void { 136 | beforeUpdateAllCalled = callOrder++; 137 | } 138 | 139 | update(time: number, delta: number, entity: Entity): void { 140 | updateCalled = callOrder++; 141 | } 142 | 143 | afterUpdateAll(time: number, entities: Entity[]): void { 144 | afterUpdateAllCalled = callOrder++; 145 | } 146 | 147 | enter(entity: Entity): void { 148 | enterCalled = callOrder++; 149 | } 150 | 151 | exit(entity: Entity): void { 152 | exitCalled = callOrder++; 153 | } 154 | 155 | change(entity: Entity, added?: Component, removed?: Component): void { 156 | changedCalled = callOrder++; 157 | } 158 | } 159 | 160 | let calledSystem = new Sys(ComponentA.type); 161 | 162 | // Control, should never invoke methods of this instance 163 | let notCalledSystem = new Sys(ComponentC.type); 164 | 165 | let entity = new Entt(); 166 | let entity2 = new Entt(); 167 | 168 | let world = new ECS([calledSystem, notCalledSystem]); 169 | 170 | // init 171 | expect(enterCalled).to.eql(0); 172 | expect(changedCalled).to.eql(0); 173 | expect(beforeUpdateAllCalled).to.eql(0); 174 | expect(updateCalled).to.eql(0); 175 | expect(afterUpdateAllCalled).to.eql(0); 176 | expect(exitCalled).to.eql(0); 177 | 178 | 179 | // update without entities match, do 180 | world.update(); 181 | 182 | expect(enterCalled).to.eql(0); 183 | expect(changedCalled).to.eql(0); 184 | expect(beforeUpdateAllCalled).to.eql(0); 185 | expect(updateCalled).to.eql(0); 186 | expect(afterUpdateAllCalled).to.eql(0); 187 | expect(exitCalled).to.eql(0); 188 | 189 | // does nothing, does not have the expected features of the system 190 | world.addEntity(entity); 191 | 192 | expect(enterCalled).to.eql(0); 193 | expect(changedCalled).to.eql(0); 194 | expect(beforeUpdateAllCalled).to.eql(0); 195 | expect(updateCalled).to.eql(0); 196 | expect(afterUpdateAllCalled).to.eql(0); 197 | expect(exitCalled).to.eql(0); 198 | 199 | // expect enter 200 | let componentA = new ComponentA(100); 201 | entity.add(componentA); 202 | 203 | // expect enter 204 | (global as any).setImmediateExec(); 205 | 206 | expect(enterCalled).to.eql(1); 207 | expect(changedCalled).to.eql(0); 208 | expect(beforeUpdateAllCalled).to.eql(0); 209 | expect(updateCalled).to.eql(0); 210 | expect(afterUpdateAllCalled).to.eql(0); 211 | expect(exitCalled).to.eql(0); 212 | clear(); 213 | 214 | // do nothing, system is not interested in this kind of component 215 | let componentB = new ComponentB(100); 216 | entity.add(componentB); 217 | (global as any).setImmediateExec(); 218 | 219 | expect(enterCalled).to.eql(0); 220 | expect(changedCalled).to.eql(0); 221 | expect(beforeUpdateAllCalled).to.eql(0); 222 | expect(updateCalled).to.eql(0); 223 | expect(afterUpdateAllCalled).to.eql(0); 224 | expect(exitCalled).to.eql(0); 225 | clear(); 226 | 227 | 228 | // again, do nothing, system is not interested in this kind of component 229 | entity.remove(componentB); 230 | (global as any).setImmediateExec(); 231 | 232 | expect(enterCalled).to.eql(0); 233 | expect(changedCalled).to.eql(0); 234 | expect(beforeUpdateAllCalled).to.eql(0); 235 | expect(updateCalled).to.eql(0); 236 | expect(afterUpdateAllCalled).to.eql(0); 237 | expect(exitCalled).to.eql(0); 238 | clear(); 239 | 240 | // expect change 241 | let componentA2 = new ComponentA(100); 242 | entity.add(componentA2); 243 | (global as any).setImmediateExec(); 244 | 245 | expect(enterCalled).to.eql(0); 246 | expect(changedCalled).to.eql(1); 247 | expect(beforeUpdateAllCalled).to.eql(0); 248 | expect(updateCalled).to.eql(0); 249 | expect(afterUpdateAllCalled).to.eql(0); 250 | expect(exitCalled).to.eql(0); 251 | clear(); 252 | 253 | // again, expect change 254 | entity.remove(componentA2); 255 | (global as any).setImmediateExec(); 256 | 257 | expect(enterCalled).to.eql(0); 258 | expect(changedCalled).to.eql(1); 259 | expect(beforeUpdateAllCalled).to.eql(0); 260 | expect(updateCalled).to.eql(0); 261 | expect(afterUpdateAllCalled).to.eql(0); 262 | expect(exitCalled).to.eql(0); 263 | clear(); 264 | 265 | // expect update, before and after 266 | world.update(); 267 | 268 | expect(enterCalled).to.eql(0); 269 | expect(changedCalled).to.eql(0); 270 | expect(beforeUpdateAllCalled).to.eql(1); 271 | expect(updateCalled).to.eql(2); 272 | expect(afterUpdateAllCalled).to.eql(3); 273 | expect(exitCalled).to.eql(0); 274 | 275 | // again 276 | world.update(); 277 | 278 | expect(enterCalled).to.eql(0); 279 | expect(changedCalled).to.eql(0); 280 | expect(beforeUpdateAllCalled).to.eql(4); 281 | expect(updateCalled).to.eql(5); 282 | expect(afterUpdateAllCalled).to.eql(6); 283 | expect(exitCalled).to.eql(0); 284 | clear(); 285 | 286 | 287 | // on remove entity 288 | world.removeEntity(entity); 289 | 290 | expect(enterCalled).to.eql(0); 291 | expect(changedCalled).to.eql(0); 292 | expect(beforeUpdateAllCalled).to.eql(0); 293 | expect(updateCalled).to.eql(0); 294 | expect(afterUpdateAllCalled).to.eql(0); 295 | expect(exitCalled).to.eql(1); 296 | 297 | }); 298 | }); 299 | 300 | it("must create without systems", () => { 301 | let world = new ECS(); 302 | 303 | expect((world as any).systems).to.eql([]); 304 | }); 305 | 306 | it("must create with systems", () => { 307 | 308 | class Sys extends System { 309 | constructor() { 310 | super([-1]); 311 | } 312 | } 313 | 314 | let world = new ECS([new Sys]); 315 | 316 | expect((world as any).systems.length).to.eql(1); 317 | }); 318 | }); 319 | -------------------------------------------------------------------------------- /test/Iterator.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /test/Iterator.test.ts: -------------------------------------------------------------------------------- 1 | import {describe} from "mocha"; 2 | import {Iterator} from "../src"; 3 | import {expect} from "chai"; 4 | 5 | 6 | describe("Iterator", () => { 7 | 8 | it("must invoke generator asynchronously", () => { 9 | let r = -1; 10 | let c = 0; 11 | let it = new Iterator(i => { 12 | if (i < 5) { 13 | c++; 14 | expect(r).to.eql(c - 2); 15 | return i; 16 | } 17 | return undefined; 18 | }); 19 | 20 | it.each(item => { 21 | r = item; 22 | expect(r).to.eql(c - 1); 23 | }); 24 | }); 25 | 26 | it("must cache data", () => { 27 | let c = 0; 28 | let it = new Iterator(i => { 29 | if (i < 5) { 30 | c++; 31 | return i; 32 | } 33 | return undefined; 34 | }); 35 | 36 | // iterate all 37 | it.each(item => { 38 | 39 | }); 40 | 41 | expect(c).to.eql(5); 42 | 43 | c = 0; 44 | 45 | // iterate again 46 | it.each(item => { 47 | 48 | }); 49 | 50 | expect(c).to.eql(0); 51 | }); 52 | 53 | it("each() - should stop returning false", () => { 54 | let c = 0; 55 | let it = new Iterator(i => { 56 | if (i < 5) { 57 | c++; 58 | return i; 59 | } 60 | return undefined; 61 | }); 62 | 63 | let r; 64 | it.each(item => { 65 | r = item; 66 | if (item == 2) { 67 | return false 68 | } 69 | }); 70 | 71 | expect(r).to.eql(2); 72 | expect(c).to.eql(3); 73 | }); 74 | 75 | it("each() - must iterate over all items", () => { 76 | let c = 0; 77 | let it = new Iterator(i => { 78 | if (i < 5) { 79 | c++; 80 | return i; 81 | } 82 | return undefined; 83 | }); 84 | 85 | let r; 86 | it.each(item => { 87 | r = item; 88 | }); 89 | 90 | expect(r).to.eql(4); 91 | expect(c).to.eql(5); 92 | }); 93 | 94 | it("find() - must find an item", () => { 95 | let c = 0; 96 | let it = new Iterator(i => { 97 | if (i < 5) { 98 | c++; 99 | return i; 100 | } 101 | return undefined; 102 | }); 103 | 104 | let r = it.find(item => item === 3); 105 | 106 | expect(r).to.eql(3); 107 | expect(c).to.eql(4); 108 | }); 109 | 110 | it("find() - should return undefined when not finding an item", () => { 111 | let c = 0; 112 | let it = new Iterator(i => { 113 | if (i < 5) { 114 | c++; 115 | return i; 116 | } 117 | return undefined; 118 | }); 119 | 120 | let r = it.find(item => item === 6); 121 | 122 | expect(r).to.eql(undefined); 123 | expect(c).to.eql(5); 124 | }); 125 | 126 | it("filter() - must apply informed filter", () => { 127 | let c = 0; 128 | let it = new Iterator(i => { 129 | if (i < 5) { 130 | c++; 131 | return i; 132 | } 133 | return undefined; 134 | }); 135 | 136 | let r = it.filter(item => item % 2 == 0); 137 | 138 | expect(r).to.eql([0, 2, 4]); 139 | expect(c).to.eql(5); 140 | }); 141 | 142 | it("map() - must map iterator data", () => { 143 | let c = 0; 144 | let it = new Iterator(i => { 145 | if (i < 5) { 146 | c++; 147 | return i; 148 | } 149 | return undefined; 150 | }); 151 | 152 | let r = it.map(item => item * 2); 153 | 154 | expect(r).to.eql([0, 2, 4, 6, 8]); 155 | expect(c).to.eql(5); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | -r test/setup.js 2 | --require ts-node/register 3 | --require source-map-support/register 4 | --full-trace 5 | --bail 6 | test/*.test.ts 7 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | let setImmediateCallbacks = []; 2 | 3 | global.setImmediate = global.requestAnimationFrame = function (fn) { 4 | setImmediateCallbacks.push(fn); 5 | }; 6 | 7 | global.setImmediateExec = function () { 8 | setImmediateCallbacks.forEach(fn => fn()); 9 | setImmediateCallbacks = []; 10 | }; 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "rootDir": "src", 7 | "declaration": true, 8 | "emitDeclarationOnly": true, 9 | "outDir": "./", 10 | "baseUrl": "./", 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "skipLibCheck": true, 14 | "moduleResolution": "node", 15 | "esModuleInterop": true, 16 | "lib": [ 17 | "dom", 18 | "esnext", 19 | "es5" 20 | ], 21 | "paths": { 22 | "*": [ 23 | "node_modules/*" 24 | ] 25 | }, 26 | "types": [ 27 | "node" 28 | ] 29 | }, 30 | "exclude": [ 31 | "node_modules", 32 | "dist", 33 | "example", 34 | "test" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /v-release.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Altera a versão para RELEASE antes do build 3 | */ 4 | const fs = require('fs'); 5 | const http = require('http'); 6 | const semver = require('semver'); 7 | 8 | var options = { 9 | hostname: 'registry.npmjs.org', 10 | port: 80, 11 | path: '/ecs-lib', 12 | method: 'GET', 13 | headers: { 14 | 'Accept': 'application/json' 15 | } 16 | }; 17 | 18 | var req = http.request(options, function (res) { 19 | var json = ''; 20 | res.setEncoding('utf8'); 21 | res.on('data', function (data) { 22 | json += data; 23 | 24 | }); 25 | 26 | res.on('end', function () { 27 | let info = JSON.parse(json); 28 | 29 | // No formato '0.1.0', '0.2.0' 30 | var release = '0.0.0'; 31 | 32 | var version = ''; 33 | for (var v in info.versions) { 34 | if (semver.prerelease(v)) { 35 | continue; 36 | } else if (semver.gt(v, release)) { 37 | release = v; 38 | } 39 | } 40 | 41 | // Numero da nova versão SNAPHSOT (pre) 42 | var version = semver.inc(release, 'minor'); 43 | console.log('Incrementing ecs-lib version to: "' + version + '"'); 44 | 45 | var packageJson = require('./package.json'); 46 | 47 | packageJson.version = version; 48 | 49 | fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 4)); 50 | }); 51 | }); 52 | 53 | req.on('error', function (e) { 54 | throw e; 55 | }); 56 | 57 | req.end(); 58 | -------------------------------------------------------------------------------- /v-snapshot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Altera a versão para SNAPSHOT antes do build 3 | */ 4 | const fs = require('fs'); 5 | const http = require('http'); 6 | const semver = require('semver'); 7 | 8 | var options = { 9 | hostname: 'registry.npmjs.org', 10 | port: 80, 11 | path: '/ecs-lib', 12 | method: 'GET', 13 | headers: { 14 | 'Accept': 'application/json' 15 | } 16 | }; 17 | 18 | var req = http.request(options, function (res) { 19 | var json = ''; 20 | res.setEncoding('utf8'); 21 | res.on('data', function (data) { 22 | json += data; 23 | 24 | }); 25 | 26 | res.on('end', function () { 27 | let info = JSON.parse(json); 28 | 29 | // No formato '0.1.0', '0.2.0' 30 | var release = '0.0.0'; 31 | 32 | // No formato '0.1.0-pre.0', '0.1.0-pre.1', '0.1.0-pre.2' 33 | var snapshot = '0.0.0'; 34 | var version = ''; 35 | for (var v in info.versions) { 36 | if (semver.prerelease(v)) { 37 | // Pre-release 38 | if (semver.gt(v, snapshot)) { 39 | snapshot = v; 40 | } 41 | } else if (semver.gt(v, release)) { 42 | release = v; 43 | } 44 | } 45 | 46 | // Se não possui snapshot da versão atual, zera para garantir o uso do preminor 47 | if (semver.gt(release, snapshot)) { 48 | snapshot = '0.0.0'; 49 | } 50 | 51 | // Numero da nova versão SNAPHSOT (pre) 52 | // Se já possui um prerelease, apenas incrementa a versão do snapshot 53 | // Ex. Se existir '0.1.0-pre.0', a proxima será '0.1.0-pre.1' 54 | if (snapshot != '0.0.0') { 55 | version = semver.inc(snapshot, 'prerelease', 'pre'); 56 | } else { 57 | version = semver.inc(release, 'preminor', 'pre'); 58 | } 59 | 60 | console.log('Incrementing ecs-lib version to: "' + version + '"'); 61 | 62 | var packageJson = require('./package.json'); 63 | 64 | packageJson.version = version; 65 | 66 | fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 4)); 67 | }); 68 | }); 69 | 70 | req.on('error', function (e) { 71 | throw e; 72 | }); 73 | 74 | req.end(); 75 | --------------------------------------------------------------------------------