💾 Archived View for kelgors.me › posts › 2022 › tips-about-developing-game-for-gameboy.gmi captured on 2024-05-12 at 15:04:40. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-12-28)
-=-=-=-=-=-=-
2022-01-22
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".
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!
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.
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 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, // ... };
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, // ... };
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 }, //, ... };
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; }
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.
How Graphics worked on the Nintendo Game Boy
Tags: gameboy gbdk clang development gamedev game retro
Last-Updated: 2022-02-13