diff --git a/src/ecs/index.js b/src/ecs/index.js index 01ac9422..63ebbd77 100644 --- a/src/ecs/index.js +++ b/src/ecs/index.js @@ -1,5 +1,5 @@ export * from './archetype/index.js' -export * from './query.js' +export * from './query/index.js' export * from './registry.js' export * from './schedule/index.js' export * from './typestore.js' diff --git a/src/ecs/query.js b/src/ecs/query.js deleted file mode 100644 index e7c55477..00000000 --- a/src/ecs/query.js +++ /dev/null @@ -1,233 +0,0 @@ -/** @import { TableId } from './typedef/index.js'*/ -/** @import { Constructor, TypeId } from '../reflect/index.js'*/ - -import { Entity } from './entities/index.js' -import { World } from './registry.js' -import { Tables } from './tables/index.js' -import { typeid } from '../reflect/index.js' - -/** - * Enables operations to be performed on specified set - * of components components on a {@link World}. - * Ensure that the component types matches up with the - * component names given in the second parameter of - * the query in lower case. - * @example - * ```ts - * class A {} - * class B {} - * - * const world = new World() - * .registerType(A) - * .registerType(B) - * const query = new Query<[A, B]>(world, ['a','b']) - * - * // you can now use the query to perform operations - * // on entities with components `A` and `B` - * // see the {@link Query} methods to know what operations - * // are available - * ``` - * - * @template {unknown[]} T - */ -export class Query { - - /** - * @private - * @type {World} - */ - registry - - /** - * @readonly - * @type {TypeId[]} - */ - descriptors = [] - - /** - * @private - * @type {any[][]} - */ - components = [] - - /** - * @private - * @type {Map} - */ - tablemapper = new Map() - - /** - * @param {World} registry - * @param {[...TupleConstructor]} componentTypes - */ - constructor(registry, componentTypes) { - this.registry = registry - this.descriptors = componentTypes.map((c) => typeid(c)) - - for (let i = 0; i < componentTypes.length; i++) { - this.components[i] = [] - } - - this.update(registry.getTables()) - } - - /** - * @param {Tables} table - */ - update(table) { - const { descriptors, components } = this - const tableIds = table.getTableIds(descriptors, []) - - for (let i = 0; i < tableIds.length; i++) { - this.tablemapper.set(tableIds[i], i) - } - - // This will help implement query filters - // const archetypes = table.filterArchetypes((archetype)=>true) - for (let i = 0; i < descriptors.length; i++) { - for (let j = 0; j < tableIds.length; j++) { - - // instead of keeping the component lists,keep the verified archetype - // as their ids to get them later - const bin = table.get(tableIds[j]).columns.get(descriptors[i]) - - components[i].push(bin) - } - } - } - - /** - * Gets the components of a given entity. - * - * @param {Entity} entity - * @returns {T | null} - */ - get(entity) { - const entities = this.registry.getEntities() - const location = entities.get(entity.index) - - if(!location) return null - - const { tableId, index } = location - const tableid = this.tablemapper.get( - tableId - ) - - if (tableid === undefined) return null - - const components = new Array(this.descriptors.length) - - for (let i = 0; i < this.descriptors.length; i++) { - components[i] = this.components[i][tableid][index] - } - - // SAFETY: Components are fetched in same order and types as the generic. - return /** @type {T}*/(components) - } - - /** - * @param {EachFunc} callback - */ - each(callback) { - const components = new Array(this.descriptors.length) - - if (!this.components[0]) return - - for (let j = 0; j < this.components[0].length; j++) { - for (let k = 0; k < this.components[0][j].length; k++) { - for (let l = 0; l < this.descriptors.length; l++) { - components[l] = this.components[l][j][k] - } - - // @ts-ignore - // SAFETY: Components are type cast to the expected passed value - callback(components) - } - } - } - - /** - * @param {EachCombinationFunc} callback - */ - eachCombination(callback) { - const components1 = new Array(this.descriptors.length) - const components2 = new Array(this.descriptors.length) - - if (!this.components[0]) return - - // This... many people will be very upset. - for (let j = 0; j < this.components[0].length; j++) { - for (let k = 0; k < this.components[0][j].length; k++) { - for (let l = j; l < this.components[0].length; l++) { - const nextup = l === j ? k + 1 : 0 - - for (let m = nextup; m < this.components[0][l].length; m++) { - - for (let n = 0; n < this.descriptors.length; n++) { - components1[n] = this.components[n][j][k] - components2[n] = this.components[n][l][m] - } - - // @ts-ignore - // SAFETY: Components are type cast to the expected passed value - callback(components1, components2) - } - } - } - } - } - - /** - * @returns {T | null} - */ - single() { - const components = new Array(this.descriptors.length) - - if (!this.components[0] || !this.components[0][0] || this.components[0][0][0] === undefined) return null - - for (let i = 0; i < this.descriptors.length; i++) { - components[i] = this.components[i][0][0] - } - - // @ts-ignore - // SAFETY: Components are type cast to the expected return value - return components - } - - /** - * Returns the number of entities on this query. - * - * @returns {number} - */ - count() { - let sum = 0 - - if (!this.components[0]) return 0 - - for (let i = 0; i < this.components[0].length; i++) { - sum += this.components[0][i].length - } - - return sum - } -} - -/** - * @template {unknown[]} T - * @callback EachFunc - * @param {[...T]} components - * @returns {void} - */ - -/** - * @template {unknown[]} T - * @callback EachCombinationFunc - * @param {[...T]} components1 - * @param {[...T]} components2 - * @returns {void} - */ - -/** - * @template {unknown[]} T - * @typedef {{[K in keyof T]:Constructor}} TupleConstructor - */ \ No newline at end of file diff --git a/src/ecs/query/index.js b/src/ecs/query/index.js new file mode 100644 index 00000000..0c8a2e47 --- /dev/null +++ b/src/ecs/query/index.js @@ -0,0 +1 @@ +export * from './query.js' \ No newline at end of file diff --git a/src/ecs/query/query.js b/src/ecs/query/query.js new file mode 100644 index 00000000..2d0a9b58 --- /dev/null +++ b/src/ecs/query/query.js @@ -0,0 +1,282 @@ +/** @import { TableId, TableRow } from '../typedef/index.js'*/ +/** @import { Constructor, TypeId } from '../../reflect/index.js'*/ + +import { Entity } from '../entities/index.js' +import { World } from '../registry.js' +import { typeid } from '../../reflect/index.js' +import { Table } from '../tables/index.js' + +/** + * Enables operations to be performed on specified set + * of components components on a {@link World}. + * Ensure that the component types matches up with the + * component names given in the second parameter of + * the query in lower case. + * @example + * ```ts + * class A {} + * class B {} + * + * const world = new World() + * .registerType(A) + * .registerType(B) + * const query = new Query<[A, B]>(world, ['a','b']) + * + * // you can now use the query to perform operations + * // on entities with components `A` and `B` + * // see the {@link Query} methods to know what operations + * // are available + * ``` + * + * @template {unknown[]} T + */ +export class Query { + + /** + * @private + * @type {World} + */ + world + + /** + * @readonly + * @type {TypeId[]} + */ + descriptors = [] + + /** + * @private + * @type {TableId[]} + */ + tableIds = [] + + /** + * @param {World} world + * @param {[...TupleConstructor]} componentTypes + */ + constructor(world, componentTypes) { + this.world = world + this.descriptors = componentTypes.map((c) => typeid(c)) + + this.update() + } + + /** + * @returns {void} + */ + update() { + const { world } = this + const archetypes = world.getArchetypes() + + const tableIds = filterMap(archetypes.values(), (archetype) => { + if (archetype.has(this.descriptors)) { + return archetype.tableId + } + + return undefined + }) + + this.tableIds = tableIds + } + + /** + * Gets the components of a given entity. + * + * @param {Entity} entity + * @returns {T | null} + */ + get(entity) { + const { world, descriptors, tableIds } = this + const entities = world.getEntities() + const location = entities.get(entity.index) + + if (!location) return null + + const { tableId, index } = location + const table = world.getTables().get(tableId) + + if (table === undefined) return null + if (!tableIds.includes(tableId)) return null + if (index > table.size() || index < 0) return null + + const components = new Array(this.descriptors.length) + + // Safety: We check the bounds above + mapComponents(table, descriptors, index, components) + + // SAFETY: Components are fetched in same order and types as the generic. + return /** @type {T}*/ (components) + } + + /** + * @param {EachFunc} callback + */ + each(callback) { + const { tableIds, descriptors } = this + const tables = this.world.getTables() + + // SAFETY: Components are fetched below. + const components = /** @type {T}*/(new Array(this.descriptors.length)) + + for (let i = 0; i < tableIds.length; i++) { + const table = tables.get(tableIds[i]) + + if (!table) continue + + for (let row = 0; row < table.size(); row++) { + + // SAFETY: Row is in bounds + mapComponents(table, descriptors, row, components) + callback(components) + } + } + } + + /** + * @param {EachCombinationFunc} callback + */ + eachCombination(callback) { + const { tableIds, descriptors } = this + const tables = this.world.getTables() + + // SAFETY: Components are filled below + const components1 = /** @type {T}*/(new Array(this.descriptors.length)) + const components2 = /** @type {T}*/(new Array(this.descriptors.length)) + + for (let i = 0; i < tableIds.length; i++) { + const table1 = tables.get(tableIds[i]) + + if (!table1) continue + + for (let j = i; j < tableIds.length; j++) { + const table2 = tables.get(tableIds[i]) + + if (!table2) continue + + for (let row1 = 0; row1 < table1.size(); row1++) { + + // SAFETY: Row is in bounds + mapComponents(table1, descriptors, row1, components1) + + const nextup = i === j ? row1 + 1 : 0 + + for (let row2 = nextup; row2 < table2.size(); row2++) { + + // SAFETY: Row is in bounds + mapComponents(table2, descriptors, row2, components2) + callback(components1, components2) + } + } + + } + } + } + + /** + * @returns {T | null} + */ + single() { + const { descriptors, world, tableIds } = this + const tables = world.getTables() + + for (let i = 0; i < tableIds.length; i++) { + const table = tables.get(tableIds[i]) + + if (!table) continue + if (table.size() < 1) continue + + // SAFETY: Components are fetched below. + const components = /** @type {T}*/(new Array(this.descriptors.length)) + + // SAFETY: Table row is in bounds as checked above. + mapComponents(table, descriptors, 0, components) + + return components + } + + return null + } + + /** + * Returns the number of entities on this query. + * + * @returns {number} + */ + count() { + const { tableIds } = this + const tables = this.world.getTables() + let sum = 0 + + for (let i = 0; i < tableIds.length; i++) { + const table = tables.get(tableIds[i]) + + if (!table) continue + + sum += table.size() + } + + return sum + } +} + +/** + * @template T + * @template U + * @param { readonly T[] } arr + * @param { (element:T, index:number) => U | undefined } callback + * @returns { U[] } + */ +function filterMap(arr, callback) { + const results = [] + + for (let i = 0; i < arr.length; i++) { + const result = callback(arr[i], i) + + if (result !== undefined) { + results.push(result) + } + } + + return results +} + +/** + * ### Safety + * The table row should be guaranteed to be in bounds by the caller. + * The table should also contain all the components described by the descriptor. + * + * @template {unknown[]} T + * @param {Table} table + * @param {TypeId[]} descriptor + * @param {number} row + * @param {[...T]} list + * + */ +function mapComponents(table, descriptor, row, list) { + for (let i = 0; i < descriptor.length; i++) { + const type = descriptor[i] + + // SAFETY:index guaranteed to be in bounds by the caller.The value should be there. + list[i] = /** @type {unknown}*/ (table.get(type, /** @type {TableRow} */ (row))) + } +} + +/** + * @template {unknown[]} T + * @callback EachFunc + * @param {[...T]} components + * @returns {void} + */ + +/** + * @template {unknown[]} T + * @callback EachCombinationFunc + * @param {[...T]} components1 + * @param {[...T]} components2 + * @returns {void} + */ + +/** + * @template {unknown[]} T + * @typedef {{[K in keyof T]:Constructor}} TupleConstructor + */ \ No newline at end of file diff --git a/src/ecs/tests/query.test.js b/src/ecs/tests/query.test.js new file mode 100644 index 00000000..a9aabfd9 --- /dev/null +++ b/src/ecs/tests/query.test.js @@ -0,0 +1,196 @@ +import { test, describe } from "node:test"; +import { Entity, Query, World } from "../index.js"; +import assert, { strictEqual } from "node:assert"; +import { typeid } from "../../reflect/index.js"; + +describe("Testing `Query`", () => { + test('query for single component, single entity', () => { + const world = createWorld() + const query = new Query(world, [A]) + const components = query.single() + + assert(components) + + const [componentA] = components + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(componentA.constructor)), + typeid(A) + ) + }) + + test('query for multiple components, single entity', () => { + const world = createWorld() + const query = new Query(world, [A, B, C]) + const components = query.single() + + assert(components) + + const [componentA, componentB, componentC] = components + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(componentA.constructor)), + typeid(A) + ) + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(componentB.constructor)), + typeid(B) + ) + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(componentC.constructor)), + typeid(C) + ) + }) + + test('query for single component, multiple entities', () => { + const world = createWorld() + const query = new Query(world, [A]) + + query.each(([component]) => { + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(component.constructor)), + typeid(A) + ) + }) + }) + + test('query for multiple components, multiple entities', () => { + const world = createWorld() + const query = new Query(world, [A, B, C]) + + query.each(([componentA, componentB, componentC]) => { + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(componentA.constructor)), + typeid(A) + ) + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(componentB.constructor)), + typeid(B) + ) + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(componentC.constructor)), + typeid(C) + ) + }) + }) + + test('query for single component, specific entity', () => { + const world = createWorld() + const query = new Query(world, [A]) + const entity = new Entity(0) + const components = query.get(entity) + assert(components) + + const [componentA] = components + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(componentA.constructor)), + typeid(A) + ) + }) + + test('query for multiple components, specific entity', () => { + const world = createWorld() + const query = new Query(world, [A, B, C]) + const entity = new Entity(0) + const components = query.get(entity) + + assert(components) + + const [componentA, componentB, componentC] = components + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(componentA.constructor)), + typeid(A) + ) + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(componentB.constructor)), + typeid(B) + ) + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(componentC.constructor)), + typeid(C) + ) + }) + + test('query for combination of entities', () => { + const world = createWorld() + const query = new Query(world, [A, B, C]) + let count = 0 + + query.eachCombination(([componentA1, componentB1, componentC1],[componentA2, componentB2, componentC2]) => { + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(componentA1.constructor)), + typeid(A) + ) + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(componentB1.constructor)), + typeid(B) + ) + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(componentC1.constructor)), + typeid(C) + ) + + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(componentA2.constructor)), + typeid(A) + ) + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(componentB2.constructor)), + typeid(B) + ) + strictEqual( + typeid(/**@type {import("../../reflect/index.js").Constructor}*/(componentC2.constructor)), + typeid(C) + ) + count += 1 + }) + + // nCr where n = 10 and r = 2, combinatorial + strictEqual(count,45) + }) + + test('query for count of entities+', () => { + const world = createWorld() + const query = new Query(world, [A]) + + strictEqual(query.count(), 30) + }) +}) + +class A { + /** + * @param {number} [id] + */ + constructor(id) { + this.id = id + } +} +class B { + /** + * @param {number} [id] + */ + constructor(id) { + this.id = id + } +} +class C { } + +function createWorld() { + const world = new World() + + for (let i = 20; i < 30; i++) { + world.spawn([new A(i), new B(i), new C()]) + } + for (let i = 10; i < 20; i++) { + world.spawn([new A(i), new B(i)]) + } + for (let i = 0; i < 10; i++) { + world.spawn([new A(i)]) + } + for (let i = 0; i < 10; i++) { + world.spawn([new B(i)]) + } + for (let i = 0; i < 10; i++) { + world.spawn([new C()]) + } + + return world +} \ No newline at end of file diff --git a/src/render-core/systems/bin.js b/src/render-core/systems/bin.js index 24b4b4fb..af1d56c4 100644 --- a/src/render-core/systems/bin.js +++ b/src/render-core/systems/bin.js @@ -1,6 +1,6 @@ /** @import { SystemFunc } from '../../ecs/index.js' */ /** @import { Constructor } from '../../reflect/index.js' */ -import { Query } from '../../ecs/query.js' +import { Query } from '../../ecs/index.js' import { typeid } from '../../reflect/index.js' import { GlobalTransform2D, GlobalTransform3D } from '../../transform/index.js' import { Material } from '../assets/material.js'