API Docs for:
Show:

File: rendering\System.js

/**
 This system is responsible for rendering tile layers and entities that have the Sprite and Position components.
 @class RenderingSystem
 @constructor
 @param entitySystemManager {Manager} The entity system manager whose entities this system will be working on.
 */
function RenderingSystem(entitySystemManager){
    
    //=====Layers=====
    
    var tileLayerArray = [];
    //The string signifies the middle sprite layer.
    tileLayerArray.push('sprites');
    
    /**
     An interface for working with tile layers. Sprites are by default rendered as the middle layer. Adding a layer to the front will cause that layer to be rendered on top of the sprites and other layers. Adding a layer to the back will cause the layer to be rendered under the sprites and other layers.
     @property tileLayers
     @type Object
     @final
     */
    this.tileLayers = {
        
        /**
         Adds a tile layer to the front. The rendering order for front layers is LIFO; layer added to front last will be rendered first.
         @method tileLayers.addToFront
         @param tileLayerHandle {AssetHandle} Handle to the tile layer.
         */
        addToFront : function(tileLayerHandle){
            var tileLayer = tileLayerHandle.getAsset();
            //Adds the tile layer to the end of the array.
            //This tile layer will now be rendered last.
            tileLayerArray.push(tileLayer);
        },
        /**
         Adds a tile layer to the back. The rendering order for back layers is FIFO; layer added to back last will be rendered last.
         @method tileLayers.addToFront
         @param tileLayerHandle {AssetHandle} Handle to the tile layer.
         */
        addToBack : function(tileLayerHandle){
            var tileLayer = tileLayerHandle.getAsset();
            //Adds the tile layer to the front of the array.
            //This tile layer will now be rendered first.
            tileLayerArray.unshift(tileLayer);
        },
        
        /**
         Removes all previously added tile layers.
         @method tileLayers.clear
         */
        clear : function(){
            //Reset the tile layer array.
            tileLayerArray.length = 0;
            //The string signifies the middle sprite layer.
            tileLayerArray.push('sprites');
        }
    };
    
    function drawTileLayer(tileLayer, view){
        var viewHalfWidth = view.getViewWidth() / 2,
            viewHalfHeight = view.getViewHeight() / 2,
            
            //These tile offsets ensure that tiles are rendered at their apropriate positions relatively to the view,
            //given the tile layer's scroll speeds and the view's canvas transformation. The offsets are floored to ensure
            //that the tiles are always rendered at integer coordinates.
            tileOffsetX = Math.floor((view._position.x - viewHalfWidth) * (1 - tileLayer.scrollSpeedX)),
            tileOffsetY = Math.floor((view._position.y - viewHalfHeight) * (1 - tileLayer.scrollSpeedY)),
        
            beginTileX = Math.floor((view._position.x - viewHalfWidth) * tileLayer.scrollSpeedX / tileLayer.tileWidth),
            beginTileY = Math.floor((view._position.y - viewHalfHeight) * tileLayer.scrollSpeedY / tileLayer.tileHeight),
            
            endTileX = Math.ceil(((view._position.x - viewHalfWidth) * tileLayer.scrollSpeedX + view.getViewWidth()) / tileLayer.tileWidth),
            endTileY = Math.ceil(((view._position.y - viewHalfHeight) * tileLayer.scrollSpeedY + view.getViewHeight()) / tileLayer.tileHeight);
            
        //If the tiles are to not be repeated on either axis, then the corresponding tile coordinates need to be clamped.
        if(!tileLayer.repeatX){
            //If the tile layer is to not be repeated on the x axis and the beginning tile coordinate
            //is larger than or equal to the width of the tile layer then there's nothing to render and the function can exit.
            if(beginTileX >= tileLayer.layerWidth){
                return;
            }
            endTileX = Math.min(endTileX, tileLayer.layerWidth);
        }
        if(!tileLayer.repeatY){
            //If the tile layer is to not be repeated on the y axis and the beginning tile coordinate
            //is larger than or equal to the height of the tile layer then there's nothing to render and the function can exit.
            if(beginTileY >= tileLayer.layerHeight){
                return;
            }
            endTileY = Math.min(endTileY, tileLayer.layerHeight);
        }

        var spriteSheet = tileLayer.spriteSheetHandle.getAsset(),
            spriteSheetImage = spriteSheet.spriteSheetImageHandle.getAsset(),
            sprite, spriteIndex, x, y;
        
        //Draw the tiles.
        for(var i=beginTileY; i<endTileY; ++i){
            for(var j=beginTileX; j<endTileX; ++j){
                
                spriteIndex = tileLayer.tiles[(i % tileLayer.layerHeight)*tileLayer.layerWidth + (j % tileLayer.layerWidth)];
                
                //Negative sprite index means an empty tile.
                if(spriteIndex > -1){
                    //Calculate the tile's position.
                    x = j * tileLayer.tileWidth + tileOffsetX;
                    y = i * tileLayer.tileHeight + tileOffsetY;
                    //Retrieve the sprite data needed to crop the tile from the sprite sheet.
                    sprite = spriteSheet.sprites[spriteIndex];
                    //Render the tile.
                    view.getContext().drawImage(spriteSheetImage, sprite.x, sprite.y, sprite.width, sprite.height, x, y, tileLayer.tileWidth, tileLayer.tileHeight);
                }
            }
        }
    };
    
    //=====Entities=====
    
    var spriteEntities = entitySystemManager.createAspect([Position, Sprite]);
    
    var drawEntities = (function(){
        var temporaryVector = new Vector2D(),
            viewAABB = {
                position : new Vector2D(),
                halfWidth : 0,
                halfHeight : 0
            },
            spriteAABB = {
                position : new Vector2D(),
                halfWidth : 0,
                halfHeight : 0
            },
            entitiesToRender = [];
        
        function testOverlapAABBAABB(a, b){
            if(Math.abs(a.position.x - b.position.x) > (a.halfWidth + b.halfWidth)){
                return false;
            }
            
            if(Math.abs(a.position.y - b.position.y) > (a.halfHeight + b.halfHeight)){
                return false;
            }
            
            return true;
        }
        
        //This function checks if the entity is within the view's AABB and has the sprite component set,
        //if so, it'll add the entity to the array of entities to render.
        function shouldEntityBeRendered(entity){
            var positionComponent = entity.get(Position),
                spriteComponent = entity.get(Sprite);
                
            //Don't render the entity if it doesn't have the sprite sheet handle set.
            if(!spriteComponent._spriteSheetHandle){
                return;
            }
            
            var sprite = spriteComponent._spriteSheetHandle.getAsset().sprites[spriteComponent._spriteIndex];
            
            //Calculate the sprite's AABB.
            positionComponent.getPosition(spriteAABB.position);
            spriteAABB.halfWidth = sprite.width / 2;
            spriteAABB.halfHeight = sprite.height / 2;
            
            //Check if the sprite is within view.
            if(testOverlapAABBAABB(viewAABB, spriteAABB)){
                entitiesToRender.push(entity);
            }
        }
        
        function sortEntitiesBySpriteZIndex(entityA, entityB){
            var spriteA = entityA.get(Sprite),
                spriteB = entityB.get(Sprite);
            
            //The sprite with the lower z-index should have a lower
            //index in the array, so that it gets rendered first.
            if(spriteA._zIndex < spriteB._zIndex){
                return -1;
            }else if(spriteA._zIndex > spriteB._zIndex){
                return 1;
            }else{
                return 0;
            }
        }
        
        return function(view){
            //Calculate the view's AABB.
            view.getPosition(viewAABB.position);
            viewAABB.halfWidth = view.getViewWidth() / 2;
            viewAABB.halfHeight = view.getViewHeight() / 2;
            
            //Reset the array of entities to be rendered.
            entitiesToRender.length = 0;
            
            //Fill the array of entities to render.
            spriteEntities.iterate(shouldEntityBeRendered);
            
            //Sort the entity array by the z-index of the sprite components.
            entitiesToRender.sort(sortEntitiesBySpriteZIndex);
            
            var entity, positionComponent, spriteComponent, spriteSheet, spriteSheetImage, sprite;
            
            //Draw all entities that are in view.
            for(var i=0; i<entitiesToRender.length; ++i){
                entity = entitiesToRender[i];
                positionComponent = entity.get(Position);
                spriteComponent = entity.get(Sprite);
                    
                spriteSheet = spriteComponent._spriteSheetHandle.getAsset();
                spriteSheetImage = spriteSheet.spriteSheetImageHandle.getAsset();
                sprite = spriteSheet.sprites[spriteComponent._spriteIndex];
                    
                positionComponent.getPosition(temporaryVector);
                    
                //Floor the sprite's rendering position to ensure that all sprites are rendered at integer coordinates.
                var spriteRenderingPositionX = Math.floor(temporaryVector.x - (sprite.width / 2)),
                    spriteRenderingPositionY = Math.floor(temporaryVector.y - (sprite.height / 2));
                    
                view.getContext().drawImage(spriteSheetImage,
                                            sprite.x, sprite.y,
                                            sprite.width, sprite.height,
                                            spriteRenderingPositionX, spriteRenderingPositionY,
                                            sprite.width, sprite.height);
            }
        };
    })();
    
    /**
     Renders all tile layers and entities to the specified view.
     @method update
     @param view {View} View everything will be rendered to.
     */
    this.update = function(view){
        //Reset the view transform.
        view.resetContextTransform();
        view.applyViewTransform();
        
        var layer;
        for(var i=0; i<tileLayerArray.length; ++i){
            layer = tileLayerArray[i];
            if(layer === 'sprites'){
                drawEntities(view);
            }else{
                drawTileLayer(layer, view);
            }
        }
    };
    
    /**
     @method destroy
     */
    this.destroy = function(){
        spriteEntities.destroy();
    };
}