One of the quintessential design elements of most platformer games is the side-scrolling movement of your character through the game level (also called the map). This means the player is presented with a camera view on screen that is smaller than the overall level.
By contrast, a game like Space Invaders or Pac-Man is played on a screen that represents the entire level. The player's character and the camera never leave the one playing field (although flying saucers do enter and exit horizontally at the top of the screen in the case of Space Invaders. Where are they going?)
However, in a game such as Super Mario Bros. or Sonic the Hedgehog, the level is much, much larger than what fits in the screen's view at one time. So, by scrolling the character and camera as the player moves through the level, the map is revealed.
Here's the player's camera view seen on screen during gameplay:
This, on the other hand, is a view of the entire map of the same level:
In order to create a map for the level, you need to let the game engine know where the ground, platforms, obstacles, and goals are. In most 2D game engines, the level starts out as a blank grid, sort of like graph paper, and it's the level designer's job to build the map of the level's gameplay on that grid.
In the early days, this would, in fact, be created first on graph paper by the level designer, and then translated into game level code by the programmer. Here's an example from 1985's Super Mario Bros. of a level designed by Shigeru Miyamoto and Takashi Tezuka:
(This example drawing was taken from this behind-the-scenes video by Nintendo.)
Here's what the level looks like in the game engine, although this is a zoomed out view, not the player's screen:
Not only does the level design indicate where the ground, walls, platforms, obstacles, and goals are located, but we can also indicate the artwork of the level, which defines it's look. Rather than lay one huge piece of pixel art across the whole screen, the same grid system is used to display individual tiles of sprite artwork.
You'll notice there is a lot of repetition in the visual design. This is because a limited number of small sprite images are being replicated across the whole level. In fact, if you look at the example above from Super Mario Bros., there are only four sprites used to define the level (excluding power-ups and enemies).
This tiling is done to save memory. Here's an excellent guide page by Ladyada on some of the details of tile mapping and memory usage related to the NES and the Fuzebox, but the principles apply to MakeCode Arcade and the Pybadge/Pygamer as well. Here's an excerpt:
Graphic displays such as televisions, computer monitors and LCDs all display graphics made up of pixels (picture-elements). Early VGA monitors displayed 640x480 pixels. Nowadays, hi-definition (HD) displays these days can be 1920×1080 or even higher! The Fuzebox isn't powerful enough to drive HD but it can do pretty well for normal TV, at 240x224 pixels. Even though that doesn't sound like much, most original 8-bit video game consoles had the same resolution & the games were pretty cool.
Since the display changes during the course of the game, that would imply that we should store the video data in RAM. If each pixel contained 8-bits (one byte) of data, that would mean we need:
240 pixels wide * 224 pixels high * 1 byte per pixel = 53760 bytes = 52 Kilobytes
to store the video map in memory. Even though that doesn't sound like a lot, it is for a small microcontroller: the total RAM available is only 4K! Since there's no way to cram the video data into RAM we perform a trick called tiling. basically, we say "OK our simple games will have a background or images that don't change much, or repeat. So instead of having each pixel be unique, we'll use a range of predefined tiles". For example, lets look at this screenshot of Super Mario Brothers, the best known 8-bit game
Notice how the bricks at the bottom are just copies of one 'tile'? Also the clouds are cloned-looking and the bricks also look identical. Even tho this image has a resolution of 240x224, there's really only a grid of 15x14 tiles, each one 16x16 pixels big (verify this by counting how many bricks run along the bottom of the screen).
The Fuzebox uses much smaller tiles, because it actually has more processing power than the original NES. It uses 6x8 pixel tiles, for a grid of 40x28 tiles. Assuming that we reuse most of the tiles (and, in general, you'll see that many are) we can cut down the amount of memory needed to store the video graphics. In addition, because the tiles don't change (they just get swapped around), we can store them in the flash memory (which is 64K large) instead of the RAM. Basically, our video memory problems are solved!
Lets say we have 6x8 px tiles and a range of maybe 512 tiles maximum. that means we need
6 *8 * 512 = 24 KB
of flash storage to keep all the tile (which is totally reasonable since we have 64K of flash available), and then in the video RAM we will need:
40 * 28 * 2 byte address = 2240 = 2KB
of RAM memory to store the current map, about half of the 4K we have available. Now we can actually do that!
Here's an example of tile mapping and level design in action. First we design the level, indicating where the ground, obstacles, platforms, and goals are. Each color index indicates a different type of object. These can also have different conditions applied to them when the player sprite makes contact, such as points earned when the yellow tiles are touched, or loss of health when the red tiles are touched. You can think of this as the equivalent of the graph paper version of the level.
Pixel art sprites have been created for each different surface or object type used in the level.
We can see the correlation between the indexed color tiles and their associated sprite artwork. Wherever a tile map of a particular color index is placed on screen, the associated sprite artwork will appear.
Here we see the level tile map with the sprite artwork applied.
In most cases here a 16x16 pixel sprite has been used -- the two exceptions are the pipe and flagpole which are both larger sprites.
You can think of the player's view of the game screen as the view through the player "camera". We have a level map that is larger that what we can view through this camera, and as the player moves, the camera moves with them, revealing new, unseen parts of the level.
Typically, the player sprite can move around a bit in the camera view, but most of the time, the camera stays centered on the player, as if it is tracking along on a camera dolly.
The image here is used to illustrate this idea, but note that in a simple 2D game there isn't really a 3D model of the game level inside the code!
A great way to give the illusion of depth in 2D games is through motion parallax, which is the phenomenon of distant objects appearing to move more slowly than nearer objects do, relative to a the changing viewpoint of a person or camera.
You can see this in effect by simply holding up one finger in front of your face and then moving your head from side to side. The finger will appear to move a lot while objects you can see in the background will move relatively little.
This phenomenon was used to great effect in early 2D animation by Walt Disney animation studios with the invention of the multi-plane camera system. Here's a great video explainer by Disney himself (note that the example is of a camera push, but the same applies for a horizontal dolly or crab shot):
Different game engines will handle motion parallax in various ways. There may be multiple depth planes, similar to the Disney example, or just two set planes for the background and the level. In this example from MakeCode Arcade, we can think of the level tile map as being fixed in place, while the camera, character sprite, and background dolly through the scene.
Note, this is only used to illustrate the concept, there is not a 3D model in the MakeCode Arcade game engine!
Now that we have the fundamentals down, let's create a side-scrolling platformer level tile map in MakeCode Arcade!