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