A Secret Project: Afterbox's Website Launched!

Check out the official Afterbox website at http://afterbox.io

Conceived in February 2016, I have been slowly and secretly working on my most ambitious project to date: Afterbox. Afterbox is a data preservation and inheritance service to house user's meaningful and sensitive information which is too often lost over time and through passing.

While I can't share everything surrounding the application at this time (and boy, is there a lot!), the website launched this past weekend with rich information about the upcoming service. If you like what you see, be sure to follow Afterbox on social media and join the mailing list!

This summer is going to be chock full of exciting developments for Afterbox. Co-founder Noah Chrysler and I are going to be located in the beautiful Menlo Park, California from June to September. If you're located in the area, let me know! I'd love to do some networking.

Check out Afterbox at http://afterbox.io

Using ASCII to Generate Levels in GameMaker: Studio

Using ASCII to Generate a Level in GameMaker: Studio

In a recent blog post, I showcased how levels can be generated in GameMaker: Studio by means of parsing colors from images. In response to the tutorial, reddit user Myles recommended that I tackle level creation through text by associating ASCII characters with objects. Today, I'm going to do just that.

This method of constructing rooms is considerably faster than using images, allowing for sizable floors to oftentimes be generated within a single game step. As Myles pointed out in his comment, ASCII-based spawning holds ground in the game development scene as it's used in the indie sensation, Spelunky, to generate chunks.

On the topic of Spelunky from a level design perspective, I recommend this YouTube video by Mark Brown of Game Maker's Toolkit. It gives an in-depth analysis of how creator Derek Yu managed to use random preset rooms to birth engaging environments. While my walkthrough will not cover how to create a full-fledged Spelunky-esque generator, it's not out of the question for me to make a post about that in the future.

Anyway, let's get started!

The Text Document

First and foremost, a plain-text document should be created. I simply use Windows' default Notepad - though feel free to use whatever you're comfortable with - saving the file with a .txt extension. This file extension will makes things much easier as we won't have to worry about compression and other miscellaneous file type-specifics. With a simple .txt and GM:S, a file can be opened and read line-by-line without hassle. I named this file levels.txt. It's included later in the source, but can be downloaded individually here. This file should go into the resource tree folder Included FilesI'll explain what goes into a text document and how it should be structured, assuming you haven't figured it out already by looking at the image at the beginning of this post.

Plain-text document representing a level.

My game features three objects: a wall (oWall)a player (oPlayer), and a coin (oCoin). Each object is represented by an ASCII character in the text document. The ampersand (@) character represents object oWall, a capitalized P represents object oPlayer, and a capitalized C represents object oCoin. Further, a sole underscore (_) on a line signals the end of a level. This allows for the level width and height not having to fit within predefined dimensions.

You can use any regular characters you desire, so long as they are used consistently both within the text document and GM:S (explained later). Any characters not being utilized by objects nor separators will be ignored by the generator. These ignored characters are used in part to give levels their shape as no objects will spawn in their place. For example, the first level in the document uses periods to carve hallways while the second and third level use spaces.

load_levels();

The first of two scripts the project will contain is called load_levels(). This script should only be called once. It locates the text document containing the level data and organizes it into a two-dimensional array, global.data, storing information level by level, row by row. The the first dimension of the array indicates the particular level. There are three test levels created in the source project, so the index spans from 0 to 2. The second dimension increments for each row the particular level contains. The first test level contains 12 rows, so the index spans from 0 to 11. For clarity, global.data[1, 4] would hold the string data for the fifth row of the second level.

///load_levels();

/*
    This script loads levels - separated by var separator - from a text document.
*/

var separator, fname, f, file, level, row, line; // init vars

separator = "_"; // character(s) on a new line in the text doc that indicate a new level should begin
fname = "levels.txt"; // name of file levels are pulled from (should be .txt)
f = working_directory + "\" + string(fname); // file location

/*
    2d array used to house (string) level data row by row
        First index is the level number
        Second index is the level's row data
*/
global.data[0, 0] = "";

level = 0; // current level
row = 0; // current row of level

if (file_exists(f)) { // check if file exists
    file = file_text_open_read(f); // open file
    while (!file_text_eof(file)) { // repeat until the end of the file is reached
        line = file_text_read_string(file); // read line
        if (line == separator) { // if the line contains the separator, a new level begins
            level++; // increment the level counter
            row = 0; // reset the row counter
        } else { // if there is data on the line
            global.data[level, row] = line; // store line information
            row++; // increment level row counter
        }
        file_text_readln(file); // move to next line
    }
    file_text_close(file); // close file
} else {
    show_error("Cannot locate " + string(fname) + "!", true); // error loading file
}

return 0;

generate_level();

Now that all level data is neatly organized thanks to the previous script, we can begin generating a level. This script, generate_level(), takes one integer argument: the level to load. As aforementioned, there are three levels in the project so a 01, or 2 can be passed in to this script without encountering any problems. To determine how many levels were loaded from the file, function array_height_2d() can be used in conjunction with the global.data array.

Row by row, the generator spawns objects at coordinates in relation to its character position multiplied by the cell_width and cell_height variables respectively. Which object to create an instance of is determined by the switch statement. If you represent walls as anything other than an ampersand, for example, changes must be reflected here in the code. Otherwise, the character will be ignored by the generator. This system is easily modified, allowing for cases to be added or removed at your discretion.

///generate_level(level);

/*
    This script generates a particular level, arg0, from the text doc.
    Script load_levels() must be called first.
*/

var level, cell_width, cell_height, i, j, obj, char; // init vars

level = argument0; // which level, int, to generate

/*
     Horizontal and vertical size of each cell, each char in text doc.
        Objects will spawn in relation to this grid.
*/
cell_width = 64;
cell_height = 64;

for (i = 0; i < array_length_2d(global.data, level); i++) { // cycle through level's rows
    for (j = 0; j < string_length(global.data[level, i]); j++) { // cycle through row's characters
        obj = noone; // object to spawn
        char = string_char_at(global.data[level, i], j + 1); // grab character

        switch (char) { // set object to spawn based on current character
            case ("@"): // @ represents object oWall
                obj = oWall;
                break;
            case ("C"): // C represents object oCoin
                obj = oCoin;
                break;
            case ("P"): // P represents object oPlayer
                obj = oPlayer;
                break;
        }

        if (obj != noone) { // create necessary object if there was a character match
            instance_create(j * cell_width, i * cell_height, obj);
        }
    }
}

return 0;

Wrapping Up

So there you have it: how to generate levels using ASCII characters in GameMaker: Studio. As mentioned in the preface of this tutorial, I would like to expand on concepts taught today to create something along the lines of how Spelunky (or Binding of Isaac, among other games) create their worlds. There are limitations on this implementation of ASCII level generation. Without rewriting how the generator interprets text documents, this system allows for only one object per grid space. Further, the player can easily access the text file to modify existing levels and write levels of their own. Using a checksum (hashing the level's text and comparing it to see if it's been modified) can alleviate this problem. 

You can download the full source, which includes basic object interactivity, here.

As always, if you have any questions, comments, or critiques, feel free to leave them in the comments below.

GameMaker-related posts mailing list

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