GD50 Lecture 04 - Super Mario Bros.

This is part of a series where I talk about how I approach the assignment portion of the GD50 lecture. These are written so that I can review them in the future without going through the code! Since this is an assignment, I won’t be posting the code unless it is not related to the assignment. If you are stuck at one of these assignments, these posts should contain enough information to help you progress. Feel free to let me know if there is an error. :)

Super Mario Bros. is the game we take a look this time! Topics that were discussed include tile map, animation, platformer physic, basic enemy AI, and procedural level generation.

  1. Spawn the player above the ground
  2. Generate a key and a lock
  3. Spawn a goal post
  4. Regenerate a new longer level
  5. Bugs & Changes

Spawn the player above the ground

At the beginning of a level, the player is spawned at the top left corner of the screen. But because the level generation is random, the player might be spawned above a chasm and fall to death. Thus, the first task of the assignment is to always spawn the player above the ground. This can be done easily by using a loop to check each column and see if there is any tile along the Y-axis. We can make this more efficient by only checking the bottom tile of the column! Once a tile is found, the loop will break, and this will be the x position the player spawn at.

Always spawn on top of the ground
Always spawn on top of the ground

Generate a key and a lock

Next up, we will generate a key and a lock block on the map. If the player doesn’t have a key, colliding with the lock block will do nothing. On the other hand, the lock block will disappear if the player has the key. All of this is done in the Level.generate() function. This function generates the level column by column starting from left to right.

Breaking it down:

The first step is to determine where to spawn the key and lock. We could have spawned them randomly on the map, but then the player might have to do some backtracking (especially annoying when the key is at the end of the level and the lock block is at the beginning). So, what I did to reduce the backtracking is by only spawning the key on the first half of the level and spawning the lock block around the middle of the level. This can be done via math.random(math.floor(width / 2)).

Since retrieving a key or lock block can be difficult when they are above a chasm, I decided not to spawn the key/lock on top of a chasm. If the location to spawn a key/lock is on a chasm, I simply increment their location by 1. Once the level generator reaches the location to spawn the key/lock, we generate them by creating a GameObject (like how a normal block and gem is generated in the level). And a boolean is used to keep track whether the lock has been spawned as we don’t want to spawn another normal block on top of the lock.

Let’s talk about how the gem disappear when the player collides with it. Every time a player consumes a gem, the onConsume() function will be called. After the onConsume() function is called, the game will remove the object from the objects table. This means we can ‘piggyback’ on this and implement this into the lock block! If the player has the key, the lock block becomes consumable and will disappear if the player collides with it.

The key unlocks the lock!
The key unlocks the lock!

Spawn a goal post

After the lock block disappears, we want to spawn a goal post at the end of the level. This can be done in the onConsume() function of our lock block. Once the lock block is consumed, GameObjects that represents the goal post will be created. Since the goal post is made up of multiple parts, more than one GameObject will be created (i.e. create a flag GameObject and multiple poles GameObject)!

Regenerate a new longer level

Finally, when the player collides with the goal post, a new longer level will be generated. All we have to do is to reload the play state in the onCollide() function of the flag GameObject to make a new level. Remember to pass in the player’s score and the current level length as a parameter into the new Playstate to retain them. In the PlayState:enter(params) function, we can define that if there is no parameter (which means it is the first level), then it would call the LevelMaker.generate(length) with a default length. Otherwise, LevelMaker.generate(params.length) will be called and generate a longer level.

Generating a longer level after touching goal post
Generating a longer level after touching goal post

Bugs & Changes

Unwinnable level

Lock spawning in between two pillars
Lock spawning in between two pillars

Random level generation can be fun, but we also need to be careful about generating an unwinnable level. This bug does not happen very often, but it still needs to be fixed regardless. Since we already decided the location of the lock block at the beginning of the level generation, what we can do then is to explicitly ‘tell’ the program to not generate a pillar on the previous column of the lock block.

-- a chance to generate a pillar
    if not (x == width - 2) and not (x == blockLocation - 1) and math.random(8) == 1 then
        -- spawn a pillar
        ...
    end

width - 2 is the location of the goal post and blockLocation - 1 is the column right before the lock block. These two locations are the spot where we don’t want a pillar to spawn. Otherwise, there is a 1/8 chance for the level generator to spawn a pillar on a column. A similar approach is also used to prevent the key to spawn in between two pillars with a normal block on top.

This post is slightly shorter than usual, and I think that is because most of the stuff here felt straight forward and does not require much explanation. But I still hope this is informative! Till next time!