├── .github └── workflows │ └── node.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── index.js ├── libraries ├── index.js ├── lib-ape-ecs.js ├── lib-bitecs.js ├── lib-ecsy.js ├── lib-geotic.js ├── lib-goodluck.js ├── lib-harmony-ecs.js ├── lib-javelin-ecs.js ├── lib-miniplex.js ├── lib-nano-ecs.js ├── lib-perform-ecs.js ├── lib-picoes.js ├── lib-piecs.js ├── lib-tiny-ecs.js ├── lib-uecs.js ├── lib-wolf-ecs.js └── lib-yagl-ecs.js ├── output.png ├── package-lock.json ├── package.json ├── readme.md └── suites ├── index.js ├── suite-add-remove.js ├── suite-destroy.js └── suite-velocity.js /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node: 18 | - 12.x 19 | - 14.x 20 | - 15.x 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | 25 | - uses: actions/checkout@v2 26 | - name: Use Node.js ${{ matrix.node }} 27 | uses: actions/setup-node@v1 28 | with: 29 | node-version: ${{ matrix.node }} 30 | 31 | - name: Install dependencies 32 | run: npm ci 33 | 34 | - name: Run benchmarks 35 | run: npm start 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | build 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import * as libs from './libraries/index.js'; 2 | import * as suts from './suites/index.js'; 3 | import elegantSpinner from 'elegant-spinner'; 4 | import logUpdate from 'log-update'; 5 | 6 | const libraries = Object.values(libs); 7 | const suites = Object.values(suts); 8 | 9 | const now = () => { 10 | const hr = process.hrtime(); 11 | 12 | return (hr[0] * 1e9 + hr[1]) / 1000; 13 | }; 14 | 15 | const frame = elegantSpinner(); 16 | 17 | suites.forEach((suite) => { 18 | const output = []; 19 | 20 | libraries.forEach((library, libIdx) => { 21 | if (!library.suites.includes(suite.name)) { 22 | output.push({ 23 | library, 24 | sum: Infinity, 25 | updates: 0, 26 | skipped: true, 27 | }); 28 | return; 29 | } 30 | 31 | suite.setup(library); 32 | 33 | let sum = 0; 34 | 35 | for (let i = 0; i < suite.iterations; i++) { 36 | const start = now(); 37 | 38 | suite.perform(library); 39 | 40 | sum += now() - start; 41 | 42 | if (i % 200 === 0) { 43 | const progress = (i / suite.iterations) * 100; 44 | const lib = `${libIdx}/${libraries.length}`; 45 | 46 | logUpdate( 47 | `${frame()} ${suite.name} - ${lib} ${library.name 48 | } ${progress.toFixed(0)}%` 49 | ); 50 | } 51 | } 52 | 53 | const updates = library.getMovementSystemUpdateCount(); 54 | 55 | output.push({ 56 | library, 57 | sum, 58 | updates, 59 | skipped: false, 60 | }); 61 | 62 | library.cleanup(); 63 | }); 64 | 65 | logUpdate.clear(); 66 | 67 | output.sort((o1, o2) => o1.sum - o2.sum); 68 | 69 | console.log(`Suite ${suite.name} (${suite.iterations} iterations)`); 70 | output.forEach((out) => { 71 | const avg = Math.round(out.sum / suite.iterations); 72 | const percent = (out.sum / output[0].sum) * 100 - 100; 73 | 74 | let nameTxt = out.library.name.padEnd(12, ' '); 75 | let sumTxt = `${Math.round(out.sum)}`.padStart(10, ' ') + 'ms'; 76 | let avgText = `${avg}`.padStart(6, ' ') + 'ms'; 77 | let updateText = 78 | out.updates > 0 ? `${out.updates} updates`.padStart(20) : ''; 79 | 80 | let percentText = percent.toFixed(1).padStart(10, ' ') + '%'; 81 | 82 | if (percent <= 0) { 83 | percentText += ' fastest'; 84 | } else { 85 | percentText += ' slower'; 86 | } 87 | 88 | if (out.skipped) { 89 | sumTxt = ''; 90 | avgText = ''; 91 | updateText = ''; 92 | updateText = ''; 93 | percentText = ''; 94 | nameTxt = nameTxt.padEnd(44, ' ') + 'skipped'; 95 | } 96 | 97 | console.log( 98 | ` - ${nameTxt}${avgText}${sumTxt}${updateText}${percentText}` 99 | ); 100 | }); 101 | console.log(''); 102 | }); 103 | -------------------------------------------------------------------------------- /libraries/index.js: -------------------------------------------------------------------------------- 1 | export { default as miniplexLibrary } from './lib-miniplex.js'; 2 | export { default as JavelinEcsLibrary } from './lib-javelin-ecs.js'; 3 | export { default as GeoticLibrary } from './lib-geotic.js'; 4 | export { default as PerformEcsLibrary } from './lib-perform-ecs.js'; 5 | export { default as NanoEcsLibrary } from './lib-nano-ecs.js'; 6 | export { default as TinyEcsLibrary } from './lib-tiny-ecs.js'; 7 | export { default as YaglEcsLibrary } from './lib-yagl-ecs.js'; 8 | export { default as UecsEcsLibrary } from './lib-uecs.js'; 9 | export { default as BitecsEcsLibrary } from './lib-bitecs.js'; 10 | export { default as GoodluckLibrary } from './lib-goodluck.js'; 11 | export { default as PicoesLibrary } from './lib-picoes.js'; 12 | export { default as WolfEcsLibrary } from './lib-wolf-ecs.js'; 13 | export { default as PiecsLibrary } from './lib-piecs.js'; 14 | export { default as HarmonyEcsLibrary } from './lib-harmony-ecs.js'; 15 | export { default as EcsyLibrary } from './lib-ecsy.js'; 16 | export { default as ApeEcsLibrary } from './lib-ape-ecs.js'; 17 | 18 | -------------------------------------------------------------------------------- /libraries/lib-ape-ecs.js: -------------------------------------------------------------------------------- 1 | import ApeECS from 'ape-ecs'; 2 | 3 | class Position extends ApeECS.Component { 4 | static properties = { 5 | x: 0, 6 | y: 0, 7 | }; 8 | } 9 | 10 | class Velocity extends ApeECS.Component { 11 | static properties = { 12 | dx: 1.2, 13 | dy: 1.7, 14 | }; 15 | } 16 | 17 | let updateCount = 0; 18 | 19 | class MovementSystem extends ApeECS.System { 20 | init() { 21 | this.query = this.createQuery() 22 | .fromAll('Position', 'Velocity') 23 | .persist(); 24 | } 25 | 26 | update() { 27 | this.query.execute().forEach((entity) => { 28 | const position = entity.getOne('Position'); 29 | const velocity = entity.getOne('Velocity'); 30 | 31 | position.x += velocity.dx; 32 | position.y += velocity.dy; 33 | 34 | position.update(); 35 | updateCount++; 36 | }); 37 | } 38 | } 39 | 40 | export default { 41 | name: 'ape-ecs', 42 | suites: ['Add/Remove', 'Destroy', 'Velocity'], 43 | setup() { 44 | this.world = new ApeECS.World(); 45 | 46 | this.world.registerComponent(Position); 47 | this.world.registerComponent(Velocity); 48 | 49 | updateCount = 0; 50 | this.world.registerSystem('movement', MovementSystem); 51 | }, 52 | createEntity() { 53 | return this.world.createEntity({}); 54 | }, 55 | addPositionComponent(entity) { 56 | entity.addComponent({ type: 'Position' }); 57 | }, 58 | addVelocityComponent(entity) { 59 | entity.addComponent({ type: 'Velocity' }); 60 | }, 61 | removePositionComponent(entity) { 62 | entity.removeComponent(entity.getOne('Position')); 63 | }, 64 | removeVelocityComponent(entity) { 65 | entity.removeComponent(entity.getOne('Velocity')); 66 | }, 67 | destroyEntity(entity) { 68 | entity.destroy(); 69 | }, 70 | cleanup() { 71 | this.world = null; 72 | }, 73 | updateMovementSystem() { 74 | // this.world.tick(); 75 | // this.world.updateIndexes(); 76 | this.world.runSystems('movement'); 77 | }, 78 | getMovementSystemUpdateCount() { 79 | return updateCount; 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /libraries/lib-bitecs.js: -------------------------------------------------------------------------------- 1 | import { 2 | createWorld, 3 | defineComponent, 4 | defineQuery, 5 | defineSystem, 6 | addComponent, 7 | removeComponent, 8 | addEntity, 9 | removeEntity, 10 | Types, 11 | } from 'bitecs'; 12 | 13 | let updateCount = 0; 14 | 15 | export default { 16 | name: 'bitecs', 17 | suites: ['Add/Remove', 'Destroy', 'Velocity'], 18 | setup() { 19 | this.world = createWorld(); 20 | 21 | const { f32, ui16 } = Types; 22 | const Vector2 = { x: f32, y: f32 }; 23 | this.Position = defineComponent(Vector2); 24 | this.Velocity = defineComponent({ ...Vector2, speed: ui16 }); 25 | 26 | this.query = defineQuery([this.Position, this.Velocity]); 27 | 28 | this.move = defineSystem((world) => { 29 | const ents = this.query(world); 30 | for (let i = 0; i < ents.length; i++) { 31 | const eid = ents[i]; 32 | this.Position.x[eid] += this.Velocity.x[eid]; 33 | this.Position.y[eid] += this.Velocity.y[eid]; 34 | updateCount++; 35 | } 36 | }); 37 | }, 38 | createEntity() { 39 | return addEntity(this.world); 40 | }, 41 | addPositionComponent(entity) { 42 | addComponent(this.world, this.Position, entity); 43 | this.Position.x[entity] = 100; 44 | this.Position.y[entity] = 100; 45 | }, 46 | addVelocityComponent(entity) { 47 | addComponent(this.world, this.Velocity, entity); 48 | this.Velocity.x[entity] = 1.2; 49 | this.Velocity.x[entity] = 1.7; 50 | }, 51 | removePositionComponent(entity) { 52 | removeComponent(this.world, this.Position, entity); 53 | }, 54 | removeVelocityComponent(entity) { 55 | removeComponent(this.world, this.Velocity, entity); 56 | }, 57 | destroyEntity(entity) { 58 | removeEntity(this.world, entity); 59 | }, 60 | cleanup() { 61 | updateCount = 0; 62 | }, 63 | updateMovementSystem() { 64 | this.move(this.world); 65 | }, 66 | getMovementSystemUpdateCount() { 67 | return updateCount; 68 | }, 69 | }; 70 | -------------------------------------------------------------------------------- /libraries/lib-ecsy.js: -------------------------------------------------------------------------------- 1 | import { World, System, Component, TagComponent, Types } from 'ecsy'; 2 | 3 | class Position extends Component {} 4 | 5 | Position.schema = { 6 | x: { 7 | type: Types.Number, 8 | default: 0, 9 | }, 10 | y: { 11 | type: Types.Number, 12 | default: 0, 13 | }, 14 | }; 15 | 16 | class Velocity extends Component {} 17 | 18 | Velocity.schema = { 19 | dx: { 20 | type: Types.Number, 21 | default: 1.2, 22 | }, 23 | dy: { 24 | type: Types.Number, 25 | default: 1.7, 26 | }, 27 | }; 28 | 29 | let updateCount = 0; 30 | 31 | class MovementSystem extends System { 32 | execute() { 33 | this.queries.movement.results.forEach((entity) => { 34 | const velocity = entity.getComponent(Velocity); 35 | const position = entity.getMutableComponent(Position); 36 | 37 | position.x += velocity.dx; 38 | position.y += velocity.dy; 39 | 40 | updateCount++; 41 | }); 42 | } 43 | } 44 | 45 | MovementSystem.queries = { 46 | movement: { 47 | components: [Velocity, Position], 48 | }, 49 | }; 50 | 51 | let i = 0; 52 | export default { 53 | name: 'ecsy', 54 | suites: ['Add/Remove', 'Destroy', 'Velocity'], 55 | setup() { 56 | this.world = new World(); 57 | 58 | this.world.registerComponent(Position); 59 | this.world.registerComponent(Velocity); 60 | 61 | updateCount = 0; 62 | this.world.registerSystem(MovementSystem); 63 | }, 64 | createEntity() { 65 | return this.world.createEntity(); 66 | }, 67 | addPositionComponent(entity) { 68 | entity.addComponent(Position); 69 | }, 70 | addVelocityComponent(entity) { 71 | entity.addComponent(Velocity); 72 | }, 73 | removePositionComponent(entity) { 74 | entity.removeComponent(Position); 75 | }, 76 | removeVelocityComponent(entity) { 77 | entity.removeComponent(Velocity); 78 | }, 79 | destroyEntity(entity) { 80 | entity.remove(); 81 | }, 82 | cleanup() { 83 | this.world = null; 84 | }, 85 | updateMovementSystem() { 86 | this.world.execute(); 87 | }, 88 | getMovementSystemUpdateCount() { 89 | return updateCount; 90 | }, 91 | }; 92 | -------------------------------------------------------------------------------- /libraries/lib-geotic.js: -------------------------------------------------------------------------------- 1 | import { Engine, Component } from 'geotic'; 2 | 3 | class Position extends Component { 4 | static properties = { 5 | x: 0, 6 | y: 0, 7 | }; 8 | } 9 | class Velocity extends Component { 10 | static properties = { 11 | dx: 1.2, 12 | dy: 1.7, 13 | }; 14 | } 15 | 16 | const engine = new Engine(); 17 | 18 | engine.registerComponent(Position); 19 | engine.registerComponent(Velocity); 20 | 21 | class MovementSystem { 22 | updateCount = 0; 23 | 24 | constructor(world) { 25 | this.query = world.createQuery({ 26 | all: [Position, Velocity], 27 | immutableResult: false, 28 | }); 29 | } 30 | 31 | update() { 32 | for (let entity of this.query.get()) { 33 | entity.position.x += entity.velocity.dx; 34 | entity.position.y += entity.velocity.dy; 35 | this.updateCount++; 36 | }; 37 | } 38 | } 39 | 40 | export default { 41 | name: 'geotic', 42 | suites: ['Add/Remove', 'Destroy', 'Velocity'], 43 | setup() { 44 | this.world = engine.createWorld(); 45 | 46 | this.movementSystem = new MovementSystem(this.world); 47 | }, 48 | createEntity() { 49 | return this.world.createEntity(); 50 | }, 51 | addPositionComponent(entity) { 52 | entity.add(Position); 53 | }, 54 | addVelocityComponent(entity) { 55 | entity.add(Velocity); 56 | }, 57 | removePositionComponent(entity) { 58 | entity.remove(entity.position); 59 | }, 60 | removeVelocityComponent(entity) { 61 | entity.remove(entity.velocity); 62 | }, 63 | destroyEntity(entity) { 64 | entity.destroy(); 65 | }, 66 | cleanup() { 67 | this.world.destroy(); 68 | this.world = null; 69 | }, 70 | updateMovementSystem() { 71 | this.movementSystem.update(); 72 | }, 73 | getMovementSystemUpdateCount() { 74 | return this.movementSystem.updateCount; 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /libraries/lib-goodluck.js: -------------------------------------------------------------------------------- 1 | import { WorldImpl } from 'goodluck'; 2 | 3 | const HAS_POSITION = 1 << 0; 4 | const HAS_VELOCITY = 1 << 1; 5 | 6 | class World extends WorldImpl { 7 | Position = []; 8 | Velocity = []; 9 | 10 | updateCount = 0; 11 | } 12 | 13 | function position(x, y) { 14 | return (world, entity) => { 15 | world.Signature[entity] |= HAS_POSITION; 16 | world.Position[entity] = { x, y }; 17 | }; 18 | } 19 | 20 | function velocity(dx, dy) { 21 | return (world, entity) => { 22 | world.Signature[entity] |= HAS_VELOCITY; 23 | world.Velocity[entity] = { dx, dy }; 24 | }; 25 | } 26 | 27 | const QUERY_MOVE = HAS_POSITION | HAS_VELOCITY; 28 | function sys_move(world) { 29 | for (let entity = 0; entity < world.Signature.length; entity++) { 30 | if ((world.Signature[entity] & QUERY_MOVE) === QUERY_MOVE) { 31 | let entity_position = world.Position[entity]; 32 | let entity_velocity = world.Velocity[entity]; 33 | entity_position.x += entity_velocity.dx; 34 | entity_position.y += entity_velocity.dy; 35 | world.updateCount++; 36 | } 37 | } 38 | } 39 | 40 | export default { 41 | name: 'goodluck', 42 | suites: ['Add/Remove', 'Destroy', 'Velocity'], 43 | setup() { 44 | this.world = new World(); 45 | }, 46 | createEntity() { 47 | return this.world.CreateEntity(); 48 | }, 49 | addPositionComponent(entity) { 50 | position(0, 0)(this.world, entity); 51 | }, 52 | addVelocityComponent(entity) { 53 | velocity(1.2, 1.7)(this.world, entity); 54 | }, 55 | removePositionComponent(entity) { 56 | this.world.Signature[entity] &= ~HAS_POSITION; 57 | }, 58 | removeVelocityComponent(entity) { 59 | this.world.Signature[entity] &= ~HAS_VELOCITY; 60 | }, 61 | destroyEntity(entity) { 62 | this.world.DestroyEntity(entity); 63 | }, 64 | cleanup() { 65 | this.world = null; 66 | }, 67 | updateMovementSystem() { 68 | sys_move(this.world); 69 | }, 70 | getMovementSystemUpdateCount() { 71 | return this.world.updateCount; 72 | }, 73 | }; 74 | -------------------------------------------------------------------------------- /libraries/lib-harmony-ecs.js: -------------------------------------------------------------------------------- 1 | import { World, Schema, Entity, Query, Format } from 'harmony-ecs'; 2 | 3 | const Vector2 = { x: Format.float32, y: Format.float32 }; 4 | 5 | export default { 6 | name: 'harmony-ecs', 7 | suites: ['Add/Remove', 'Destroy', 'Velocity'], 8 | setup() { 9 | this.updateCount = 0; 10 | this.world = World.make(1e6); 11 | this.Position = Schema.makeBinary(this.world, Vector2); 12 | this.Velocity = Schema.makeBinary(this.world, Vector2); 13 | this.query = Query.make(this.world, [this.Position, this.Velocity]); 14 | }, 15 | 16 | createEntity() { 17 | return Entity.make(this.world); 18 | }, 19 | 20 | addPositionComponent(id) { 21 | Entity.set(this.world, id, [this.Position], [{ x: 100, y: 100 }]); 22 | }, 23 | 24 | addVelocityComponent(id) { 25 | Entity.set(this.world, id, [this.Velocity], [{ x: 1.2, y: 1.7 }]); 26 | }, 27 | 28 | removePositionComponent(id) { 29 | Entity.unset(this.world, id, [this.Position]); 30 | }, 31 | 32 | removeVelocityComponent(id) { 33 | Entity.unset(this.world, id, [this.Velocity]); 34 | }, 35 | 36 | destroyEntity(id) { 37 | Entity.destroy(this.world, id); 38 | }, 39 | 40 | cleanup() { 41 | this.updateCount = 0; 42 | }, 43 | 44 | updateMovementSystem() { 45 | for (let i = 0; i < this.query.length; i++) { 46 | const [e, [p, v]] = this.query[i]; 47 | for (let j = 0; j < e.length; j++) { 48 | p.x[j] += v.x[j]; 49 | p.y[j] += v.y[j]; 50 | this.updateCount++; 51 | } 52 | } 53 | }, 54 | 55 | getMovementSystemUpdateCount() { 56 | return this.updateCount; 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /libraries/lib-javelin-ecs.js: -------------------------------------------------------------------------------- 1 | import pkg from '@javelin/ecs'; 2 | const { createWorld, createQuery, toComponent, number } = pkg; 3 | 4 | let updateCount = 0; 5 | 6 | const movementSystem = (world) => { 7 | let movingEntities = createQuery(Position, Velocity); 8 | 9 | /* Return the system */ 10 | return (world) => { 11 | /* Now apply the velocity to the position. */ 12 | for (const [entities, [positions, velocities]] of movingEntities) { 13 | for (let i = 0; i < entities.length; i++) { 14 | positions[i].x += velocities[i].x; 15 | positions[i].y += velocities[i].y; 16 | positions[i].z += velocities[i].z; 17 | 18 | updateCount++; 19 | } 20 | } 21 | }; 22 | }; 23 | 24 | const Velocity = { 25 | x: number, 26 | y: number, 27 | z: number 28 | } 29 | const Position = { 30 | x: number, 31 | y: number, 32 | z: number 33 | } 34 | 35 | export default { 36 | name: 'javelin-ecs', 37 | suites: ['Add/Remove', 'Destroy', 'Velocity'], 38 | setup() { 39 | this.world = createWorld(); 40 | this.movementSystem = movementSystem(this.world); 41 | this.world.addSystem(this.movementSystem); 42 | }, 43 | createEntity() { 44 | return this.world.create(); 45 | }, 46 | addPositionComponent(entity) { 47 | this.world.attach(entity, toComponent({ x: 0, y: 0, z: 0 }, Position)); 48 | }, 49 | addVelocityComponent(entity) { 50 | this.world.attach(entity, toComponent({ x: 1, y: 2, z: 3 }, Velocity)); 51 | }, 52 | removePositionComponent(entity) { 53 | this.world.detach(entity, Position); 54 | }, 55 | removeVelocityComponent(entity) { 56 | this.world.detach(entity, Velocity); 57 | }, 58 | destroyEntity(entity) { 59 | this.world.destroy(entity); 60 | }, 61 | cleanup() { 62 | updateCount = 0; 63 | this.world = null; 64 | this.movementSystem = null; 65 | }, 66 | updateMovementSystem() { 67 | this.world.step(); 68 | }, 69 | getMovementSystemUpdateCount() { 70 | return updateCount; 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /libraries/lib-miniplex.js: -------------------------------------------------------------------------------- 1 | import { World } from 'miniplex'; 2 | 3 | let updateCount = 0; 4 | 5 | const movementSystem = (world) => { 6 | let movingEntities; 7 | 8 | /* Return the system */ 9 | return () => { 10 | movingEntities ||= world.with('position', 'velocity'); 11 | 12 | /* Get the index for the archetype we created earlier. */ 13 | const { entities } = movingEntities; 14 | 15 | /* Now apply the velocity to the position. */ 16 | for (const { position, velocity } of entities) { 17 | position.x += velocity.x; 18 | position.y += velocity.y; 19 | position.z += velocity.z; 20 | 21 | updateCount++; 22 | } 23 | }; 24 | }; 25 | 26 | export default { 27 | name: 'miniplex', 28 | suites: ['Add/Remove', 'Destroy', 'Velocity'], 29 | setup() { 30 | this.world = new World(); 31 | this.movementSystem = movementSystem(this.world); 32 | }, 33 | createEntity() { 34 | return this.world.add({}); 35 | }, 36 | addPositionComponent(entity) { 37 | /* Entities are just JavaScript objects, and components just properties on 38 | those objects. In Typescript, you get full type checking of all your 39 | entities and components. TypeScript is great and you should use it! (But 40 | miniplex will happily work without it, too.) */ 41 | this.world.addComponent(entity, "position", { x: 0, y: 0, z: 0 }); 42 | }, 43 | addVelocityComponent(entity) { 44 | this.world.addComponent(entity, "velocity", { x: 1, y: 2, z: 3 }); 45 | }, 46 | removePositionComponent(entity) { 47 | this.world.removeComponent(entity, 'position'); 48 | }, 49 | removeVelocityComponent(entity) { 50 | this.world.removeComponent(entity, 'velocity'); 51 | }, 52 | destroyEntity(entity) { 53 | this.world.remove(entity); 54 | }, 55 | cleanup() { 56 | updateCount = 0; 57 | this.world = null; 58 | this.movementSystem = null; 59 | }, 60 | updateMovementSystem() { 61 | this.movementSystem(); 62 | }, 63 | getMovementSystemUpdateCount() { 64 | return updateCount; 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /libraries/lib-nano-ecs.js: -------------------------------------------------------------------------------- 1 | import nano from 'nano-ecs'; 2 | 3 | function Position() { 4 | this.x = 0; 5 | this.y = 0; 6 | } 7 | 8 | function Velocity() { 9 | this.dx = 1.2; 10 | this.dy = 1.7; 11 | } 12 | 13 | class MovementSystem { 14 | updateCount = 0; 15 | 16 | constructor(world) { 17 | this.world = world; 18 | } 19 | 20 | update() { 21 | this.world.queryComponents([Position, Velocity]).forEach((entity) => { 22 | entity.position.x += entity.velocity.dx; 23 | entity.position.y += entity.velocity.dy; 24 | this.updateCount++; 25 | }); 26 | } 27 | } 28 | 29 | export default { 30 | name: 'nano-ecs', 31 | suites: ['Add/Remove', 'Destroy', 'Velocity'], 32 | setup() { 33 | this.world = nano(); 34 | 35 | this.movementSystem = new MovementSystem(this.world); 36 | }, 37 | createEntity() { 38 | return this.world.createEntity(); 39 | }, 40 | addPositionComponent(entity) { 41 | entity.addComponent(Position); 42 | }, 43 | addVelocityComponent(entity) { 44 | entity.addComponent(Velocity); 45 | }, 46 | removePositionComponent(entity) { 47 | entity.removeComponent(Position); 48 | }, 49 | removeVelocityComponent(entity) { 50 | entity.removeComponent(Velocity); 51 | }, 52 | destroyEntity(entity) { 53 | entity.remove(); 54 | }, 55 | cleanup() { 56 | this.world = null; 57 | }, 58 | updateMovementSystem() { 59 | this.movementSystem.update(); 60 | }, 61 | getMovementSystemUpdateCount() { 62 | return this.movementSystem.updateCount; 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /libraries/lib-perform-ecs.js: -------------------------------------------------------------------------------- 1 | import { 2 | ECS, 3 | Component, 4 | makeComponent, 5 | System, 6 | EntityViewFactory, 7 | } from 'perform-ecs'; 8 | 9 | class Position extends Component { 10 | x = 0; 11 | y = 0; 12 | 13 | reset() { 14 | this.x = 0; 15 | this.y = 0; 16 | } 17 | } 18 | class Velocity extends Component { 19 | dx = 1.2; 20 | dy = 1.7; 21 | 22 | reset() { 23 | this.dx = 1.2; 24 | this.dy = 1.7; 25 | } 26 | } 27 | 28 | makeComponent(Position); 29 | makeComponent(Velocity); 30 | 31 | class MovementSystem extends System { 32 | updateCount = 0; 33 | 34 | view = EntityViewFactory.createView({ 35 | components: [Position, Velocity], 36 | }); 37 | 38 | update() { 39 | this.view.entities.forEach((entity) => { 40 | entity.x += entity.dx; 41 | entity.y += entity.dy; 42 | this.updateCount++; 43 | }); 44 | } 45 | } 46 | 47 | export default { 48 | name: 'perform-ecs', 49 | suites: ['Add/Remove', 'Destroy', 'Velocity'], 50 | setup() { 51 | this.ecs = new ECS(); 52 | this.movementSystem = new MovementSystem(); 53 | 54 | this.ecs.registerSystem(this.movementSystem); 55 | }, 56 | createEntity() { 57 | // for some reason, perform-ecs isn't picking up new entites 58 | // when these components are added later, so we add them now instead. 59 | return this.ecs.createEntity([ 60 | { component: Position }, 61 | { component: Velocity }, 62 | ]); 63 | }, 64 | addPositionComponent(entity) { 65 | // this doesn't seem to update in the system 66 | this.ecs.addComponentsToEntity(entity, [{ component: Position }]); 67 | }, 68 | addVelocityComponent(entity) { 69 | // this doesn't seem to update in the system 70 | this.ecs.addComponentsToEntity(entity, [{ component: Velocity }]); 71 | }, 72 | removePositionComponent(entity) { 73 | this.ecs.removeComponentsFromEntity(entity, [Position]); 74 | }, 75 | removeVelocityComponent(entity) { 76 | this.ecs.removeComponentsFromEntity(entity, [Velocity]); 77 | }, 78 | destroyEntity(entity) { 79 | this.ecs.removeEntity(entity); 80 | }, 81 | cleanup() { 82 | this.ecs = null; 83 | }, 84 | updateMovementSystem() { 85 | this.ecs.update(); 86 | }, 87 | getMovementSystemUpdateCount() { 88 | return this.movementSystem.updateCount; 89 | }, 90 | }; 91 | -------------------------------------------------------------------------------- /libraries/lib-picoes.js: -------------------------------------------------------------------------------- 1 | import { World } from 'picoes'; 2 | 3 | class position { 4 | constructor(x = 0, y = 0) { 5 | this.x = x; 6 | this.y = y; 7 | } 8 | } 9 | 10 | class velocity { 11 | constructor(dx = 1.2, dy = 1.7) { 12 | this.dx = dx; 13 | this.dy = dy; 14 | } 15 | } 16 | 17 | class MovementSystem { 18 | run() { 19 | this.world.each('position', 'velocity', ({ position, velocity }) => { 20 | position.x += velocity.dx; 21 | position.y += velocity.dy; 22 | ++this.state.updateCount; 23 | }); 24 | } 25 | } 26 | 27 | export default { 28 | name: 'picoes', 29 | suites: ['Add/Remove', 'Destroy', 'Velocity'], 30 | setup() { 31 | this.state = { updateCount: 0 }; 32 | this.world = new World({ 33 | components: { position, velocity }, 34 | systems: [MovementSystem], 35 | context: { state: this.state }, 36 | }); 37 | }, 38 | createEntity() { 39 | return this.world.entity(); 40 | }, 41 | addPositionComponent(entity) { 42 | entity.set('position'); 43 | }, 44 | addVelocityComponent(entity) { 45 | entity.set('velocity'); 46 | }, 47 | removePositionComponent(entity) { 48 | entity.remove('position'); 49 | }, 50 | removeVelocityComponent(entity) { 51 | entity.remove('velocity'); 52 | }, 53 | destroyEntity(entity) { 54 | entity.destroy(); 55 | }, 56 | cleanup() { 57 | this.world = null; 58 | }, 59 | updateMovementSystem() { 60 | this.world.run(); 61 | }, 62 | getMovementSystemUpdateCount() { 63 | return this.state.updateCount; 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /libraries/lib-piecs.js: -------------------------------------------------------------------------------- 1 | import { World, createEntitySystem } from 'piecs/dist/index.mjs'; 2 | 3 | let updateCount = 0; 4 | 5 | export default { 6 | name: 'piecs', 7 | suites: ['Add/Remove', 'Destroy', 'Velocity'], 8 | setup() { 9 | this.world = new World(); 10 | this.pos = { 11 | id: this.world.createComponentId(), 12 | x: new Float32Array(1e6), 13 | y: new Float32Array(1e6), 14 | }; 15 | this.vel = { 16 | id: this.world.createComponentId(), 17 | x: new Float32Array(1e6), 18 | y: new Float32Array(1e6), 19 | }; 20 | this.world 21 | .registerSystem( 22 | createEntitySystem( 23 | (entities, _) => { 24 | const pos = this.pos; 25 | const vel = this.vel; 26 | for (let i = 0, l = entities.length; i < l; i++) { 27 | const entity = entities[i]; 28 | pos.x[entity] += vel.x[entity]; 29 | pos.y[entity] += vel.y[entity]; 30 | updateCount++; 31 | } 32 | }, 33 | (q) => q.every(this.pos, this.vel) 34 | ) 35 | ) 36 | .initialize(); 37 | }, 38 | createEntity() { 39 | return this.world.createEntity(); 40 | }, 41 | addPositionComponent(entity) { 42 | this.world.addComponent(entity, this.pos); 43 | this.pos.x[entity] = 100; 44 | this.pos.y[entity] = 100; 45 | }, 46 | addVelocityComponent(entity) { 47 | this.world.addComponent(entity, this.vel); 48 | this.vel.x[entity] = 1.2; 49 | this.vel.y[entity] = 1.7; 50 | }, 51 | removePositionComponent(entity) { 52 | this.world.removeComponent(entity, this.pos); 53 | }, 54 | removeVelocityComponent(entity) { 55 | this.world.removeComponent(entity, this.vel); 56 | }, 57 | destroyEntity(entity) { 58 | this.world.deleteEntity(entity); 59 | }, 60 | cleanup() { 61 | updateCount = 0; 62 | }, 63 | updateMovementSystem() { 64 | this.world.update(); 65 | }, 66 | getMovementSystemUpdateCount() { 67 | return updateCount; 68 | }, 69 | }; 70 | -------------------------------------------------------------------------------- /libraries/lib-tiny-ecs.js: -------------------------------------------------------------------------------- 1 | import tiny from 'tiny-ecs'; 2 | 3 | function Position() { 4 | this.x = 0; 5 | this.y = 0; 6 | } 7 | 8 | function Velocity() { 9 | this.dx = 1.2; 10 | this.dy = 1.7; 11 | } 12 | 13 | class MovementSystem { 14 | updateCount = 0; 15 | 16 | constructor(world) { 17 | this.world = world; 18 | } 19 | 20 | update() { 21 | this.world.queryComponents([Position, Velocity]).forEach((entity) => { 22 | entity.position.x += entity.velocity.dx; 23 | entity.position.y += entity.velocity.dy; 24 | this.updateCount++; 25 | }); 26 | } 27 | } 28 | 29 | export default { 30 | name: 'tiny-ecs', 31 | suites: ['Add/Remove', 'Destroy', 'Velocity'], 32 | setup() { 33 | this.world = new tiny.EntityManager(); 34 | 35 | this.movementSystem = new MovementSystem(this.world); 36 | }, 37 | createEntity() { 38 | return this.world.createEntity(); 39 | }, 40 | addPositionComponent(entity) { 41 | entity.addComponent(Position); 42 | }, 43 | addVelocityComponent(entity) { 44 | entity.addComponent(Velocity); 45 | }, 46 | removePositionComponent(entity) { 47 | entity.removeComponent(Position); 48 | }, 49 | removeVelocityComponent(entity) { 50 | entity.removeComponent(Velocity); 51 | }, 52 | destroyEntity(entity) { 53 | entity.remove(); 54 | }, 55 | cleanup() { 56 | this.world = null; 57 | }, 58 | updateMovementSystem() { 59 | this.movementSystem.update(); 60 | }, 61 | getMovementSystemUpdateCount() { 62 | return this.movementSystem.updateCount; 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /libraries/lib-uecs.js: -------------------------------------------------------------------------------- 1 | import { World } from 'uecs'; 2 | 3 | class Position { 4 | x = 0; 5 | y = 0; 6 | } 7 | 8 | class Velocity { 9 | dx = 1.2; 10 | dy = 1.7; 11 | } 12 | 13 | class MovementSystem { 14 | updateCount = 0; 15 | view = null; 16 | 17 | constructor(world) { 18 | this.view = world.view(Position, Velocity); 19 | } 20 | 21 | update() { 22 | this.view.each((_, pos, vel) => { 23 | pos.x += vel.dx; 24 | pos.y += vel.dy; 25 | this.updateCount++; 26 | }); 27 | } 28 | } 29 | 30 | export default { 31 | name: 'uecs', 32 | suites: ['Add/Remove', 'Destroy', 'Velocity'], 33 | setup() { 34 | this.world = new World(); 35 | this.movementSystem = new MovementSystem(this.world); 36 | }, 37 | createEntity() { 38 | const entity = this.world.create(); 39 | return entity; 40 | }, 41 | addPositionComponent(entity) { 42 | this.world.emplace(entity, new Position()); 43 | }, 44 | addVelocityComponent(entity) { 45 | this.world.emplace(entity, new Velocity()); 46 | }, 47 | removePositionComponent(entity) { 48 | this.world.remove(entity, Position); 49 | }, 50 | removeVelocityComponent(entity) { 51 | this.world.remove(entity, Velocity); 52 | }, 53 | destroyEntity(entity) { 54 | this.world.destroy(entity); 55 | }, 56 | cleanup() {}, 57 | updateMovementSystem() { 58 | this.movementSystem.update(); 59 | }, 60 | getMovementSystemUpdateCount() { 61 | return this.movementSystem.updateCount; 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /libraries/lib-wolf-ecs.js: -------------------------------------------------------------------------------- 1 | import { ECS, all, not, any, types as t } from 'wolf-ecs'; 2 | 3 | export default { 4 | name: 'wolf-ecs', 5 | suites: ['Add/Remove', 'Destroy', 'Velocity'], 6 | setup() { 7 | this.updateCount = 0; 8 | this.ecs = new ECS(1e6); 9 | const vec = { x: t.f32, y: t.f32 }; 10 | this.pos = this.ecs.defineComponent(vec); 11 | this.vel = this.ecs.defineComponent(vec); 12 | this.query = this.ecs.createQuery(this.pos, this.vel); 13 | 14 | this.sys = function () { 15 | const lpos = this.pos; 16 | const lvel = this.vel; 17 | for (let i = 0, l = this.query.archetypes.length; i < l; i++) { 18 | const arch = this.query.archetypes[i].entities; 19 | for (let j = 0, l = arch.length; j < l; j++) { 20 | const id = arch[j]; 21 | lpos.x[id] += lvel.x[id]; 22 | lpos.y[id] += lvel.y[id]; 23 | this.updateCount++; 24 | } 25 | } 26 | }; 27 | }, 28 | 29 | createEntity() { 30 | return this.ecs.createEntity(); 31 | }, 32 | 33 | addPositionComponent(id) { 34 | this.ecs.addComponent(id, this.pos); 35 | this.pos.x[id] = 100; 36 | this.pos.y[id] = 100; 37 | }, 38 | 39 | addVelocityComponent(id) { 40 | this.ecs.addComponent(id, this.vel); 41 | this.vel.x[id] = 1.2; 42 | this.vel.x[id] = 1.7; 43 | }, 44 | 45 | removePositionComponent(id) { 46 | this.ecs.removeComponent(id, this.pos); 47 | }, 48 | 49 | removeVelocityComponent(id) { 50 | this.ecs.removeComponent(id, this.vel); 51 | }, 52 | 53 | destroyEntity(id) { 54 | this.ecs.destroyEntity(id); 55 | }, 56 | 57 | cleanup() { 58 | this.updateCount = 0; 59 | }, 60 | 61 | updateMovementSystem() { 62 | this.sys(); 63 | }, 64 | 65 | getMovementSystemUpdateCount() { 66 | return this.updateCount; 67 | }, 68 | }; 69 | -------------------------------------------------------------------------------- /libraries/lib-yagl-ecs.js: -------------------------------------------------------------------------------- 1 | import ECS from 'yagl-ecs'; 2 | 3 | const Position = { 4 | name: 'position', 5 | defaults: { 6 | x: 0, 7 | y: 0, 8 | }, 9 | }; 10 | 11 | const Velocity = { 12 | name: 'velocity', 13 | defaults: { 14 | dx: 1.2, 15 | dy: 1.7, 16 | }, 17 | }; 18 | 19 | class MovementSystem extends ECS.System { 20 | updateCount = 0; 21 | 22 | test(entity) { 23 | return !!(entity.components.position && entity.components.velocity); 24 | } 25 | 26 | update(entity) { 27 | entity.components.position.x += entity.components.velocity.dx; 28 | entity.components.position.y += entity.components.velocity.dy; 29 | this.updateCount++; 30 | } 31 | } 32 | 33 | export default { 34 | name: 'yagl-ecs', 35 | suites: ['Add/Remove', 'Destroy', 'Velocity'], 36 | setup() { 37 | this.ecs = new ECS(); 38 | this.movementSystem = new MovementSystem(); 39 | 40 | // need to create an entity so `Position` and `Velocity` are registered (?) 41 | new ECS.Entity(undefined, [Position, Velocity]); 42 | 43 | this.ecs.addSystem(this.movementSystem); 44 | }, 45 | createEntity() { 46 | const entity = new ECS.Entity(); 47 | 48 | this.ecs.addEntity(entity); 49 | 50 | return entity; 51 | }, 52 | addPositionComponent(entity) { 53 | entity.addComponent('position'); 54 | }, 55 | addVelocityComponent(entity) { 56 | entity.addComponent('velocity'); 57 | }, 58 | removePositionComponent(entity) { 59 | entity.removeComponent('position'); 60 | }, 61 | removeVelocityComponent(entity) { 62 | entity.removeComponent('velocity'); 63 | }, 64 | destroyEntity(entity) { 65 | this.ecs.removeEntity(entity); 66 | }, 67 | cleanup() {}, 68 | updateMovementSystem() { 69 | this.ecs.update(); 70 | }, 71 | getMovementSystemUpdateCount() { 72 | return this.movementSystem.updateCount; 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddmills/js-ecs-benchmarks/ad4a9dce0b21ac99af851e25c35c1dbd03a63802/output.png -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-ecs-benchmarks", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "js-ecs-benchmarks", 9 | "version": "1.0.0", 10 | "dependencies": { 11 | "@javelin/ecs": "^1.0.0-alpha.13", 12 | "ape-ecs": "^1.3.1", 13 | "bitecs": "^0.3.40", 14 | "ecsy": "^0.4.2", 15 | "elegant-spinner": "^3.0.0", 16 | "geotic": "^4.3.2", 17 | "goodluck": "^7.0.0", 18 | "harmony-ecs": "^0.0.12", 19 | "log-update": "^6.0.0", 20 | "miniplex": "^2.0.0", 21 | "nano-ecs": "^2.4.0", 22 | "perform-ecs": "^0.7.8", 23 | "picoes": "^1.1.1", 24 | "piecs": "^0.4.0", 25 | "tiny-ecs": "^2.0.0", 26 | "uecs": "0.4.2", 27 | "wolf-ecs": "^2.1.3", 28 | "yagl-ecs": "^1.1.0" 29 | }, 30 | "devDependencies": { 31 | "prettier": "^3.2.5" 32 | } 33 | }, 34 | "node_modules/@hmans/id": { 35 | "version": "0.0.1", 36 | "resolved": "https://registry.npmjs.org/@hmans/id/-/id-0.0.1.tgz", 37 | "integrity": "sha512-2BxHxZziST8F0P5Dt+UuttF2lwCaKK7QFR814PwEp708l6dm86pjWJAFr7BKyAGu27mRpYcghvCAUAucNdsCdw==" 38 | }, 39 | "node_modules/@hmans/queue": { 40 | "version": "0.0.1", 41 | "resolved": "https://registry.npmjs.org/@hmans/queue/-/queue-0.0.1.tgz", 42 | "integrity": "sha512-Mm8oQRFRoiEYQ3QD9CD14DTaShd21nzNrUFyBhFxy+TduKf2PdMRfHHx7lDvEpbODGNlj+e8mcWGj/YjWmC3Bw==" 43 | }, 44 | "node_modules/@javelin/core": { 45 | "version": "1.0.0-alpha.12", 46 | "resolved": "https://registry.npmjs.org/@javelin/core/-/core-1.0.0-alpha.12.tgz", 47 | "integrity": "sha512-U3KgZjpMNwO6SF1cVcn4XedpE+AE17aaoNr1S6W973qF7MBrfhRaeoEMjiPM5U2g9VxBJMr4rD9XzZRtyZPaYA==" 48 | }, 49 | "node_modules/@javelin/ecs": { 50 | "version": "1.0.0-alpha.13", 51 | "resolved": "https://registry.npmjs.org/@javelin/ecs/-/ecs-1.0.0-alpha.13.tgz", 52 | "integrity": "sha512-TzBOEtDtb8SEsH1Kpjp3fkmLN81/XTXm4N0S3eWBn05bPYCkqr9iHTREIZT2KPz4URaaMnIqzWSE+slXNegeZA==", 53 | "dependencies": { 54 | "@javelin/core": "1.0.0-alpha.12", 55 | "@javelin/isomorphic-utils": "1.0.0-alpha.12" 56 | } 57 | }, 58 | "node_modules/@javelin/isomorphic-utils": { 59 | "version": "1.0.0-alpha.12", 60 | "resolved": "https://registry.npmjs.org/@javelin/isomorphic-utils/-/isomorphic-utils-1.0.0-alpha.12.tgz", 61 | "integrity": "sha512-TBz3kD75v3QOdibJWCShVjexhhor1e+p4lHprQM+wq27HneDKz1e1JjZd8dY4N916fwgCaMgV/p1FYTtfw+Tng==" 62 | }, 63 | "node_modules/@miniplex/bucket": { 64 | "version": "2.0.0", 65 | "resolved": "https://registry.npmjs.org/@miniplex/bucket/-/bucket-2.0.0.tgz", 66 | "integrity": "sha512-jdAW0vG0tLel0dD0lyQqkQ4g4G9CscbVtenu35jB48fnVK3zmfFmQKIxXHsUvFs4KAN44UESAgaVOroTetFSRw==", 67 | "dependencies": { 68 | "eventery": "^0.0.4" 69 | } 70 | }, 71 | "node_modules/ansi-escapes": { 72 | "version": "6.2.0", 73 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", 74 | "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", 75 | "dependencies": { 76 | "type-fest": "^3.0.0" 77 | }, 78 | "engines": { 79 | "node": ">=14.16" 80 | }, 81 | "funding": { 82 | "url": "https://github.com/sponsors/sindresorhus" 83 | } 84 | }, 85 | "node_modules/ansi-regex": { 86 | "version": "6.0.1", 87 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", 88 | "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", 89 | "engines": { 90 | "node": ">=12" 91 | }, 92 | "funding": { 93 | "url": "https://github.com/chalk/ansi-regex?sponsor=1" 94 | } 95 | }, 96 | "node_modules/ansi-styles": { 97 | "version": "6.2.1", 98 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", 99 | "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", 100 | "engines": { 101 | "node": ">=12" 102 | }, 103 | "funding": { 104 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 105 | } 106 | }, 107 | "node_modules/ape-ecs": { 108 | "version": "1.3.1", 109 | "resolved": "https://registry.npmjs.org/ape-ecs/-/ape-ecs-1.3.1.tgz", 110 | "integrity": "sha512-6QvLDdxjpjtc0mCnpdNeCUlNAGO0B0VJzTaqnRNwc6rDjDDSthyTMUCywvb6WHEcdxkue77xtl+2H38FBL2RJA==" 111 | }, 112 | "node_modules/bitecs": { 113 | "version": "0.3.40", 114 | "resolved": "https://registry.npmjs.org/bitecs/-/bitecs-0.3.40.tgz", 115 | "integrity": "sha512-wAylY4pNfX8IeIH5phtwt1lUNtHKrkoSNrArI7Ris2Y4nEQWFIVvXdgAuqprEg9bq8Wolmlj0gVfeG6MFmtI2Q==" 116 | }, 117 | "node_modules/camelcase": { 118 | "version": "6.0.0", 119 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.0.0.tgz", 120 | "integrity": "sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==", 121 | "engines": { 122 | "node": ">=10" 123 | }, 124 | "funding": { 125 | "url": "https://github.com/sponsors/sindresorhus" 126 | } 127 | }, 128 | "node_modules/cli-cursor": { 129 | "version": "4.0.0", 130 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", 131 | "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", 132 | "dependencies": { 133 | "restore-cursor": "^4.0.0" 134 | }, 135 | "engines": { 136 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 137 | }, 138 | "funding": { 139 | "url": "https://github.com/sponsors/sindresorhus" 140 | } 141 | }, 142 | "node_modules/ecsy": { 143 | "version": "0.4.2", 144 | "resolved": "https://registry.npmjs.org/ecsy/-/ecsy-0.4.2.tgz", 145 | "integrity": "sha512-dVKOkuz1IsRvS7GlHcWghUtMZzXr70h6b+SQEHlKy2RgsCRNDzdYr6a43Q6HafuquA/67YFZCQt8zqadq/jlLA==" 146 | }, 147 | "node_modules/elegant-spinner": { 148 | "version": "3.0.0", 149 | "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-3.0.0.tgz", 150 | "integrity": "sha512-nWUuor3FWTGYAch7SY0unb5qLzs7eAc24ic9PBh+eQctFNQ4IDWJqBpBgsL4SrrGHHN0mJoL7CpWZby5t2KjFg==", 151 | "engines": { 152 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 153 | }, 154 | "funding": { 155 | "url": "https://github.com/sponsors/sindresorhus" 156 | } 157 | }, 158 | "node_modules/emoji-regex": { 159 | "version": "10.3.0", 160 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", 161 | "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==" 162 | }, 163 | "node_modules/eventery": { 164 | "version": "0.0.4", 165 | "resolved": "https://registry.npmjs.org/eventery/-/eventery-0.0.4.tgz", 166 | "integrity": "sha512-rQbXJG8WX9z2cm3IJadkSUZLWFXuwGMIJ40oWyB23DhVb1e92T7Jx3qF0UwtgHI7D4VZzVswbZ7MXfW88cFF4Q==" 167 | }, 168 | "node_modules/geotic": { 169 | "version": "4.3.2", 170 | "resolved": "https://registry.npmjs.org/geotic/-/geotic-4.3.2.tgz", 171 | "integrity": "sha512-kZrQXqY9ulqs15i/0qRbjJ/szPJEQ/AdNAdHqgMAKlxuEBG4FHpxTZW04BGNO9dfx0fry9YzTcdyJADHjhIKmA==", 172 | "dependencies": { 173 | "camelcase": "6.0.0" 174 | } 175 | }, 176 | "node_modules/get-east-asian-width": { 177 | "version": "1.2.0", 178 | "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", 179 | "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", 180 | "engines": { 181 | "node": ">=18" 182 | }, 183 | "funding": { 184 | "url": "https://github.com/sponsors/sindresorhus" 185 | } 186 | }, 187 | "node_modules/goodluck": { 188 | "version": "7.0.0", 189 | "resolved": "https://registry.npmjs.org/goodluck/-/goodluck-7.0.0.tgz", 190 | "integrity": "sha512-ml0lMI5CGXkSHRg7Po3wpyNm0qMDshrquiXEVFdAYyehvh8qT5SsQuLvK+T6YjUMJE27d1EkqVhNBp1ufsZEZA==", 191 | "engines": { 192 | "node": ">=12.0.0" 193 | } 194 | }, 195 | "node_modules/harmony-ecs": { 196 | "version": "0.0.12", 197 | "resolved": "https://registry.npmjs.org/harmony-ecs/-/harmony-ecs-0.0.12.tgz", 198 | "integrity": "sha512-he27c8UUVi3TeCGYxMMQ0q7L+Y5xWTiA7UZXvt2h/97pnRUle0aJtdToTKPKXmb3ulqfo8+otun+iqPxzH2CEg==", 199 | "engines": { 200 | "node": ">=14.18.1" 201 | } 202 | }, 203 | "node_modules/is-fullwidth-code-point": { 204 | "version": "5.0.0", 205 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", 206 | "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", 207 | "dependencies": { 208 | "get-east-asian-width": "^1.0.0" 209 | }, 210 | "engines": { 211 | "node": ">=18" 212 | }, 213 | "funding": { 214 | "url": "https://github.com/sponsors/sindresorhus" 215 | } 216 | }, 217 | "node_modules/log-update": { 218 | "version": "6.0.0", 219 | "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz", 220 | "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", 221 | "dependencies": { 222 | "ansi-escapes": "^6.2.0", 223 | "cli-cursor": "^4.0.0", 224 | "slice-ansi": "^7.0.0", 225 | "strip-ansi": "^7.1.0", 226 | "wrap-ansi": "^9.0.0" 227 | }, 228 | "engines": { 229 | "node": ">=18" 230 | }, 231 | "funding": { 232 | "url": "https://github.com/sponsors/sindresorhus" 233 | } 234 | }, 235 | "node_modules/mimic-fn": { 236 | "version": "2.1.0", 237 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 238 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 239 | "engines": { 240 | "node": ">=6" 241 | } 242 | }, 243 | "node_modules/miniplex": { 244 | "version": "2.0.0", 245 | "resolved": "https://registry.npmjs.org/miniplex/-/miniplex-2.0.0.tgz", 246 | "integrity": "sha512-pJlxmlPf5Qyx12amgOCyRE6Lzw28ct2G0lF9xn7/xudLtA/xDOUnCIU2xOxCk8GkjePYctcNpjmFshJp/Ht66A==", 247 | "dependencies": { 248 | "@hmans/id": "^0.0.1", 249 | "@hmans/queue": "^0.0.1", 250 | "@miniplex/bucket": "2.0.0" 251 | } 252 | }, 253 | "node_modules/nano-ecs": { 254 | "version": "2.4.0", 255 | "resolved": "https://registry.npmjs.org/nano-ecs/-/nano-ecs-2.4.0.tgz", 256 | "integrity": "sha512-joV4lvMWQaxgEzkwpeNCF/R/iOwPXzhFe/Ekc4zZGI0HgurYnoULXEPnxR7yv6QFkQEwoAPx5eEcrT1lbHTSpQ==", 257 | "dependencies": { 258 | "reuse-pool": "^1.0.2", 259 | "typedef": "~1.0.2" 260 | } 261 | }, 262 | "node_modules/onetime": { 263 | "version": "5.1.2", 264 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", 265 | "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", 266 | "dependencies": { 267 | "mimic-fn": "^2.1.0" 268 | }, 269 | "engines": { 270 | "node": ">=6" 271 | }, 272 | "funding": { 273 | "url": "https://github.com/sponsors/sindresorhus" 274 | } 275 | }, 276 | "node_modules/perform-ecs": { 277 | "version": "0.7.8", 278 | "resolved": "https://registry.npmjs.org/perform-ecs/-/perform-ecs-0.7.8.tgz", 279 | "integrity": "sha512-+4WYfCW6/ZVYQDF+78RJc7oWmK2dqtfsdDRfB/kyiHfbQfRIWldJqn2hpQt0ByDWQgtFQrMGGpED1S9wQj3WOw==" 280 | }, 281 | "node_modules/picoes": { 282 | "version": "1.1.1", 283 | "resolved": "https://registry.npmjs.org/picoes/-/picoes-1.1.1.tgz", 284 | "integrity": "sha512-SlyURkd1awkI0rm+tOYtJTZ038hR0En/7NCtJqhSP4KHMlerHAwDFpc+7TkRU2ufasBjEtl977UdwaIXo7EWmg==" 285 | }, 286 | "node_modules/piecs": { 287 | "version": "0.4.0", 288 | "resolved": "https://registry.npmjs.org/piecs/-/piecs-0.4.0.tgz", 289 | "integrity": "sha512-NiGuWHykjFCahsYzn7U3+Q0nk9wUr0wITRioTVCZkK6nk8P4Px/arWYviezyMDT5OaqNmLg4eL00Udj4txlhrQ==" 290 | }, 291 | "node_modules/prettier": { 292 | "version": "3.2.5", 293 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", 294 | "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", 295 | "dev": true, 296 | "bin": { 297 | "prettier": "bin/prettier.cjs" 298 | }, 299 | "engines": { 300 | "node": ">=14" 301 | }, 302 | "funding": { 303 | "url": "https://github.com/prettier/prettier?sponsor=1" 304 | } 305 | }, 306 | "node_modules/restore-cursor": { 307 | "version": "4.0.0", 308 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", 309 | "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", 310 | "dependencies": { 311 | "onetime": "^5.1.0", 312 | "signal-exit": "^3.0.2" 313 | }, 314 | "engines": { 315 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 316 | }, 317 | "funding": { 318 | "url": "https://github.com/sponsors/sindresorhus" 319 | } 320 | }, 321 | "node_modules/reuse-pool": { 322 | "version": "1.0.2", 323 | "resolved": "https://registry.npmjs.org/reuse-pool/-/reuse-pool-1.0.2.tgz", 324 | "integrity": "sha1-hPCRVk2N/EagPKeCWss3E3W21Fw=" 325 | }, 326 | "node_modules/signal-exit": { 327 | "version": "3.0.7", 328 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", 329 | "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" 330 | }, 331 | "node_modules/slice-ansi": { 332 | "version": "7.1.0", 333 | "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", 334 | "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", 335 | "dependencies": { 336 | "ansi-styles": "^6.2.1", 337 | "is-fullwidth-code-point": "^5.0.0" 338 | }, 339 | "engines": { 340 | "node": ">=18" 341 | }, 342 | "funding": { 343 | "url": "https://github.com/chalk/slice-ansi?sponsor=1" 344 | } 345 | }, 346 | "node_modules/string-width": { 347 | "version": "7.1.0", 348 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", 349 | "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", 350 | "dependencies": { 351 | "emoji-regex": "^10.3.0", 352 | "get-east-asian-width": "^1.0.0", 353 | "strip-ansi": "^7.1.0" 354 | }, 355 | "engines": { 356 | "node": ">=18" 357 | }, 358 | "funding": { 359 | "url": "https://github.com/sponsors/sindresorhus" 360 | } 361 | }, 362 | "node_modules/strip-ansi": { 363 | "version": "7.1.0", 364 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", 365 | "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", 366 | "dependencies": { 367 | "ansi-regex": "^6.0.1" 368 | }, 369 | "engines": { 370 | "node": ">=12" 371 | }, 372 | "funding": { 373 | "url": "https://github.com/chalk/strip-ansi?sponsor=1" 374 | } 375 | }, 376 | "node_modules/tiny-ecs": { 377 | "version": "2.0.0", 378 | "resolved": "https://registry.npmjs.org/tiny-ecs/-/tiny-ecs-2.0.0.tgz", 379 | "integrity": "sha1-lmuB+UqXXasI1GCmkdYl90wpVBw=", 380 | "dependencies": { 381 | "typedef": "~1.0.2" 382 | } 383 | }, 384 | "node_modules/type-fest": { 385 | "version": "3.13.1", 386 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", 387 | "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", 388 | "engines": { 389 | "node": ">=14.16" 390 | }, 391 | "funding": { 392 | "url": "https://github.com/sponsors/sindresorhus" 393 | } 394 | }, 395 | "node_modules/typedef": { 396 | "version": "1.0.4", 397 | "resolved": "https://registry.npmjs.org/typedef/-/typedef-1.0.4.tgz", 398 | "integrity": "sha1-+DOIgWkWBk2UYAjZrrKXyqq/ODs=" 399 | }, 400 | "node_modules/uecs": { 401 | "version": "0.4.2", 402 | "resolved": "https://registry.npmjs.org/uecs/-/uecs-0.4.2.tgz", 403 | "integrity": "sha512-oPwSibOqllZeBu6U2v/wkqIq/XSiwaf0bff1aJhuPEBOqRWuo8iH/RP+bzPPUDdwQ6vRljIkksy0plmEIfbc3w==" 404 | }, 405 | "node_modules/wolf-ecs": { 406 | "version": "2.1.3", 407 | "resolved": "https://registry.npmjs.org/wolf-ecs/-/wolf-ecs-2.1.3.tgz", 408 | "integrity": "sha512-zTmUsc85VrvFWi7BtZGgF2ZVtcohtMEeSC3fQrWVOENDJkXEm380EFsthr6tFWRJACFLWkw0zUCR6aBFuyTq+A==" 409 | }, 410 | "node_modules/wrap-ansi": { 411 | "version": "9.0.0", 412 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", 413 | "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", 414 | "dependencies": { 415 | "ansi-styles": "^6.2.1", 416 | "string-width": "^7.0.0", 417 | "strip-ansi": "^7.1.0" 418 | }, 419 | "engines": { 420 | "node": ">=18" 421 | }, 422 | "funding": { 423 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 424 | } 425 | }, 426 | "node_modules/yagl-ecs": { 427 | "version": "1.1.0", 428 | "resolved": "https://registry.npmjs.org/yagl-ecs/-/yagl-ecs-1.1.0.tgz", 429 | "integrity": "sha1-dM1Qe/USpKEqmIYHFAR9/CQaaaY=", 430 | "engines": { 431 | "node": ">=0.10.0" 432 | } 433 | } 434 | }, 435 | "dependencies": { 436 | "@hmans/id": { 437 | "version": "0.0.1", 438 | "resolved": "https://registry.npmjs.org/@hmans/id/-/id-0.0.1.tgz", 439 | "integrity": "sha512-2BxHxZziST8F0P5Dt+UuttF2lwCaKK7QFR814PwEp708l6dm86pjWJAFr7BKyAGu27mRpYcghvCAUAucNdsCdw==" 440 | }, 441 | "@hmans/queue": { 442 | "version": "0.0.1", 443 | "resolved": "https://registry.npmjs.org/@hmans/queue/-/queue-0.0.1.tgz", 444 | "integrity": "sha512-Mm8oQRFRoiEYQ3QD9CD14DTaShd21nzNrUFyBhFxy+TduKf2PdMRfHHx7lDvEpbODGNlj+e8mcWGj/YjWmC3Bw==" 445 | }, 446 | "@javelin/core": { 447 | "version": "1.0.0-alpha.12", 448 | "resolved": "https://registry.npmjs.org/@javelin/core/-/core-1.0.0-alpha.12.tgz", 449 | "integrity": "sha512-U3KgZjpMNwO6SF1cVcn4XedpE+AE17aaoNr1S6W973qF7MBrfhRaeoEMjiPM5U2g9VxBJMr4rD9XzZRtyZPaYA==" 450 | }, 451 | "@javelin/ecs": { 452 | "version": "1.0.0-alpha.13", 453 | "resolved": "https://registry.npmjs.org/@javelin/ecs/-/ecs-1.0.0-alpha.13.tgz", 454 | "integrity": "sha512-TzBOEtDtb8SEsH1Kpjp3fkmLN81/XTXm4N0S3eWBn05bPYCkqr9iHTREIZT2KPz4URaaMnIqzWSE+slXNegeZA==", 455 | "requires": { 456 | "@javelin/core": "1.0.0-alpha.12", 457 | "@javelin/isomorphic-utils": "1.0.0-alpha.12" 458 | } 459 | }, 460 | "@javelin/isomorphic-utils": { 461 | "version": "1.0.0-alpha.12", 462 | "resolved": "https://registry.npmjs.org/@javelin/isomorphic-utils/-/isomorphic-utils-1.0.0-alpha.12.tgz", 463 | "integrity": "sha512-TBz3kD75v3QOdibJWCShVjexhhor1e+p4lHprQM+wq27HneDKz1e1JjZd8dY4N916fwgCaMgV/p1FYTtfw+Tng==" 464 | }, 465 | "@miniplex/bucket": { 466 | "version": "2.0.0", 467 | "resolved": "https://registry.npmjs.org/@miniplex/bucket/-/bucket-2.0.0.tgz", 468 | "integrity": "sha512-jdAW0vG0tLel0dD0lyQqkQ4g4G9CscbVtenu35jB48fnVK3zmfFmQKIxXHsUvFs4KAN44UESAgaVOroTetFSRw==", 469 | "requires": { 470 | "eventery": "^0.0.4" 471 | } 472 | }, 473 | "ansi-escapes": { 474 | "version": "6.2.0", 475 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", 476 | "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", 477 | "requires": { 478 | "type-fest": "^3.0.0" 479 | } 480 | }, 481 | "ansi-regex": { 482 | "version": "6.0.1", 483 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", 484 | "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" 485 | }, 486 | "ansi-styles": { 487 | "version": "6.2.1", 488 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", 489 | "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" 490 | }, 491 | "ape-ecs": { 492 | "version": "1.3.1", 493 | "resolved": "https://registry.npmjs.org/ape-ecs/-/ape-ecs-1.3.1.tgz", 494 | "integrity": "sha512-6QvLDdxjpjtc0mCnpdNeCUlNAGO0B0VJzTaqnRNwc6rDjDDSthyTMUCywvb6WHEcdxkue77xtl+2H38FBL2RJA==" 495 | }, 496 | "bitecs": { 497 | "version": "0.3.40", 498 | "resolved": "https://registry.npmjs.org/bitecs/-/bitecs-0.3.40.tgz", 499 | "integrity": "sha512-wAylY4pNfX8IeIH5phtwt1lUNtHKrkoSNrArI7Ris2Y4nEQWFIVvXdgAuqprEg9bq8Wolmlj0gVfeG6MFmtI2Q==" 500 | }, 501 | "camelcase": { 502 | "version": "6.0.0", 503 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.0.0.tgz", 504 | "integrity": "sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==" 505 | }, 506 | "cli-cursor": { 507 | "version": "4.0.0", 508 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", 509 | "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", 510 | "requires": { 511 | "restore-cursor": "^4.0.0" 512 | } 513 | }, 514 | "ecsy": { 515 | "version": "0.4.2", 516 | "resolved": "https://registry.npmjs.org/ecsy/-/ecsy-0.4.2.tgz", 517 | "integrity": "sha512-dVKOkuz1IsRvS7GlHcWghUtMZzXr70h6b+SQEHlKy2RgsCRNDzdYr6a43Q6HafuquA/67YFZCQt8zqadq/jlLA==" 518 | }, 519 | "elegant-spinner": { 520 | "version": "3.0.0", 521 | "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-3.0.0.tgz", 522 | "integrity": "sha512-nWUuor3FWTGYAch7SY0unb5qLzs7eAc24ic9PBh+eQctFNQ4IDWJqBpBgsL4SrrGHHN0mJoL7CpWZby5t2KjFg==" 523 | }, 524 | "emoji-regex": { 525 | "version": "10.3.0", 526 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", 527 | "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==" 528 | }, 529 | "eventery": { 530 | "version": "0.0.4", 531 | "resolved": "https://registry.npmjs.org/eventery/-/eventery-0.0.4.tgz", 532 | "integrity": "sha512-rQbXJG8WX9z2cm3IJadkSUZLWFXuwGMIJ40oWyB23DhVb1e92T7Jx3qF0UwtgHI7D4VZzVswbZ7MXfW88cFF4Q==" 533 | }, 534 | "geotic": { 535 | "version": "4.3.2", 536 | "resolved": "https://registry.npmjs.org/geotic/-/geotic-4.3.2.tgz", 537 | "integrity": "sha512-kZrQXqY9ulqs15i/0qRbjJ/szPJEQ/AdNAdHqgMAKlxuEBG4FHpxTZW04BGNO9dfx0fry9YzTcdyJADHjhIKmA==", 538 | "requires": { 539 | "camelcase": "6.0.0" 540 | } 541 | }, 542 | "get-east-asian-width": { 543 | "version": "1.2.0", 544 | "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", 545 | "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==" 546 | }, 547 | "goodluck": { 548 | "version": "7.0.0", 549 | "resolved": "https://registry.npmjs.org/goodluck/-/goodluck-7.0.0.tgz", 550 | "integrity": "sha512-ml0lMI5CGXkSHRg7Po3wpyNm0qMDshrquiXEVFdAYyehvh8qT5SsQuLvK+T6YjUMJE27d1EkqVhNBp1ufsZEZA==" 551 | }, 552 | "harmony-ecs": { 553 | "version": "0.0.12", 554 | "resolved": "https://registry.npmjs.org/harmony-ecs/-/harmony-ecs-0.0.12.tgz", 555 | "integrity": "sha512-he27c8UUVi3TeCGYxMMQ0q7L+Y5xWTiA7UZXvt2h/97pnRUle0aJtdToTKPKXmb3ulqfo8+otun+iqPxzH2CEg==" 556 | }, 557 | "is-fullwidth-code-point": { 558 | "version": "5.0.0", 559 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", 560 | "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", 561 | "requires": { 562 | "get-east-asian-width": "^1.0.0" 563 | } 564 | }, 565 | "log-update": { 566 | "version": "6.0.0", 567 | "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz", 568 | "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", 569 | "requires": { 570 | "ansi-escapes": "^6.2.0", 571 | "cli-cursor": "^4.0.0", 572 | "slice-ansi": "^7.0.0", 573 | "strip-ansi": "^7.1.0", 574 | "wrap-ansi": "^9.0.0" 575 | } 576 | }, 577 | "mimic-fn": { 578 | "version": "2.1.0", 579 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 580 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" 581 | }, 582 | "miniplex": { 583 | "version": "2.0.0", 584 | "resolved": "https://registry.npmjs.org/miniplex/-/miniplex-2.0.0.tgz", 585 | "integrity": "sha512-pJlxmlPf5Qyx12amgOCyRE6Lzw28ct2G0lF9xn7/xudLtA/xDOUnCIU2xOxCk8GkjePYctcNpjmFshJp/Ht66A==", 586 | "requires": { 587 | "@hmans/id": "^0.0.1", 588 | "@hmans/queue": "^0.0.1", 589 | "@miniplex/bucket": "2.0.0" 590 | } 591 | }, 592 | "nano-ecs": { 593 | "version": "2.4.0", 594 | "resolved": "https://registry.npmjs.org/nano-ecs/-/nano-ecs-2.4.0.tgz", 595 | "integrity": "sha512-joV4lvMWQaxgEzkwpeNCF/R/iOwPXzhFe/Ekc4zZGI0HgurYnoULXEPnxR7yv6QFkQEwoAPx5eEcrT1lbHTSpQ==", 596 | "requires": { 597 | "reuse-pool": "^1.0.2", 598 | "typedef": "~1.0.2" 599 | } 600 | }, 601 | "onetime": { 602 | "version": "5.1.2", 603 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", 604 | "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", 605 | "requires": { 606 | "mimic-fn": "^2.1.0" 607 | } 608 | }, 609 | "perform-ecs": { 610 | "version": "0.7.8", 611 | "resolved": "https://registry.npmjs.org/perform-ecs/-/perform-ecs-0.7.8.tgz", 612 | "integrity": "sha512-+4WYfCW6/ZVYQDF+78RJc7oWmK2dqtfsdDRfB/kyiHfbQfRIWldJqn2hpQt0ByDWQgtFQrMGGpED1S9wQj3WOw==" 613 | }, 614 | "picoes": { 615 | "version": "1.1.1", 616 | "resolved": "https://registry.npmjs.org/picoes/-/picoes-1.1.1.tgz", 617 | "integrity": "sha512-SlyURkd1awkI0rm+tOYtJTZ038hR0En/7NCtJqhSP4KHMlerHAwDFpc+7TkRU2ufasBjEtl977UdwaIXo7EWmg==" 618 | }, 619 | "piecs": { 620 | "version": "0.4.0", 621 | "resolved": "https://registry.npmjs.org/piecs/-/piecs-0.4.0.tgz", 622 | "integrity": "sha512-NiGuWHykjFCahsYzn7U3+Q0nk9wUr0wITRioTVCZkK6nk8P4Px/arWYviezyMDT5OaqNmLg4eL00Udj4txlhrQ==" 623 | }, 624 | "prettier": { 625 | "version": "3.2.5", 626 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", 627 | "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", 628 | "dev": true 629 | }, 630 | "restore-cursor": { 631 | "version": "4.0.0", 632 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", 633 | "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", 634 | "requires": { 635 | "onetime": "^5.1.0", 636 | "signal-exit": "^3.0.2" 637 | } 638 | }, 639 | "reuse-pool": { 640 | "version": "1.0.2", 641 | "resolved": "https://registry.npmjs.org/reuse-pool/-/reuse-pool-1.0.2.tgz", 642 | "integrity": "sha1-hPCRVk2N/EagPKeCWss3E3W21Fw=" 643 | }, 644 | "signal-exit": { 645 | "version": "3.0.7", 646 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", 647 | "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" 648 | }, 649 | "slice-ansi": { 650 | "version": "7.1.0", 651 | "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", 652 | "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", 653 | "requires": { 654 | "ansi-styles": "^6.2.1", 655 | "is-fullwidth-code-point": "^5.0.0" 656 | } 657 | }, 658 | "string-width": { 659 | "version": "7.1.0", 660 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", 661 | "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", 662 | "requires": { 663 | "emoji-regex": "^10.3.0", 664 | "get-east-asian-width": "^1.0.0", 665 | "strip-ansi": "^7.1.0" 666 | } 667 | }, 668 | "strip-ansi": { 669 | "version": "7.1.0", 670 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", 671 | "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", 672 | "requires": { 673 | "ansi-regex": "^6.0.1" 674 | } 675 | }, 676 | "tiny-ecs": { 677 | "version": "2.0.0", 678 | "resolved": "https://registry.npmjs.org/tiny-ecs/-/tiny-ecs-2.0.0.tgz", 679 | "integrity": "sha1-lmuB+UqXXasI1GCmkdYl90wpVBw=", 680 | "requires": { 681 | "typedef": "~1.0.2" 682 | } 683 | }, 684 | "type-fest": { 685 | "version": "3.13.1", 686 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", 687 | "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==" 688 | }, 689 | "typedef": { 690 | "version": "1.0.4", 691 | "resolved": "https://registry.npmjs.org/typedef/-/typedef-1.0.4.tgz", 692 | "integrity": "sha1-+DOIgWkWBk2UYAjZrrKXyqq/ODs=" 693 | }, 694 | "uecs": { 695 | "version": "0.4.2", 696 | "resolved": "https://registry.npmjs.org/uecs/-/uecs-0.4.2.tgz", 697 | "integrity": "sha512-oPwSibOqllZeBu6U2v/wkqIq/XSiwaf0bff1aJhuPEBOqRWuo8iH/RP+bzPPUDdwQ6vRljIkksy0plmEIfbc3w==" 698 | }, 699 | "wolf-ecs": { 700 | "version": "2.1.3", 701 | "resolved": "https://registry.npmjs.org/wolf-ecs/-/wolf-ecs-2.1.3.tgz", 702 | "integrity": "sha512-zTmUsc85VrvFWi7BtZGgF2ZVtcohtMEeSC3fQrWVOENDJkXEm380EFsthr6tFWRJACFLWkw0zUCR6aBFuyTq+A==" 703 | }, 704 | "wrap-ansi": { 705 | "version": "9.0.0", 706 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", 707 | "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", 708 | "requires": { 709 | "ansi-styles": "^6.2.1", 710 | "string-width": "^7.0.0", 711 | "strip-ansi": "^7.1.0" 712 | } 713 | }, 714 | "yagl-ecs": { 715 | "version": "1.1.0", 716 | "resolved": "https://registry.npmjs.org/yagl-ecs/-/yagl-ecs-1.1.0.tgz", 717 | "integrity": "sha1-dM1Qe/USpKEqmIYHFAR9/CQaaaY=" 718 | } 719 | } 720 | } 721 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-ecs-benchmarks", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node --experimental-modules --es-module-specifier-resolution=node index.js", 9 | "pretty": "prettier --write ." 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/ddmills/js-ecs-benchmarks.git" 14 | }, 15 | "dependencies": { 16 | "@javelin/ecs": "^1.0.0-alpha.13", 17 | "ape-ecs": "^1.3.1", 18 | "bitecs": "^0.3.40", 19 | "ecsy": "^0.4.2", 20 | "elegant-spinner": "^3.0.0", 21 | "geotic": "^4.3.2", 22 | "goodluck": "^7.0.0", 23 | "harmony-ecs": "^0.0.12", 24 | "log-update": "^6.0.0", 25 | "miniplex": "^2.0.0", 26 | "nano-ecs": "^2.4.0", 27 | "perform-ecs": "^0.7.8", 28 | "picoes": "^1.1.1", 29 | "piecs": "^0.4.0", 30 | "tiny-ecs": "^2.0.0", 31 | "uecs": "0.4.2", 32 | "wolf-ecs": "^2.1.3", 33 | "yagl-ecs": "^1.1.0" 34 | }, 35 | "devDependencies": { 36 | "prettier": "^3.2.5" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # JS ECS Benchmarks 2 | 3 | Includes: 4 | 5 | - [Ape ECS](https://github.com/fritzy/ape-ecs) 6 | - [bitECS](https://github.com/NateTheGreatt/bitECS) 7 | - [ecsy](https://github.com/ecsyjs/ecsy) 8 | - [geotic](https://github.com/ddmills/geotic) 9 | - [goodluck](https://github.com/piesku/goodluck) 10 | - [harmony-ecs](https://github.com/3mcd/harmony-ecs) 11 | - [miniplex](https://github.com/hmans/miniplex) 12 | - [nano-ecs](https://github.com/noffle/nano-ecs) 13 | - [perform-ecs](https://github.com/fireveined/perform-ecs) 14 | - [piecs](https://github.com/sondresj/piecs) 15 | - [tiny-ecs](https://github.com/bvalosek/tiny-ecs) 16 | - [uecs](https://github.com/jprochazk/uecs) 17 | - [wolf-ecs](https://github.com/EnderShadow8/wolf-ecs) 18 | - [yagl-ecs](https://github.com/yagl/ecs) 19 | - [javelin-ecs](https://github.com/3mcd/javelin) 20 | 21 | Pull requests are welcome! Benchmarks were generated using `node v20.11.0`. 22 | 23 | ``` 24 | Suite Add/Remove (5000 iterations) 25 | - piecs 32ms 160964ms 25010000 updates 0.0% fastest 26 | - harmony-ecs 38ms 190127ms 25010000 updates 18.1% slower 27 | - wolf-ecs 42ms 208967ms 25010000 updates 29.8% slower 28 | - uecs 44ms 220170ms 25010000 updates 36.8% slower 29 | - perform-ecs 52ms 258500ms 25010000 updates 60.6% slower 30 | - bitecs 52ms 260959ms 25010000 updates 62.1% slower 31 | - javelin-ecs 61ms 302799ms 25010000 updates 88.1% slower 32 | - miniplex 66ms 332181ms 25010000 updates 106.4% slower 33 | - goodluck 95ms 475316ms 25010000 updates 195.3% slower 34 | - yagl-ecs 103ms 512644ms 25010000 updates 218.5% slower 35 | - tiny-ecs 121ms 605166ms 25010000 updates 276.0% slower 36 | - picoes 153ms 765603ms 25010000 updates 375.6% slower 37 | - nano-ecs 171ms 854997ms 25010000 updates 431.2% slower 38 | - geotic 251ms 1254544ms 25010000 updates 679.4% slower 39 | - ape-ecs 475ms 2375773ms 25010000 updates 1376.0% slower 40 | - ecsy 1831ms 9157038ms 25010000 updates 5588.9% slower 41 | 42 | Suite Destroy (100000 iterations) 43 | - goodluck 0ms 17105ms 0.0% fastest 44 | - piecs 0ms 23574ms 37.8% slower 45 | - wolf-ecs 0ms 24390ms 42.6% slower 46 | - bitecs 1ms 88174ms 415.5% slower 47 | - uecs 1ms 95106ms 456.0% slower 48 | - miniplex 1ms 101520ms 493.5% slower 49 | - harmony-ecs 1ms 110036ms 543.3% slower 50 | - javelin-ecs 1ms 117501ms 586.9% slower 51 | - picoes 1ms 136197ms 696.2% slower 52 | - tiny-ecs 2ms 172166ms 906.5% slower 53 | - nano-ecs 2ms 220160ms 1187.1% slower 54 | - perform-ecs 2ms 231468ms 1253.2% slower 55 | - geotic 3ms 275307ms 1509.5% slower 56 | - ape-ecs 6ms 555421ms 3147.1% slower 57 | - yagl-ecs 16ms 1551402ms 8969.8% slower 58 | - ecsy 18ms 1810336ms 10483.5% slower 59 | 60 | Suite Velocity (2000 iterations) 61 | - piecs 9ms 17715ms 2001000 updates 0.0% fastest 62 | - harmony-ecs 10ms 19220ms 2001000 updates 8.5% slower 63 | - uecs 11ms 21155ms 2001000 updates 19.4% slower 64 | - wolf-ecs 11ms 21406ms 2001000 updates 20.8% slower 65 | - bitecs 12ms 24346ms 2001000 updates 37.4% slower 66 | - geotic 14ms 27887ms 2001000 updates 57.4% slower 67 | - perform-ecs 15ms 29040ms 2001000 updates 63.9% slower 68 | - yagl-ecs 15ms 30627ms 2001000 updates 72.9% slower 69 | - goodluck 16ms 32466ms 2001000 updates 83.3% slower 70 | - javelin-ecs 18ms 35238ms 2001000 updates 98.9% slower 71 | - picoes 19ms 38281ms 2001000 updates 116.1% slower 72 | - miniplex 20ms 40951ms 2001000 updates 131.2% slower 73 | - tiny-ecs 26ms 51263ms 2001000 updates 189.4% slower 74 | - nano-ecs 42ms 84449ms 2001000 updates 376.7% slower 75 | - ape-ecs 90ms 180107ms 2001000 updates 916.7% slower 76 | - ecsy 399ms 797711ms 2001000 updates 4403.1% slower 77 | ``` 78 | -------------------------------------------------------------------------------- /suites/index.js: -------------------------------------------------------------------------------- 1 | export { default as SuiteVelocity } from './suite-velocity.js'; 2 | export { default as SuiteAddRemove } from './suite-add-remove.js'; 3 | export { default as SuiteDestroy } from './suite-destroy.js'; 4 | -------------------------------------------------------------------------------- /suites/suite-add-remove.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'Add/Remove', 3 | iterations: 5000, 4 | setup(ctx) { 5 | ctx.setup(); 6 | }, 7 | perform(ctx) { 8 | const entity1 = ctx.createEntity(); 9 | const entity2 = ctx.createEntity(); 10 | 11 | ctx.addPositionComponent(entity1); 12 | ctx.addVelocityComponent(entity1); 13 | 14 | ctx.addPositionComponent(entity2); 15 | ctx.addVelocityComponent(entity2); 16 | 17 | ctx.updateMovementSystem(); 18 | 19 | ctx.removePositionComponent(entity1); 20 | 21 | ctx.updateMovementSystem(); 22 | 23 | ctx.destroyEntity(entity1); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /suites/suite-destroy.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'Destroy', 3 | iterations: 100000, 4 | setup(ctx) { 5 | ctx.setup(); 6 | }, 7 | perform(ctx) { 8 | const entity = ctx.createEntity(); 9 | 10 | ctx.addPositionComponent(entity); 11 | ctx.addVelocityComponent(entity); 12 | 13 | ctx.destroyEntity(entity); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /suites/suite-velocity.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'Velocity', 3 | iterations: 2000, 4 | setup(ctx) { 5 | ctx.setup(); 6 | }, 7 | perform(ctx) { 8 | const entity = ctx.createEntity(); 9 | 10 | ctx.addPositionComponent(entity); 11 | ctx.addVelocityComponent(entity); 12 | 13 | ctx.updateMovementSystem(); 14 | }, 15 | }; 16 | --------------------------------------------------------------------------------