UNPKG

@byloth/micro-ecs

Version:

A simple & lightweight ECS (Entity Component System) library for JavaScript and TypeScript. 🕹

431 lines (351 loc) • 13.6 kB
import { Publisher, ReferenceException } from "@byloth/core"; import type { CallbackMap, Constructor, InternalsEventsMap, ReadonlyMapView, SmartIterator } from "@byloth/core"; import type Entity from "./entity.js"; import type Component from "./component.js"; import type System from "./system.js"; import type Resource from "./resource.js"; import WorldContext from "./contexts/world.js"; import { AttachmentException, DependencyException } from "./exceptions.js"; import QueryManager from "./query-manager.js"; import type { Instances, SignalEventsMap } from "./types.js"; type P = SignalEventsMap & InternalsEventsMap; // eslint-disable-next-line @typescript-eslint/no-empty-object-type export default class World<T extends CallbackMap<T> = { }> { private readonly _entities: Map<number, Entity>; public get entities(): ReadonlyMap<number, Entity> { return this._entities; } private readonly _resources: Map<Constructor<Resource>, Resource>; public get resources(): ReadonlyMap<Constructor<Resource>, Resource> { return this._resources; } private readonly _systems: Map<Constructor<System>, System>; private readonly _enabledSystems: System[]; public get systems(): ReadonlyMap<Constructor<System>, System> { return this._systems; } private readonly _contexts: Map<System, WorldContext<CallbackMap>>; private readonly _dependencies: Map<Resource, Set<System>>; private readonly _queryManager: QueryManager; private readonly _publisher: Publisher; private _onContextDispose = (context: WorldContext): void => { const system = context["_system"]; for (const dependency of context.dependencies) { const dependants = this._dependencies.get(dependency)!; dependants.delete(system); if (dependants.size === 0) { this._dependencies.delete(dependency); } } this._contexts.delete(system); }; public constructor() { this._entities = new Map(); this._resources = new Map(); this._systems = new Map(); this._enabledSystems = []; this._contexts = new Map(); this._dependencies = new Map(); this._queryManager = new QueryManager(this._entities); this._publisher = new Publisher(); } private _enableEntity(entity: Entity): void { for (const component of entity.components.values()) { if (!(component.isEnabled)) { continue; } this._enableEntityComponent(entity, component); } } private _disableEntity(entity: Entity): void { for (const component of entity.components.values()) { if (!(component.isEnabled)) { continue; } this._disableEntityComponent(entity, component); } } private _enableEntityComponent(entity: Entity, component: Component): void { this._queryManager["_onEntityComponentEnable"](entity, component); } private _disableEntityComponent(entity: Entity, component: Component): void { this._queryManager["_onEntityComponentDisable"](entity, component); } private _enableSystem(system: System): void { let left = 0; let right = this._enabledSystems.length; while (left < right) { const middle = Math.floor((left + right) / 2); const other = this._enabledSystems[middle]; if (system.priority < other.priority) { right = middle; } else { left = middle + 1; } } this._enabledSystems.splice(left, 0, system); } private _disableSystem(system: System): void { const index = this._enabledSystems.indexOf(system); if (index === -1) { return; } this._enabledSystems.splice(index, 1); } private _addDependency(system: System, type: Constructor<Resource>): Resource { const dependency = this._resources.get(type); if (!(dependency)) { throw new DependencyException("The dependency doesn't exist in the world."); } const dependants = this._dependencies.get(dependency); if (dependants) { if (dependants.has(system)) { throw new DependencyException("The dependant already depends on this resource."); } dependants.add(system); } else { this._dependencies.set(dependency, new Set([system])); } return dependency; } private _removeDependency(system: System, type: Constructor<Resource>): Resource { const dependency = this._resources.get(type)!; const dependants = this._dependencies.get(dependency); if (!(dependants?.delete(system))) { throw new DependencyException("The dependant doesn't depend on this resource."); } if (dependants.size === 0) { this._dependencies.delete(dependency); } return dependency; } public addEntity<E extends Entity>(entity: E): E { if (this._entities.has(entity.id)) { throw new ReferenceException("The entity already exists in the world."); } try { entity.onAttach(this); } catch (error) { throw new AttachmentException("It wasn't possible to attach this entity to the world.", error); } this._entities.set(entity.id, entity); if (entity.isEnabled) { this._enableEntity(entity); } return entity; } public removeEntity<E extends Entity = Entity>(entityId: number): E; public removeEntity<E extends Entity>(entity: E): E; public removeEntity<E extends Entity>(entity: number | E): E { const entityId = (typeof entity === "number") ? entity : entity.id; const _entity = this._entities.get(entityId) as E | undefined; if (!(_entity)) { throw new ReferenceException("The entity doesn't exist in the world."); } if (_entity.isEnabled) { this._disableEntity(_entity); } this._entities.delete(_entity.id); try { _entity.onDetach(); } catch (error) { // eslint-disable-next-line no-console console.warn("An error occurred while detaching this entity from the world.\n\nSuppressed", error); } return _entity; } public getFirstComponent<C extends Constructor<Component>, R extends InstanceType<C> = InstanceType<C>>( type: C ): R | undefined { return this._queryManager.pickOne<C, R>(type); } public getFirstComponents<C extends Constructor<Component>[], R extends Instances<C> = Instances<C>>( ...types: C ): R | undefined { return this._queryManager.findFirst<C, R>(...types); } public findAllComponents<C extends Constructor<Component>[], R extends Instances<C> = Instances<C>>( ...types: C ): SmartIterator<R> { return this._queryManager.findAll<C, R>(...types); } public getComponentView<C extends Constructor<Component>[], R extends Instances<C> = Instances<C>>( ...types: C ): ReadonlyMapView<Entity, R> { return this._queryManager.getView<C, R>(...types); } public addResource<R extends Resource>(resource: R): R { const type = resource.constructor as Constructor<Resource>; if (this._resources.has(type)) { throw new ReferenceException("The resource already exists in the world."); } try { resource.onAttach(this); } catch (error) { throw new AttachmentException("It wasn't possible to attach this resource to the world.", error); } this._resources.set(type, resource); return resource; } public removeResource<R extends Resource>(type: Constructor<R>): R; public removeResource<R extends Resource>(resource: R): R; public removeResource<R extends Resource>(resource: Constructor<R> | R): R { const type = (typeof resource === "function") ? resource : resource.constructor as Constructor<Resource>; const _resource = this._resources.get(type) as R | undefined; if (!(_resource)) { throw new ReferenceException("The resource doesn't exist in the world."); } if (this._dependencies.has(_resource)) { throw new DependencyException( "The resource has dependants and cannot be removed. Remove them first." ); } this._resources.delete(_resource.constructor as Constructor<Resource>); try { _resource.onDetach(); } catch (error) { // eslint-disable-next-line no-console console.warn("An error occurred while detaching this resource from the world.\n\nSuppressed", error); } return _resource; } public addSystem<S extends System>(system: S): S { const type = system.constructor as Constructor<System>; if (this._systems.has(type)) { throw new ReferenceException("The system already exists in the world."); } try { system.onAttach(this); } catch (error) { throw new AttachmentException("It wasn't possible to attach this system to the world.", error); } this._systems.set(type, system); if (system.isEnabled) { this._enableSystem(system); } return system; } public removeSystem<S extends System>(type: Constructor<S>): S; public removeSystem<S extends System>(system: S): S; public removeSystem<S extends System>(system: Constructor<S> | S): S { const type = (typeof system === "function") ? system : system.constructor as Constructor<System>; const _system = this._systems.get(type) as S | undefined; if (!(_system)) { throw new ReferenceException("The system doesn't exist in the world."); } const context = this._contexts.get(_system); if (context) { try { context.dispose(); } catch (error) { // eslint-disable-next-line no-console console.warn("An error occurred while disposing the context of the system.\n\nSuppressed", error); } this._contexts.delete(_system); } if (_system.isEnabled) { this._disableSystem(_system); } this._systems.delete(_system.constructor as Constructor<System>); try { _system.onDetach(); } catch (error) { // eslint-disable-next-line no-console console.warn("An error occurred while detaching this system from the world.\n\nSuppressed", error); } return _system; } // eslint-disable-next-line @typescript-eslint/no-empty-object-type public getContext<U extends CallbackMap<U> = { }>(system: System): WorldContext<U & T> { let context = this._contexts.get(system); if (context) { return context; } context = new WorldContext(system, this._publisher.createScope()); context["_onDispose"] = this._onContextDispose; this._contexts.set(system, context); return context; } public emit<K extends keyof T>(event: K & string, ...args: Parameters<T[K]>): ReturnType<T[K]>[]; public emit<K extends keyof P>(event: K & string, ...args: Parameters<P[K]>): ReturnType<P[K]>[]; public emit(event: string, ...args: unknown[]): unknown[] { return this._publisher.publish(event, ...args); } public update(deltaTime: number): void { for (const system of this._enabledSystems) { system.update(deltaTime); } } public dispose(): void { this._queryManager.dispose(); try { for (const system of this._systems.values()) { system.onDetach(); system.dispose(); } } catch (error) { // eslint-disable-next-line no-console console.warn("An error occurred while disposing systems of the world.\n\nSuppressed", error); } this._systems.clear(); this._enabledSystems.length = 0; try { for (const resource of this._resources.values()) { resource.onDetach(); resource.dispose(); } } catch (error) { // eslint-disable-next-line no-console console.warn("An error occurred while disposing resources of the world.\n\nSuppressed", error); } this._resources.clear(); try { for (const entity of this._entities.values()) { entity.onDetach(); entity.dispose(); } } catch (error) { // eslint-disable-next-line no-console console.warn("An error occurred while disposing entities of the world.\n\nSuppressed", error); } this._entities.clear(); try { for (const context of this._contexts.values()) { context.dispose(); } } catch (error) { // eslint-disable-next-line no-console console.warn("An error occurred while disposing contexts of the world.\n\nSuppressed", error); } this._contexts.clear(); this._publisher.clear(); } }