//=====Entity code=====
/**
This constructor is used internally by the manager to create entities and is public only for the purpouse of type checking.
@class Entity
@constructor
*/
function Entity(manager, id, components){
this._manager = manager;
this._id = id;
this._components = components;
this._destroyed = false;
}
/**
@method getId
@return {Number} The entity's unique identifier.
*/
Entity.prototype.getId = function(){
return this._id;
};
/**
Adds the specified component to the entity.
@method add
@param component A component object. Every component type should have a unique constructor, since all components are referenced by their constructors.
*/
Entity.prototype.add = function(component){
if(this.has(component.constructor)){
return;
}
this._components[component.constructor.prototype._componentIdentifier] = component;
this._manager._entityAddedComponent(this, component.constructor);
};
/**
Removes the specified component from the entity.
@method remove
@param constructor {Function} Constructor of the component to be removed.
*/
Entity.prototype.remove = function(componentConstructor){
if(!this.has(componentConstructor)){
return;
}
this._manager._entityRemovedComponent(this, componentConstructor);
this._components[componentConstructor.prototype._componentIdentifier] = null;
};
/**
Checks if the entity has a specified component.
@method has
@param constructor {Function} Constructor of the component to be checked.
@return {Boolean}
*/
Entity.prototype.has = function(componentConstructor){
return Boolean(this._components[componentConstructor.prototype._componentIdentifier]);
};
/**
Retrieves the specified component from the entity.
@method get
@param constructor {Function} Constructor of the component to be retrieved.
@return The retrived component. If the entity doesn't have the specified component, the value will be undefined or null.
*/
Entity.prototype.get = function(componentConstructor){
return this._components[componentConstructor.prototype._componentIdentifier];
};
/**
Destroys the entity, completely removing it from the entity system.
@method destroy
*/
Entity.prototype.destroy = function(){
if(this._destroyed){
throw new Error('The entity you\'re trying to destroy has already been destroyed!');
}
this._manager._removeEntity(this);
this._destroyed = true;
};
//=====Aspect code=====
/**
This constructor is used internally by the manager to create aspects and is public only for the purpouse of type checking.
@class Aspect
@constructor
*/
function Aspect(manager, componentsWanted, componentsUnwanted){
var entities = new LinkedList(),
destroyed = false;
this._componentsWanted = componentsWanted;
this._componentsUnwanted = componentsUnwanted;
this._manager = manager;
this._addedCallback = null;
this._removedCallback = null;
/**
Function used for working on the entities of an aspect.
@method iterate
@param iterator {Function} The iterator function should accept one parameter, an entity.
@example
aspect.iterate(function(entity){
var position = entity.get(Position);
position.x += 5;
position.y += 3;
});
*/
this.iterate = function(iterator){
for(var node=entities.getFirst(); node; node=node.next){
iterator(node.value);
}
};
/**
Subscribes a function that will be called back each time an entity is added to the aspect.
@method subscribeAdded
@param callback {Function} Function that will be called back when ever an entity is added to the aspect.
*/
this.subscribeAdded = function(callback){
this._addedCallback = callback;
};
/**
Removes the subscription.
@method unsubscribeAdded
*/
this.unsubscribeAdded = function(){
this._addedCallback = null;
};
/**
Subscribes a function that will be called back each time an entity is removed from the aspect.
@method subscribeRemoved
@param callback {Function} Function that will be called back when ever an entity is removed from the aspect.
*/
this.subscribeRemoved = function(callback){
this._removedCallback = callback;
};
/**
Removes the subscription.
@method unsubscribeAdded
*/
this.unsubscribeRemoved = function(){
this._removedCallback = null;
};
/**
Destroys the aspect, completely removing it from the entity system. If a removed callback is subscribed, it will be called for every entity.
@method destroy
*/
this.destroy = function(){
if(destroyed){
throw new Error('The aspect you\'re trying to destroy has already been destroyed!');
}
if(this._removedCallback !== null){
for(var node=entities.getFirst(); node; node=node.next){
this._removedCallback(node.value);
}
}
//Clearing the list of entities is not strictly necessary, but it makes garbage collection a lot smoother.
entities.clear();
this._manager._removeAspect(this);
destroyed = true;
};
this._addEntity = function(entity){
entities.insert(0, entity);
if(this._addedCallback !== null){
this._addedCallback(entity);
}
};
this._removeEntity = function(entity){
if(entities.removeValue(entity) && this._removedCallback !== null){
this._removedCallback(entity);
}
};
}
//=====Manager code=====
/**
Object reponsible for managing entities and aspects.
@class Manager
*/
function Manager(){
//=====Component registration=====
var nextUniqueComponentIdentifier = 0;
/**
Every component, before it's used, needs to be registered with the manager. This function adds a field to the constructor's prototype named "_componentIdentifier". If dynamically adding a field is an issue, do it yourself when writing components' constructors.
@method registerComponent
@param constructor {Function} Constructor of the component to be registered.
*/
this.registerComponent = function(componentConstructor){
componentConstructor.prototype._componentIdentifier = nextUniqueComponentIdentifier++;
//Resize each entity's component array.
for(var i=0; i<entities.length; ++i){
if(entities[i]){
entities[i]._components.push(null);
}
}
//Resize the aspect arrays.
aspectsByComponentWanted.push(null);
aspectsByComponentUnwanted.push(null);
};
//=====Entity management=====
var entities = [],
availableEntityIdentifiers = [];
/**
@method createEntity
@return {Entity}
*/
this.createEntity = function(){
var entity;
if(availableEntityIdentifiers.length){
var id = availableEntityIdentifiers.pop();
//Next unique component identifier is also the total amount of components registered
entity = new Entity(this, id, new Array(nextUniqueComponentIdentifier));
entities[id] = entity;
}else{
//Next unique component identifier is also the total amount of components registered
entity = new Entity(this, entities.length, new Array(nextUniqueComponentIdentifier));
entities.push(entity);
}
return entity;
};
this._removeEntity = function(entity){
for(var node=aspects.getFirst(); node; node=node.next){
node.value._removeEntity(entity);
}
availableEntityIdentifiers.push(entity._id);
entities[entity._id] = null;
};
this._entityAddedComponent = function(entity, componentConstructor){
var aspectList = aspectsByComponentUnwanted[componentConstructor.prototype._componentIdentifier], node;
if(aspectList){
for(node=aspectList.getFirst(); node; node=node.next){
node.value._removeEntity(entity);
}
}
aspectList = aspectsByComponentWanted[componentConstructor.prototype._componentIdentifier];
if(aspectList){
for(node=aspectList.getFirst(); node; node=node.next){
if(entityHasComponents(entity, node.value._componentsWanted) && entityDoesNotHaveComponents(entity, node.value._componentsUnwanted)){
node.value._addEntity(entity);
}
}
}
};
this._entityRemovedComponent = function(entity, componentConstructor){
var aspectList = aspectsByComponentWanted[componentConstructor.prototype._componentIdentifier], node;
if(aspectList){
for(node=aspectList.getFirst(); node; node=node.next){
node.value._removeEntity(entity);
}
}
aspectList = aspectsByComponentUnwanted[componentConstructor.prototype._componentIdentifier];
if(aspectList){
for(node=aspectList.getFirst(); node; node=node.next){
if(entityHasComponents(entity, node.value._componentsWanted) && entityDoesNotHaveComponentsExluding(entity, node.value._componentsUnwanted, componentConstructor)){
node.value._addEntity(entity);
}
}
}
};
function entityHasComponents(entity, componentConstructors){
for(var i=0; i<componentConstructors.length; ++i){
if(!entity.has(componentConstructors[i])){
return false;
}
}
return true;
}
function entityDoesNotHaveComponents(entity, componentConstructors){
for(var i=0; i<componentConstructors.length; ++i){
if(entity.has(componentConstructors[i])){
return false;
}
}
return true;
}
//This function allows entities to call _entityRemovedComponent before the component is actualy removed from the entity.
function entityDoesNotHaveComponentsExluding(entity, componentConstructors, excludeConstructor){
for(var i=0; i<componentConstructors.length; ++i){
if(entity.has(componentConstructors[i]) && componentConstructors[i] !== excludeConstructor){
return false;
}
}
return true;
}
//=====Aspect management=====
var aspects = new LinkedList(),
aspectsByComponentWanted = [],
aspectsByComponentUnwanted = [];
/**
@method createAspect
@param componentsWanted {Array} Array of component constructors that this aspect requires.
@param [componentsUnwanted] {Array} Array of component constructors that this aspect requires not to be present on an entity.
@return {Aspect}
@example
var aspect = manager.createAspect([Position, Velocity]);
var anotherAspect = manager.createAspect([Position, Velocity], [Health]);
*/
this.createAspect = function(componentsWanted, componentsUnwanted){
var wanted = componentsWanted.slice(0),
unwanted = componentsUnwanted ? componentsUnwanted.slice(0) : [],
aspect = new Aspect(this, wanted, unwanted), componentIdentifier, i;
aspects.insert(0, aspect);
for(i=0; i<wanted.length; ++i){
componentIdentifier = wanted[i].prototype._componentIdentifier;
if(!aspectsByComponentWanted[componentIdentifier]){
aspectsByComponentWanted[componentIdentifier] = new LinkedList();
}
aspectsByComponentWanted[componentIdentifier].insert(0, aspect);
}
for(i=0; i<unwanted.length; ++i){
componentIdentifier = unwanted[i].prototype._componentIdentifier;
if(!aspectsByComponentUnwanted[componentIdentifier]){
aspectsByComponentUnwanted[componentIdentifier] = new LinkedList();
}
aspectsByComponentUnwanted[componentIdentifier].insert(0, aspect);
}
for(i=0; i<entities.length; ++i){
if(entities[i]){
if(entityHasComponents(entities[i], wanted) && entityDoesNotHaveComponents(entities[i], unwanted)){
aspect._addEntity(entities[i]);
}
}
}
return aspect;
};
this._removeAspect = function(aspect){
var i;
aspects.removeValue(aspect);
for(i=0; i<aspect._componentsWanted.length; ++i){
aspectsByComponentWanted[aspect._componentsWanted[i].prototype._componentIdentifier].removeValue(aspect);
}
for(i=0; i<aspect._componentsUnwanted.length; ++i){
aspectsByComponentUnwanted[aspect._componentsUnwanted[i].prototype._componentIdentifier].removeValue(aspect);
}
};
}