diff --git a/src/classes/CompactComponentManager.ts b/src/classes/CompactComponentManager.ts index 2b615bd..ccb23b0 100644 --- a/src/classes/CompactComponentManager.ts +++ b/src/classes/CompactComponentManager.ts @@ -6,9 +6,12 @@ export class CompactComponentManager implements ComponentManager { public eidsToIndices = new DoubleMap() - public register(eid: number, component: T) { + public register(eid: number, component: T, verbooe = true) { if (this.eidsToIndices.getFromHead(eid) !== undefined) { - throw new Error(`Component with eid ${eid} already registered`) + if (verbooe) { + throw new Error(`Component with eid ${eid} already registered`) + } + return } const index = this.components.push(component) - 1 @@ -16,11 +19,14 @@ export class CompactComponentManager implements ComponentManager { this.eidsToIndices.set(eid, index) } - public unregister(eid: number) { + public unregister(eid: number, verboose = true) { const index = this.eidsToIndices.getFromHead(eid) if (index === undefined) { - throw new Error(`Entity ${eid} was never registered`) + if (verboose) { + throw new Error(`Entity ${eid} was never registered`) + } + return } this.eidsToIndices.deleteByHead(eid) @@ -50,6 +56,16 @@ export class CompactComponentManager implements ComponentManager { return this.components[index] } + public setComponentByEid(eid: number, value: T) { + const index = this.eidsToIndices.getFromHead(eid) + + if (index === undefined) { + throw new Error(`Cannot find component with eid: ${eid}`) + } + + this.components[index] = value + } + public mutateAll(callback: (old: T) => T) { for (let index = 0; index < this.components.length; index++) { const oldComponent = this.components[index] diff --git a/src/classes/Ecs.ts b/src/classes/Ecs.ts index c092787..e932e1e 100644 --- a/src/classes/Ecs.ts +++ b/src/classes/Ecs.ts @@ -1,35 +1,42 @@ +import { ComponentManagerClass } from './../types/ComponentManager' +import { + SystemMap, + ComponentManagerMap, + ComponentList, + ComponentManagerBuilderMap, + FunctionalManagerBuilder +} from '../types/EcsHelpers' import { System } from './../types/System' import { EidGenerator } from './EidGenerator' import { ComponentManager } from '../types/ComponentManager' import { MappedComponentManager } from './MappedComponentManager' - -type ComponentManagerClassMap = { - [K in keyof T]: { new (capacity: number): ComponentManager } -} - -type ComponentManagerMap = { - [K in keyof T]: ComponentManager -} - -type ComponentList = (keyof T)[] -type SystemMap = { [K in keyof T]: System[] } - -export class Ecs { +import { + typesafeIterable, + typesafeKeys, + typesafeEntries +} from '../helpers/typesafeIterable' +import { withProp } from '../helpers/whichHaveProp' + +export class Ecs { private componentLists = new MappedComponentManager>() private systems = {} as SystemMap public components = {} as ComponentManagerMap public constructor( - components: ComponentManagerClassMap, + components: ComponentManagerBuilderMap, public capacity = 10000, private generator = new EidGenerator() ) { - for (const [key, Component] of Object.entries(components)) { - this.components[ - key - ] = new (Component as typeof components[keyof typeof components])( - capacity - ) + for (const [key, Component] of typesafeEntries(components)) { + try { + this.components[key] = (Component as FunctionalManagerBuilder< + T[keyof T] + >)(this) + } catch { + this.components[key] = new (Component as ComponentManagerClass< + T[keyof T] + >)(capacity) + } } for (const key in components) { @@ -37,24 +44,41 @@ export class Ecs { } } - public create(components: Partial) { + public create(components: Partial>) { const eid = this.generator.create() - for (const name in components) { - const typedName = name as keyof typeof components + // perform beforeCreate hook + for (const name of typesafeIterable(Object.keys(components))) + for (const system of withProp(this.systems[name], 'beforeCreate')) { + const result = system.beforeCreate(components[name]!) - this.components[typedName].register(eid, components[typedName]!) - } + if (result === false) { + this.generator.destroy(eid) - // perform didCreate hook - for (const name in components) { - const typedName = name as keyof typeof components + return eid + } + } + + for (const name of typesafeKeys(components)) { + let component = components[name]! + + // perform onUpdate hook + for (const system of withProp(this.systems[name], 'onCreate')) { + const result = system.onCreate(component) - for (const system of this.systems[typedName]) { - if (system.didCreate) { - system.didCreate!(components[typedName]!, eid) + if (result !== undefined) { + component = result as typeof component } } + + this.components[name].register(eid, component) + } + + // perform didCreate hook + for (const name of typesafeKeys(components)) { + for (const system of withProp(this.systems[name], 'didCreate')) { + system.didCreate(components[name]!, eid) + } } this.componentLists.register( @@ -89,6 +113,8 @@ export class Ecs { name: K ) { this.systems[name].push(new SystemClass(this.components[name])) + + return this } public getComponentsByEid(eid: number) { @@ -131,6 +157,11 @@ export class Ecs { const componentManager = this.components[componentName] + // run beforeUpdate hook + for (const system of hasBeforeUpdate) { + system.beforeUpdate!(componentManager) + } + // We don't need to iterate over everything if we don't need the mutations if (needUpdate.length) { componentManager.mutateAll(oldComponent => { @@ -159,18 +190,9 @@ export class Ecs { for (const componentName in this.systems) { const componentManager = this.components[componentName] - const needRender = this.systems[componentName].filter( - system => system.onRender - ) - - // We don't need to iterate over everything if we don't need the mutations - if (!needRender.length) { - return - } - - for (const system of needRender) { + for (const system of withProp(this.systems[componentName], 'onRender')) { for (const component of componentManager) { - system.onRender!(component) + system.onRender(component) } } } diff --git a/src/classes/EcsBuilder.ts b/src/classes/EcsBuilder.ts new file mode 100644 index 0000000..d57803d --- /dev/null +++ b/src/classes/EcsBuilder.ts @@ -0,0 +1,51 @@ +import { Ecs } from './Ecs' +import { + ComponentManagerBuilderMap, + ComponentManagerBuilder, + ExtractComponentManagerType +} from '../types/EcsHelpers' +import { GroupComponentManager } from './GroupComponentManager' + +class EcsSafeBuilder { + public constructor(private managers: ComponentManagerBuilderMap) {} + + public addManager( + name: K, + manager: ComponentManagerBuilder + ) { + return new EcsSafeBuilder>({ + ...this.managers, + [name]: manager + } as any) + } + + public group( + name: K, + components: Exclude[] + ) { + const group = (ecs: Ecs) => + new GroupComponentManager(ecs.capacity, ecs, components) + + return new EcsSafeBuilder< + T & Record>> + >({ + ...this.managers, + [name]: group + } as any) + } + + public build(capacity = 10000) { + return new Ecs(this.managers, capacity) + } +} + +export class EcsBuilder { + public addManager( + name: K, + manager: ComponentManagerBuilder + ) { + return new EcsSafeBuilder>({ + [name]: manager + } as any) + } +} diff --git a/src/classes/GroupComponentManager.ts b/src/classes/GroupComponentManager.ts new file mode 100644 index 0000000..0505d2d --- /dev/null +++ b/src/classes/GroupComponentManager.ts @@ -0,0 +1,75 @@ +import { Ecs } from './Ecs' +import { ComponentManager } from '../types/ComponentManager' +import { typesafeKeys } from '../helpers/typesafeIterable' + +export class GroupComponentManager + implements ComponentManager> { + private eids = new Set() + + public constructor( + public capacity: number, + private ecs: Ecs, + private components: K[] + ) {} + + private get managers() { + return this.components.map( + name => [this.ecs.components[name], name] as const + ) + } + + public register(eid: number, components: Pick) { + for (const [manager, name] of this.managers) { + manager.register(eid, components[name], false) + } + + this.eids.add(eid) + } + + public unregister(eid: number) { + for (const [manager] of this.managers) { + manager.unregister(eid, false) + } + + this.eids.delete(eid) + } + + public getComponentByEid(eid: number) { + if (!this.eids.has(eid)) { + throw new Error(`Cannot find component with eid ${eid}`) + } + + const result = {} as Pick + + for (const [manager, name] of this.managers) { + result[name] = manager.getComponentByEid(eid) + } + + return result + } + + public setComponentByEid(eid: number, value: Pick) { + if (!this.eids.has(eid)) { + throw new Error(`Cannot find component with eid ${eid}`) + } + + for (const [manager, name] of this.managers) { + manager.setComponentByEid(eid, value[name]) + } + } + + public mutateAll(callback: (v: Pick) => Pick) { + for (const eid of this.eids) { + const original = this.getComponentByEid(eid) + const changed = callback(original) + + this.setComponentByEid(eid, changed) + } + } + + public *[Symbol.iterator]() { + for (const eid of this.eids) { + yield this.getComponentByEid(eid) + } + } +} diff --git a/src/classes/MappedComponentManager.ts b/src/classes/MappedComponentManager.ts index ef23385..b38f4db 100644 --- a/src/classes/MappedComponentManager.ts +++ b/src/classes/MappedComponentManager.ts @@ -21,6 +21,14 @@ export class MappedComponentManager implements ComponentManager { return result } + public setComponentByEid(eid: number, value: T) { + if (!this.valueMap.has(eid)) { + throw new Error(`Cannot find component with eid ${eid}`) + } + + this.valueMap.set(eid, value) + } + public mutateAll(callback: (old: T) => T) { for (const [eid, value] of this.valueMap) { const newValue = callback(value) diff --git a/src/classes/UmbrellaComponentManager.ts b/src/classes/UmbrellaComponentManager.ts index a214823..74b0453 100644 --- a/src/classes/UmbrellaComponentManager.ts +++ b/src/classes/UmbrellaComponentManager.ts @@ -18,7 +18,17 @@ export class UmbrellaComponentManager implements ComponentManager { this.values = new Array(capacity) } - public register(eid: number, component: T) { + public register(eid: number, component: T, verboose = true) { + const index = this.getIndex(eid) + + if (index !== undefined) { + if (verboose) { + throw new Error(`Component ${eid} already registered`) + } + + return + } + this.values[this.length] = component this.sparse[eid] = this.length @@ -27,22 +37,28 @@ export class UmbrellaComponentManager implements ComponentManager { this.length++ } - public unregister(eid: number) { + public unregister(eid: number, verboose = true) { const index = this.getIndex(eid) - if (index !== undefined) { - if (this.length > 1) { - const last = this.length - 1 - const lastEid = this.dense[last] + if (index === undefined) { + if (verboose) { + throw new Error(`Cannot find index for component ${eid}`) + } + + return + } - this.dense[index] = lastEid - this.sparse[lastEid] = index + if (this.length > 1) { + const last = this.length - 1 + const lastEid = this.dense[last] - this.values[index] = this.values[last] - } + this.dense[index] = lastEid + this.sparse[lastEid] = index - this.length-- + this.values[index] = this.values[last] } + + this.length-- } public getComponentByEid(eid: number) { @@ -55,6 +71,16 @@ export class UmbrellaComponentManager implements ComponentManager { return this.values[index] } + public setComponentByEid(eid: number, value: T) { + const index = this.getIndex(eid) + + if (index === undefined) { + throw new Error(`Cannot find component with eid ${eid}`) + } + + this.values[index] = value + } + private getIndex(eid: number) { const index = this.sparse[eid] diff --git a/src/helpers/typedManager.ts b/src/helpers/typedManager.ts index b4f9238..64370b5 100644 --- a/src/helpers/typedManager.ts +++ b/src/helpers/typedManager.ts @@ -1,3 +1,5 @@ import { ComponentManagerClass } from '../types/ComponentManager' export const typedManager = (manager: ComponentManagerClass) => manager + +export type typedManager = ComponentManagerClass diff --git a/src/helpers/typesafeIterable.ts b/src/helpers/typesafeIterable.ts new file mode 100644 index 0000000..a9d9426 --- /dev/null +++ b/src/helpers/typesafeIterable.ts @@ -0,0 +1,7 @@ +export const typesafeIterable = (array: unknown[]) => array as T[] + +export const typesafeKeys = (object: T) => + typesafeIterable(Object.keys(object)) + +export const typesafeEntries = (object: T) => + typesafeIterable<[keyof T, T[keyof T]]>(Object.entries(object)) diff --git a/src/helpers/whichHaveProp.ts b/src/helpers/whichHaveProp.ts new file mode 100644 index 0000000..7f43bf4 --- /dev/null +++ b/src/helpers/whichHaveProp.ts @@ -0,0 +1,4 @@ +export const withProp = ( + items: T[], + property: K +) => items.filter(item => !!item[property]) as Array>> diff --git a/src/index.ts b/src/index.ts index 7c227ff..7efdcce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,10 @@ export * from './types/ComponentManager' export * from './types/System' +export * from './types/ExtractComponentType' export * from './classes/Ecs' +export * from './classes/EcsBuilder' + export * from './classes/MappedComponentManager' export * from './classes/CompactComponentManager' export * from './classes/UmbrellaComponentManager' diff --git a/src/types/ComponentManager.ts b/src/types/ComponentManager.ts index 509f1d1..8c674b2 100644 --- a/src/types/ComponentManager.ts +++ b/src/types/ComponentManager.ts @@ -1,7 +1,8 @@ export interface ComponentManager { - register(eid: number, compoennt: T): void - unregister(eid: number): void + register(eid: number, compoennt: T, verboose?: boolean): void + unregister(eid: number, verboose?: boolean): void getComponentByEid(eid: number): T + setComponentByEid(eid: number, value: T): void mutateAll(callback: (value: T) => T): void [Symbol.iterator](): IterableIterator diff --git a/src/types/EcsHelpers.ts b/src/types/EcsHelpers.ts new file mode 100644 index 0000000..76951a0 --- /dev/null +++ b/src/types/EcsHelpers.ts @@ -0,0 +1,24 @@ +import { Ecs } from '../classes/Ecs' +import { System } from './System' +import { ComponentManager, ComponentManagerClass } from './ComponentManager' + +export type FunctionalManagerBuilder = (ecs: Ecs) => ComponentManager + +export type ComponentManagerBuilder = + | FunctionalManagerBuilder + | ComponentManagerClass + +export type ComponentManagerBuilderMap = { + [K in keyof T]: ComponentManagerBuilder +} + +export type ComponentManagerMap = { + [K in keyof T]: ComponentManager +} + +export type ComponentList = (keyof T)[] +export type SystemMap = { [K in keyof T]: System[] } + +export type ExtractComponentManagerType = M extends ComponentManager + ? R + : never diff --git a/src/types/ExtractComponentType.ts b/src/types/ExtractComponentType.ts new file mode 100644 index 0000000..d9a866c --- /dev/null +++ b/src/types/ExtractComponentType.ts @@ -0,0 +1,6 @@ +import { Ecs } from '../classes/Ecs' + +export type ExtractComponentType< + E, + K extends E extends Ecs ? keyof E['components'] : never +> = E extends Ecs ? R[K] : never diff --git a/src/types/System.ts b/src/types/System.ts index 92ebfde..e793929 100644 --- a/src/types/System.ts +++ b/src/types/System.ts @@ -1,24 +1,16 @@ -import { ComponentManager } from './../types/ComponentManager' +import { ComponentManager } from './ComponentManager' export interface System { - // implemented - beforeUpdate?(manager: ComponentManager): void + beforeUpdate?(manager: ComponentManager): boolean | void + beforeRender?(manager: ComponentManager): boolean | void + beforeCreate?(component: T): boolean | void - // implemented + onCreate?(component: T): T | void onUpdate?(component: T): T | void - - // implemented onRender?(component: T): void - // implemented didUpdate?(manager: ComponentManager): void - - // implemented + didRender?(manager: ComponentManager): void didCreate?(component: T, eid: number): void - - // implemented didDestroy?(component: T, eid: number): void - - // not implemented - beforeCreate?(component: T): T | void }