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;
}