UNPKG

ecspresso

Version:

A minimal Entity-Component-System library for typescript and javascript.

298 lines (297 loc) 14.8 kB
import type { Entity, FilteredEntity, RemoveEntityOptions, HierarchyEntry, HierarchyIteratorOptions } from "./types"; import QueryCache from "./query-cache"; export default class EntityManager<ComponentTypes> { private nextId; private entities; private componentIndices; /** * Callbacks registered for component additions */ private addedCallbacks; /** * Callbacks registered for component removals */ private removedCallbacks; /** * Hierarchy manager for parent-child relationships */ private hierarchyManager; /** * Per-type component dispose callbacks. * Called when a component is removed (explicit removal, entity destruction, or replacement). */ private disposeCallbacks; /** * Per-entity per-component change sequence tracking. * Flat storage: changeSeqs[entityId][componentIdx] = seq number when last changed. * Component names are mapped to dense indices via componentNameToIdx. * Uint32Array zero-init means "never changed" (seq numbers start at 1). */ private changeSeqs; private componentNameToIdx; private _idxCache0Name; private _idxCache0Idx; private _idxCache1Name; private _idxCache1Idx; /** * Subscription bitmap for change tracking. `null` means track all (the * default); a Uint8Array means explicit-only, with 1 at indices that opted * in via `subscribeChanged`. Indices outside the array's bounds are * treated as 0 (not tracked). */ private _subscribedComponentIdx; /** * Monotonic sequence counter for change detection. * Each markChanged call increments this and stamps the new value. */ private _changeSeq; private _afterComponentAddedHooks; private _afterEntityMutatedHooks; private _afterComponentRemovedHooks; private _beforeEntityRemovedHooks; private _afterParentChangedHooks; /** * Incrementally-maintained query result cache. Caches the static portion * (with / without / parentHas) of each registered query shape and is * updated via the lifecycle hook arrays above. Lazily created on the * first cacheable query lookup. */ private readonly _queryCache; private _batchingDepth; private _batchedEntityIds; /** Component keys being added in the current addComponents batch, if any. * Used by required component resolution to skip auto-adding explicitly provided components. */ _pendingBatchKeys: ReadonlySet<keyof ComponentTypes> | null; get entityCount(): number; createEntity(): Entity<ComponentTypes>; /** * Register a dispose callback for a component type. * Called when a component is removed (explicit removal, entity destruction, or replacement). * Later registrations replace earlier ones for the same component type. * @param componentName The component type to register disposal for * @param callback Function receiving the component value being disposed and the entity ID */ registerDispose<ComponentName extends keyof ComponentTypes>(componentName: ComponentName, callback: (ctx: { value: ComponentTypes[ComponentName]; entityId: number; }) => void): void; /** * Get all registered dispose callbacks. * @internal Used by ECSpresso for plugin installation */ getDisposeCallbacks(): Map<keyof ComponentTypes, (ctx: { value: unknown; entityId: number; }) => void>; /** * Invoke the dispose callback for a component, if registered. * Errors are caught and logged to prevent blocking removal. */ private invokeDispose; addComponent<ComponentName extends keyof ComponentTypes>(entityId: number, componentName: ComponentName, data: ComponentTypes[ComponentName]): this; /** * Add multiple components to an entity at once * @param entityId Entity ID to add components to * @param components Object with component names as keys and component data as values */ addComponents<T extends { [K in keyof ComponentTypes]?: ComponentTypes[K]; }>(entityId: number, components: T & Record<Exclude<keyof T, keyof ComponentTypes>, never>): this; removeComponent<ComponentName extends keyof ComponentTypes>(entityId: number, componentName: ComponentName): this; getComponent<ComponentName extends keyof ComponentTypes>(entityId: number, componentName: ComponentName): ComponentTypes[ComponentName] | undefined; getEntitiesWithQuery<WithComponents extends keyof ComponentTypes = never, WithoutComponents extends keyof ComponentTypes = never>(required?: ReadonlyArray<WithComponents>, excluded?: ReadonlyArray<WithoutComponents>, changed?: ReadonlyArray<keyof ComponentTypes>, changeThreshold?: number, parentHas?: ReadonlyArray<keyof ComponentTypes>, changedIdx?: ReadonlyArray<number>): Array<FilteredEntity<ComponentTypes, WithComponents extends never ? never : WithComponents, WithoutComponents extends never ? never : WithoutComponents>>; /** * Fill an existing array with entities matching the query, clearing it first. * Returns the same array reference for convenience. * * `changedIdx`, when supplied, skips the per-call name→idx resolution loop. * The framework pre-resolves it once at system registration; ad-hoc callers * may omit it and pay the per-call lookup cost. */ getEntitiesWithQueryInto<WithComponents extends keyof ComponentTypes = never, WithoutComponents extends keyof ComponentTypes = never>(output: Array<FilteredEntity<ComponentTypes, WithComponents extends never ? never : WithComponents, WithoutComponents extends never ? never : WithoutComponents>>, required?: ReadonlyArray<WithComponents>, excluded?: ReadonlyArray<WithoutComponents>, changed?: ReadonlyArray<keyof ComponentTypes>, changeThreshold?: number, parentHas?: ReadonlyArray<keyof ComponentTypes>, changedIdx?: ReadonlyArray<number>): Array<FilteredEntity<ComponentTypes, WithComponents extends never ? never : WithComponents, WithoutComponents extends never ? never : WithoutComponents>>; /** Test-only accessor for the internal query cache. @internal */ get _queryCacheForTesting(): QueryCache<ComponentTypes>; removeEntity(entityId: number, options?: RemoveEntityOptions): boolean; /** * Internal method to remove a single entity without cascade logic */ private removeEntityInternal; getEntity(entityId: number): Entity<ComponentTypes> | undefined; /** * Register a callback when a specific component is added to any entity * @param componentName The component key * @param handler Function receiving the new component value and the entity * @returns Unsubscribe function to remove the callback */ onComponentAdded<ComponentName extends keyof ComponentTypes>(componentName: ComponentName, handler: (ctx: { value: ComponentTypes[ComponentName]; entity: Entity<ComponentTypes>; }) => void): () => void; /** * Register a callback when a specific component is removed from any entity * @param componentName The component key * @param handler Function receiving the old component value and the entity * @returns Unsubscribe function to remove the callback */ onComponentRemoved<ComponentName extends keyof ComponentTypes>(componentName: ComponentName, handler: (ctx: { value: ComponentTypes[ComponentName]; entity: Entity<ComponentTypes>; }) => void): () => void; onAfterComponentAdded(hook: (entityId: number, componentName: keyof ComponentTypes) => void): () => void; onAfterEntityMutated(hook: (entityId: number) => void): () => void; onAfterComponentRemoved(hook: (entityId: number, componentName: keyof ComponentTypes) => void): () => void; onBeforeEntityRemoved(hook: (entityId: number) => void): () => void; onAfterParentChanged(hook: (childId: number) => void): () => void; /** * The current monotonic change sequence value. * Each markChanged call increments this before stamping. */ get changeSeq(): number; /** * Mark a component as changed on an entity, stamping the next sequence number. * @param entityId The entity ID * @param componentName The component that changed */ markChanged<K extends keyof ComponentTypes>(entityId: number, componentName: K): void; /** * Fast-path companion to markChanged that skips the component-name lookup. * Use after resolving names to indices once via getOrAssignComponentIdx. */ markChangedByIdx(entityId: number, componentIdx: number): void; getOrAssignComponentIdx<K extends keyof ComponentTypes>(componentName: K): number; /** * @internal Subscribe a component to change tracking. First call transitions * from default track-all (null bitmap) to explicit-only mode; subsequent calls * extend the subscription set. Called by ECSpresso._registerSystem for each * component named in a query's `changed:` filter. */ subscribeChanged<K extends keyof ComponentTypes>(componentName: K): void; /** * @internal Opt out of change tracking entirely. Installs an empty bitmap, * making every markChanged call a no-op until subscribeChanged is called. * Used by `.disableChangeTracking()` on the builder for worlds with no * reactive consumers. */ disableChangeTracking(): void; /** * @internal True if at least one of `idxs` is currently subscribed for * change tracking (or the bitmap is null = track-all). Used by the * auto-mark walk to skip entity iteration when every mark would be a no-op. */ hasAnySubscribed(idxs: ReadonlyArray<number>): boolean; /** * Get the sequence number at which a component was last changed on an entity * @param entityId The entity ID * @param componentName The component to check * @returns The sequence number when last changed, or -1 if never changed */ getChangeSeq<K extends keyof ComponentTypes>(entityId: number, componentName: K): number; /** * Create an entity as a child of another entity with initial components * @param parentId The parent entity ID * @param components Initial components to add * @returns The created child entity */ spawnChild<T extends { [K in keyof ComponentTypes]?: ComponentTypes[K]; }>(parentId: number, components: T & Record<Exclude<keyof T, keyof ComponentTypes>, never>): FilteredEntity<ComponentTypes, keyof T & keyof ComponentTypes>; /** * Set the parent of an entity * @param childId The entity ID to set as a child * @param parentId The entity ID to set as the parent */ setParent(childId: number, parentId: number): this; /** * Remove the parent relationship for an entity (orphan it) * @param childId The entity ID to orphan * @returns true if a parent was removed, false if entity had no parent */ removeParent(childId: number): boolean; /** * Get the parent of an entity * @param entityId The entity ID to get the parent of * @returns The parent entity ID, or null if no parent */ getParent(entityId: number): number | null; /** * Get all children of an entity in insertion order * @param parentId The parent entity ID * @returns Readonly array of child entity IDs */ getChildren(parentId: number): readonly number[]; /** * Get a child at a specific index * @param parentId The parent entity ID * @param index The index of the child * @returns The child entity ID, or null if index is out of bounds */ getChildAt(parentId: number, index: number): number | null; /** * Get the index of a child within its parent's children list * @param parentId The parent entity ID * @param childId The child entity ID to find * @returns The index of the child, or -1 if not found */ getChildIndex(parentId: number, childId: number): number; /** * Get all ancestors of an entity in order [parent, grandparent, ...] * @param entityId The entity ID to get ancestors of * @returns Readonly array of ancestor entity IDs */ getAncestors(entityId: number): readonly number[]; /** * Get all descendants of an entity in depth-first order * @param entityId The entity ID to get descendants of * @returns Readonly array of descendant entity IDs */ getDescendants(entityId: number): readonly number[]; /** * Get the root ancestor of an entity (topmost parent), or self if no parent * @param entityId The entity ID to get the root of * @returns The root entity ID */ getRoot(entityId: number): number; /** * Get siblings of an entity (other children of the same parent) * @param entityId The entity ID to get siblings of * @returns Readonly array of sibling entity IDs */ getSiblings(entityId: number): readonly number[]; /** * Check if an entity is a descendant of another entity * @param entityId The potential descendant ID * @param ancestorId The potential ancestor ID * @returns true if entityId is a descendant of ancestorId */ isDescendantOf(entityId: number, ancestorId: number): boolean; /** * Check if an entity is an ancestor of another entity * @param entityId The potential ancestor ID * @param descendantId The potential descendant ID * @returns true if entityId is an ancestor of descendantId */ isAncestorOf(entityId: number, descendantId: number): boolean; /** * Returns true when at least one parent-child relationship exists. */ get hasHierarchy(): boolean; /** * Get all root entities (entities that have children but no parent) * @returns Readonly array of root entity IDs */ getRootEntities(): readonly number[]; /** * Traverse the hierarchy in parent-first (breadth-first) order. * Parents are guaranteed to be visited before their children. * @param callback Function called for each entity with (entityId, parentId, depth) * @param options Optional traversal options (roots to filter to specific subtrees) */ forEachInHierarchy(callback: (entityId: number, parentId: number | null, depth: number) => void, options?: HierarchyIteratorOptions): void; /** * Generator-based hierarchy traversal in parent-first (breadth-first) order. * Supports early termination via break. * @param options Optional traversal options (roots to filter to specific subtrees) * @yields HierarchyEntry for each entity in parent-first order */ hierarchyIterator(options?: HierarchyIteratorOptions): Generator<HierarchyEntry, void, unknown>; }