Generating a Level From an Image in GameMaker: Studio

When converting from an image to a level in GameMaker: Studio, each object is represented by a different color pixel in the image. In this case, blue represents the walls, green represents the player, and yellow represents coins.

Today's tutorial will be a quick and easy one. GameMaker: Studio comes with a built-in room editor, but that isn't the only way to create levels in the software. It's common for games to bring objects into the world by reading text files and creating instances at given coordinates. In a similar and less-often used way, levels can be created by reading an image. By associating game objects with a color, entities can be spawned in relation to the colored pixels in an image file. The picture on the left shows that the reference image's blue pixels become walls, yellow pixels become coins, and the green pixel becomes a player.

Why one would want to create levels this way is dependent on their project and personal preferences. Like nearly everything when it comes to coding, there are pros and cons of using particular methods. Building rooms through images can prove to be useful if developing a procedural generated game like Binding of Isaac, where pre-constructed spaces are spawned adjacent to one another. The disadvantage is that this type of generation in GM:S uses an extremely slow function called surface_getpixel(), which returns the color of a given pixel from a surface. While lag from reading the pixels of a tiny image (e.g. 16x12) will hardly be noticeable to the user, excessive use of surface_getpixel() should be avoided unless you're okay with hanging performance.

You can download the project source here.

Below is the code for the level generator object, which takes a non-transparent reference image (sprite sLevel) and creates objects (oWall, oCoin, oPlayer) based on pixel colors. Objects are spawned in relation to (0, 0) on a grid, where the width of each cell is the room's width divided by the image's width and the height of each cell is the room's height divided by the image's height. It's simple math which allows us to proportionally scale a 16x12 image to fit a 1024x768 room (each object's associated sprite is 64x64 [1024/16 = 64; 768/12 = 64]). Once a surface the size of the image is created, the image is drawn to it then looped over. Based on the color of each given coordinate, an object is created. This example uses a switch statement and GM:S color constants to reference and create particular objects. Using the eyedropper tool in the Sprite Editor, you can get the RGB color value of any pixel and use that instead of c_color. Because switch statements only accept constant values, ensure the make_color_rgb() color is defined either in Macros or a variable before the switch statement is called.

This code can be expanded to add tiles to a room with relative ease - replacing instance_create() with tile_add()

/// Create Event of level generator object
var img, w, h, cell_w, cell_h, surf, i, j, col, obj; // init vars

img = sLevel; // image in which a level will be generated from
w = sprite_get_width(img); // image width
h = sprite_get_height(img); // image height

// cell size in which objects will "snap" to
cell_w = floor(room_width / w);
cell_h = floor(room_height / h);

surf = surface_create(w, h); // create a surface

surface_set_target(surf); // set the surface target
draw_sprite(img, 0, 0, 0); // draw the image to the surface
surface_reset_target(); // reset the surface target

for (i = 0; i < w; i++) { // cycle through width of image
    for (j = 0; j < h; j++) { // cycle through height of image
        // get the pixel color at the given coordinates (SLOW FUNCTION, use graciously)
        col = surface_getpixel(surf, i, j);
        obj = noone; // object to create at coordinates

        switch (col) {
            // blue represents walls
            case (c_blue):
                obj = oWall;
                break;
                // yellow represents coins
            case (c_yellow):
                obj = oCoin;
                break;
                // lime green represents a player
            case (c_lime):
                obj = oPlayer;
                break;
        }

        // if there is a color match, create the associated object at the given coordinates (px * grid)
        if (obj != noone) {
            instance_create(i * cell_w, j * cell_h, obj);
        }
    }
}

surface_free(surf); // free the surface from memory

That was pretty straightforward, eh? If you have any questions, comments, or critiques feel free to leave them below as always!

3/23/17 Update - reddit user Mordwaith posted an alternate, wonderfully more efficient means of parsing colors from an image on /r/gamemaker. His approach uses buffers and bitwise operations rather than surface_getpixel(). Check it out here.

GameMaker-related posts mailing list