Home Reference Source

src/world.js

import { Entity } from './entity.js'
import { SystemStorage } from './system_storage.js'
import { EntityStorage } from './entity_storage.js'

/**
 * Class for world.
 *
 * @class World (name)
 */
export class World {
  /**
   * Constructs an instance of the world.
   *
   * @param {object} [options] - The initial systems, components, and context to setup in the world.
   * Each one is optional. See below for registering these after world construction.
   *
   * @example
   * const world = new World({
   *   components: { position, velocity },
   *   systems: [Input, Physics, Render],
   *   context: { state },
   * })
   */
  constructor(options) {
    /** @ignore */
    this._systems = new SystemStorage(this)
    /** @ignore */
    this.entities = new EntityStorage(this)

    // Register components, context, and systems
    if (options) {
      if (options.components) {
        this.components = options.components
      }
      if (options.context) {
        this.context = options.context
      }
      if (options.systems) {
        this.systems = options.systems
      }
    }
  }

  /**
   * Removes all entities from the world.
   * Does not affect any registered systems or components.
   *
   * @example
   * world.clear()
   */
  clear() {
    this.entities.clear()
  }

  /**
   * Registers a component type to the world. Components must be constructable. If the component has
   * an onCreate(), it is passed all of the arguments from methods like entity.set(). Also, components
   * can have an onRemove() method, which gets called when removing that component from an entity.
   *
   * @param {string}   name           - The name
   * @param {function} componentClass - The component class, must be a constructable class or function
   *
   * @example
   * world.component('myComponent', class {
   *     // It is highly recommended to use onCreate() over constructor(), because the component
   *     // will have already been added to the entity. In the constructor(), it is not safe to use
   *     // "entity" because it does not contain the current component while still in the constructor.
   *     onCreate(some, args) {
   *         this.some = some
   *         this.args = args
   *         this.entity.set('whatever') // this.entity is auto-injected, and this is safe to do here
   *     }
   * })
   * // entity === the new entity object
   * // some === 10
   * // args === 500
   * world.entity().set('myComponent', 10, 500)
   *
   * @return {string} Registered component name on success, undefined on failure
   */
  component(name, componentClass) {
    this.entities.registerComponent(name, componentClass)
  }

  /**
   * Registers all components in an object. Merges with existing registered components.
   *
   * @example
   * world.components = { position: Position }
   */
  set components(comps) {
    for (let key in comps) {
      this.entities.registerComponent(key, comps[key])
    }
  }

  /**
   * Returns currently registered components.
   *
   * @example
   * const { position: Position } = world.components
   */
  get components() {
    return this.entities.componentClasses
  }

  /**
   * Creates a new entity in the world
   *
   * @example
   * world.entity()
   *
   * @return {Entity} The new entity created
   */
  entity() {
    return this.entities.createEntity()
  }

  /**
   * Sets a context object that is automatically injected into all existing and new systems.
   * Calling this multiple times will overwrite any previous contexts passed. One caveat is that
   * you can only start to use the injected context in systems starting with init(). It is not
   * available in the constructor.
   *
   * @param {Object} data - The object to use as context to pass to systems.
   * All the keys inside the context object will be spread into the top-level of the system.
   *
   * @example
   * const state = { app: new PIXI.Application() }
   * const world = new World()
   * world.context = state // new and existing systems can directly use this.app
   * world.system(...)
   */
  set context(data) {
    this._systems.setContext(data)
  }

  /**
   * Returns currently set context object.
   *
   * @example
   * const { app } = world.context
   */
  get context() {
    return this._systems.context
  }

  /**
   * Registers a system to the world.
   * The order the systems get registered, is the order then run in.
   *
   * @example
   * // Movement system (basic example)
   * class MovementSystem {
   *   run(dt) {
   *     world.each('position', 'velocity', ({ position, velocity }) => {
   *       position.x += velocity.x * dt
   *       position.y += velocity.y * dt
   *     })
   *   }
   * }
   * // Input system (advanced example)
   * class InputSystem {
   *   init(key) {
   *     // Have access to this.keyboard here, but not in constructor
   *     this.key = key
   *   }
   *   run(dt) {
   *     if (this.keyboard.isPressed(this.key)) {
   *       world.each('controlled', 'velocity', ({ velocity }, entity) => {
   *         // Start moving all controlled entities to the right
   *         velocity.x = 1
   *         velocity.y = 0
   *         // Can also use the full entity here, in this case to add a new component
   *         entity.set('useFuel')
   *       })
   *     }
   *   }
   * }
   * // Inject context (see world.context)
   * world.context = { keyboard: new Keyboard() }
   * // Register systems in order (this method)
   * world.system(InputSystem, 'w') // pass arguments to init/constructor
   * world.system(MovementSystem)
   * // Run systems (can get dt or frame time)
   * world.run(1000.0 / 60.0)
   *
   * @param {Function} systemClass - The system class to instantiate. Can contain a
   * constructor(), init(), run(), or any other custom methods/properties.
   *
   * @param {...Object} [args] - The arguments to forward to the system's constructor and init.
   * Note that it is recommended to use init if using context, see world.context.
   * Passing args here is still useful, because it can be specific to each system, where
   * the same context is passed to all systems.
   */
  system(systemClass, ...args) {
    this._systems.register(systemClass, ...args)
  }

  /**
   * Registers additional systems, in the order specified. See world.system().
   *
   * @example
   * world.systems = [inputSystem, movementSystem]
   */
  set systems(values) {
    for (const sys of values) {
      this._systems.register(sys)
    }
  }

  /**
   * Returns currently added systems, in the order added.
   *
   * @example
   * const [inputSystem, movementSystem] = world.systems
   */
  get systems() {
    return this._systems.systems
  }

  /**
   * Calls run() on all systems. These methods can return true to cause an additional rerun of all systems.
   * Reruns will not receive the args passed into run(), as a way to identify reruns.
   *
   * @example
   * world.run(deltaTime)
   *
   * @example
   * // Example flow of method call order:
   * // Setup systems:
   * world.system(systemA)
   * world.system(systemB)
   * // During world.run():
   * // systemA.run()
   * // systemB.run()
   *
   * @param {...Object} [args] - The arguments to forward to the systems' methods
   */
  run(...args) {
    this._systems.run(...args)
  }

  /**
   * Iterate through components and entities with all of the specified component names
   *
   * @example
   * // Use a callback to process entities one-by-one
   * // This is the recommended way, as it is higher performance than allocating and
   * // returning an array
   * world.each('comp', ({ comp }) => { comp.value = 0 })
   *
   * @example
   * // Get an array of entities
   * const entities = world.each('comp')
   * for (const entity of entities) {...}
   *
   * @example
   * // Pass multiple components, arrays, use extra entity parameter,
   * // and destructure components outside the query
   * world.each('compA', ['more', 'comps'], 'compB', ({ entity, compA, compC }) => {
   *   if (compC) compC.foo(compC.bar)
   *   compA.foo = 'bar'
   *   entity.remove('compB')
   * })
   *
   * @param {...Object} args - Can pass component names, arrays of component names, and a callback,
   * in any order.
   *
   * **{...string}**: The component names to match entities with. This checks if the entity
   * has ALL of the specified components, but does not check for additional components.
   *
   * **{Function}**: The callback to call for each matched entity. Takes (entity.data, entity).
   * Entity data is an object of {[componentName]: [component]}, that can be destructured with syntax
   * shown in the examples.
   *
   * @return {Entity[]} If no callback is specified, then returns an array of the entity results.
   */
  each(...args) {
    return this.entities.each(...args)
  }
}