Objects and Layered rendering

Fri May 02 2025 15:06:07 GMT+0100 (British Summer Time) canvasgamedevtilemaptutorialmapsjavascripthtmlarchived

Map Objects

Now we've begun making our map engine more robust, we'll look at adding fixed-position objects to the game map, and using multi-level drawing to draw objects on different layers.

Firstly, we'll be using a slightly updated tileset image to provide these new object graphics.

Tileset
Map objects and layers example

Object types

In the globals area at the top of our code, we'll add some code to handle the way in which objects can handle collision; at the moment we'll just have two object collision types, solid, which cannot be passed through, and none, which will not block characters movement.

var objectCollision = {
    none        : 0,
    solid        : 1
};

We'll now create our list of object types, objectTypes. Each entry will associate a numeric ID with a map of key/value pairs describing the object. The properties required for each object will be name, which is just a string naming the object type (we aren't making use of this yet, but it may come in handy later, and really helps identifying the object in the list); sprite, which as with previous sprites is an array of frame objects that specify the x, y position of the frame on the tileset, and w, h that specify the frame dimensions.

Objects also an offset property, which is the offset in pixels the sprite will be drawn at relative to the top-left of the tile on which the object is placed, a collision property specifying the objectCollision property of the object type, and zIndex - the layer on which the object will be drawn.

var objectTypes = {
    1 : {
        name : "Box",
        sprite : [{x:40,y:160,w:40,h:40}],
        offset : [0,0],
        collision : objectCollision.solid,
        zIndex : 1
    },
    2 : {
        name : "Broken Box",
        sprite : [{x:40,y:200,w:40,h:40}],
        offset : [0,0],
        collision : objectCollision.none,
        zIndex : 1
    },
    3 : {
        name : "Tree top",
        sprite : [{x:80,y:160,w:80,h:80}],
        offset : [-20,-20],
        collision : objectCollision.solid,
        zIndex : 3
    }
};

MapObject class

Objects on our map will be tracked by MapObject class instances. The class instances are created with one argument, the id of the object type in the objectTypes array, and also have x, y properties that will be set later.

function MapObject(nt)
{
    this.x        = 0;
    this.y        = 0;
    this.type    = nt;
}

The MapObject class has a placeAt method, which takes 2 arguments; the x, y (nx, ny) position at which the object should be placed on the map. Firstly, this method checks if the object is already placed on a map tiles. If so, it begins by removing that association:

MapObject.prototype.placeAt = function(nx, ny)
{
    if(mapTileData.map[toIndex(this.x, this.y)].object==this)
    {
        mapTileData.map[toIndex(this.x, this.y)].object = null;
    }

We can then set the x, y position of this object, and set the Tile.object property of the corresponding tile in the mapTileData global to reference this object.

    this.x = nx;
    this.y = ny;
    
    mapTileData.map[toIndex(nx, ny)].object = this;
};

Modifying existing code

We'll modify some object types and code we've created in previous lessons to accomodate our lessons. Firstly, we'll add an object property to the Tile class:

    this.object    = null;

To our TileMap object, we'll add a levels property, which will specify how many different layers we'll be drawing objects and tiles to. For now, we'll hard-code this as 4. This means we'll have layers 0 to 3. Floor tiles will be drawn on layer 0, the bottom-most layer.

    this.levels    = 4;

Blocking Character movement

We'll also update the Character.canMoveTo method to accomodate impassable objects. Before we return true from this method, but after we've completed the other checks, we'll look and see if the target tile has an object. If so, we'll see if the objectTypes entry for this object's collision is solid (objectCollision.solid). If so, the character cannot move here and we can return false.

    if(mapTileData.map[toIndex(x,y)].object!=null)
    {
        var o = mapTileData.map[toIndex(x,y)].object;
        if(objectTypes[o.type].collision==objectCollision.solid)
        {
            return false;
        }
    }

Adding example objects

We can now add some example objects to our map in the window.onload method. Somewhere after we've called the buildMapFromData method, we'll create a bunch of example objects - I won't go in to detail on this, as it should be reasonably self-evident what's begin done. Suffice to say, we create our object instances then placeAt where we want them on the map.

    var mo1 = new MapObject(1); mo1.placeAt(2, 4);
    var mo2 = new MapObject(2); mo2.placeAt(2, 3);
    
    var mo11 = new MapObject(1); mo11.placeAt(6, 4);
    var mo12 = new MapObject(2); mo12.placeAt(7, 4);
    
    var mo4 = new MapObject(3); mo4.placeAt(4, 5);
    var mo5 = new MapObject(3); mo5.placeAt(4, 8);
    var mo6 = new MapObject(3); mo6.placeAt(4, 11);
    
    var mo7 = new MapObject(3); mo7.placeAt(2, 6);
    var mo8 = new MapObject(3); mo8.placeAt(2, 9);
    var mo9 = new MapObject(3); mo9.placeAt(2, 12);

Layered drawing method

We'll now modify the drawGame method - particularly the nested drawing loops. Firstly, before these loops begin we'll add an additional loop, which will iterate over our TileMap's levels.

    for(var z = 0; z < mapTileData.levels; z++)
    {

Our code for drawing floor tiles will remain unchanged, but we'll surround it with an if statement so that floor tiles are only drawn on level 0:

            if(z==0)
            {
            var tile = tileTypes[mapTileData.map[toIndex(x,y)].type];

            var sprite = getFrame(tile.sprite, tile.spriteDuration,
                gameTime, tile.animated);
            ctx.drawImage(tileset,
                sprite.x, sprite.y, sprite.w, sprite.h,
                viewport.offset[0] + (x*tileW), viewport.offset[1] + (y*tileH),
                tileW, tileH);
            }

We'll also check if there's an object on the current tile. If so, and if the corresponding objectTypes entry's zIndex is equal to the level we're currently drawing, we'll draw the sprite for this object.

            var o = mapTileData.map[toIndex(x,y)].object;
            if(o!=null && objectTypes[o.type].zIndex==z)
            {
                var ot = objectTypes[o.type];
                 
                ctx.drawImage(tileset,
                    ot.sprite[0].x, ot.sprite[0].y,
                    ot.sprite[0].w, ot.sprite[0].h,
                    viewport.offset[0] + (x*tileW) + ot.offset[0],
                    viewport.offset[1] + (y*tileH) + ot.offset[1],
                    ot.sprite[0].w, ot.sprite[0].h);
            }

We also want to modify our roof drawing code a bit - the if statement, that checks if the roofType is not 0, and checks the player is not currently under the roof, will now also check that the current z loop value is 2 - this is the layer we'll draw our roofs on in our example:

            if(z==2 &&
                mapTileData.map[toIndex(x,y)].roofType!=0 &&
                mapTileData.map[toIndex(x,y)].roof!=playerRoof1 &&
                mapTileData.map[toIndex(x,y)].roof!=playerRoof2)

Finally, after our nested y, x drawing loops, but before we end the z loop, we'll draw our player character if the current level is 1:

        if(z==1)
        {
            var sprite = player.sprites[player.direction];
            ctx.drawImage(tileset,
                sprite[0].x, sprite[0].y,
                sprite[0].w, sprite[0].h,
                viewport.offset[0] + player.position[0],
                viewport.offset[1] + player.position[1],
                player.dimensions[0], player.dimensions[1]);
        }
    
    }

We draw the player here so that higher level objects, such as our tree, are always drawn above the Character. Try adding some more custom objects at different levels on the map to get a good understanding of this concept!

Modified source code

<!DOCTYPE html>
<html>
<head>

<script type="text/javascript">
var ctx = null;
var gameMap = [
    0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1,
1, 1, 1, 1, 0,
    0, 2, 2, 2, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 2, 1, 0, 0, 0, 0, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 4, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
    0, 1, 1, 2, 2, 2, 2, 2, 0, 4, 4, 4, 1, 1, 1, 0, 2, 2, 2, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 1, 1, 4, 1, 1, 1, 0, 2, 2, 2, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 1, 1, 4, 1, 1, 1, 0, 2, 2, 2, 0,
    0, 1, 1, 2, 1, 0, 0, 0, 0, 1, 1, 4, 1, 1, 0, 0, 0, 2, 0, 0,
    0, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 2, 2, 2, 2, 2, 2, 1, 4, 4, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 4, 1, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
];
var mapTileData = new TileMap();

var roofList = [
    { x:5, y:3, w:4, h:7, data: [
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11
    ]},
    { x:15, y:5, w:5, h:4, data: [
        10, 10, 11, 11, 11,
        10, 10, 11, 11, 11,
        10, 10, 11, 11, 11,
        10, 10, 11, 11, 11
    ]},
    { x:14, y:9, w:6, h:7, data: [
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11
    ]}
];

var tileW = 40, tileH = 40;
var mapW = 20, mapH = 20;
var currentSecond = 0, frameCount = 0, framesLastSecond = 0, lastFrameTime = 0;

var tileset = null, tilesetURL = "tileset.jpg", tilesetLoaded = false;

var gameTime = 0;
var gameSpeeds = [
    {name:"Normal", mult:1},
    {name:"Slow", mult:0.3},
    {name:"Fast", mult:3},
    {name:"Paused", mult:0}
];
var currentSpeed = 0;

var objectCollision = {
    none        : 0,
    solid        : 1
};
var objectTypes = {
    1 : {
        name : "Box",
        sprite : [{x:40,y:160,w:40,h:40}],
        offset : [0,0],
        collision : objectCollision.solid,
        zIndex : 1
    },
    2 : {
        name : "Broken Box",
        sprite : [{x:40,y:200,w:40,h:40}],
        offset : [0,0],
        collision : objectCollision.none,
        zIndex : 1
    },
    3 : {
        name : "Tree top",
        sprite : [{x:80,y:160,w:80,h:80}],
        offset : [-20,-20],
        collision : objectCollision.solid,
        zIndex : 3
    }
};
function MapObject(nt)
{
    this.x        = 0;
    this.y        = 0;
    this.type    = nt;
}
MapObject.prototype.placeAt = function(nx, ny)
{
    if(mapTileData.map[toIndex(this.x, this.y)].object==this)
    {
        mapTileData.map[toIndex(this.x, this.y)].object = null;
    }
    
    this.x = nx;
    this.y = ny;
    
    mapTileData.map[toIndex(nx, ny)].object = this;
};

var floorTypes = {
    solid    : 0,
    path    : 1,
    water    : 2,
    ice        : 3,
    conveyorU    : 4,
    conveyorD    : 5,
    conveyorL    : 6,
    conveyorR    : 7,
    grass        : 8
};
var tileTypes = {
    0 : { colour:"#685b48", floor:floorTypes.solid, sprite:[{x:0,y:0,w:40,h:40}]    },
    1 : { colour:"#5aa457", floor:floorTypes.grass,    sprite:[{x:40,y:0,w:40,h:40}]    },
    2 : { colour:"#e8bd7a", floor:floorTypes.path,    sprite:[{x:80,y:0,w:40,h:40}]    },
    3 : { colour:"#286625", floor:floorTypes.solid,    sprite:[{x:120,y:0,w:40,h:40}]    },
    4 : { colour:"#678fd9", floor:floorTypes.water,    sprite:[
            {x:160,y:0,w:40,h:40,d:200}, {x:200,y:0,w:40,h:40,d:200},
            {x:160,y:40,w:40,h:40,d:200}, {x:200,y:40,w:40,h:40,d:200},
            {x:160,y:40,w:40,h:40,d:200}, {x:200,y:0,w:40,h:40,d:200}
        ]},
    5 : { colour:"#eeeeff", floor:floorTypes.ice,    sprite:[{x:120,y:120,w:40,h:40}]},
    6 : { colour:"#cccccc", floor:floorTypes.conveyorL,    sprite:[
            {x:0,y:40,w:40,h:40,d:200}, {x:40,y:40,w:40,h:40,d:200},
            {x:80,y:40,w:40,h:40,d:200}, {x:120,y:40,w:40,h:40,d:200}
        ]},
    7 : { colour:"#cccccc", floor:floorTypes.conveyorR,    sprite:[
            {x:120,y:80,w:40,h:40,d:200}, {x:80,y:80,w:40,h:40,d:200},
            {x:40,y:80,w:40,h:40,d:200}, {x:0,y:80,w:40,h:40,d:200}
        ]},
    8 : { colour:"#cccccc", floor:floorTypes.conveyorD,    sprite:[
            {x:160,y:200,w:40,h:40,d:200}, {x:160,y:160,w:40,h:40,d:200},
            {x:160,y:120,w:40,h:40,d:200}, {x:160,y:80,w:40,h:40,d:200}
        ]},
    9 : { colour:"#cccccc", floor:floorTypes.conveyorU,    sprite:[
            {x:200,y:80,w:40,h:40,d:200}, {x:200,y:120,w:40,h:40,d:200},
            {x:200,y:160,w:40,h:40,d:200}, {x:200,y:200,w:40,h:40,d:200}
        ]},
    
    10 : { colour:"#ccaa00", floor:floorTypes.solid, sprite:[{x:40,y:120,w:40,h:40}]},
    11 : { colour:"#ccaa00", floor:floorTypes.solid, sprite:[{x:80,y:120,w:40,h:40}]}
};

function Tile(tx, ty, tt)
{
    this.x            = tx;
    this.y            = ty;
    this.type        = tt;
    this.roof        = null;
    this.roofType    = 0;
    this.eventEnter    = null;
    this.object        = null;
}

function TileMap()
{
    this.map    = [];
    this.w        = 0;
    this.h        = 0;
    this.levels    = 4;
}
TileMap.prototype.buildMapFromData = function(d, w, h)
{
    this.w        = w;
    this.h        = h;
    
    if(d.length!=(w*h)) { return false; }
    
    this.map.length    = 0;
    
    for(var y = 0; y < h; y++)
    {
        for(var x = 0; x < w; x++)
        {
            this.map.push( new Tile(x, y, d[((y*w)+x)]) );
        }
    }
    
    return true;
};
TileMap.prototype.addRoofs = function(roofs)
{
    for(var i in roofs)
    {
        var r = roofs[i];
        
        if(r.x < 0 || r.y < 0 || r.x >= this.w || r.y >= this.h ||
            (r.x+r.w)>this.w || (r.y+r.h)>this.h ||
            r.data.length!=(r.w*r.h))
        {
            continue;
        }
        
        for(var y = 0; y < r.h; y++)
        {
            for(var x = 0; x < r.w; x++)
            {
                var tileIdx = (((r.y+y)*this.w)+r.x+x);
                
                this.map[tileIdx].roof = r;
                this.map[tileIdx].roofType = r.data[((y*r.w)+x)];
            }
        }
    }
};

var directions = {
    up        : 0,
    right    : 1,
    down    : 2,
    left    : 3
};

var keysDown = {
    37 : false,
    38 : false,
    39 : false,
    40 : false
};

var viewport = {
    screen        : [0,0],
    startTile    : [0,0],
    endTile        : [0,0],
    offset        : [0,0],
    update        : function(px, py) {
        this.offset[0] = Math.floor((this.screen[0]/2) - px);
        this.offset[1] = Math.floor((this.screen[1]/2) - py);

        var tile = [ Math.floor(px/tileW), Math.floor(py/tileH) ];

        this.startTile[0] = tile[0] - 1 - Math.ceil((this.screen[0]/2) / tileW);
        this.startTile[1] = tile[1] - 1 - Math.ceil((this.screen[1]/2) / tileH);

        if(this.startTile[0] < 0) { this.startTile[0] = 0; }
        if(this.startTile[1] < 0) { this.startTile[1] = 0; }

        this.endTile[0] = tile[0] + 1 + Math.ceil((this.screen[0]/2) / tileW);
        this.endTile[1] = tile[1] + 1 + Math.ceil((this.screen[1]/2) / tileH);

        if(this.endTile[0] >= mapW) { this.endTile[0] = mapW-1; }
        if(this.endTile[1] >= mapH) { this.endTile[1] = mapH-1; }
    }
};

var player = new Character();

function Character()
{
    this.tileFrom    = [1,1];
    this.tileTo        = [1,1];
    this.timeMoved    = 0;
    this.dimensions    = [30,30];
    this.position    = [45,45];

    this.delayMove    = {};
    this.delayMove[floorTypes.path]            = 400;
    this.delayMove[floorTypes.grass]        = 800;
    this.delayMove[floorTypes.ice]            = 300;
    this.delayMove[floorTypes.conveyorU]    = 200;
    this.delayMove[floorTypes.conveyorD]    = 200;
    this.delayMove[floorTypes.conveyorL]    = 200;
    this.delayMove[floorTypes.conveyorR]    = 200;

    this.direction    = directions.up;
    this.sprites = {};
    this.sprites[directions.up]        = [{x:0,y:120,w:30,h:30}];
    this.sprites[directions.right]    = [{x:0,y:150,w:30,h:30}];
    this.sprites[directions.down]    = [{x:0,y:180,w:30,h:30}];
    this.sprites[directions.left]    = [{x:0,y:210,w:30,h:30}];
}
Character.prototype.placeAt = function(x, y)
{
    this.tileFrom    = [x,y];
    this.tileTo        = [x,y];
    this.position    = [((tileW*x)+((tileW-this.dimensions[0])/2)),
        ((tileH*y)+((tileH-this.dimensions[1])/2))];
};
Character.prototype.processMovement = function(t)
{
    if(this.tileFrom[0]==this.tileTo[0] && this.tileFrom[1]==this.tileTo[1]) { return false; }

    var moveSpeed = this.delayMove[tileTypes[mapTileData.map[toIndex(this.tileFrom[0],this.tileFrom[1])].type].floor];

    if((t-this.timeMoved)>=moveSpeed)
    {
        this.placeAt(this.tileTo[0], this.tileTo[1]);

        if(mapTileData.map[toIndex(this.tileTo[0], this.tileTo[1])].eventEnter!=null)
        {
            mapTileData.map[toIndex(this.tileTo[0], this.tileTo[1])].eventEnter(this);
        }

        var tileFloor = tileTypes[mapTileData.map[toIndex(this.tileFrom[0], this.tileFrom[1])].type].floor;

        if(tileFloor==floorTypes.ice)
        {
            if(this.canMoveDirection(this.direction))
            {
                this.moveDirection(this.direction, t);
            }
        }
        else if(tileFloor==floorTypes.conveyorL && this.canMoveLeft())    { this.moveLeft(t); }
        else if(tileFloor==floorTypes.conveyorR && this.canMoveRight()) { this.moveRight(t); }
        else if(tileFloor==floorTypes.conveyorU && this.canMoveUp())    { this.moveUp(t); }
        else if(tileFloor==floorTypes.conveyorD && this.canMoveDown())    { this.moveDown(t); }
    }
    else
    {
        this.position[0] = (this.tileFrom[0] * tileW) + ((tileW-this.dimensions[0])/
2);
        this.position[1] = (this.tileFrom[1] * tileH) + ((tileH-this.dimensions[1])/2);

        if(this.tileTo[0] != this.tileFrom[0])
        {
            var diff = (tileW / moveSpeed) * (t-this.timeMoved);
            this.position[0]+= (this.tileTo[0]<this.tileFrom[0] ? 0 - diff : diff);
        }
        if(this.tileTo[1] != this.tileFrom[1])
        {
            var diff = (tileH / moveSpeed) * (t-this.timeMoved);
            this.position[1]+= (this.tileTo[1]<this.tileFrom[1] ? 0 - diff : diff);
        }

        this.position[0] = Math.round(this.position[0]);
        this.position[1] = Math.round(this.position[1]);
    }

    return true;
}
Character.prototype.canMoveTo = function(x, y)
{
    if(x < 0 || x >= mapW || y < 0 || y >= mapH) { return false; }
    if(typeof this.delayMove[tileTypes[mapTileData.map[toIndex(x,y)].type].floor]=='undefined') { return false; }
    if(mapTileData.map[toIndex(x,y)].object!=null)
    {
        var o = mapTileData.map[toIndex(x,y)].object;
        if(objectTypes[o.type].collision==objectCollision.solid)
        {
            return false;
        }
    }
    return true;
};
Character.prototype.canMoveUp        = function() { return this.canMoveTo(this.tileFrom[0], this.tileFrom[1]-1); };
Character.prototype.canMoveDown     = function() { return this.canMoveTo(this.tileFrom[0], this.tileFrom[1]+1); };
Character.prototype.canMoveLeft     = function() { return this.canMoveTo(this.tileFrom[0]-1, this.tileFrom[1]); };
Character.prototype.canMoveRight     = function() { return this.canMoveTo(this.tileFrom[0]+1, this.tileFrom[1]); };
Character.prototype.canMoveDirection = function(d) {
    switch(d)
    {
        case directions.up:
            return this.canMoveUp();
        case directions.down:
            return this.canMoveDown();
        case directions.left:
            return this.canMoveLeft();
        default:
            return this.canMoveRight();
    }
};

Character.prototype.moveLeft    = function(t) { this.tileTo[0]-=1; this.timeMoved = t; this.direction = directions.left; };
Character.prototype.moveRight    = function(t) { this.tileTo[0]+=1; this.timeMoved = t; this.direction = directions.right; };
Character.prototype.moveUp        = function(t) { this.tileTo[1]-=1; this.timeMoved = t; this.direction = directions.up; };
Character.prototype.moveDown    = function(t) { this.tileTo[1]+=1; this.timeMoved = t; this.direction = directions.down; };
Character.prototype.moveDirection = function(d, t) {
    switch(d)
    {
        case directions.up:
            return this.moveUp(t);
        case directions.down:
            return this.moveDown(t);
        case directions.left:
            return this.moveLeft(t);
        default:
            return this.moveRight(t);
    }
};

function toIndex(x, y)
{
    return((y * mapW) + x);
}

function getFrame(sprite, duration, time, animated)
{
    if(!animated) { return sprite[0]; }
    time = time % duration;

    for(x in sprite)
    {
        if(sprite[x].end>=time) { return sprite[x]; }
    }
}

window.onload = function()
{
    ctx = document.getElementById('game').getContext("2d");
    requestAnimationFrame(drawGame);
    ctx.font = "bold 10pt sans-serif";

    window.addEventListener("keydown", function(e) {
        if(e.keyCode>=37 && e.keyCode<=40) { keysDown[e.keyCode] = true; }
    });
    window.addEventListener("keyup", function(e) {
        if(e.keyCode>=37 && e.keyCode<=40) { keysDown[e.keyCode] = false; }
        if(e.keyCode==83)
        {
            currentSpeed = (currentSpeed>=(gameSpeeds.length-1) ? 0 : currentSpeed+1);
        }
    });

    viewport.screen = [document.getElementById('game').width,
        document.getElementById('game').height];

    tileset = new Image();
    tileset.onerror = function()
    {
        ctx = null;
        alert("Failed loading tileset.");
    };
    tileset.onload = function() { tilesetLoaded = true; };
    tileset.src = tilesetURL;

    for(x in tileTypes)
    {
        tileTypes[x]['animated'] = tileTypes[x].sprite.length > 1 ? true : false;

        if(tileTypes[x].animated)
        {
            var t = 0;
            
            for(s in tileTypes[x].sprite)
            {
                tileTypes[x].sprite[s]['start'] = t;
                t+= tileTypes[x].sprite[s].d;
                tileTypes[x].sprite[s]['end'] = t;
            }

            tileTypes[x]['spriteDuration'] = t;
        }
    }
    
    mapTileData.buildMapFromData(gameMap, mapW, mapH);
    mapTileData.addRoofs(roofList);
    mapTileData.map[((2*mapW)+2)].eventEnter = function()
        { console.log("Entered tile 2,2"); };
    
    var mo1 = new MapObject(1); mo1.placeAt(2, 4);
    var mo2 = new MapObject(2); mo2.placeAt(2, 3);
    
    var mo11 = new MapObject(1); mo11.placeAt(6, 4);
    var mo12 = new MapObject(2); mo12.placeAt(7, 4);
    
    var mo4 = new MapObject(3); mo4.placeAt(4, 5);
    var mo5 = new MapObject(3); mo5.placeAt(4, 8);
    var mo6 = new MapObject(3); mo6.placeAt(4, 11);
    
    var mo7 = new MapObject(3); mo7.placeAt(2, 6);
    var mo8 = new MapObject(3); mo8.placeAt(2, 9);
    var mo9 = new MapObject(3); mo9.placeAt(2, 12);
};

function drawGame()
{
    if(ctx==null) { return; }
    if(!tilesetLoaded) { requestAnimationFrame(drawGame); return; }

    var currentFrameTime = Date.now();
    var timeElapsed = currentFrameTime - lastFrameTime;
    gameTime+= Math.floor(timeElapsed * gameSpeeds[currentSpeed].mult);

    var sec = Math.floor(Date.now()/1000);
    if(sec!=currentSecond)
    {
        currentSecond = sec;
        framesLastSecond = frameCount;
        frameCount = 1;
    }
    else { frameCount++; }

    if(!player.processMovement(gameTime) && gameSpeeds[currentSpeed].mult!=0)
    {
        if(keysDown[38] && player.canMoveUp())            { player.moveUp(gameTime); }
        else if(keysDown[40] && player.canMoveDown())    { player.moveDown(gameTime); }
        else if(keysDown[37] && player.canMoveLeft())    { player.moveLeft(gameTime); }
        else if(keysDown[39] && player.canMoveRight())    { player.moveRight(gameTime); }
    }

    viewport.update(player.position[0] + (player.dimensions[0]/2),
        player.position[1] + (player.dimensions[1]/2));
    
    var playerRoof1 = mapTileData.map[toIndex(
        player.tileFrom[0], player.tileFrom[1])].roof;
    var playerRoof2 = mapTileData.map[toIndex(
        player.tileTo[0], player.tileTo[1])].roof;

    ctx.fillStyle = "#000000";
    ctx.fillRect(0, 0, viewport.screen[0], viewport.screen[1]);
    
    for(var z = 0; z < mapTileData.levels; z++)
    {

    for(var y = viewport.startTile[1]; y <= viewport.endTile[1]; ++y)
    {
        for(var x = viewport.startTile[0]; x <= viewport.endTile[0]; ++x)
        {
            if(z==0)
            {
            var tile = tileTypes[mapTileData.map[toIndex(x,y)].type];

            var sprite = getFrame(tile.sprite, tile.spriteDuration,
                gameTime, tile.animated);
            ctx.drawImage(tileset,
                sprite.x, sprite.y, sprite.w, sprite.h,
                viewport.offset[0] + (x*tileW), viewport.offset[1] + (y*tileH),
                tileW, tileH);
            }
            
            var o = mapTileData.map[toIndex(x,y)].object;
            if(o!=null && objectTypes[o.type].zIndex==z)
            {
                var ot = objectTypes[o.type];
                 
                ctx.drawImage(tileset,
                    ot.sprite[0].x, ot.sprite[0].y,
                    ot.sprite[0].w, ot.sprite[0].h,
                    viewport.offset[0] + (x*tileW) + ot.offset[0],
                    viewport.offset[1] + (y*tileH) + ot.offset[1],
                    ot.sprite[0].w, ot.sprite[0].h);
            }
            
            if(z==2 &&
                mapTileData.map[toIndex(x,y)].roofType!=0 &&
                mapTileData.map[toIndex(x,y)].roof!=playerRoof1 &&
                mapTileData.map[toIndex(x,y)].roof!=playerRoof2)
            {
                tile = tileTypes[mapTileData.map[toIndex(x,y)].roofType];
                sprite = getFrame(tile.sprite, tile.spriteDuration,
                    gameTime, tile.animated);
                ctx.drawImage(tileset,
                    sprite.x, sprite.y, sprite.w, sprite.h,
                    viewport.offset[0] + (x*tileW),
                    viewport.offset[1] + (y*tileH),
                    tileW, tileH);
            }
        }
    }
    
        if(z==1)
        {
            var sprite = player.sprites[player.direction];
            ctx.drawImage(tileset,
                sprite[0].x, sprite[0].y,
                sprite[0].w, sprite[0].h,
                viewport.offset[0] + player.position[0],
                viewport.offset[1] + player.position[1],
                player.dimensions[0], player.dimensions[1]);
        }
    
    }

    ctx.fillStyle = "#ff0000";
    ctx.fillText("FPS: " + framesLastSecond, 10, 20);
    ctx.fillText("Game speed: " + gameSpeeds[currentSpeed].name, 10, 40);

    lastFrameTime = currentFrameTime;
    requestAnimationFrame(drawGame);
}
</script>

</head>
<body>

<canvas id="game" width="400" height="400"></canvas>

</body>
</html>

Objects and Layers source code

<!DOCTYPE html>
<html>
<head>

<script type="text/javascript">
var ctx = null;
var gameMap = [
    0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 2, 2, 2, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 2, 1, 0, 0, 0, 0, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 4, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
    0, 1, 1, 2, 2, 2, 2, 2, 0, 4, 4, 4, 1, 1, 1, 0, 2, 2, 2, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 1, 1, 4, 1, 1, 1, 0, 2, 2, 2, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 1, 1, 4, 1, 1, 1, 0, 2, 2, 2, 0,
    0, 1, 1, 2, 1, 0, 0, 0, 0, 1, 1, 4, 1, 1,
 0, 0, 0, 2, 0, 0,
    0, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 2, 2, 2, 2, 2, 2, 1, 4, 4, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 4, 1, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
];
var mapTileData = new TileMap();

var roofList = [
    { x:5, y:3, w:4, h:7, data: [
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11
    ]},
    { x:15, y:5, w:5, h:4, data: [
        10, 10, 11, 11, 11,
        10, 10, 11, 11, 11,
        10, 10, 11, 11, 11,
        10, 10, 11, 11, 11
    ]},
    { x:14, y:9, w:6, h:7, data: [
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11
    ]}
];

var tileW = 40, tileH = 40;
var mapW = 20, mapH = 20;
var currentSecond = 0, frameCount = 0, framesLastSecond = 0, lastFrameTime = 0;

var tileset = null, tilesetURL = "tileset.jpg", tilesetLoaded = false;

var gameTime = 0;
var gameSpeeds = [
    {name:"Normal", mult:1},
    {name:"Slow", mult:0.3},
    {name:"Fast", mult:3},
    {name:"Paused", mult:0}
];
var currentSpeed = 0;

var objectCollision = {
    none        : 0,
    solid        : 1
};
var objectTypes = {
    1 : {
        name : "Box",
        sprite : [{x:40,y:160,w:40,h:40}],
        offset : [0,0],
        collision : objectCollision.solid,
        zIndex : 1
    },
    2 : {
        name : "Broken Box",
        sprite : [{x:40,y:200,w:40,h:40}],
        offset : [0,0],
        collision : objectCollision.none,
        zIndex : 1
    },
    3 : {
        name : "Tree top",
        sprite : [{x:80,y:160,w:80,h:80}],
        offset : [-20,-20],
        collision : objectCollision.solid,
        zIndex : 3
    }
};
function MapObject(nt)
{
    this.x        = 0;
    this.y        = 0;
    this.type    = nt;
}
MapObject.prototype.placeAt = function(nx, ny)
{
    if(mapTileData.map[toIndex(this.x, this.y)].object==this)
    {
        mapTileData.map[toIndex(this.x, this.y)].object = null;
    }
    
    this.x = nx;
    this.y = ny;
    
    mapTileData.map[toIndex(nx, ny)].object = this;
};

var floorTypes = {
    solid    : 0,
    path    : 1,
    water    : 2,
    ice        : 3,
    conveyorU    : 4,
    conveyorD    : 5,
    conveyorL    : 6,
    conveyorR    : 7,
    grass        : 8
};
var tileTypes = {
    0 : { colour:"#685b48", floor:floorTypes.solid, sprite:[{x:0,y:0,w:40,h:40}]    },
    1 : { colour:"#5aa457", floor:floorTypes.grass,    sprite:[{x:40,y:0,w:40,h:40}]    },
    2 : { colour:"#e8bd7a", floor:floorTypes.path,    sprite:[{x:80,y:0,w:40,h:40}]    },
    3 : { colour:"#286625", floor:floorTypes.solid,    sprite:[{x:120,y:0,w:40,h:40}]    },
    4 : { colour:"#678fd9", floor:floorTypes.water,    sprite:[
            {x:160,y:0,w:40,h:40,d:200}, {x:200,y:0,w:40,h:40,d:200},
            {x:160,y:40,w:40,h:40,d:200}, {x:200,y:40,w:40,h:40,d:200},
            {x:160,y:40,w:40,h:40,d:200}, {x:200,y:0,w:40,h:40,d:200}
        ]},
    5 : { colour:"#eeeeff", floor:floorTypes.ice,    sprite:[{x:120,y:120,w:40,h:40}]},
    6 : { colour:"#cccccc", floor:floorTypes.conveyorL,    sprite:[
            {x:0,y:40,w:40,h:40,d:200}, {x:40,y:40,w:40,h:40,d:200},
            {x:80,y:40,w:40,h:40,d:200}, {x:120,y:40,w:40,h:40,d:200}
        ]},
    7 : { colour:"#cccccc", floor:floorTypes.conveyorR,    sprite:[
            {x:120,y:80,w:40,h:40,d:200}, {x:80,y:80,w:40,h:40,d:200},
            {x:40,y:80,w:40,h:40,d:200}, {x:0,y:80,w:40,h:40,d:200}
        ]},
    8 : { colour:"#cccccc", floor:floorTypes.conveyorD,    sprite:[
            {x:160,y:200,w:40,h:40,d:200}, {x:160,y:160,w:40,h:40,d:200},
            {x:160,y:120,w:40,h:40,d:200}, {x:160,y:80,w:40,h:40,d:200}
        ]},
    9 : { colour:"#cccccc", floor:floorTypes.conveyorU,    sprite:[
            {x:200,y:80,w:40,h:40,d:200}, {x:200,y:120,w:40,h:40,d:200},
            {x:200,y:160,w:40,h:40,d:200}, {x:200,y:200,w:40,h:40,d:200}
        ]},
    
    10 : { colour:"#ccaa00", floor:floorTypes.solid, sprite:[{x:40,y:120,w:40,h:40}]},
    11 : { colour:"#ccaa00", floor:floorTypes.solid, sprite:[{x:80,y:120,w:40,h:40}]}
};

function Tile(tx, ty, tt)
{
    this.x            = tx;
    this.y            = ty;
    this.type        = tt;
    this.roof        = null;
    this.roofType    = 0;
    this.eventEnter    = null;
    this.object        = null;
}

function TileMap()
{
    this.map    = [];
    this.w        = 0;
    this.h        = 0;
    this.levels    = 4;
}
TileMap.prototype.buildMapFromData = function(d, w, h)
{
    this.w        = w;
    this.h        = h;
    
    if(d.length!=(w*h)) { return false; }
    
    this.map.length    = 0;
    
    for(var y = 0; y < h; y++)
    {
        for(var x = 0; x < w; x++)
        {
            this.map.push( new Tile(x, y, d[((y*w)+x)]) );
        }
    }
    
    return true;
};
TileMap.prototype.addRoofs = function(roofs)
{
    for(var i in roofs)
    {
        var r = roofs[i];
        
        if(r.x < 0 || r.y < 0 || r.x >= this.w || r.y >= this.h ||
            (r.x+r.w)>this.w || (r.y+r.h)>this.h ||
            r.data.length!=(r.w*r.h))
        {
            continue;
        }
        
        for(var y = 0; y < r.h; y++)
        {
            for(var x = 0; x < r.w; x++)
            {
                var tileIdx = (((r.y+y)*this.w)+r.x+x);
                
                this.map[tileIdx].roof = r;
                this.map[tileIdx].roofType = r.data[((y*r.w)+x)];
            }
        }
    }
};

var directions = {
    up        : 0,
    right    : 1,
    down    : 2,
    left    : 3
};

var keysDown = {
    37 : false,
    38 : false,
    39 : false,
    40 : false
};

var viewport = {
    screen        : [0,0],
    startTile    : [0,0],
    endTile        : [0,0],
    offset        : [0,0],
    update        : function(px, py) {
        this.offset[0] = Math.floor((this.screen[0]/2) - px);
        this.offset[1] = Math.floor((this.screen[1]/2) - py);

        var tile = [ Math.floor(px/tileW), Math.floor(py/tileH) ];

        this.startTile[0] = tile[0] - 1 - Math.ceil((this.screen[0]/2) / tileW);
        this.startTile[1] = tile[1] - 1 - Math.ceil((this.screen[1]/2) / tileH);

        if(this.startTile[0] < 0) { this.startTile[0] = 0; }
        if(this.startTile[1] < 0) { this.startTile[1] = 0; }

        this.endTile[0] = tile[0] + 1 + Math.ceil((this.screen[0]/2) / tileW);
        this.endTile[1] = tile[1] + 1 + Math.ceil((this.screen[1]/2) / tileH);

        if(this.endTile[0] >= mapW) { this.endTile[0] = mapW-1; }
        if(this.endTile[1] >= mapH) { this.endTile[1] = mapH-1; }
    }
};

var player = new Character();

function Character()
{
    this.tileFrom    = [1,1];
    this.tileTo        = [1,1];
    this.timeMoved    = 0;
    this.dimensions    = [30,30];
    this.position    = [45,45];

    this.delayMove    = {};
    this.delayMove[floorTypes.path]            = 400;
    this.delayMove[floorTypes.grass]        = 800;
    this.delayMove[floorTypes.ice]            = 300;
    this.delayMove[floorTypes.conveyorU]    = 200;
    this.delayMove[floorTypes.conveyorD]    = 200;
    this.delayMove[floorTypes.conveyorL]    = 200;
    this.delayMove[floorTypes.conveyorR]    = 200;

    this.direction    = directions.up;
    this.sprites = {};
    this.sprites[directions.up]        = [{x:0,y:120,w:30,h:30}];
    this.sprites[directions.right]    = [{x:0,y:150,w:30,h:30}];
    this.sprites[directions.down]    = [{x:0,y:180,w:30,h:30}];
    this.sprites[directions.left]    = [{x:0,y:210,w:30,h:30}];
}
Character.prototype.placeAt = function(x, y)
{
    this.tileFrom    = [x,y];
    this.tileTo        = [x,y];
    this.position    = [((tileW*x)+((tileW-this.dimensions[0])/2)),
        ((tileH*y)+((tileH-this.dimensions[1])/2))];
};
Character.prototype.processMovement = function(t)
{
    if(this.tileFrom[0]==this.tileTo[0] && this.tileFrom[1]==this.tileTo[1]) { return false; }

    var moveSpeed = this.delayMove[tileTypes[mapTileData.map[toIndex(this.tileFrom[0],this.tileFrom[1])].type].floor];

    if((t-this.timeMoved)>=moveSpeed)
    {
        this.placeAt(this.tileTo[0], this.tileTo[1]);

        if(mapTileData.map[toIndex(this.tileTo[0], this.tileTo[1])].eventEnter!=null)
        {
            mapTileData.map[toIndex(this.tileTo[0], this.tileTo[1])].eventEnter(this);
        }

        var tileFloor = tileTypes[mapTileData.map[toIndex(this.tileFrom[0], this.tileFrom[1])].type].floor;

        if(tileFloor==floorTypes.ice)
        {
            if(this.canMoveDirection(this.direction))
            {
                this.moveDirection(this.direction, t);
            }
        }
        else if(tileFloor==floorTypes.conveyorL && this.canMoveLeft())    { this.moveLeft(t); }
        else if(tileFloor==floorTypes.conveyorR && this.canMoveRight()) { this.moveRight(t); }
        else if(tileFloor==floorTypes.conveyorU && this.canMoveUp())    { this.moveUp(t); }
        else if(tileFloor==floorTypes.conveyorD && this.canMoveDown())    { this.moveDown(t); }
    }
    else
    {
        this.position[0] = (this.tileFrom[0] * tileW) + ((tileW-this.dimensions[0])/2);
        this.position[1] = (this.tileFrom[1] * tileH) + ((tileH-this.dimensions[1])/2);

        if(this.tileTo[0] != this.tileFrom[0])
        {
            var diff = (tileW / moveSpeed) * (t-this.timeMoved);
            this.position[0]+= (this.tileTo[0]<this.tileFrom[0] ? 0 - diff : diff);
        }
        if(this.tileTo[1] != this.tileFrom[1])
        {
            var diff = (tileH / moveSpeed) * (t-this.timeMoved);
            this.position[1]+= (this.tileTo[1]<this.tileFrom[1] ? 0 - diff : diff);
        }

        this.position[0] = Math.round(this.position[0]);
        this.position[1] = Math.round(this.position[1]);
    }

    return
true;
}
Character.prototype.canMoveTo = function(x, y)
{
    if(x < 0 || x >= mapW || y < 0 || y >= mapH) { return false; }
    if(typeof this.delayMove[tileTypes[mapTileData.map[toIndex(x,y)].type].floor]=='undefined') { return false; }
    if(mapTileData.map[toIndex(x,y)].object!=null)
    {
        var o = mapTileData.map[toIndex(x,y)].object;
        if(objectTypes[o.type].collision==objectCollision.solid)
        {
            return false;
        }
    }
    return true;
};
Character.prototype.canMoveUp        = function() { return this.canMoveTo(this.tileFrom[0], this.tileFrom[1]-1); };
Character.prototype.canMoveDown     = function() { return this.canMoveTo(this.tileFrom[0], this.tileFrom[1]+1); };
Character.prototype.canMoveLeft     = function() { return this.canMoveTo(this.tileFrom[0]-1, this.tileFrom[1]); };
Character.prototype.canMoveRight     = function() { return this.canMoveTo(this.tileFrom[0]+1, this.tileFrom[1]); };
Character.prototype.canMoveDirection = function(d) {
    switch(d)
    {
        case directions.up:
            return this.canMoveUp();
        case directions.down:
            return this.canMoveDown();
        case directions.left:
            return this.canMoveLeft();
        default:
            return this.canMoveRight();
    }
};

Character.prototype.moveLeft    = function(t) { this.tileTo[0]-=1; this.timeMoved = t; this.direction = directions.left; };
Character.prototype.moveRight    = function(t) { this.tileTo[0]+=1; this.timeMoved = t; this.direction = directions.right; };
Character.prototype.moveUp        = function(t) { this.tileTo[1]-=1; this.timeMoved = t; this.direction = directions.up; };
Character.prototype.moveDown    = function(t) { this.tileTo[1]+=1; this.timeMoved = t; this.direction = directions.down; };
Character.prototype.moveDirection = function(d, t) {
    switch(d)
    {
        case directions.up:
            return this.moveUp(t);
        case directions.down:
            return this.moveDown(t);
        case directions.left:
            return this.moveLeft(t);
        default:
            return this.moveRight(t);
    }
};

function toIndex(x, y)
{
    return((y * mapW) + x);
}

function getFrame(sprite, duration, time, animated)
{
    if(!animated) { return sprite[0]; }
    time = time % duration;

    for(x in sprite)
    {
        if(sprite[x].end>=time) { return sprite[x]; }
    }
}

window.onload = function()
{
    ctx = document.getElementById('game').getContext("2d");
    requestAnimationFrame(drawGame);
    ctx.font = "bold 10pt sans-serif";

    window.addEventListener("keydown", function(e) {
        if(e.keyCode>=37 && e.keyCode<=40) { keysDown[e.keyCode] = true; }
    });
    window.addEventListener("keyup", function(e) {
        if(e.keyCode>=37 && e.keyCode<=40) { keysDown[e.keyCode] = false; }
        if(e.keyCode==83)
        {
            currentSpeed = (currentSpeed>=(gameSpeeds.length-1) ? 0 : currentSpeed+1);
        }
    });

    viewport.screen = [document.getElementById('game').width,
        document.getElementById('game').height];

    tileset = new Image();
    tileset.onerror = function()
    {
        ctx = null;
        alert("Failed loading tileset.");
    };
    tileset.onload = function() { tilesetLoaded = true; };
    tileset.src = tilesetURL;

    for(x in tileTypes)
    {
        tileTypes[x]['animated'] = tileTypes[x].sprite.length > 1 ? true : false;

        if(tileTypes[x].animated)
        {
            var t = 0;
            
            for(s in tileTypes[x].sprite)
            {
                tileTypes[x].sprite[s]['start'] = t;
                t+= tileTypes[x].sprite[s].d;
                tileTypes[x].sprite[s]['end'] = t;
            }

            tileTypes[x]['spriteDuration'] = t;
        }
    }
    
    mapTileData.buildMapFromData(gameMap, mapW, mapH);
    mapTileData.addRoofs(roofList);
    mapTileData.map[((2*mapW)+2)].eventEnter = function()
        { console.log("Entered tile 2,2"); };
    
    var mo1 = new MapObject(1); mo1.placeAt(2, 4);
    var mo2 = new MapObject(2); mo2.placeAt(2, 3);
    
    var mo11 = new MapObject(1); mo11.placeAt(6, 4);
    var mo12 = new MapObject(2); mo12.placeAt(7, 4);
    
    var mo4 = new MapObject(3); mo4.placeAt(4, 5);
    var mo5 = new MapObject(3); mo5.placeAt(4, 8);
    var mo6 = new MapObject(3); mo6.placeAt(4, 11);
    
    var mo7 = new MapObject(3); mo7.placeAt(2, 6);
    var mo8 = new MapObject(3); mo8.placeAt(2, 9);
    var mo9 = new MapObject(3); mo9.placeAt(2, 12);
};

function drawGame()
{
    if(ctx==null) { return; }
    if(!tilesetLoaded) { requestAnimationFrame(drawGame); return; }

    var currentFrameTime = Date.now();
    var timeElapsed = currentFrameTime - lastFrameTime;
    gameTime+= Math.floor(timeElapsed * gameSpeeds[currentSpeed].mult);

    var sec = Math.floor(Date.now()/1000);
    if(sec!=currentSecond)
    {
        currentSecond = sec;
        framesLastSecond = frameCount;
        frameCount = 1;
    }
    else { frameCount++; }

    if(!player.processMovement(gameTime) && gameSpeeds[currentSpeed].mult!=0)
    {
        if(keysDown[38] && player.canMoveUp())            { player.moveUp(gameTime); }
        else if(keysDown[40] && player.canMoveDown())    { player.moveDown(gameTime); }
        else if(keysDown[37] && player.canMoveLeft())    { player.moveLeft(gameTime); }
        else if(keysDown[39] && player.canMoveRight())    { player.moveRight(gameTime); }
    }

    viewport.update(player.position[0] + (player.dimensions[0]/2),
        player.position[1] + (player.dimensions[1]/2));
    
    var playerRoof1 = mapTileData.map[toIndex(
        player.tileFrom[0], player.tileFrom[1])].roof;
    var playerRoof2 = mapTileData.map[toIndex(
        player.tileTo[0], player.tileTo[1])].roof;

    ctx.fillStyle = "#000000";
    ctx.fillRect(0, 0, viewport.screen[0], viewport.screen[1]);
    
    for(var z = 0; z < mapTileData.levels; z++)
    {

    for(var y = viewport.startTile[1]; y <= viewport.endTile[1]; ++y)
    {
        for(var x = viewport.startTile[0]; x <= viewport.endTile[0]; ++x)
        {
            if(z==0)
            {
            var tile = tileTypes[mapTileData.map[toIndex(x,y)].type];

            var sprite = getFrame(tile.sprite, tile.spriteDuration,
                gameTime, tile.animated);
            ctx.drawImage(tileset,
                sprite.x, sprite.y, sprite.w, sprite.h,
                viewport.offset[0] + (x*tileW), viewport.offset[1] + (y*tileH),
                tileW, tileH);
            }
            
            var o = mapTileData.map[toIndex(x,y)].object;
            if(o!=null && objectTypes[o.type].zIndex==z)
            {
                var ot = objectTypes[o.type];
                 
                ctx.drawImage(tileset,
                    ot.sprite[0].x, ot.sprite[0].y,
                    ot.sprite[0].w, ot.sprite[0].h,
                    viewport.offset[0] + (x*tileW) + ot.offset[0],
                    viewport.offset[1] + (y*tileH) + ot.offset[1],
                    ot.sprite[0].w, ot.sprite[0].h);
            }
            
            if(z==2 &&
                mapTileData.map[toIndex(x,y)].roofType!=0 &&
                mapTileData.map[toIndex(x,y)].roof!=playerRoof1 &&
                mapTileData.map[toIndex(x,y)].roof!=playerRoof2)
            {
                tile = tileTypes[mapTileData.map[toIndex(x,y)].roofType];
                sprite = getFrame(tile.sprite, tile.spriteDuration,
                    gameTime, tile.animated);
                ctx.drawImage(tileset,
                    sprite.x, sprite.y, sprite.w, sprite.h,
                    viewport.offset[0] + (x*tileW),
                    viewport.offset[1] + (y*tileH),
                    tileW, tileH);
            }
        }
    }
    
        if(z==1)
        {
            var sprite = player.sprites[player.direction];
            ctx.drawImage(tileset,
                sprite[0].x, sprite[0].y,
                sprite[0].w, sprite[0].h,
                viewport.offset[0] + player.position[0],
                viewport.offset[1] + player.position[1],
                player.dimensions[0], player.dimensions[1]);
        }
    
    }

    ctx.fillStyle = "#ff0000";
    ctx.fillText("FPS: " + framesLastSecond, 10, 20);
    ctx.fillText("Game speed: " + gameSpeeds[currentSpeed].name, 10, 40);

    lastFrameTime = currentFrameTime;
    requestAnimationFrame(drawGame);
}
</script>

</head>
<body>

<canvas id="game" width="400" height="400"></canvas>

</body>
</html>