10
\$\begingroup\$

I am trying to remake Super Mario Bros. in JavaScript and I am trying to figure out if there is a more efficient way to create and store the level data. I have created 1-1 and here's what the code looks like:

const repeatBlock = (block, start, end, array) => {
    let newData = {
        "tp1": "tlp",
        "tp0": "trp",
        "p1": "lp",
        "p0": "rp"
    };

    for (let i = start; i <= end; i++) {
        let oldBlock = block;

        if (newData.hasOwnProperty(block+(end-i))) {
            block = newData[block+(end-i)];
        }

        array[i] = block;

        block = oldBlock;
    }
}

let level1_1Overworld = [];

for (let i = 0; i <= 14; i++) {
    level1_1Overworld[i] = [];
    repeatBlock("", 0, 227, level1_1Overworld[i]);
}

level1_1Overworld[2][198] = "f";

level1_1Overworld[5][22] = "q c";
repeatBlock("b", 80, 87, level1_1Overworld[5]);
repeatBlock("b", 91, 93, level1_1Overworld[5]);
level1_1Overworld[5][94] = "q c";
level1_1Overworld[5][109] = "q p";
repeatBlock("b", 121, 123, level1_1Overworld[5]);
level1_1Overworld[5][128] = "b";
repeatBlock("q c", 129, 130, level1_1Overworld[5]);
level1_1Overworld[5][131] = "b";
repeatBlock("s", 188, 189, level1_1Overworld[5]);

repeatBlock("s", 187, 189, level1_1Overworld[6]);

repeatBlock("s", 186, 189, level1_1Overworld[7]);

level1_1Overworld[8][65] = "h";
repeatBlock("", 66, 184, level1_1Overworld[8]);
repeatBlock("s", 185, 189, level1_1Overworld[8]);
level1_1Overworld[8][202] = "C";

level1_1Overworld[9][16] = "q c";
level1_1Overworld[9][20] = "b";
level1_1Overworld[9][21] = "q p";
level1_1Overworld[9][22] = "b";
level1_1Overworld[9][23] = "q c";
level1_1Overworld[9][24] = "b";
repeatBlock("tp", 46, 47, level1_1Overworld[9]);
repeatBlock("tp", 57, 58, level1_1Overworld[9]);
level1_1Overworld[9][77] = "b";
level1_1Overworld[9][78] = "q p";
level1_1Overworld[9][79] = "b";
level1_1Overworld[9][94] = "b m";
level1_1Overworld[9][100] = "b";
level1_1Overworld[9][101] = "b s";
level1_1Overworld[9][106] = "q c";
level1_1Overworld[9][109] = "q c";
level1_1Overworld[9][112] = "q c";
level1_1Overworld[9][118] = "b";
repeatBlock("b", 129, 130, level1_1Overworld[9]);
level1_1Overworld[9][137] = "s";
level1_1Overworld[9][140] = "s";
repeatBlock("s", 151, 152, level1_1Overworld[9]);
level1_1Overworld[9][155] = "s";
repeatBlock("b", 168, 169, level1_1Overworld[9]);
level1_1Overworld[9][170] = "q c";
level1_1Overworld[9][171] = "b";
repeatBlock("", 172, 183, level1_1Overworld[9]);
repeatBlock("s", 184, 189, level1_1Overworld[9]);

repeatBlock("tp", 38, 39, level1_1Overworld[10]);
repeatBlock("p", 46, 47, level1_1Overworld[10]);
repeatBlock("p", 57, 58, level1_1Overworld[10]);
repeatBlock("s", 136, 137, level1_1Overworld[10]);
repeatBlock("s", 140, 141, level1_1Overworld[10]);
repeatBlock("s", 150, 152, level1_1Overworld[10]);
repeatBlock("s", 155, 156, level1_1Overworld[10]);
repeatBlock("s", 183, 189, level1_1Overworld[10]);

repeatBlock("tp", 28, 29, level1_1Overworld[11]);
repeatBlock("p", 38, 39, level1_1Overworld[11]);
repeatBlock("p", 46, 47, level1_1Overworld[11]);
repeatBlock("p", 57, 58, level1_1Overworld[11]);
repeatBlock("s", 135, 137, level1_1Overworld[11]);
repeatBlock("s", 140, 142, level1_1Overworld[11]);
repeatBlock("s", 149, 152, level1_1Overworld[11]);
repeatBlock("s", 155, 157, level1_1Overworld[11]);
repeatBlock("tp", 163, 164, level1_1Overworld[11]);
repeatBlock("tp", 179, 180, level1_1Overworld[11]);
repeatBlock("s", 182, 189, level1_1Overworld[11]);

repeatBlock("p", 28, 29, level1_1Overworld[12]);
repeatBlock("p", 38, 39, level1_1Overworld[12]);
repeatBlock("p", 46, 47, level1_1Overworld[12]);
repeatBlock("p", 57, 58, level1_1Overworld[12]);
repeatBlock("s", 134, 137, level1_1Overworld[12]);
repeatBlock("s", 140, 143, level1_1Overworld[12]);
repeatBlock("s", 148, 152, level1_1Overworld[12]);
repeatBlock("s", 155, 158, level1_1Overworld[12]);
repeatBlock("p", 163, 164, level1_1Overworld[12]);
repeatBlock("p", 179, 180, level1_1Overworld[12]);
repeatBlock("s", 181, 189, level1_1Overworld[12]);
level1_1Overworld[12][198] = "s";

for (let i = 13; i <= 14; i++) {
    repeatBlock("g", 0, 68, level1_1Overworld[i]);
    repeatBlock("g", 71, 85, level1_1Overworld[i]);
    repeatBlock("g", 89, 152, level1_1Overworld[i]);
    repeatBlock("g", 155, 227, level1_1Overworld[i]);
}

//1-1 Bonus 16x15
let level1_1Bonus = [];

for (let i = 0; i <= 15; i++) {
    level1_1Bonus[i] = [];
    repeatBlock("", 0, 15, level1_1Bonus[i]);
}

for (let i = 2; i <= 12; i++) {
    level1_1Bonus[i][0] = "b";

    if (i < 11) {
        level1_1Bonus[i][15] = "lp";
    }
}

repeatBlock("b", 4, 10, level1_1Bonus[2]);

repeatBlock("c", 5, 9, level1_1Bonus[5]);
repeatBlock("c", 4, 10, level1_1Bonus[7]);
repeatBlock("c", 4, 10, level1_1Bonus[9]);

for (let i = 10; i <= 12; i++) {
    repeatBlock("b", 4, 10, level1_1Bonus[i]);
}

level1_1Bonus[11][13] = "spul";
level1_1Bonus[11][14] = "spum";
level1_1Bonus[11][15] = "spur";

level1_1Bonus[12][13] = "spll";
level1_1Bonus[12][14] = "splm";
level1_1Bonus[12][15] = "splr";

for (let i = 13; i <= 14; i++) {
    repeatBlock("g", 0, 15, level1_1Bonus[i]);
}

After the arrays I have a function that translates each index of the array to what it's corresponding type would be. This code works and I am able to render everything properly but I feel like there could be a better way to create this data especially when this is only the first level. Any ideas?

\$\endgroup\$
5
  • 12
    \$\begingroup\$ What do you want to optimize for? Is it storage size? Load time? Ease of use? Readability? Something else? - Edit: this is not the typical question for this site, as right now this looks more like something for codereview.stackexchange.com since - you claim - the code is working correctly. But if there is a problem you are trying to solve (e.g. this code is not good for X) please clarify. \$\endgroup\$
    – Theraot
    Commented Jun 15, 2023 at 22:11
  • \$\begingroup\$ I guess I am trying to figure out if this is the only way to create a level, or if there is a shorter way to do it because it was tedious trying to do this. \$\endgroup\$
    – Jake Miahn
    Commented Jun 16, 2023 at 0:32
  • 7
    \$\begingroup\$ A spreadsheet is an underrated level editor. You can use codes in each cell for tile types, and use background colours to show the level in a more human way. Then just export as CSV for your game. \$\endgroup\$ Commented Jun 16, 2023 at 14:46
  • \$\begingroup\$ @SteveSmith A spreadsheet is a very powerful editor of repetitive data, period. I've gone so far as to read the .XLSM files directly so the user doesn't have to remember to save as CSV. \$\endgroup\$ Commented Jun 18, 2023 at 21:45
  • 1
    \$\begingroup\$ Is there any reason no one suggested an image stored as bmp or lossless png yet? I've done it that way in the past. Certain color codes correlate to certain blocks. Easy to edit and visualize, can be compressed if needed. Tad bit trickier to read out, but other than that... Am I missing some reasons not to use bmp? \$\endgroup\$ Commented Jun 19, 2023 at 5:22

2 Answers 2

29
\$\begingroup\$

You should have the data for your level in an external source, such as an XML, JSON, YAML, CSV, or even a .txt file. Then you write your logic to read from the file, and generate the level procedurally.

This will let you play all of the levels with the same code, and you can make changes to levels, or add new levels, without touching the code. This also lets designers who aren't familiar with programming to create fun levels.

This also opens the door for user-generated content. You can create a level editor that auto-generates this file. A level editor will also make it easier to make fun levels, since they can be visually designed, instead of by writing text into a doc.

For example, your external file may look something like this:

                                F
          ?*?    C              F
                                F
     M           G              F
======================    ======= 
  • M - Mario's starting position
  • G - Goomba
  • ? - Breakable Block
  • * - Breakable Coin Block
  • C - Coin
  • = - Ground
  • F - Flag

Then your (pseudo)code would look something like this:

Level CreateLevel(File file)
{
    char[][] blocks = file.GetASCIICharacters();
    int width = blocks.Length;
    int height = blocks[0].Length;
    Level level = new Level(width, height);
    for(int y = 0; y < height; y++)
        for(int x = 0; x < width; i++)
        {
            char block = blocks[x][y];
            if(block == `M`)
            {
                Mario mario = new Mario();
                level.SetBlock(x, y, mario);
            }
            else if(block == `G`)
            {
                Goomba goomba = new Goomba();
                level.SetBlock(x, y, goomba);
            }
            else if(block == `?`)
            {
                BreakableBlock breakableBlock = new BreakableBlock();
                level.SetBlock(x, y, breakableBlock);
            }
            else if(block == `*`)
            {
                CoinBlock coinBlock = new CoinBlock();
                level.SetBlock(x, y, coinBlock);
            }
            else if(block == `C`)
            {
                Coin coin = new Coin();
                level.SetBlock(x, y, coin);
            }
            else if(block == `=`)
            {
                Ground ground = new Ground();
                level.SetBlock(x, y, ground);
            }
            else if(block == `F`)
            {
                Flag flag = new Flag();
                level.SetBlock(x, y, flag);
            }
        }
    }
}

NOTE: There are some significant issues with this implementation. Specifically, if you wanted to give a Goomba a different amount of health, one ASCII character isn't enough to store that data. But by using a format such as YAML, you could put extra data in there. This is intended to be a simple example. It's also not well architected code.

\$\endgroup\$
9
  • 3
    \$\begingroup\$ This appears to be closer to a binary packing format. Human readable formats have their place, but I'm not sure that's a requirement here. \$\endgroup\$
    – Basic
    Commented Jun 15, 2023 at 23:45
  • 16
    \$\begingroup\$ +1 for using an external file. For small levels like in SMB, a human-readable ASCII format is not a bad idea (the files will still be quite small despite the non-bit-packed storage, and the ability to inspect/author/edit them with a text editor rather than a bespoke tool is a huge win). You can find many SMB levels already encoded in such a format as part of the Video Game Level Corpus (VGLC). \$\endgroup\$
    – DMGregory
    Commented Jun 16, 2023 at 1:29
  • 3
    \$\begingroup\$ +1 for leveraging software to do tedious tasks. Even something really basic like CSV format + a spreadsheet app allows you to fill a swath of cells with a few click versus a bunch of hardcode. Manually hardcoded level are okay for prototypes but are brittle when it's time to work on level design at scale. \$\endgroup\$
    – Pikalek
    Commented Jun 16, 2023 at 5:21
  • 2
    \$\begingroup\$ @JakeMiahn The main difference is that in code, you need to be explicit about the coordinates of each individual tile. When writing it out in a text file, you have a "snapshot" of the level - the tiles go where they are in the text file. Or, you can create a map editor (which can be as simple as an excel file you export into a text file or CSV)... or use an existing one, like Tiled, which is a very well designed generic tool you can use to export to all sorts of formats including JSON... or your own. Or just integrate a simple map editor straight into your game so anyone can create levels :) \$\endgroup\$
    – Luaan
    Commented Jun 16, 2023 at 6:09
  • 5
    \$\begingroup\$ I'd wager a guess that debugging the code to get the exact level you want is kinda tedious and possibly takes even longer than just to open a text file and move a character. Better yet, get a text editor that supports multi cursors, so you can copy/paste entire sections. \$\endgroup\$ Commented Jun 16, 2023 at 7:57
8
\$\begingroup\$

String literals

An easy way could be to use a string-literal and a function which loops over it and creates the level:

const level1_1 = `
    ggggg   ggggg  
       p        p
ssssssssssssssssss   
`

function parseLevel(level) {
  const lines = level.split()
  for (let y=0;y<lines;y++) {
    const l=lines[y]
    for (let x=0;x<l.length;x++) {
      const blockChar = l[x]
      levelArray[y][x] = createBlock(blockChar)
    }
  }
}

In this way you can easily edit the levels and visually see the layout while editing. You can also use a lot of tools from your text-editor for copy/paste and multiline editing. You can put each level in its own "level1-1.js" file and import the files.

Pro/Con to external files (like XML,JSON,...)

I think external files can be a great solution especially when using a tool like a level-editor ("tiled" is great and open source). But if you don't want to use an external editor I'd prefer an inline solution.

When using inline levels you don't need additional file handling and error handling logic to load the files. And with JS you don't compile your code anyway, so just let the browser do the loading and parsing (via import). And you can still put the levels in their own js-files, so you don't have to touch the complex code when editing levels.

\$\endgroup\$
1
  • 5
    \$\begingroup\$ Yep, I think this answer may help OP understands what others mean by external file (which this answer also suggest), via the visualization in this answer. +1. OP should be able to see that it's much easier to write the level this way rather than assigning each coordinate separately by a line of code. \$\endgroup\$
    – justhalf
    Commented Jun 16, 2023 at 11:07

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .