Home Reference Source

src/entity_storage.js

import { Entity } from './entity.js'
import { invoke } from './utilities.js'

/** @ignore */
export class EntityStorage {
  /** EntityStorage constructor, needs reference back to world */
  constructor(world) {
    /** World instance */
    this.world = world
    /** Component keys to component classes */
    this.componentClasses = {}
    /** Used for generating entity IDs */
    this.nextEntityId = 1
    /** Maps entity IDs to entities */
    this.entities = new Map()
    /** Maps component keys to entities */
    this.index = new Map()
  }

  /**
   * Removes all entities along with their components.
   * This is more efficient than calling entity.destroy() on each, since
   * this will only call onRemove on each and then clear the index.
   */
  clear() {
    // Call onRemove on all components of all entities
    for (const { data } of this.entities.values()) {
      for (const componentName in data) {
        invoke(data[componentName], 'onRemove')
      }
    }

    // Clear entities
    this.entities.clear()
    this.index.clear()
  }

  /** Registers a function/class with a name as a component */
  registerComponent(name, componentClass) {
    if (typeof componentClass !== 'function') {
      throw new Error('Component is not a valid function or class.')
    }
    this.componentClasses[name] = componentClass
  }

  /** Creates a new entity attached to the world */
  createEntity() {
    const entityId = this.nextEntityId++
    const entity = new Entity(this.world, entityId)
    this.entities.set(entityId, entity)
    return entity
  }

  /** Forwards query args from world */
  each(...args) {
    return this.queryIndex(this.queryArgs(...args))
  }

  /** Returns an existing or new index */
  accessIndex(component) {
    return (
      this.index.get(component) ||
      this.index.set(component, new Map()).get(component)
    )
  }

  /** Add component with an entity to the index */
  addToIndex(entity, compName) {
    this.accessIndex(compName).set(entity.id, entity)
  }

  /** Remove component from the index for an entity */
  removeFromIndex(entity, compName) {
    this.accessIndex(compName).delete(entity.id)
  }

  /** Get component names and optional callback from query args */
  queryArgs(...args) {
    const result = {
      componentNames: [],
      callback: null,
    }
    for (const arg of args) {
      if (typeof arg === 'string') {
        result.componentNames.push(arg)
      } else if (typeof arg === 'function') {
        result.callback = arg
      } else if (Array.isArray(arg)) {
        // Add 1-level deep arrays of strings as separate component names
        for (const name of arg) {
          result.componentNames.push(name)
        }
      } else {
        throw new Error(
          `Unknown argument ${arg} with type ${typeof arg} passed to world.each().`
        )
      }
    }
    return result
  }

  /**
   * Uses an existing index or builds a new index, to get entities with the specified components
   * If callback is defined, it will be called for each entity with component data, and returns undefined
   * If callback is not defined, an array of entities will be returned
   */
  queryIndex({ componentNames, callback }) {
    // Return all entities (array if no callback)
    if (componentNames.length === 0) {
      const iter = this.entities.values()
      if (!callback) {
        return [...iter]
      }
      for (const entity of iter) {
        if (callback(entity.data, entity) === false) {
          break
        }
      }
      return
    }

    // Sort component names by number of components in each index
    // This allows us to get the minimum component index as the first element
    // This also helps short-circuit much faster during the full query loops with .every()
    componentNames.sort(
      (a, b) => this.accessIndex(a).size - this.accessIndex(b).size
    )
    const iter = this.accessIndex(componentNames.shift()).values()

    // Return array of matched entities
    if (!callback) {
      const results = []
      for (const entity of iter) {
        const { data } = entity
        if (componentNames.every(name => name in data)) {
          results.push(entity)
        }
      }
      return results
    }

    // Invoke callback for each matched entity
    for (const entity of iter) {
      const { data } = entity
      if (
        componentNames.every(name => name in data) &&
        callback(data, entity) === false
      ) {
        return
      }
    }
  }
}