diff --git a/packages/piecs/package.json b/packages/piecs/package.json index b03fb5d..7d7f8e0 100644 --- a/packages/piecs/package.json +++ b/packages/piecs/package.json @@ -1,6 +1,6 @@ { "name": "piecs", - "version": "0.1.1", + "version": "0.2.0", "description": "PIECS is an entity component system with some batteries included", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/piecs/performance/add_remove.mjs b/packages/piecs/performance/add_remove.mjs index d58806a..b62b23f 100644 --- a/packages/piecs/performance/add_remove.mjs +++ b/packages/piecs/performance/add_remove.mjs @@ -1,28 +1,28 @@ -import { World, prefab, query } from '../dist/index.mjs' +import { World, prefab, query, createEntitySystem } from '../dist/index.mjs' export default function createAddRemove(count) { const world = new World() - const A = world.getNextComponentId() - const B = world.getNextComponentId() + const A = world.createComponentId() + const B = world.createComponentId() const prefabA = world.prefabricate([A]) const prefabAB = world.prefabricate([A, B]) world - .registerSystem(function addB(entities, world) { + .registerSystem(createEntitySystem(function addB(entities, world) { const lpab = prefabAB for (let i = entities.length - 1; i >= 0; i--) { // world.addComponentId(entities[i], B) world.transformEntity(entities[i], lpab) } - }, query(prefab(prefabA))) - .registerSystem(function removeB(entities, world) { + }, query(prefab(prefabA)))) + .registerSystem(createEntitySystem(function removeB(entities, world) { const lpa = prefabA for (let i = entities.length - 1; i >= 0; i--) { // world.removeComponentId(entities[i], B) world.transformEntity(entities[i], lpa) } - }, query(prefab(prefabAB))) + }, query(prefab(prefabAB)))) .initialize() for (let i = 0; i < count; i++) { diff --git a/packages/piecs/performance/entity_cycle.mjs b/packages/piecs/performance/entity_cycle.mjs index 7a2789c..570d1f7 100644 --- a/packages/piecs/performance/entity_cycle.mjs +++ b/packages/piecs/performance/entity_cycle.mjs @@ -1,26 +1,26 @@ -import { World, prefab, query } from '../dist/index.mjs' +import { World, prefab, query, createEntitySystem } from '../dist/index.mjs' export default function createEntityCycle(count) { const world = new World() - const A = world.getNextComponentId() - const B = world.getNextComponentId() + const A = world.createComponentId() + const B = world.createComponentId() const prefabA = world.prefabricate([A]) const prefabB = world.prefabricate([B]) world - .registerSystem(function spawnBs(entities, world) { + .registerSystem(createEntitySystem(function spawnBs(entities, world) { const lpb = prefabB for (let i = 0, l = entities.length; i < l; i++) { world.transformEntity(world.createEntity(), lpb) world.transformEntity(world.createEntity(), lpb) } - }, query(prefab(prefabA))) - .registerSystem(function deleteBs(entities, world) { + }, query(prefab(prefabA)))) + .registerSystem(createEntitySystem(function deleteBs(entities, world) { for (let i = entities.length - 1; i >= 0; i--) { world.deleteEntity(entities[i]) } - }, query(prefab(prefabB))) + }, query(prefab(prefabB)))) .initialize() for (let i = 0; i < count; i++) { diff --git a/packages/piecs/performance/frag_iter.mjs b/packages/piecs/performance/frag_iter.mjs index 5222e09..3694296 100644 --- a/packages/piecs/performance/frag_iter.mjs +++ b/packages/piecs/performance/frag_iter.mjs @@ -1,26 +1,26 @@ -import { World, all, query } from '../dist/index.mjs' +import { World, all, query, createEntitySystem } from '../dist/index.mjs' export default function createFragIter(count) { const world = new World() const components = [] for (let i = 0; i < 26; i++) { - components.push(world.getNextComponentId()) + components.push(world.createComponentId()) } const Data = { - id: world.getNextComponentId(), + id: world.createComponentId(), arr: new Uint32Array(count*26).fill(1) } const prefabs = components.map(c => world.prefabricate([Data.id, c])) world - .registerSystem(function dataSystem(entities) { + .registerSystem(createEntitySystem(function dataSystem(entities) { const DataArray = Data.arr for (let i = 0, l = entities.length; i < l; i++) { DataArray[entities[i]] *= 2 } - }, query(all(Data.id))) + }, query(all(Data.id)))) .initialize() for (let i = 0; i < count; i++) { diff --git a/packages/piecs/performance/packed_1.mjs b/packages/piecs/performance/packed_1.mjs index a52213a..faccd35 100644 --- a/packages/piecs/performance/packed_1.mjs +++ b/packages/piecs/performance/packed_1.mjs @@ -1,25 +1,25 @@ -import { World, prefab, query } from '../dist/index.mjs' +import { World, prefab, query, createEntitySystem } from '../dist/index.mjs' export default function createPacked1(count) { const world = new World() const A = { - id: world.getNextComponentId(), + id: world.createComponentId(), arr: new Uint32Array(count).fill(0) } - const B = world.getNextComponentId() - const C = world.getNextComponentId() - const D = world.getNextComponentId() - const E = world.getNextComponentId() + const B = world.createComponentId() + const C = world.createComponentId() + const D = world.createComponentId() + const E = world.createComponentId() const p = world.prefabricate([A.id, B, C, D, E]) world - .registerSystem(function systemAp1(entities) { + .registerSystem(createEntitySystem(function systemAp1(entities) { const AArray = A.arr for (let i = 0, l = entities.length; i < l; i++) { AArray[entities[i]] *= 2 } - }, query(prefab(p))) + }, query(prefab(p)))) .initialize() for (let i = 0; i < count; i++) { diff --git a/packages/piecs/performance/packed_5.mjs b/packages/piecs/performance/packed_5.mjs index b54a0f2..b400631 100644 --- a/packages/piecs/performance/packed_5.mjs +++ b/packages/piecs/performance/packed_5.mjs @@ -1,62 +1,62 @@ -import { World, prefab, query } from '../dist/index.mjs' +import { World, prefab, query, createEntitySystem } from '../dist/index.mjs' export default function createPacked5(count) { const world = new World() const A = { - id: world.getNextComponentId(), + id: world.createComponentId(), arr: new Uint32Array(count).fill(1) } const B = { - id: world.getNextComponentId(), + id: world.createComponentId(), arr: new Uint32Array(count).fill(1) } const C = { - id: world.getNextComponentId(), + id: world.createComponentId(), arr: new Uint32Array(count).fill(1) } const D = { - id: world.getNextComponentId(), + id: world.createComponentId(), arr: new Uint32Array(count).fill(1) } const E = { - id: world.getNextComponentId(), + id: world.createComponentId(), arr: new Uint32Array(count).fill(1) } const p = world.prefabricate([A.id, B.id, C.id, D.id, E.id]) world - .registerSystem(function systemAp5(entities) { + .registerSystem(createEntitySystem(function systemAp5(entities) { const arr = A.arr for (let i = 0, l = entities.length; i < l; i++) { arr[entities[i]] *= 2 } - }, query(prefab(p))) - .registerSystem(function systemBp5(entities) { + }, query(prefab(p)))) + .registerSystem(createEntitySystem(function systemBp5(entities) { const arr = B.arr for (let i = 0, l = entities.length; i < l; i++) { arr[entities[i]] *= 2 } - }, query(prefab(p))) - .registerSystem(function systemCp5(entities) { + }, query(prefab(p)))) + .registerSystem(createEntitySystem(function systemCp5(entities) { const arr = C.arr for (let i = 0, l = entities.length; i < l; i++) { arr[entities[i]] *= 2 } - }, query(prefab(p))) - .registerSystem(function systemDp5(entities) { + }, query(prefab(p)))) + .registerSystem(createEntitySystem(function systemDp5(entities) { const arr = D.arr for (let i = 0, l = entities.length; i < l; i++) { arr[entities[i]] *= 2 } - }, query(prefab(p))) - .registerSystem(function systemEp5(entities) { + }, query(prefab(p)))) + .registerSystem(createEntitySystem(function systemEp5(entities) { const arr = E.arr for (let i = 0, l = entities.length; i < l; i++) { arr[entities[i]] *= 2 } - }, query(prefab(p))) + }, query(prefab(p)))) .initialize() for (let i = 0; i < count; i++) { diff --git a/packages/piecs/performance/simple_iter.mjs b/packages/piecs/performance/simple_iter.mjs index 47885dd..d27ee04 100644 --- a/packages/piecs/performance/simple_iter.mjs +++ b/packages/piecs/performance/simple_iter.mjs @@ -1,25 +1,25 @@ -import { World, prefab, query } from '../dist/index.mjs' +import { World, prefab, query, createEntitySystem } from '../dist/index.mjs' export default function createSimpleIter(count) { const world = new World() const A = { - id: world.getNextComponentId(), + id: world.createComponentId(), arr: new Uint32Array(count*4).fill(0) } const B = { - id: world.getNextComponentId(), + id: world.createComponentId(), arr: new Uint32Array(count*4).fill(0) } const C = { - id: world.getNextComponentId(), + id: world.createComponentId(), arr: new Uint32Array(count*4).fill(0) } const D = { - id: world.getNextComponentId(), + id: world.createComponentId(), arr: new Uint32Array(count*4).fill(0) } const E = { - id: world.getNextComponentId(), + id: world.createComponentId(), arr: new Uint32Array(count*4).fill(0) } @@ -29,7 +29,7 @@ export default function createSimpleIter(count) { const prefab4 = world.prefabricate([A.id, B.id, C.id, E.id]) world - .registerSystem(function systemAB(entities) { + .registerSystem(createEntitySystem(function systemAB(entities) { const lA = A.arr const lB = B.arr for (let i = 0, l = entities.length; i < l; i++) { @@ -39,8 +39,8 @@ export default function createSimpleIter(count) { lA[entity] = b lB[entity] = a } - }, query(prefab(prefab1))) - .registerSystem(function systemCD(entities) { + }, query(prefab(prefab1)))) + .registerSystem(createEntitySystem(function systemCD(entities) { const lC = C.arr const lD = D.arr for (let i = 0, l = entities.length; i < l; i++) { @@ -50,8 +50,8 @@ export default function createSimpleIter(count) { lC[entity] = d lD[entity] = c } - }, query(prefab(prefab3))) - .registerSystem(function systemCE(entities) { + }, query(prefab(prefab3)))) + .registerSystem(createEntitySystem(function systemCE(entities) { const lC = C.arr const lE = E.arr for (let i = 0, l = entities.length; i < l; i++) { @@ -61,7 +61,7 @@ export default function createSimpleIter(count) { lC[entity] = e lE[entity] = c } - }, query(prefab(prefab4))) + }, query(prefab(prefab4)))) .initialize() for (let i = 0; i < count; i++) { diff --git a/packages/piecs/src/Archetype.ts b/packages/piecs/src/Archetype.ts index ee40c32..5615e48 100644 --- a/packages/piecs/src/Archetype.ts +++ b/packages/piecs/src/Archetype.ts @@ -1,8 +1,31 @@ import type { ReadonlyBitSet } from './collections/BitSet' import { createSparseSet, SparseSet } from './collections/SparseSet' -export type InternalArchetype = { +export type Archetype = { + /** + * The id of the archetype is a hexadecimal representation of a set of unique bits for all of the `componentIds` + */ readonly id: string + /** + * All the `componentIds` constituting this archetype + */ + readonly componentIds: ReadonlyArray + /** + * Check if an entity is currently included in this archetype + */ + hasEntity: (entity: number) => boolean + /** + * Check if this archetype has a `componentId`. + * This is typically much faster than checking if `componentIds` includes a given componentId + */ + hasComponentId: (componentId: number) => boolean + /** + * Returns all the entities currently in this archetype + */ + getEntities(): ArrayLike +} + +export type InternalArchetype = Archetype & { readonly mask: ReadonlyBitSet readonly entitySet: SparseSet readonly adjacent: InternalArchetype[] @@ -10,23 +33,33 @@ export type InternalArchetype = { readonly componentIds: number[] } -export type Archetype = { - readonly id: string - readonly componentIds: ReadonlyArray -} - export function createArchetype(id: string, mask: ReadonlyBitSet, parent: InternalArchetype | null): InternalArchetype { const entitySet = createSparseSet() const adjacent: InternalArchetype[] = [] const componentIds: number[] = [] + function hasEntity(entity: number): boolean { + return entitySet.has(entity) + } + + function hasComponentId(componentId: number): boolean { + return mask.has(componentId) + } + + function getEntities(): ArrayLike { + return entitySet.values + } + return Object.freeze({ id, mask, entitySet, adjacent, parent, - componentIds + componentIds, + hasEntity, + hasComponentId, + getEntities }) } @@ -55,7 +88,7 @@ export function transformArchetype(archetype: InternalArchetype, componentId: nu transformed.adjacent[componentId] = archetype archetype.adjacent[componentId] = transformed if (!existingArchetype) - transformed.componentIds.push(componentId) + transformed.componentIds.push(...archetype.componentIds, componentId) return transformed } diff --git a/packages/piecs/src/Errors.ts b/packages/piecs/src/Errors.ts index 50f0ed5..a3bc32b 100644 --- a/packages/piecs/src/Errors.ts +++ b/packages/piecs/src/Errors.ts @@ -25,14 +25,3 @@ export class WorldNotInitializedError extends Error { super('World not initialized') } } - -export class PrefabricationError extends Error { - constructor(componentIds: number[], nextComponentId: number) { - super(` -Cannot prefabricate using componentIds that would conflict with existing componentIds. -Make sure you either prefabricate before generating componentIds with world.getNextComponentId(), -or prefabricate using componentIds generated by world.getNextComponentId(). -Conflicting componentIds: [${componentIds.filter(id => id < nextComponentId).join(', ')}] -`) - } -} diff --git a/packages/piecs/src/Query.ts b/packages/piecs/src/Query.ts index 1283864..71a58c3 100644 --- a/packages/piecs/src/Query.ts +++ b/packages/piecs/src/Query.ts @@ -14,22 +14,35 @@ function makeMask(componentIds: Array): BitSet { return mask } +/** + * Query for a prefabricated `archetype`. + * May match descendant archetypes, ie archetypes with all of the component ids in the prefab *and* additional component ids added to entities in the prefabricated archetype or descendant archetypes + */ export function prefab(archetype: Archetype): QueryMatcher { return target => target.contains((archetype).mask) } +/** + * Archetypes that has *all* of the `componentIds` will be included in the result + */ export function all(...componentIds: Array): QueryMatcher { if (!componentIds.length) return alwaysFalse const mask = makeMask(componentIds) return target => target.contains(mask) } +/** + * Archetypes that has *any* of the `componentIds` will be included in the result + */ export function any(...componentIds: Array): QueryMatcher { if (!componentIds.length) return alwaysTrue const mask = makeMask(componentIds) return target => target.intersects(mask) } +/** + * Archetypes that *has* the `componentIds` will *not* be included in the result + */ export function not(...componentIds: Array): QueryMatcher { if (!componentIds.length) return alwaysTrue const mask = makeMask(componentIds) @@ -47,13 +60,22 @@ export function or(matcher: QueryMatcher, ...matchers: QueryMatcher[]): QueryMat } export type Query = { + /** + * All archetypes that matches the query + */ readonly archetypes: ReadonlyArray } -export type InternalQuery = { +export type InternalQuery = Query & { readonly tryAdd: (archetype: InternalArchetype) => boolean -} & Query + readonly archetypes: ReadonlyArray +} +/** + * Create a query that can be matched against archetypes + * @param matchers return value of any combination of `and | or | all | any | not`. + * @note Empty argument list returns a query which is always true + */ export const query = (...matchers: Array): Query => { const archetypes: Archetype[] = [] let matcher: QueryMatcher diff --git a/packages/piecs/src/System.ts b/packages/piecs/src/System.ts new file mode 100644 index 0000000..7b98100 --- /dev/null +++ b/packages/piecs/src/System.ts @@ -0,0 +1,57 @@ +import { Archetype, Query, World } from '.' + +type BaseSystem = { + readonly query: Query +} + +export type EntitySystem = BaseSystem & { + /** + * 0 = entitySystem + * 1 = archetypeSystem + */ + readonly type: 0 + execute(entities: ArrayLike, world: World): void +} + +export type ArchetypeSystem = BaseSystem & { + /** + * 0 = entitySystem + * 1 = archetypeSystem + */ + readonly type: 1 + execute(archetypes: ArrayLike, world: World): void +} + +export type System = EntitySystem | ArchetypeSystem + +/** + * An entity system is a system that will be executed for each archetype that contains 1 or more entities matching the query. + * In other words, it may be executed multiple times in each update. + * If you need the system to only execute once in each update, use the `ArchetypeSystem` created by `createArchetypeSystem` + * @param execute + * @param query + * @returns + */ +export function createEntitySystem(execute: (entities: ArrayLike, world: World) => void, query: Query): EntitySystem { + return Object.freeze({ + execute, + query, + type: 0 + }) +} + +/** + * An archetype system is a system that that will only execute once in each update with all the archetypes matching the query. + * This is usefull when your query potentially matches 2 or more archetypes and you need to check for the presence of a componentId on entities. + * The differing components can be checked for once for each archetype instead of for each entity. + * @param execute + * @param query + * @returns + */ +export function createArchetypeSystem(execute: (archetypes: ArrayLike, world: World) => void, query: Query): ArchetypeSystem { + return Object.freeze({ + execute, + query, + type: 1 + }) +} diff --git a/packages/piecs/src/World.ts b/packages/piecs/src/World.ts index 1bcbc92..5ad4d8f 100644 --- a/packages/piecs/src/World.ts +++ b/packages/piecs/src/World.ts @@ -1,8 +1,9 @@ -import type { System, InsideWorld, OutsideWorld } from './types' -import type { InternalQuery, Query } from './Query' +import type { InsideWorld, OutsideWorld } from './types' +import type { InternalQuery } from './Query' +import type { System } from './System' import { createArchetype, InternalArchetype, transformArchetype, traverseArchetypeGraph, Archetype } from './Archetype' import { createBitSet } from './collections/BitSet' -import { EntityDeletedError, EntityNotExistError, EntityUndefinedError, PrefabricationError, WorldNotInitializedError } from './Errors' +import { EntityDeletedError, EntityNotExistError, EntityUndefinedError, WorldNotInitializedError } from './Errors' export class World implements OutsideWorld, InsideWorld { private rootArchetype: InternalArchetype = createArchetype('root', createBitSet(255), null) @@ -10,10 +11,7 @@ export class World implements OutsideWorld, InsideWorld { private deletedEntities: number[] = [] private nextEntityId = 0 >>> 0 private nextComponentId = 0 >>> 0 - private systems: System[] = [] - private queries: InternalQuery[] = [] // 1 to 1 with systems - private deferred: (() => void)[] = [] private initialized = false @@ -27,24 +25,45 @@ export class World implements OutsideWorld, InsideWorld { } private _tryAddArchetypeToQueries(archetype: InternalArchetype) { - const queries = this.queries + const systems = this.systems - for (let i = 0, l = queries.length; i < l; i++) { - queries[i]!.tryAdd(archetype) + for (let i = 0, l = systems.length; i < l; i++) { + (systems[i]!.query).tryAdd(archetype) } } - getNextComponentId() { + createComponentId() { return this.nextComponentId++ } - registerSystem(system: System, query: Query) { + prefabricate(componentIds: number[]): Archetype { + const max = Math.max(...componentIds) + if (max >= this.nextComponentId) { + this.nextComponentId = (max + 1) >>> 0 + } + let archetype = this.rootArchetype + + for (let i = 0, l = componentIds.length; i < l; i++) { + const componentId = componentIds[i]! + + if (archetype.adjacent[componentId]) { + archetype = archetype.adjacent[componentId]! + } else { + archetype = transformArchetype(archetype, componentId) + if (this.initialized) { + this._tryAddArchetypeToQueries(archetype) + } + } + } + return archetype + } + + registerSystem(system: System) { this.systems.push(system) - this.queries.push(query) if (this.initialized) { traverseArchetypeGraph(this.rootArchetype, (archetype) => { - (query).tryAdd(archetype) + (system.query).tryAdd(archetype) return true }) } @@ -62,25 +81,23 @@ export class World implements OutsideWorld, InsideWorld { }) } - /** - * Update the world, invoking all systems. - * Typically you want to update each animation frame (@see window.requestAnimationFrame) - * @returns number of milliseconds that the update took - */ update() { if (!this.initialized) throw new WorldNotInitializedError() const systems = this.systems - const queries = this.queries for (let s = 0, sl = systems.length; s < sl; s++) { const system = systems[s]! - const query = queries[s]! - // reverse iterating in case a system adds/removes component resulting in new archetype that matches query for the system - for (let a = query.archetypes.length - 1; a >= 0; a--) { - const entities = (query.archetypes)[a]!.entitySet.values - if (entities.length > 0) { - system(entities, this) + const query = system.query as InternalQuery + if (system.type === 1) { + system.execute(query.archetypes, this) + } else { + // reverse iterating in case a system adds/removes component resulting in new archetype that matches query for the system + for (let a = query.archetypes.length - 1; a >= 0; a--) { + const entities = query.archetypes[a]!.entitySet.values + if (entities.length > 0) { + system.execute(entities, this) + } } } } @@ -88,41 +105,10 @@ export class World implements OutsideWorld, InsideWorld { this._executeDeferred() } - /** - * Defer execution of an action until the end of the update cycle (after all systems has been executed) - * @param action The action to defer - * @returns this - */ defer(action: () => void) { this.deferred.push(action) } - prefabricate(componentIds: number[]): Archetype { - const max = Math.max(...componentIds) - if (max >= this.nextComponentId) { - if (Math.min(...componentIds) < this.nextComponentId) { - throw new PrefabricationError(componentIds, this.nextComponentId) - } - - this.nextComponentId = (max + 1) >>> 0 - } - let archetype = this.rootArchetype - - for (let i = 0, l = componentIds.length; i < l; i++) { - const componentId = componentIds[i]! - - if (archetype.adjacent[componentId]) { - archetype = archetype.adjacent[componentId]! - } else { - archetype = transformArchetype(archetype, componentId) - if (this.initialized) { - this._tryAddArchetypeToQueries(archetype) - } - } - } - return archetype - } - hasEntity(entity: number): boolean { return this.entityArchetype[entity] !== undefined } @@ -156,14 +142,6 @@ export class World implements OutsideWorld, InsideWorld { this.deletedEntities.push(entity) } - /** - * Transform the entity to that of a prefabricated archetype. - * Any components added to the entity that does not exist in the prefabricate will be removed. - * This is a sligthly faster operation than adding/subtracting components - * @param entity Entity to transform - * @param prefab Archetype to apply on entity - * @returns - */ transformEntity(entity: number, prefab: Archetype) { if (this.entityArchetype[entity] === undefined) { if (entity === undefined) { @@ -176,7 +154,7 @@ export class World implements OutsideWorld, InsideWorld { if (this.entityArchetype[entity] === prefab) return - // Transform resets all components on the entity that of the prefab.. + // Transform resets all components on the entity to that of the prefab.. this.entityArchetype[entity]!.entitySet.remove(entity) const archetype = prefab as InternalArchetype archetype.entitySet.add(entity) diff --git a/packages/piecs/src/__tests__/World.test.ts b/packages/piecs/src/__tests__/World.test.ts index 18a5a03..70c49be 100644 --- a/packages/piecs/src/__tests__/World.test.ts +++ b/packages/piecs/src/__tests__/World.test.ts @@ -1,23 +1,24 @@ import { and, all, not, any, query } from '../Query' import { World } from '../World' +import { createEntitySystem } from '../System' describe('World', () => { it('works', () => { const world = new World() const foo = { - id: world.getNextComponentId(), + id: world.createComponentId(), arr: new Uint8Array(100) } const bar = { - id: world.getNextComponentId(), + id: world.createComponentId(), arr: new Array() } const baz = { - id: world.getNextComponentId(), + id: world.createComponentId(), } world - .registerSystem(function addBaz(entities, _) { + .registerSystem(createEntitySystem(function addBaz(entities, _) { for (let e = entities.length - 1; e >= 0; e--) { const entity = entities[e]! expect(entity).toBe(0) @@ -27,15 +28,15 @@ describe('World', () => { }) expect(world.hasComponentId(entity, baz.id)).toBeFalsy() } - }, query(and(all(foo.id), not(baz.id)))) - .registerSystem(function deleteBaz(entities, world) { + }, query(and(all(foo.id), not(baz.id))))) + .registerSystem(createEntitySystem(function deleteBaz(entities, world) { for (let e = entities.length - 1; e >= 0; e--) { const entity = entities[e]! expect(world.hasComponentId(entity, baz.id)) world.deleteEntity(entity) expect(world.hasEntity(entity)).toBeFalsy() } - }, query(any(baz.id))) + }, query(any(baz.id)))) .initialize() const e0 = world.createEntity() expect(e0).toBe(0) diff --git a/packages/piecs/src/index.ts b/packages/piecs/src/index.ts index 0b818f3..66b1a16 100644 --- a/packages/piecs/src/index.ts +++ b/packages/piecs/src/index.ts @@ -1,7 +1,8 @@ export type { Query } from './Query' export type { WorldStatistics } from './utils' export type { Archetype } from './Archetype' -export type { System } from './types' +export type { System } from './System' export { World } from './World' export { and, or, not, any, all, prefab, query } from './Query' export { getStatistics } from './utils' +export { createArchetypeSystem, createEntitySystem } from './System' diff --git a/packages/piecs/src/types.ts b/packages/piecs/src/types.ts index f14e8e1..0d3e4d2 100644 --- a/packages/piecs/src/types.ts +++ b/packages/piecs/src/types.ts @@ -1,23 +1,80 @@ import type { Archetype } from './Archetype' -import type { Query } from './Query' - -export type System = (results: ArrayLike, world: InsideWorld) => void +import type { System } from './System' export interface InsideWorld { + /** + * Check if the entity exists in the world + */ hasEntity(entity: number): boolean + /** + * Create an entity. + * An entity is just an Id. + * Previously deleted entity id's will be reused + */ createEntity(): number + /** + * Delete an entity, removing it from its current archetype (loosing all of its components). + * @throws {EntityUndefinedError | EntityDeletedError | EntityNotExistError} + */ deleteEntity(entity: number): void + /** + * Defer execution of an action until the end of the update cycle (after all systems has been executed) + * For best performance you try to defer a batched action instead of many small actions, or avoid defering if possbile + */ defer(action: () => void): void - addComponentId(entity: number, componentId: number): void + /** + * Check if the entity has a componentId + */ hasComponentId(entity: number, componentId: number): boolean + /** + * Adds the componentId to the entity. + * The entity will be moved to a different archetype + * @throws {EntityUndefinedError | EntityDeletedError | EntityNotExistError} + */ + addComponentId(entity: number, componentId: number): void + /** + * Removes the componentId from the entity. + * The entity will be moved to a different archetype + * @throws {EntityUndefinedError | EntityDeletedError | EntityNotExistError} + */ removeComponentId(entity: number, componentId: number): void + /** + * Transform the entity to that of a prefabricated archetype. + * Any components added to the entity that does not exist in the prefabricate will be removed. + * This is a sligthly faster operation than adding/subtracting components + * @throws {EntityUndefinedError | EntityDeletedError | EntityNotExistError} + */ transformEntity(entity: number, prefabricate: Archetype): void } export interface OutsideWorld extends InsideWorld { - getNextComponentId(): number - registerSystem(system: System, query: Query): OutsideWorld + /** + * Component Id is an incrementing number + * Use the id in conjunction with your component values, assign it to entities and query for componet ids + */ + createComponentId(): number + /** + * Provide a known combination of `componentIds` constituting an archetype. + * The component ids can be of your choosing, but be carefull not to use the same id for different components. + * You should either create all `componentIds` using `createComponentId` first and use the created component ids in the prefacbricate, + * Or make all of you prefabricates before creating new component ids using `createComponentId` + */ prefabricate(componentIds: number[]): Archetype + /** + * Registers a system to be executed for each update cycle. + * Use the `createEntitySystem` or `createArchetypeSystem` helpers to create the system. + * A system may not be executed if it's `Query` does not match any `Archetype`s. + */ + registerSystem(system: System): OutsideWorld + /** + * Initialize the world, must be done before the first update. + * Subsequent calls to initialize will be voided. + */ initialize(): void + /** + * Update the world, executing all registered systems with queries matching 1 or more `Archetype`. + * Typically you want to call `update` on each animation frame (`window.requestAnimationFrame`). + * @throws {WorldNotInitializedError} if `initialized` has not been called + */ update(): void } diff --git a/packages/piecs/src/utils.ts b/packages/piecs/src/utils.ts index 7376abb..93c57fb 100644 --- a/packages/piecs/src/utils.ts +++ b/packages/piecs/src/utils.ts @@ -1,11 +1,21 @@ import type { World } from './World' +import type { InternalQuery } from './Query' import { Archetype, InternalArchetype, traverseArchetypeGraph } from './Archetype' type ArchetypeStatistics = { + /** + * Count of entities in the archetype + */ entities: number + /** + * The id of the archetype that was transformed into this archetype + */ parent: string | null + /** + * The id of archetypes with 1 differing componentId + */ adjacent: string[] -} & Archetype +} & Pick function getArchetypeStatistics(archetype: InternalArchetype): ArchetypeStatistics { return { @@ -18,12 +28,30 @@ function getArchetypeStatistics(archetype: InternalArchetype): ArchetypeStatisti } export type WorldStatistics = { + /** + * Count of entities in the world + */ entities: number + /** + * Count of components in the world + */ components: number + /** + * Name or index of all systems registered in the world + */ systems: string[] + /** + * Statistics for all queries in the world + */ queries: { + /** + * `ArchetypeStatistics` for matching archetypes in this query + */ archetypes: ArchetypeStatistics[] }[] + /** + * `ArchetypeStatistics` for all archetypes in the world + */ archetypes: ArchetypeStatistics[] } @@ -39,10 +67,10 @@ export function getStatistics(world: World): WorldStatistics { // @ts-ignore components: world.nextComponentId, // @ts-ignore - systems: world.systems.map((system, i) => system.name || i.toString()), + systems: world.systems.map((system, i) => system.execute.name || i.toString()), // @ts-ignore - queries: world.queries.map((query) => ({ - archetypes: query.archetypes.map(a => getArchetypeStatistics(a as any)) + queries: world.systems.map((system) => ({ + archetypes: (system.query).archetypes.map(a => getArchetypeStatistics(a as any)) })), archetypes, }