ecs/manager.js

import { ArchetypeTable } from "../dataStructures/index.js"
import { Clock } from "../math/index.js"
import { EventDispatcher } from "../events/index.js"
import { Query } from "./query.js"
export class Manager {
  /**
   * RAF number of current frame.Used for pausing the manager.
   * 
   * @private
   * @type {number}
   */
  _rafID = 0
  /**
   * @private
   */
  _table = new ArchetypeTable()
  /**
   * 
   * @private
   * @type {SystemFunc[]}
   */
  _systems = []
  /**
   * 
   * @private
   * @type {boolean}
   */
  _initialized = false
  /**
   * @private
   * @type {Record<string,any>}
   */
  _resources = {}
  /**
   * Whether or not the manager is playing.
   * 
   * @type {boolean}
   */
  playing = false
  /**
   * 
   * @private
   * @type {Clock}
   */
  clock = new Clock()
  /**
   * 
   * @private
   * @type {number}
   */
  _accumulator = 0
  /**
   * Ideal framerate of the manager.Not implemented corrretly.
   * TODO correct it
   * 
   * @type {number}
   */
  frameRate = 0
  /**
   * @readonly
   * @type {EventDispatcher}
   */
  events = new EventDispatcher()
  /**
   * @private
   * @param {number} accumulate
   */
  _update = accumulate => {
    const dt = Clock.update(this.clock, accumulate);

    if (this._accumulator < this.frameRate) {
      this._accumulator += dt;
      this.RAF();
      return
    }
    this.events.trigger("updateStart", dt);
    this.update(dt)
    this._accumulator = 0;
    this.events.trigger("update", dt);
    this.events.trigger("updateEnd", dt);
    this.RAF();
  }
  /**
   * Creates a new instance of Manager class
   * @param {ManagerOptions} options
   **/
  constructor(options = {
    autoplay: true
  }) {
    options = Object.assign({
      autoplay: true,
    }, options)
    this.init()
    if (options.autoplay) this.play()
  }
  /**
   * This initializes the manager.
   * No need to call this function directly.
   * This is called after the preloader finishes loading all its files.
   * 
   */
  init() {
    this.events.trigger("init", this);
    this._initialized = true;
    if (this.playing) this.play();
  }

  /**
   * Adds an entity to the manager and initializes it.
   * 
   * @param {Record<string,any>} components The entity to add
   */
  create(components) {
    const entity = this._table.insert(components)
    this.events.trigger("add", {
      entity,
      components
    })
    return entity
  }
  /**
   * @template {Object} T
   * @param {T[]} entities
   */
  createMany(entities){
    for (let i = 0; i < entities.length; i++) {
      this.create(entities[i])
    }
  }
  /**
   * Removes an entity from the manager.
   * Note that this doesn't destroy the entity, only removes it and its components from the manager.
   * To destroy the entity,use `Entity.destroy()` method.
   * 
   * @param {Entity} entity The entity to remove
   */
  remove(entity) {
    this.events.trigger("remove", entity);
    this._table.remove(entity)
  }
  /**
   * @template T
   * @param {Entity} entity
   * @param { string[]  } compNames
   * @returns {T}
   */
  get(entity, ...compNames) {
    // @ts-ignore
    return this._table.get(entity, compNames)
  }
  /**
   * @template T
   * @param {Entity} entity
   * @param { T } components
   */
  set(entity, components) {
    throw "not implemented yet"
  }
  /**
   * @template T
   * @param { string[]  } compNames
   * @returns {Query<T>}
   */
  query(...compNames) {
    const query = new Query(...compNames)
    query.components = this._table.query(compNames)
    return query
  }
  /**
   * @param {string} name
   */
  queryEvent(name) {
    return this.events.getEvent(name)
  }
  /**
   * @template T
   * @param {string} name
   * @returns {T}
   */
  getResource(name) {
    return this._resources[name]
  }
  /**
   * @template T
   * @param {string} name
   * @param {T} resource
   */
  setResource(name, resource) {
    this._resources[name] = resource
  }
  /**
   * This removes all of the entities and components from the manager
   */
  clear() {
    this.events.trigger("clear", this)
    this._table.clear()
  }
  /**
   * This method requests an animation frame from the browser
   * 
   * @private
   */
  RAF() {
    this._rafID = requestAnimationFrame(this._update);
  }
  /**
   * This starts up the update loop of the manager
   */
  play() {
    if (!this._initialized) {
      this.playing = true;
      return
    }
    this.RAF();
    this.events.trigger("play", this);
  }
  /**
   * This stops the update loop of the manager
   */
  pause() {
    if (!this._initialized) {
      this.playing = false;
      return
    }
    cancelAnimationFrame(this._rafID);
    this.events.trigger("pause", this);
  }
  /**
   * Marches the update loop forward,updating
   * the systems
   * You shouldn't mess with this/call it or everything will explode with undetectable Loggerors.
   * 
   * @private
   */
  update(dt = 0.016) {
    const systems = this._systems;
    this.setResource("delta",dt)
    for (let i = 0; i < systems.length; i++) {
      systems[i](this);
    }
  }
  /**
   * Used to register a system
   *
   * @param {SystemFunc} sys The system to be addad
   * 
   */
  registerSystem(sys) {
    this._systems.push(sys)
  }
  /**
   * @param {Plugin} plugin
  */
  registerPlugin(plugin){
    plugin.register(this)
  }
}

/**
 * @callback SystemFunc
 * @param {Manager} manager
 * @returns {void}
 */
/**
 * @typedef ManagerOptions
 * @property {boolean} autoplay
 */
 /**
  * @typedef Plugin
  * @property {SystemFunc} register
 */