Demo: http://nicolahibbert.com/demo/javascript-tile-map-editor/
If you’ve been reading along recently, you’ll know that HTML5 Breakout is at a stage where I’m not entirely embarrassed by it, and I’m making a start on my next JavaScript game.
I’ve had an idea for a 2d sidescrolling platformer for a while, but I specifically want the player character to be able to move up and down slopes and curves (ala Sonic the Hedgehog). JavaScript game engines are still generally quite young and aren’t particularly full featured – I don’t know of any that implement this functionality out of the box, so I’m going to show you how I’m doing it. It involves separating out the alpha channel using the HTML5 Canvas API, and using this data to build a pixel-level map of where the character’s position should be at any single point on the tile.
Please note that this is a discussion of static per-pixel collision detection only, i.e., character vs. background tile map. I’m assuming some knowledge of tile based collision maps and of the Canvas API, but I’ll do a quick overview just in case.
Static collision detection
Your standard collision map in JavaScript is just a multidimensional array:
var map = [
[1,0,0,0],
[1,1,0,0],
[1,1,1,0],
[1,1,1,1]
];
The zeros represent an empty space on the map, and the ones represent where a collision will take place. Each ‘one’ is a rectangle: the dimensions of the tile, multiplied by it’s position in the map, drawn to the stage at it’s respective x and y co-ordinate. This map will usually be stored in a JSON format with some other information – tile dimensions, image file locations and the like.
This is great if you want flat platforms, but not so great if you want your character to be able to traverse curved and sloped surfaces.
Using the Canvas API to extract alpha channel data
Creating a sloped or curved tile is easy – you just have to ensure that your tile image has an alpha transparent component representing the path that you want your character to follow. This tile is saved with a bunch of other tiles to a sprite sheet. You can then use Canvas to extract the alpha channel data, and include this data as a part of your collision map.
buildMap : function(e) {
var alpha = [], // used to store alpha data
numTiles = 10, // size of tilemap (10 tiles high, 10 tiles wide)
tileSize = 32, // size of tile in pixels
pixels, // store pixel data array
len, // cache length of pixel data array
x, y, z;
for (x = 0; x < numTiles; x++) { // tiles across
alpha[x] = [];
for (y = 0; y < numTiles; y++) { // tiles down
pixels = map.getImageData(y * tileSize, x * tileSize, tileSize, tileSize),
len = pixels.data.length;
alpha[x][y] = [];
for (z = 0; z < len; z += 4) {
alpha[x][y][z / 4] = pixels.data[z + 3]; // store alpha data only
}
if (alpha[x][y].indexOf(0) === -1) { // solid tile
alpha[x][y] = 1;
} else if (alpha[x][y].indexOf(255) === -1) { // transparent tile
alpha[x][y] = 0;
} else { // partial alpha, build pixel map
alpha[x][y] = pixels;
}
}
}
}
This bit of code retrieves image data from a canvas, breaks it down into tile sized chunks, and then stores the image data for each of these tiles into an array. It then checks the alpha data for each tile: if the tile is comprised entirely of alpha data, then the tile is transparent – it’s value is set to zero. If there isn’t any alpha on the tile, then the tile is completely solid – it’s value is set to one. If there is partial alpha data, then the pixel data for the alpha channel is stored so we can use it to resolve collisions at a per-pixel level later on.
The array containing the alpha channel for each tile can become pretty large. For these particular tiles, there are 1024 bits of alpha channel data (32 * 32). If you were to JSON.stringify() this, just one tile would take up a huge chunk of space in your map file. Thankfully, we can reduce this information down into a much more manageable state.
sortPartial : function(arr) {
var len = arr.length,
temp = [],
i, j;
for (i = 0; i < tileSize; i++) {
temp[i] = [];
for (j = 0; j < len; j++) {
if (j % tileSize === j) {
temp[i][j] = arr[j * tileSize + i];
}
}
temp[i] = temp[i].indexOf(255);
}
return temp;
}
This function will filter the array of part-alpha tiles, and return an array containing the position of each first solid pixel on the y axis. Just call this function on the part-alpha array above, like so –
... else { // partial alpha, build pixel map
alpha[x][y] = app.sortPartial(alpha[x][y]);
}
Now for each tile we’ll get back an array 32 items, rather than 1024 items long –
[28,28,26,26,24,24,22,22,20,20,18,18,16,16,14,14,12,12,10,10,8,8,6,6,4,4,2,2,0,0,0,0]
We can use this information to resolve the x/y position of a character in a 2d platform game when it’s interacting with curved or sloped terrain. Neat eh? 🙂
A minor problem…
Half way through writing this tutorial, I realised it would be kind of lame not to have a working demonstration of this technique in action. Unfortunately to do that, I needed a tile map editor. I couldn’t find an open source JavaScript based tile map editor, so I started writing one. So much for not reinventing the wheel! It’s definitely a minimum viable product at this early stage – it only outputs collision maps, and it has very limited functionality.
What it does have though, is a lot of potential. If there are any JavaScript game engine developers out there reading this, and you’re in need of a tile map editor (and your project is open source), I’d be happy to contribute to your project and spend a bit more time on this; otherwise it’s just going to be developed as and when I get the time.
For a demonstration of per-pixel collision detection on sloped and curved tiles, check out the demo. Create and build a map, and hit the test button to see it in action. Works in Chrome, Safari and Firefox.
Check out the demo here: JavaScript tile map editor
Get the source on Github