Using Paint/Photoshop/GIMP as a MapEditor

December 15, 2011

Many times I've seen developers go nuts to either: A) Find a map editor that can adjust to their game, B) Adjust their game to a predefined format exported by a map editor or C) Decided to make their own map editor.

Truth be told, map editors are extremely useful tools that can help us ease the development of levels for our game, but in practice, most of the time map editors are just grid editors that basically translate that a given graphic corresponds to a given tile type. While it's true that some of them also supports adding additional properties to each tile, in the most basic form that's all they do. Their sole purpose is to ease the creation of levels.

Some time ago I decided to make a small shooter game and had to pick one of those 3 options. After I carefully considered all three, I said to myself "I don't need all this mumbo-jumbo" and like most developers I convinced myself that I could get away by making my own solution, so I set to it and half an hour later I had a working sample.

As my game was going to be based on a two-dimensional matrix i figured out that i could create an image of, for example, 32 x 24 pixels, meaning that my matrix would have 32 columns and 24 rows (or rather, the size of the image). Then i'd choose a given color, such as '#fff' (white) and say "ok, this color is going to represent walls", '#000' (black) is going to represent an empty position, '#ff00' (red) is going to represent the red base and '#00ff' is going to represent the blue base. The entire map image i used is shown below, it's a bit small because it's only 32px x 24px:

Map Example

After i literally painted ('shopped, in my case) my map i exported the image as a GIF and started working on the JavaScript file to interpret the format.

First i defined 3 different tile types:

var Tile = {
    WALL: {
        index: 0,
        identifier: 'ffffff'
    },
    RED: {
        index: 1,
        identifier: '00ff'
    },
    BLUE: {
        index: 2,
        identifier: 'ff00'
    }
}

Then i developed a class called "Level" that looked like this:

var Level = function (mapImage) 
{
    this.mapData = {
        size: null,
	data: null,
	base: {
	    red: 0,
	    blue: 0
	}
    }

    var map = new Image();
    map.src = mapImage;

    this.loadMap(map);
};

The last line of the Level constructor calls the loadMap() method which will be in charge of converting the binary image into a matrix that we'll later interpret to display our map.

In essence, Level.loadMap() will dynamically create an HTML5 Canvas object with the size of the level image, paint the image on it using the drawImage() method and then will use the getImageData() method to read the pixel information of the canvas.

As it loops through all the pixels, it'll check if the color of the current pixel matches one of the identifiers defined in the Tile object and will automatically fill a matrix with the right index values. Obviously, the identifier is an hexadecimal number and getImageData() returns an array of pixels in the RGBA format (Red, Green, Blue, Alpha), so we'll also need to convert each color value (which will oscillate between 0 and 255) to hex. We can do that easily by doing:

rgbColor.toString(16);

Let's take a look at the whole method:

Level.prototype.loadMap = function (map) 
{
    // Create an HTML5 Canvas object, define the 2D Context and create an empty array to store the tileData
    var c = this.c,
        cnv = document.createElement("canvas"),
        ctx = cnv.getContext("2d"),
        idata = null,
        tileData = [];

    // Set the size of the map
    this.mapData.size = {
        x: map.width,
        y: map.height
    }

    // Adjust the size of the canvas to match the size of the image
    cnv.width = map.width;
    cnv.height = map.height;

    // Paint the map on the new canvas
    ctx.drawImage(map, 0, 0);

    // Read the pixel data array
    idata = ctx.getImageData(0, 0, cnv.width, cnv.height);

    // Start cycling through all the pixels of the image
    for (var i = 0, my = map.height; i < my; i++) {
        for (var j = 0, mx = map.width; j < mx; j++) {
            // Convert the RGB values to hex
            var r = (idata.data[((mx * i) + j) * 4]).toString(16),
                g = (idata.data[((mx * i) + j) * 4 + 1]).toString(16),
                b = (idata.data[((mx * i) + j) * 4 + 2]).toString(16),
                hex = r + g + b; 

            tileData[j] = tileData[j] || [];

            switch(hex) {
                case Tile.WALL.identifier:
                    tileData[j][i] = Tile.WALL.index;
                    break;
                case Tile.BLUE.identifier:
                    tileData[j][i] = Tile.BLUE.index;
                    this.mapData.base.blue = {x: j, y: i};
                    break;
                case Tile.RED.identifier:
                    tileData[j][i] = Tile.RED.index;
                    this.mapData.base.red = {x: j, y: i};
                    break;
            }
        }
    }

    // Replace the level data with the values of the tileData matrix.
    this.mapData.data = tileData;
};

Quite simple, right? Ok then, now let's interpret the result and see how we can use this object to display our level. In my case i decided to set the default background using CSS to reduce the number of drawImage() calls when painting the level. Now it's time to implement the JavaScript code that will use the Level class:

window.onload = function() 
{
    var canvas = document.getElementById('viewport');
    var c = canvas.getContext('2d');

    var wall = new Image(),
        red = new Image(),
        blue = new Image(),
        level = new Level('levels/map1.gif');

    wall.src = 'images/wall.gif';
    red.src = 'images/red.png';
    blue.src = 'images/blue.png';

    // In our case tiles are 20px by 20px, so we'll need to resize the canvas.
    canvas.width = level.mapData.size.x * 20;
    canvas.height = level.mapData.size.y * 20;      
    
    // Print the map
    for (var y = 0; y < level.mapData.size.y; y++) {
        for (var x = 0; x < level.mapData.size.x; x++) {
            if (level.mapData.data[x][y] !== undefined) {
                switch (level.mapData.data[x][y]) {
                    case Tile.WALL.index:
                        c.drawImage(wall, x * 20, y * 20);
                        break;
                    case Tile.RED.index:
                        c.drawImage(red, (x * 20) - 20, (y * 20) - 20);
                        break;
                    case Tile.BLUE.index:
                        c.drawImage(blue, (x * 20) - 20, (y * 20) - 20);
                        break;
                }
            }
        }
    }           
}

You'll need to put that either in a new JavaScript file or inside a <script> tag. In my case i decided to use a file called game.js:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>Level Loader</title>
        <script src="levelLoader.js" charset="UTF-8"></script>
        <script src="game.js" charset="UTF-8"></script>
        <style type="text/css">
            canvas#viewport {
                background: black url(images/floor.gif) repeat;
            }
        </style>
    </head>
    <body>
        <canvas id="viewport"></canvas>
    </body>
</html>

As always, the code is available on GitHub.