API Docs for:
Show:

File: dependencies\EntitySystem.js

//=====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);
        }
    };
}