Source: Engine.js

Source: Engine.js

var EventEmitter = require('./EventEmitter');
var ComponentGroup = require('./ComponentGroup');
var BitSet = require('./BitSet');
var Entity = require('./Entity');
var SystemBuilder = require('./SystemBuilder');
var DEFAULT_SYSTEM_PRIORITY = 1000;

/**
 * Represents an game Engine.
 * It's like hub of all the objects - {@link Entity}, {@link Component}, etc.
 * It contains all the Entity in the game, and all the Component used,
 * and all the System.
 * @constructor
 * @extends EventEmitter
 * @see Entity
 * @see Component
 * @see System
 * @see ComponentGroup
 */
function Engine() {
  EventEmitter.call(this);
  this._entities = {};
  this._entitiesArray = [];
  this._entityPos = 0;
  this._components = [];
  this._componentConstructors = {};
  this._componentPos = 0;
  this._componentGroups = [];
  this._componentGroupEntities = [];
  /**
   * Array of all the {@link System} in the Engine.
   * You shouldn't edit this array directly, use
   * {@link Engine#addSystem} and {@link Engine#removeSystem} instead.
   * @var {Array}
   */
  this.systems = [];
  this._systemTable = {};
  this._systemPos = 0;
  this._systemsSortRequired = false;
}

Engine.prototype = Object.create(EventEmitter.prototype);
Engine.prototype.constructor = Engine;

/**
 * Registers a {@link Component} type to the Engine.
 * @param key {String} - {@link Component}'s string key.
 * @param constructor {Function] - {@link Component}'s constructor.
 */
Engine.prototype.registerComponent = function(key, constructor) {
  this._componentConstructors[key] = constructor;
  var index = this._components.indexOf(key);
  if(index != -1) return index;
  this._components.push(key);
  return this._componentPos ++;
}

/**
 * Registers a {@link Component} type to the Engine.
 * @function
 * @param key {String} - {@link Component}'s string key.
 * @param constructor {Function} - {@link Component}'s constructor.
 */
Engine.prototype.c = Engine.prototype.registerComponent;

/**
 * Returns {@link Component}'s constructor registered in the Engine/
 * @param key {String} - {@link Component}'s string key.
 * @return the Component's constructor.
 */
Engine.prototype.getComponentConstructor = function(key) {
  return this._componentConstructors[key];
}

/**
 * Returns {@link Component} type's unique ID in the Engine.
 * This unique ID is used in {@link BitSet}'s bit position.
 * This will call {@link Engine#registerComponent} if needed.
 * @param key {String} - {@link Component}'s string key.
 * @return the Component's unique ID.
 */
Engine.prototype.getComponentBit = function(key) {
  var bitPos = this._components.indexOf(key);
  if(bitPos == -1) {
    return this.registerComponent(key);
  } else {
    return bitPos;
  }
}

/**
 * Returns {@link Component} type's key by its unique ID.
 * @param key {Number} - {@link Component}'s unique ID
 * @return the Component's string key.
 */
Engine.prototype.getComponentName = function(key) {
  return this._components[key];
}

/**
 * Returns a BitSet holding combination of {@link Component}s type's unique ID.
 * @param components {Array} - An array holding {@link Component} keys.
 * @return {BitSet} A BitSet holding combination of Components.
 */
Engine.prototype.getComponentsBitSet = function(components) {
  var bits = new BitSet();
  for(var i = 0; i < components.length; ++i) {
    bits.set(this.getComponentBit(components[i]), true);
  }
  return bits;
}

/**
 * Returns a new Entity ID to use.
 * This changes its value each time it's called.
 * @return {Number} An integer that is used for Entity's ID
 * @see Entity
 */
Engine.prototype.obtainEntityId = function() {
  return this._entityPos ++;
}

/**
 * Adds an Entity to the Engine.
 * @param entity {Entity} - An Entity to add
 * @fires Engine#entityAdded
 * @fires ComponentGroup#entityAdded
 */
Engine.prototype.addEntity = function(entity) {
  if(entity.id != null) {
    // Already have an entity with that id
    if(this._entities[entity.id]) return;
    // Engine is not defined
    if(entity._engine != this) return;
  } else {
    entity.id = this.obtainEntityId();
  }
  entity._engine = this;
  this._entities[entity.id] = entity;
  this._entitiesArray.push(entity);
  /**
   * This event is fired when a Entity has added to {@link Engine}.
   *
   * @event Engine#entityAdded
   * @type {Entity}
   */
  this.emit('entityAdded', entity);
  this.updateComponentGroup(entity);
  entity.on('componentAdded', this.updateComponentGroup, this);
  entity.on('componentRemoved', this.updateComponentGroup, this);
}

/**
 * Creates an empty Entity and adds to the Engine.
 * @return {Entity} an empty Entity.
 * @fires Engine#entityAdded
 */
Engine.prototype.createEntity = function() {
  var entity = new Entity(this);
  this.addEntity(entity);
  return entity;
}

/**
 * Executes Entity related functions by its arguments.
 *
 * If no argument is provided, it returns new Entity.
 * If a number is provided, it returns a Entity with that ID.
 * If an object is provided, it returns new Entity populated with the template.
 * Otherwise, it returns an array of Entity having provided list components.
 */
Engine.prototype.e = function(id) {
  if(arguments.length === 0) {
    return this.createEntity();
  } else if(arguments.length === 1) {
    if(typeof id === 'number') {
      return this.getEntity(id);
    }
    if(typeof id === 'object') {
      var entity = this.createEntity();
      // Create components as template
      for(var key in id) {
        entity.c(key, id[key]);
      }
      return entity;
    }
  }
  return this.getEntitiesFor.apply(this, arguments);
}

/**
 * Removes an Entity from the Engine.
 * @param entity {Entity} - An Entity to remove
 * @fires Engine#entityRemoved
 * @fires ComponentGroup#entityRemoved
 */
Engine.prototype.removeEntity = function(entity) {
  var entityPos = this._entitiesArray.indexOf(entity);
  if(entityPos == -1) return;
  delete this._entities[entity.id];
  this._entitiesArray.splice(entityPos, 1);
  /**
   * This event is fired when a Entity has removed from {@link Engine}.
   *
   * @event Engine#entityRemoved
   * @type {Entity}
   */
  this.emit('entityRemoved', entity);
  entity.removeListener('componentAdded', this.updateComponentGroup);
  entity.removeListener('componentRemoved', this.updateComponentGroup);
  // TODO Optimiziation
  for(var i = 0; i < this._componentGroups.length; ++i) {
    var componentGroup = this._componentGroups[i];
    if(entity.componentGroupBits.get(componentGroup.id)) {
      var componentEntities = this._componentGroupEntities[i];
      componentEntities.splice(componentEntities.indexOf(entity), 1);
      componentGroup.emit('entityRemoved', entity);
    }
  }
}

/**
 * Removes all {@link Entity} from the Engine.
 * @fires Engine#entityRemoved
 * @fires ComponentGroup#entityRemoved
 */
Engine.prototype.removeAllEntities = function(entity) {
  while(this._entitiesArray.length > 0) {
    this.removeEntity(this._entitiesArray[0]);
  }
}

/**
 * Returns an Entity with given ID.
 * This will return null if it can't be found.
 * @param id {Number} - An ID to find the Entity
 * @returns {Entity} The entity with given ID
 */
Engine.prototype.getEntity = function(id) {
  return this._entities[id];
}

/**
 * Returns an array of Entity.
 * It shouldn't be edited since the Engine uses it directly.
 * @returns {Array} An array holding {@link Entity}
 */
Engine.prototype.getEntities = function() {
  return this._entitiesArray;
}

/**
 * Updates the Entity to find if it matches {@link ComponentGroup}s' criteria.
 * @param entity {Entity} - the Entity to update
 * @private
 * @see ComponentGroup
 */
Engine.prototype.updateComponentGroup = function(entity) {
  for(var i = 0; i < this._componentGroups.length; ++i) {
    var componentGroup = this._componentGroups[i];
    if(componentGroup.matches(entity)) {
      if(!entity.componentGroupBits.get(componentGroup.id)) {
        // 추가
        entity.componentGroupBits.set(componentGroup.id, true);
        this._componentGroupEntities[i].push(entity);
        componentGroup.emit('entityAdded', entity);
      }
    } else {
      if(entity.componentGroupBits.get(componentGroup.id)) {
        // 삭제
        entity.componentGroupBits.set(componentGroup.id, false);
        var componentEntities = this._componentGroupEntities[i];
        componentEntities.splice(componentEntities.indexOf(entity), 1);
        componentGroup.emit('entityRemoved', entity);
      }
    }
  }
}

/**
 * Registers the ComponentGroup to the Engine.
 * Engine will check if an Entity matches ComponentGroup's criteria every time
 * when a Entity is added or removed.
 * @param componentGroup {ComponentGroup} - The ComponentGroup to register
 * @returns {Array} An array holding {@link Entity} matching its criteria
 * @fires ComponentGroup#entityAdded
 * @see Entity
 */
Engine.prototype.registerComponentGroup = function(componentGroup) {
  if(componentGroup.id != null &&
      this._componentGroups[componentGroup.id] == componentGroup) {
    return this._componentGroupEntities[componentGroup.id];
  }
  for(var i = 0 ; i < this._componentGroups.length; ++i) {
    if(this._componentGroups[i].equals(componentGroup)) {
      return this._componentGroupEntities[i];
    }
  }
  componentGroup.id = this._componentGroups.length;
  componentGroup._engine = this;
  this._componentGroups.push(componentGroup);
  var componentGroupEntity = [];
  this._componentGroupEntities.push(componentGroupEntity);
  // initialize componentGroup array
  this.getEntities().forEach(function(entity) {
    if(componentGroup.matches(entity)) {
      // 추가
      entity.componentGroupBits.set(componentGroup.id, true);
      componentGroupEntity.push(entity);
      componentGroup.emit('entityAdded', entity);
    }
  });
  return componentGroupEntity;
}

/**
 * Returns an array having {@link Entity} matching ComponentGroup's criteria.
 * The array will keep updated, so you can call this method only once
 * and check its contents if needed.
 * It's an alias function to {@link Engine#registerComponentGroup}
 * @param componentGroup {ComponentGroup} - The ComponentGroup
 * @returns {Array} An array holding {@link Entity} matching its criteria
 * @fires ComponentGroup#entityAdded
 * @see Engine#registerComponentGroup
 */
Engine.prototype.getEntitiesFor = function(componentGroup) {
  if(componentGroup instanceof ComponentGroup) {
    return this.registerComponentGroup(componentGroup);
  } else {
    // Build ComponentGroup with arguments
    var builder = ComponentGroup.createBuilder(this);
    builder.contain.apply(builder, arguments);
    var componentGroup = builder.build();
    return this.registerComponentGroup(componentGroup);
  }
}

/**
 * Searches ComponentGroup by its array.
 * @param entities {Array} An array of ComponentGroup
 * @returns {ComponentGroup} The ComponentGroup linked with the array
 */
Engine.prototype.getComponentGroup = function(entities) {
  return this._componentGroups[this._componentGroupEntities.indexOf(entities)];
}

/**
 * Adds the System to the Engine.
 * The System will be triggered when the {@link Engine#update} is called.
 * This will trigger {@link System#add}.
 * @param key {String} - The System's string key
 * @param system {System} - The System to add
 */
Engine.prototype.addSystem = function(key, system) {
  if(this.systems.indexOf(system) != -1) return;
  this._systemTable[key] = system;
  system._id = this._systemPos ++;
  this.systems.push(system);
  if(typeof system.add == 'function') {
    system.add(this);
  }
  if(system.priority == null) {
    system.priority = DEFAULT_SYSTEM_PRIORITY;
  }
  system.engine = this;
  this._systemsSortRequired = true;
}

/**
 * Returns a new SystemBuilder associated the Engine.
 * @param key {String} - The System's string key to use.
 * @returns {SystemBuilder} A new SystemBuilder
 */
Engine.prototype.createSystem = function(key) {
  return new SystemBuilder(key, this);
}

/**
 * Executes System related functions by its arguments.
 *
 * If a key is provided and the Engine doesn't have a system with that key,
 * it returns a SystemBuilder.
 * If a key is provided and the Engine has a system with that key, it returns
 * that system.
 * If a key is provided and a system is provided, it registers the system.
 */
Engine.prototype.s = function(key, system) {
  if(system == null) {
    if(this._systemTable[key] == null) return this.createSystem(key);
    return this.getSystem(key);
  }
  return this.addSystem(key, system);
}

/**
 * Returns the System registered to the Engine.
 * @param key {String} - The System's string key
 * @returns {System} The system registered to the Engine
 */
Engine.prototype.getSystem = function(key) {
  return this._systemTable[key];
}

/**
 * Removes the System from the Engine.
 * This will trigger {@link System#remove}.
 * @param key {String} - The System's string key to remove.
 */
Engine.prototype.removeSystem = function(key) {
  var system = this._systemTable[key];
  var systemPos = this.systems.indexOf(system);
  if(systemPos == -1) return;
  delete this._systemTable[key];
  this.systems.splice(systemPos, 1);
  if(typeof system.remove == 'function') {
    system.remove(this);
  }
}

/**
 * Sorts the System array to match their priority.
 * @private
 * @see System
 */
Engine.prototype.sortSystems = function() {
  if(this._systemsSortRequired) {
    this.systems.sort(function(a, b) {
      if(a.priority > b.priority) {
        return 1;
      } else if(a.priority < b.priority) {
        return -1;
      } else {
        if(a._id > b._id) {
          return 1;
        } else if(a._id < b._id) {
          return -1;
        } else {
          return 0;
        }
      }
    });
    this._systemsSortRequired = false;
  }
}

/**
 * An update function called by every tick of the game.
 */
Engine.prototype.update = function(delta) {
  this.sortSystems();
  this.systems.forEach(function(system) {
    if(system.update) {
      system.update(delta);
    }
  })
}

/**
 * Serializes the Engine object.
 * Note that systems won't be included in serialized object, so user should
 * load the systems on their own.
 * @return {Object} serialized Engine object
 */
Engine.prototype.toJSON = function() {
  var obj = {};
  obj.entities = this._entitiesArray.map(function(v) {
    return v.toJSON();
  });
  obj.entityPos = this._entityPos;
  // For future use
  obj.systems = Object.keys(this._systemTable);
  obj.components = Object.keys(this._componentConstructors);
  return obj;
}

Engine.prototype.serialize = Engine.prototype.toJSON;

/**
 * Deserializes the Engine object and places on provided engine.
 * The provided Engine object should be clean; it shouldn't contain any
 * Entity object. But it can contain pre-defined Components and Systems.
 * Note that systems won't be included in serialized object, so user should
 * load the systems on their own.
 * @param {Object} serialized Engine object
 */
Engine.prototype.deserialize = function(data) {
  this.removeAllEntities();
  // Add entities and resets entityPos
  data.entities.forEach(function(v) {
    var entity = Entity.fromJSON(this, v);
    this.addEntity(entity);
  }, this);
  this._entityPos = data.entityPos;
}

Engine.prototype.fromJSON = Engine.prototype.fromJSON;

if(typeof module !== 'undefined') {
  module.exports = Engine;
}