diff --git a/README.md b/README.md index 8376005..3ee89ff 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ A suite of benchmarks designed to test and compare JavaScript ECS library perfor | picoes | 6,814 | 4,223 | 12,368 | 2,679 | 4,303 | | tiny-ecs | 16,391 | 35,488 | 45,760 | 194 | 1,082 | | uecs | 29,855 | 14,747 | 9,861 | 1,724 | 5,207 | +| elics | 43,719 | 30,758 | 87,590 | 2,761 | 3,694 | The best result for each benchmark is marked in bold text. Note that run to run variance for these benchmarks is typically 1-4%. Any benchmarks within a few percent of each other should be considered “effectively equal”. The above benchmarks are run on node v17.8.0. @@ -44,6 +45,7 @@ The best result for each benchmark is marked in bold text. Note that run to run - [`tiny-ecs`](https://github.com/bvalosek/tiny-ecs) - [`uecs`](https://github.com/jprochazk/uecs) - [`wolf-ecs`](https://github.com/EnderShadow8/wolf-ecs) +- [`elics`](https://github.com/elixr-games/elics) ## Benchmarks diff --git a/package-lock.json b/package-lock.json index bc2f17d..152a92f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "@lastolivegames/becsy": "0.8.4", "bitecs": "0.3.34", "ecsy": "0.4.2", + "elics": "2.0.4", "geotic": "4.1.6", "goodluck": "7.0.0", "harmony-ecs": "0.0.12", @@ -54,6 +55,19 @@ "resolved": "https://registry.npmjs.org/bitecs/-/bitecs-0.3.34.tgz", "integrity": "sha512-WFtNl+7rthxnrNcuQbvnGjOvXYRkOlyz0x/BWZ2a1nJC/k8z6zksDX1B+AH1KH9JqT11V8SzlECAmpcCM+Hkow==" }, + "node_modules/bitset": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bitset/-/bitset-5.2.3.tgz", + "integrity": "sha512-uZ7++Z60MC9cZ+7YzQ1v9yPDydcjhmcMjGx2yoGTjjSXBoVMmTr2LCRbkpI19S9P/C75hhP7Bsakj+gVzVUDbQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/camelcase": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.0.0.tgz", @@ -67,6 +81,22 @@ "resolved": "https://registry.npmjs.org/ecsy/-/ecsy-0.4.2.tgz", "integrity": "sha512-dVKOkuz1IsRvS7GlHcWghUtMZzXr70h6b+SQEHlKy2RgsCRNDzdYr6a43Q6HafuquA/67YFZCQt8zqadq/jlLA==" }, + "node_modules/elics": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/elics/-/elics-2.0.4.tgz", + "integrity": "sha512-FW+QzS5c+aDjoQ0RG7/YskbcQvQyYYwMz/9uJCYWCQaAJQNEaZosatZo+/FK01Z00FQaKfMqL3meHGw7NH3RDQ==", + "license": "MIT", + "dependencies": { + "bitset": "^5.2.3" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/felixtrz" + } + }, "node_modules/geotic": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/geotic/-/geotic-4.1.6.tgz", @@ -186,6 +216,11 @@ "resolved": "https://registry.npmjs.org/bitecs/-/bitecs-0.3.34.tgz", "integrity": "sha512-WFtNl+7rthxnrNcuQbvnGjOvXYRkOlyz0x/BWZ2a1nJC/k8z6zksDX1B+AH1KH9JqT11V8SzlECAmpcCM+Hkow==" }, + "bitset": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bitset/-/bitset-5.2.3.tgz", + "integrity": "sha512-uZ7++Z60MC9cZ+7YzQ1v9yPDydcjhmcMjGx2yoGTjjSXBoVMmTr2LCRbkpI19S9P/C75hhP7Bsakj+gVzVUDbQ==" + }, "camelcase": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.0.0.tgz", @@ -196,6 +231,14 @@ "resolved": "https://registry.npmjs.org/ecsy/-/ecsy-0.4.2.tgz", "integrity": "sha512-dVKOkuz1IsRvS7GlHcWghUtMZzXr70h6b+SQEHlKy2RgsCRNDzdYr6a43Q6HafuquA/67YFZCQt8zqadq/jlLA==" }, + "elics": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/elics/-/elics-2.0.4.tgz", + "integrity": "sha512-FW+QzS5c+aDjoQ0RG7/YskbcQvQyYYwMz/9uJCYWCQaAJQNEaZosatZo+/FK01Z00FQaKfMqL3meHGw7NH3RDQ==", + "requires": { + "bitset": "^5.2.3" + } + }, "geotic": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/geotic/-/geotic-4.1.6.tgz", diff --git a/package.json b/package.json index 6d45aff..78ea92a 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@lastolivegames/becsy": "0.8.4", "bitecs": "0.3.34", "ecsy": "0.4.2", + "elics": "^2.0.4", "geotic": "4.1.6", "goodluck": "7.0.0", "harmony-ecs": "0.0.12", diff --git a/src/bench.js b/src/bench.js index 3d17ad7..9c3cfb3 100644 --- a/src/bench.js +++ b/src/bench.js @@ -21,6 +21,7 @@ const LIBRARIES = [ { kind: OBJ, name: "picoes" }, { kind: OBJ, name: "tiny-ecs" }, { kind: OBJ, name: "uecs" }, + { kind: OBJ, name: "elics" }, ]; const BENCHMARKS = { diff --git a/src/cases/elics/add_remove.js b/src/cases/elics/add_remove.js new file mode 100644 index 0000000..0baeda9 --- /dev/null +++ b/src/cases/elics/add_remove.js @@ -0,0 +1,43 @@ +import { World, createComponent, createSystem } from "elics"; + +const A = createComponent({}); + +const B = createComponent({}); + +class AddB extends createSystem({ + ANotB: { required: [A], excluded: [B] }, +}) { + update() { + this.queries.ANotB.entities.forEach((entity) => { + entity.addComponent(B); + }); + } +} + +class RemoveB extends createSystem({ + B: { required: [B] }, +}) { + update() { + this.queries.B.entities.forEach((entity) => { + entity.removeComponent(B); + }); + } +} + +export default (count) => { + let world = new World({ checksOn: false, deferredEntityUpdates: true }); + + world + .registerComponent(A) + .registerComponent(B) + .registerSystem(AddB) + .registerSystem(RemoveB); + + for (let i = 0; i < count; i++) { + world.createEntity().addComponent(A); + } + + return () => { + world.update(1, 1); + }; +}; diff --git a/src/cases/elics/entity_cycle.js b/src/cases/elics/entity_cycle.js new file mode 100644 index 0000000..69539ee --- /dev/null +++ b/src/cases/elics/entity_cycle.js @@ -0,0 +1,43 @@ +import { Types, World, createComponent, createSystem } from "elics"; + +const A = createComponent({ value: { type: Types.Int16, default: 0 } }); + +const B = createComponent({ value: { type: Types.Int16, default: 0 } }); + +class SpawnB extends createSystem({ + A: { required: [A] }, +}) { + update() { + this.queries.A.entities.forEach((entity) => { + this.world + .createEntity() + .addComponent(B, { value: A.data.value[entity.index] }); + }); + } +} + +class KillB extends createSystem({ B: { required: [B] } }) { + update() { + this.queries.B.entities.forEach((entity) => { + entity.destroy(); + }); + } +} + +export default (count) => { + let world = new World({ checksOn: false }); + + world + .registerComponent(A) + .registerComponent(B) + .registerSystem(SpawnB) + .registerSystem(KillB); + for (let i = 0; i < count; i++) { + const entity = world.createEntity(); + entity.addComponent(A, { value: i }); + } + + return () => { + world.update(1, 1); + }; +}; diff --git a/src/cases/elics/frag_iter.js b/src/cases/elics/frag_iter.js new file mode 100644 index 0000000..046bedb --- /dev/null +++ b/src/cases/elics/frag_iter.js @@ -0,0 +1,61 @@ +import { Types, World, createComponent, createSystem } from "elics"; + +const COMPS = Array.from("ABCDEFGHIJKLMNOPQRSTUVWXYZ").map((_, _index) => + createComponent({ value: { type: Types.Int16, default: 0 } }) +); + +const Z = COMPS[25]; + +const Data = createComponent({ value: { type: Types.Int16, default: 0 } }); + +class DataSystem extends createSystem({ data: { required: [Data] } }) { + init() { + this.value = Data.data.value; + } + + update() { + this.queries.data.entities.forEach((entity) => { + const idx = entity.index; + this.value[idx] *= 2; + }); + } +} + +class ZSystem extends createSystem({ Z: { required: [Z] } }) { + init() { + this.value = Z.data.value; + } + + update() { + this.queries.Z.entities.forEach((entity) => { + const idx = entity.index; + this.value[idx] *= 2; + }); + } +} + +export default (count) => { + let world = new World({ checksOn: false }); + + COMPS.forEach((Comp) => { + world.registerComponent(Comp); + }); + + world + .registerComponent(Data) + .registerSystem(DataSystem) + .registerSystem(ZSystem); + + for (let i = 0; i < count; i++) { + COMPS.forEach((Comp) => { + world + .createEntity() + .addComponent(Comp, { value: 0 }) + .addComponent(Data, { value: 0 }); + }); + } + + return () => { + world.update(1, 1); + }; +}; diff --git a/src/cases/elics/packed_5.js b/src/cases/elics/packed_5.js new file mode 100644 index 0000000..e02c9ba --- /dev/null +++ b/src/cases/elics/packed_5.js @@ -0,0 +1,102 @@ +import { Types, World, createComponent, createSystem } from "elics"; + +const A = createComponent({ value: { type: Types.Int16, default: 0 } }); +const B = createComponent({ value: { type: Types.Int16, default: 0 } }); +const C = createComponent({ value: { type: Types.Int16, default: 0 } }); +const D = createComponent({ value: { type: Types.Int16, default: 0 } }); +const E = createComponent({ value: { type: Types.Int16, default: 0 } }); + +class ASystem extends createSystem({ A: { required: [A] } }) { + init() { + this.value = A.data.value; + } + + update() { + this.queries.A.entities.forEach((entity) => { + const idx = entity.index; + this.value[idx] *= 2; + }); + } +} + +class BSystem extends createSystem({ B: { required: [B] } }) { + init() { + this.value = B.data.value; + } + + update() { + this.queries.B.entities.forEach((entity) => { + const idx = entity.index; + this.value[idx] *= 2; + }); + } +} + +class CSystem extends createSystem({ C: { required: [C] } }) { + init() { + this.value = C.data.value; + } + + update() { + this.queries.C.entities.forEach((entity) => { + const idx = entity.index; + this.value[idx] *= 2; + }); + } +} + +class DSystem extends createSystem({ D: { required: [D] } }) { + init() { + this.value = D.data.value; + } + + update() { + this.queries.D.entities.forEach((entity) => { + const idx = entity.index; + this.value[idx] *= 2; + }); + } +} + +class ESystem extends createSystem({ E: { required: [E] } }) { + init() { + this.value = E.data.value; + } + + update() { + this.queries.E.entities.forEach((entity) => { + const idx = entity.index; + this.value[idx] *= 2; + }); + } +} + +export default (count) => { + let world = new World({ checksOn: false }); + + world + .registerComponent(A) + .registerComponent(B) + .registerComponent(C) + .registerComponent(D) + .registerComponent(E) + .registerSystem(ASystem) + .registerSystem(BSystem) + .registerSystem(CSystem) + .registerSystem(DSystem) + .registerSystem(ESystem); + + for (let i = 0; i < count; i++) { + world + .createEntity() + .addComponent(A, { value: 0 }) + .addComponent(B, { value: 0 }) + .addComponent(C, { value: 0 }) + .addComponent(D, { value: 0 }) + .addComponent(E, { value: 0 }); + } + + return () => { + world.update(1, 1); + }; +}; diff --git a/src/cases/elics/simple_iter.js b/src/cases/elics/simple_iter.js new file mode 100644 index 0000000..2878117 --- /dev/null +++ b/src/cases/elics/simple_iter.js @@ -0,0 +1,100 @@ +import { Types, World, createComponent, createSystem } from "elics"; + +const A = createComponent({ value: { type: Types.Int16, default: 0 } }); +const B = createComponent({ value: { type: Types.Int16, default: 0 } }); +const C = createComponent({ value: { type: Types.Int16, default: 0 } }); +const D = createComponent({ value: { type: Types.Int16, default: 0 } }); +const E = createComponent({ value: { type: Types.Int16, default: 0 } }); + +class ABSystem extends createSystem({ AB: { required: [A, B] } }) { + init() { + this.valueA = A.data.value; + this.valueB = B.data.value; + } + + update() { + this.queries.AB.entities.forEach((entity) => { + const idx = entity.index; + const x = this.valueA[idx]; + this.valueA[idx] = this.valueB[idx]; + this.valueB[idx] = x; + }); + } +} + +class CDSystem extends createSystem({ CD: { required: [C, D] } }) { + init() { + this.valueC = C.data.value; + this.valueD = D.data.value; + } + + update() { + this.queries.CD.entities.forEach((entity) => { + const idx = entity.index; + const x = this.valueC[idx]; + this.valueC[idx] = this.valueD[idx]; + this.valueD[idx] = x; + }); + } +} + +class CESystem extends createSystem({ CE: { required: [C, E] } }) { + init() { + this.valueC = C.data.value; + this.valueE = E.data.value; + } + + update() { + this.queries.CE.entities.forEach((entity) => { + const idx = entity.index; + const x = this.valueC[idx]; + this.valueC[idx] = this.valueE[idx]; + this.valueE[idx] = x; + }); + } +} + +export default (count) => { + let world = new World({ checksOn: false }); + + world.registerComponent(A, 4000); + world.registerComponent(B, 4000); + world.registerComponent(C, 4000); + world.registerComponent(D, 4000); + world.registerComponent(E, 4000); + + world.registerSystem(ABSystem); + world.registerSystem(CDSystem); + world.registerSystem(CESystem); + + for (let i = 0; i < count; i++) { + world + .createEntity() + .addComponent(A, { value: 0 }) + .addComponent(B, { value: 1 }); + + world + .createEntity() + .addComponent(A, { value: 0 }) + .addComponent(B, { value: 1 }) + .addComponent(C, { value: 2 }); + + world + .createEntity() + .addComponent(A, { value: 0 }) + .addComponent(B, { value: 1 }) + .addComponent(C, { value: 2 }) + .addComponent(D, { value: 3 }); + + world + .createEntity() + .addComponent(A, { value: 0 }) + .addComponent(B, { value: 1 }) + .addComponent(C, { value: 2 }) + .addComponent(E, { value: 4 }); + } + + return () => { + world.update(1, 1); + }; +};