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