UNPKG

@javelin/ecs

Version:

184 lines 7.12 kB
import { assert, createStackPool, ErrorType, mutableEmpty, } from "@javelin/core"; import { getSchemaId, registerSchema, } from "./component"; import { UNSAFE_internals } from "./internal"; import { normalizeType, typeIsSuperset } from "./type"; const ERROR_MSG_UNBOUND_QUERY = "a query must be executed within a system or bound to a world using Query.bind()"; /** * Check if a query signature (type) matches a type signature, accounting for * query filters (if any). */ function matchesTypeInner(typeA, typeB, filters) { return typeIsSuperset(typeB, typeA) && typeB.every(c => !filters.not.has(c)); } function createQueryInternal(options) { var _a, _b; const length = options.select.length; const filters = (_a = options.filters) !== null && _a !== void 0 ? _a : { not: new Set() }; const type = normalizeType(options.select.map(schema => registerSchema(schema))); const layout = ((_b = options.include) !== null && _b !== void 0 ? _b : options.select).map(schema => registerSchema(schema)); const recordsIndex = []; const pool = createStackPool(() => [], components => { mutableEmpty(components); return components; }, 1000); let boundWorldId = options.boundWorldId; /** * Attempt to register an archetype with this query. If the archetype is * matched, a live record of the archetype's entities, columns, and entity * index is pushed into the query's registry. */ function maybeRegisterArchetype(archetype, records) { if (matchesTypeInner(type, archetype.type, filters)) { const columns = layout.map(schemaId => archetype.table[archetype.type.indexOf(schemaId)]); records.push([ archetype.entities, columns, archetype.indices, ]); } } /** * Create a new index of archetype records for the provided world, attempt * register existing archetypes, and subscribe to newly created ones. */ function registerWorld(worldId) { const world = UNSAFE_internals.worlds[worldId]; const records = []; recordsIndex[worldId] = records; world.storage.archetypes.forEach(archetype => maybeRegisterArchetype(archetype, records)); world.storage.archetypeCreated.subscribe(archetype => maybeRegisterArchetype(archetype, records)); return records; } function forEach(iteratee) { const worldId = boundWorldId !== null && boundWorldId !== void 0 ? boundWorldId : UNSAFE_internals.currentWorldId; assert(worldId !== null && worldId !== -1, ERROR_MSG_UNBOUND_QUERY, ErrorType.Query); const records = recordsIndex[worldId] || registerWorld(worldId); const components = pool.retain(); for (let i = 0; i < records.length; i++) { const [entities, columns] = records[i]; for (let j = 0; j < entities.length; j++) { for (let k = 0; k < length; k++) { components[k] = columns[k][j]; } iteratee(entities[j], components); } } pool.release(components); } function iterator() { const worldId = boundWorldId !== null && boundWorldId !== void 0 ? boundWorldId : UNSAFE_internals.currentWorldId; assert(worldId !== null && worldId !== -1, ERROR_MSG_UNBOUND_QUERY, ErrorType.Query); return (recordsIndex[worldId] || registerWorld(worldId))[Symbol.iterator](); } function not(...exclude) { return createQueryInternal({ ...options, filters: { not: new Set(exclude .map(schema => UNSAFE_internals.schemaIndex.get(schema)) .filter((x) => typeof x === "number")), }, }); } function select(...include) { return createQueryInternal({ ...options, include }); } function get(entity, out = []) { const worldId = boundWorldId !== null && boundWorldId !== void 0 ? boundWorldId : UNSAFE_internals.currentWorldId; const records = recordsIndex[worldId]; for (let i = 0; i < records.length; i++) { const [, columns, indices] = records[i]; const index = indices[entity]; if (index !== undefined) { for (let i = 0; i < columns.length; i++) { out[i] = columns[i][index]; } return out; } } throw new Error("Failed to get components of query: entity does not match query"); } function bind(world) { return createQueryInternal({ ...options, boundWorldId: world.id, }); } function test(entity) { const worldId = boundWorldId !== null && boundWorldId !== void 0 ? boundWorldId : UNSAFE_internals.currentWorldId; const records = recordsIndex[worldId]; for (let i = 0; i < records.length; i++) { const record = records[i]; if (record[2][entity] !== undefined) { return true; } } return false; } let _type = type; function matchesType(type) { return matchesTypeInner(_type, type, filters); } function equals(query) { if (query.type.length !== type.length) { return false; } for (let i = 0; i < query.type.length; i++) { if (query.type[i] !== type[i]) { return false; } } if (query.filters.not.size !== filters.not.size) { return false; } let result = true; query.filters.not.forEach(schemaId => (result = result && filters.not.has(schemaId))); return result; } function match(components, out = []) { for (let i = 0; i < layout.length; i++) { out[i] = null; } for (let i = 0; i < components.length; i++) { const component = components[i]; const index = layout.indexOf(getSchemaId(component)); if (index !== -1) { out[index] = component; } } return out; } const query = forEach; query[Symbol.iterator] = iterator; query.type = type; query.filters = filters; query.not = not; query.select = select; query.get = get; query.bind = bind; query.test = test; query.matchesType = matchesType; query.equals = equals; query.match = match; return query; } /** * Create a query that can be used to iterate entities that match a selector. * Maintains a live-updating cache of entities, and can be used across multiple * worlds. * @example * const burning = createQuery(Player, Burn) * burning((entity, [player, burn]) => { * player.health -= burn.damage * }) * @example * for (const [entities, [p, b]] of burning) { * for (let i = 0; i < entities.length; i++) { * p[i].health -= b[i].damage * } * } */ export function createQuery(...select) { return createQueryInternal({ select }); } //# sourceMappingURL=query.js.map