Drawing Large Maps in GBDK

The Gameboy’s background layer is 256×256 pixels in size. This equates to 32×32 tiles. For many games, this is a sufficient size. However, some games may require maps larger than that default background size. For this, GBDK-2020 has the “set_bkg_submap” and “set_bkg_based_submap” functions.

This page will explain those functions, how they work, and the “large map” example that comes with GBDK 2020.

gbdk large maps example demo

You can learn more about drawing backgrounds in my tutorial: Drawing on the Background and Window Layers

The Background vs The Camera

Something key to remember, the actual background layer never moves. The “move_bkg” function, “scroll_bkg” function, and SCY_REG/SCX_REG registers actually change the camera position.

  • If the camera moves to the left, the objects in the camera’s view appear to move to the right.
  • If the camera moves to the right, the objects in the camera’s view appear to move to the left
  • If the camera moves upwards, the objects in the camera’s view appear to move downwards
  • If the camera moves downwards, the objects in the camera’s view appear to move upwards.

Note: This means that objects move in the direction opposite of the values of “scroll_bkg

Another key thing to remember: The camera view wraps around on all sides.

For example: If the camera moves to the right, eventually the right-side of that camera view will show tiles from left side of the background layer.

large maps example camera scrolling diagram

Updating the camera view

The “large map” example calls a function called “set_camera” whenever the camera changes position. In this function the “set_bkg_submap” and “set_bkg_based_submap” functions will help us with scrolling.

These 2 functions are variants of the “set_bkg_tiles” and “set_bkg_based_tiles” functions. They are mostly “useful for scrolling implementations of maps larger than 32×32 tiles” (straight from the documentation). They both take one additional parameter, the width of the tilemap (in tiles). This parameter helps GBDK know how to grab tiles from different rows.

The key to scrolling large maps using these functions to change background layer tiles before/when they wrap into camera view.

Changing tiles only when necessary.

Despite being over 30 years old, the gameboy’s processor can run at 60fps.Therefore, to avoid slowing the game down, we shouldn’t try to change the background layer tiles each frame.

The “large map” example uses the “map_pos_y” and “map_pos_x” variable to save the top/left cell of the camera view. In addition, the previous values are saved in the “old_map_pos_y” and “old_map_pos_x” variables.

When the camera is being updated, we’ll re-calculate the values of “map_pos_x” and “map_pos_y“. To get these values, we simply divide the camera’s y and x positions by 8.

Remember: Multiplication/Division by powers of 2 is the same as bit-shifting by that same power. Except the bit-shifting is faster. Dividing by 8 is the same as shifting all the bits in a number rightward 3 places.

void set_camera() {
    
    ...

    // up or down
    map_pos_y = (uint8_t)(camera_y >> 3u);
    if (map_pos_y != old_map_pos_y) { 
        
        ... Copy tiles from the tilemap

        old_map_pos_y = map_pos_y; 
    }
    // left or right
    map_pos_x = (uint8_t)(camera_x >> 3u);
    if (map_pos_x != old_map_pos_x) {

        ... Copy tiles from the tilemap

        old_map_pos_x = map_pos_x;
    }
    
    ...
}

After getting these values, the “large map” example will compare them with their “old” counterparts. If they are different, then the a new column/row has come into the camera view. Two things must be done in that case:

  1. Copy tiles from our tilemap to the proper column/row of the background layer. For this we’ll use the “set_bkg_submap” or “set_bkg_based_submap” functions.
  2. Update our “old_map_pos_x” and/or “old_map_pos_y” variables. setting their values to equal “map_pos_x” and/or “map_pos_y” respectively.

By updating the “old” map values, our “submap” functions won’t be called again immediately.

Using the “submap” functions to copy tiles from our Tilemap

When the time comes to update our background layer, we can finally make use of our “submap” functions.

  • For horizontal movement, we’ll update a 19-tile column on the left or right of the screen.
  • For vertical movement, we’ll update a 21-tile row on the top or bottom of the screen

NOTE: The Gameboy screen is only 20×18 tiles in size. We update 1 more tile in each direction to avoid blank tiles, columns, or rows.

Which direction is the camera moving in?

One question remains however: Which direction was the camera moving? For this question, the “large map” example has two variables called “old_camera_x” and “old_camera_y“. These variables are updated (using their non “old_” counterparts) at the end of each call to “set_camera“.

void set_camera() {
    
    ...
    
    // set old camera position to current camera position
    old_camera_x = camera_x, old_camera_y = camera_y;
}

Using all the camera position variables (current and their “old” counterparts), we can know how the camera is moving horizontally and/or vertically.

Vertical Movement

gbdk large maps example vertical
  • If “old_camera_y” is greater than (below) “camera_y“, then the camera is moving upward. In this case, we want to update the top row of tiles.
  • If “old_camera_y” is lesser than (above) “camera_y“, then the camera is moving downward. In this case we want to update the bottom row of tiles.
void set_camera() {
    
    ...

    // up or down
    map_pos_y = (uint8_t)(camera_y >> 3u);
    if (map_pos_y != old_map_pos_y) { 
        if (camera_y < old_camera_y) {
            set_bkg_submap(map_pos_x, map_pos_y, MIN(21u, bigmap_mapWidth-map_pos_x), 1, bigmap_map, bigmap_mapWidth);
        } else {
            if ((bigmap_mapHeight - 18u) > map_pos_y) set_bkg_submap(map_pos_x, map_pos_y + 18u, MIN(21u, bigmap_mapWidth-map_pos_x), 1, bigmap_map, bigmap_mapWidth);     
        }
        old_map_pos_y = map_pos_y; 
    }

    ...
}

Horizontal movement

gbdk large maps example horizontal
  • If “old_camera_x” is greater than (to the right of) “camera_x“, then the camera is moving to the left. In this case, we want to update the right column of tiles.
  • If “old_camera_x” is smaller than (to the left of) “camera_x“, then the camera is moving to the right. In this case we want to update the left column of tiles.
void set_camera() {
    
    ...

    // left or right
    map_pos_x = (uint8_t)(camera_x >> 3u);
    if (map_pos_x != old_map_pos_x) {
        if (camera_x < old_camera_x) {
            set_bkg_submap(map_pos_x, map_pos_y, 1, MIN(19u, bigmap_mapHeight - map_pos_y), bigmap_map, bigmap_mapWidth);     
        } else {
            if ((bigmap_mapWidth - 20u) > map_pos_x) set_bkg_submap(map_pos_x + 20u, map_pos_y, 1, MIN(19u, bigmap_mapHeight - map_pos_y), bigmap_map, bigmap_mapWidth);     
        }
        old_map_pos_x = map_pos_x;
    }
    
    ..
}

Staying in bounds

There are two important bound checks for both horizontal and vertical movement. When moving to the right or downward, we need to make sure the camera hasn’t exceeded the right and/or bottom edge of the tilemap.

We don’t want to copy more tiles than we have left in the current column/row. This means:

  • For horizontal movement, we want to copy a column of 19 tiles from the tilemap to the left/right edge of the camera view. However, Once the right edge of the camera view reaches the right-edge of the tilemap we’ll have less and less tiles to copy.
  • For vertical movement, we want to copy a row of 21 tiles from the tilemap to the top/botom edge of the camera view. However, Once the bottom edge of the camera view reaches the bottom-edge of the tilemap, we’ll have less and less tiles to copy.

Both of these checks make sure not to copy more tiles than we actually have. The gameboy and GBDK don’t have internal restraints for this manner. Without the checks you can end up writing the wrong data into the wrong place. Which can and will cause you many headaces.

The Rest of the Example

The above section “Updating the Camera” explains the “set_camera” function. That function is the core of this explanation. For clarity’s sake, here’s a brief explanation of the rest of the example:

Setup

In the example’s main function we perform some common operations:

  • Turn on the display
  • Put background tiles in VRAM
  • Show the initial background
  • Default all our camera variables
void main(){
    DISPLAY_OFF;
    SHOW_BKG;
    set_bkg_data(0, 241u, bigmap_tiles);

    map_pos_x = map_pos_y = 0; 
    old_map_pos_x = old_map_pos_y = 255;
    set_bkg_submap(map_pos_x, map_pos_y, 20, 18, bigmap_map, bigmap_mapWidth);
    DISPLAY_ON;
    
    camera_x = camera_y = 0;
    old_camera_x = camera_x; old_camera_y = camera_y;

    redraw = FALSE;

    SCX_REG = camera_x; SCY_REG = camera_y; 
    
    ... Our Game Loop
}

The Game Loop

The game loop simply moves the camera_x and camera_y variables in the direction you press. When either changes, the camera is updated (by calling the “set_camera” function.


void main(){
    
    ...

    while (TRUE) {
        joy = joypad();
        // up or down
        if (joy & J_UP) {
            if (camera_y) {
                camera_y--;
                redraw = TRUE;
            }
        } else if (joy & J_DOWN) {
            if (camera_y < camera_max_y) {
                camera_y++;
                redraw = TRUE;
            }
        } 
        // left or right
        if (joy & J_LEFT) {
            if (camera_x) {
                camera_x--;
                redraw = TRUE;
            }
        } else if (joy & J_RIGHT) {
            if (camera_x < camera_max_x) {
                camera_x++;
                redraw = TRUE;
            }
        } 
        if (redraw) {
            wait_vbl_done();
            set_camera();
            redraw = FALSE;
        } else wait_vbl_done();
    }
}

That’s it! You can find more information about GBDK on the official documentation or in my How to make Games for Gameboy tutorial series. If you found this helpful please share on Social Media, it helps The Junkyard grow. If you have any questions, critique, or comments, leave a comment below. Your input help The Junkyard grow better by teaching me (Larold) how to clearly explain things.