Tips about developing game for Game Boy

The Game Boy

If like me, you have done a lot of personal projects, you may have explored the video game development part. For me, I developed a lot of them but few came out, either because the Game Design was bad, or because a lack of graphical materials. Luckily, there are alternatives for us poor people who lack artistic sense. The Game Boy is one of them and I will explain my adventure in this universe.

Today, I'm 32 years old and when I was young, I owned and used my Game Boy in particular on "Zelda: Link's awakening". I didn't realize it was a technological marvel. I'm not going to give you a 3 hours speech on the subject, I like this handheld console for its simplicity and its good design. Let's get to the heart of the matter, I can't wait!

The Game Boy is in my opinion a Game&Watch. The Game&Watch got a transparent screen on which certain parts of the screen can be activated or deactivated. The background is a wallpaper behind the screen. While the Game Boy has 3 data layers in VRAM: BKG, SPR, WIN. To make it short, it has a background layer and a window layer divided into tiles of 8x8 pixels and a sprite layer, the moving objects, rendered using 8x8px or 8x16px tiles which can move pixel by pixel. The Game Boy has a screen of 160x144 pixels which gives us a screen of 20x18 tiles of 8x8px. I could only recommend the "ModernVintageGamer" video on this subject: "How Graphics worked on the Nintendo Game Boy".

Languages

Development can be done roughly in three ways to date: in assembler, in C or with GB Studio. I tried to use GB Studio, telling myself that if I start doing C or ASM, I would forget everything in 2 months and if I use tools well thought out by people who are without any doubt much better than me on the subject, it would be smarter. I'm sure GB Studio is great but my passion in life is code, not configuration via an editor that brings you problems other than code.

Regarding the assembler, my mind is not formed in this sense and even if I learn it, I remain convinced that compilers would do a much better job than me, that's why I used C. You can form your own opinion on the question by trying, many resources are available on how to develop on Game Boy on this gbdev.io.

About the C, I used it 10 years ago to train myself personally in the code. After playing around with C# & XNA Framework quite a bit, I headed to C++ & SDL2. Looking back, I'm telling myself that I did not understand anything of what I was doing xD The language does not scare me, you have to be rigorous about malloc and free otherwise for the rest, it's a language like another (I'm simplifying).

So how do you develop on Game Boy in C? The answer: GBDK-2020. I really enjoyed coding with this library. You know sometimes, we have to code with this or that lib and sometimes it's a pain but then with GBDK-2020, besides my shortcomings in C, it was a pleasure!

Architecture

I'm not here to give you a tutorial on how to display a background and move the player, there are dozens of them on the net, no need to add one. Here I will get to the heart of the matter by thinking that you know how to display an image in the form of a tile, that you already have experience in independent video game design even if you have never published a game.

Memory organization

I just want to remind you that I do not hold the right word, I let you make up your own mind and your mistakes. One thing I only understood by fixing my mistakes: don't use malloc on the Game Boy, it's useless and you'll be surprised when the Game Boy tells you shit when you malloc too many times ( I had 3 of them in the code for small structs).

The Game Boy works with cartridges which, in their simplest device, contain just ROM memory. What I felt about the best approach is to start with your game having a state of 0 which is stored in the cartridge and then the Game Boy's RAM is used to store variables which will be used to modify the rendering of what is used from the cartridge.

You remember I told you about 8x8px tiles. Well, the image you see below is an assembly of tiles. So I defined an array that corresponds to my tileset and an array that corresponds to the mapping of the tileset on the screen. It's all stored in ROM.

You can see that we have a switch at the top right, if the player activates it, we must be able to change its state. It is this state that will be stored in the console's RAM. When I load the map, if this state is 1, I change the tile otherwise I do nothing.

I remind you, the Game Boy has 8KB of RAM memory is both a lot and a little, it is better to work with the ROM and some variables than with everything variable. We can have plenty of ROM. It waqs my first mistake. How does this translate to C? With the const keyword.

Let's see some code.

The tileset used for each map of the level

The Game Boy only has four colors and the pixels are defined as 4 by 4 in each value.

// assets/laboratory/tileset.c
const unsigned char LaboratoryTileset[] =
{
  0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
  0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
  0x00,0xFF,0x00,0xFF,0x00,0xFF,0x00,0xFF,
  0x00,0xFF,0x00,0xFF,0x00,0xFF,0x00,0xFF,
  0x00,0xFF,0x00,0xFF,0x10,0xEF,0x00,0xFF,
  0x04,0xFB,0x20,0xDF,0x00,0xFF,0x00,0xFF,
  0x00,0xFF,0x42,0xBD,0x00,0xFF,0x00,0xFF,
  0x00,0xFF,0x08,0xF7,0x00,0xFF,0x00,0xFF,
  0x00,0xFF,0x04,0xFB,0x00,0xFF,0x00,0xFF,
  // ...
};

The on-screen tile mapping table

These are simple indexes corresponding to the tileset.

assets/laboratory/laboratory1.c
#define MapLaboratory1Width 20
#define MapLaboratory1Height 18
const unsigned char MapLaboratory1[] =
{
  0x29,0x29,0x29,0x29,0x29,0x29,0x29,0x29,0x29,0x29,
  0x29,0x29,0x29,0x29,0x29,0x29,0x29,0x29,0x29,0x29,
  0x29,0x29,0x29,0x29,0x29,0x29,0x29,0x29,0x29,0x29,
  0x29,0x29,0x29,0x29,0x29,0x29,0x29,0x29,0x29,0x29,
  0x29,0x29,0x33,0x41,0x41,0x41,0x41,0x41,0x41,0x41,
  // ...
};

Definition of laboratory maps

Completely custom code to organize the maps. As much as the maps & tilesets, you can give them raw to GBDK, as much here, it's my personal code.

maps/laboratory.c
// {direction}MapIndex : Index of joins between maps (on the right it leads to index 14, at the bottom to index 2, 0 is ignored)
// .entities : the table of entities and their default values
// .data : Corresponds to the tiles table seen previously
const Map LABORATORY_BOARDS_LIST[LABORATORY_BOARDS_LIST_SIZE] = {
  {
    .width = MapLaboratory7Width,
    .height = MapLaboratory7Height,
    .topMapIndex = 9U,
    .rightMapIndex = 0U,
    .bottomMapIndex = 0U,
    .leftMapIndex = 6U,
    .animatedTiles = {
      { .x = 0U, .y = 0U }, { .x = 0U, .y = 0U }, { .x = 0U, .y = 0U },
      { .x = 0U, .y = 0U }, { .x = 0U, .y = 0U }, { .x = 0U, .y = 0U },
      { .x = 0U, .y = 0U }, { .x = 0U, .y = 0U }, { .x = 0U, .y = 0U },
      { .x = 0U, .y = 0U }, { .x = 0U, .y = 0U }, { .x = 0U, .y = 0U },
      { .x = 0U, .y = 0U }, { .x = 0U, .y = 0U }, { .x = 0U, .y = 0U }
    },
    .entities = {
      { .type = SWITCH, .spriteId = 0U, .x = 16U * 8U, .y = 3U * 8U, .tiles = { 0U, 0U }, .data = { 0x00U, 0U, 0U } },
      { .type = NULL, .spriteId = 0U, .x = 0U, .y = 0U, .tiles = { 0U, 0U }, .data = { 0U, 0U, 0U } },
      { .type = NULL, .spriteId = 0U, .x = 0U, .y = 0U, .tiles = { 0U, 0U }, .data = { 0U, 0U, 0U } },
      { .type = NULL, .spriteId = 0U, .x = 0U, .y = 0U, .tiles = { 0U, 0U }, .data = { 0U, 0U, 0U } },
      { .type = NULL, .spriteId = 0U, .x = 0U, .y = 0U, .tiles = { 0U, 0U }, .data = { 0U, 0U, 0U } },
      { .type = NULL, .spriteId = 0U, .x = 0U, .y = 0U, .tiles = { 0U, 0U }, .data = { 0U, 0U, 0U } }
    },
    .data = MapLaboratory7,
    .tileset = NULL
  },
  //, ...
};

The function that allows to load the Map and the entities in vram

Here in the case of a switch, we check if the state is not 0, in which case, we change its tile. setBkgVramByte is a method that changes the byte in VRAM without using gbdk functions. You could do the same with set_bkg_tile_xy(x, y, tile_index)

maps.c
void displayMapEntities(const Map *map)
{
  const Entity *entity;
  for (uint8_t i = 0U; i < MAPS_ENTITIES_SIZE; i++)
  {
    entity = &map->entities[i];
    if (entity->type == NULL) continue;
    switch (entity->type)
    {
      case SWITCH:
        if (switchesStates[entity->data[0U]])
        {
          setBkgVramByte(entity->x / TILE_WIDTH, entity->y / TILE_WIDTH, TILE_SWITCH_OFF);
        }
        break;
      // ...
    }
  }
}
void loadMap(const Map *map)
{
  if (currentMap)
  {
    // switch to previous map bank
    switchRomBank(loadedMapBankLevel);
    hideMapEntities(currentMap);
  }
  // display bkg data
  switchRomBank(currentLevelBank);
  set_bkg_tiles(0U, 0U, map->width, map->height, map->data);
  // show sprites
  displayMapEntities(map);
  // preload vram address of animated bkg tiles
  uint8_t iVramAnimatedSprites = 0U, i = 0U;
  for (; i < MAPS_ANIMATED_TILES_SIZE; i++)
  {
    if (map->animatedTiles[i].x == 0U && map->animatedTiles[i].y == 0U) break;
    vramAnimatedSprites[iVramAnimatedSprites++] = getBkgVramAddr(map->animatedTiles[i].x, map->animatedTiles[i].y);
  }
  // reset other vram address to NULL
  for (; iVramAnimatedSprites < MAPS_ANIMATED_TILES_SIZE; iVramAnimatedSprites++)
  {
    vramAnimatedSprites[iVramAnimatedSprites] = NULL;
  }
  currentMap = map;
  loadedMapBankLevel = currentLevelBank;
}

ROM Banking

To make it short, the Game Boy is an 8-bit console, it has a memory addressing limit so you can't access all the ROM you want. In embedded systems, ROM Banking is a well-known concept. What does it mean ? If you have 1MB of ROM, you can only access 8Kb at a time. Basically, ROM Banking, you change the index of the bank you want to access. Bank 1 brings you between 8KB & 16KB of your 1MB ROM knowing that bank 0 is permanently loaded. Well, it's not more complicated than that in the case of the Game Boy. Then, you have to know how to juggle between each bank intelligently.


This is what I can tell you first, there were lots of other things that happened during development but these two things are really what made me waste my time. I could also talk to you about how I generated the code for maps & tilesets with recent tools. This may be a subject in the future.

References

Last-Updated:

Content also available on gemini://kelgors.me/posts/2022/tips-about-developing-game-for-gameboy.gmi

More info about Gemini protocol