Game Boy Assembly Programming For The Modern Game Developer
Game Boy Assembly Programming For The Modern Game Developer
Martin Ahrnbom
This book and its source code is released under CC BY-NC-SA 4.0. You can
read more about it here: https://creativecommons.org/licenses/by-nc-sa/
4.0/.
3
4 CONTENTS
7 Practical aspects 65
7.1 Debugging in BGB . . . . . . . . . . . . . . . . . . . . . . . . . 65
7.2 Emulator versus real hardware . . . . . . . . . . . . . . . . . . 65
8 Final notes 67
9 Version history 69
Chapter 1
What is the purpose of this book? This book attempts to explain the entire
process of making homebrew Game Boy games, from idea to finished ROM
file. It explains how Game Boy’s assembler programming works in a way that
is hopefully understandable for modern programmers.
5
6 CHAPTER 1. ABOUT THIS BOOK
full games. This book tries to explain things detailed enough to get people
started in making actual games, but without too many details to bore the
reader. It is not intended as a complete documentation, and it only briefly
mentions many things that I do not consider necessary to make games. For
example, things that are implemented in the GingerBread library (which was
made alongside this book and used many times) are usually not explained in
further detail than how to use the functionality in the library. Once the reader
is finished with this book, they should have the skills and understanding to
look for more information on topics that might interest them. Because of this,
I sincerely believe this book wastes the reader’s time as little as possible.
There are two main reasons for writing this book.
The first is to help people who want to make Game Boy games. I had
dreams of making my own Game Boy games when I was six years old, and
20 years later I made that dream a reality. By writing this, I hope I can help
others do the same for themselves.
The second reason is for preservation purposes. The art and craft of pro-
gramming assembler is becoming increasingly rare as it is not often required
for modern programming tasks. By keeping this knowledge alive, we can
better understand and appreciate the games made for the Game Boy, by dis-
assembling their code and understanding it directly, or by making games of
our own and comparing with commercial releases from that era.
The Game Boy stands out among other retro consoles as an attractive
platform to develop for. Many people have Game Boys that still work, thanks
to their build quality. It is possible to obtain relatively cheap reproduction
cartridges that can be reflashed, allowing for a distribution of the game that
works on real hardware. And the Game Boy is an iconic and nostalgic piece
of gaming history.
I do not know every detail about Game Boy development. I have made
a complete Game Boy game, and this book is based on the experience and
knowledge I gathered when making it. It is my opinion that the currently
existing literature doesn’t explain the topic well enough (either leaving out
far too much, or by including far too many unnecessary details and assuming
too much previous knowledge) that I believe that this book should be useful
to many, even if there might be some errors, or some important things left
out. If you have any suggestions for improvements, please contact me at
[email protected].
There is a project called "Awesome Game Boy Development", available
at https://github.com/gbdev/awesome-gbdev which tries to gather as much
1.3. ASSEMBLER VS C 7
1.3 Assembler vs C
When making Game Boy games today, a developer has two main options:
either one can write the game in ASM, or in C. While C might seem easier,
in that the GBDK compiler takes care of a lot of "gotchas" that you have to
worry about yourself in ASM, and in C you’ll much more quickly get a simple
working ROM to start with. But as soon as one starts going beyond that,
the weakness of C becomes obvious, especially from a pedagogical standpoint.
The C language is designed from the ground up to hide processor implementa-
tion details, to allow a single piece of code to work on multiple processors. You
still need extensive knowledge of the Game Boy CPU’s limitations, but coding
in C does nothing to inform the programmer of those limitations. Sooner or
later, one will write a line of code that would work perfectly on any modern
computers, but it simply cannot run on the Game Boy’s limited CPU (or, at
the very least, not fast enough), and the compiler won’t give any warnings or
errors. This is extremely counterintuitive, but unavoidable when coding in C.
The C language is actively trying to hide information that you need, making
it your enemy instead of your ally (at least from a pedagogical perspective).
Furthermore, emulators support ASM debugging, but not C, and it is impos-
sible or difficult to use existing (compiled) games’ code as references without
knowledge in ASM.
Even if one has decided to write a game in C, some knowledge of ASM is
still very helpful. Perhaps this book can be of use in such cases as well.
Another argument for using ASM is that it’s more authentic in the sense
that it’s the way games were made back in the day. Knowing ASM helps
preserving a part of gaming history that would otherwise risk getting lost in
time.
The downside of using ASM is mainly that the counterintuitive syntax
tends to result in less readable code.
In this book, the Rednex Game Boy Development System (RGBDS) is
used for all ASM syntax and examples. It is available as open source at this
link and works on most modern computers: https://github.com/rednex/
rgbds It creates Game Boy ROM files that can be played either on an emulator
8 CHAPTER 1. ABOUT THIS BOOK
9
10 CHAPTER 2. BASICS OF GAME BOY ASSEMBLY
cropped from that and displayed on the screen. Moving this viewport around
is used for creating scrolling effects.
There are significant limitations to the graphics, like the number of sprites
that can be displayed at once on screen. Many games, especially early ones,
have flickering sprites as a result of trying to display more than possible on
screen.
The Game Boy has limited sound capabilities, but can still produce pleas-
ant music and sounds if used correctly. There are four audio channels: two
square wave channels, a white noise channel and a customizable wave shape
channel. The latter can be used to produce sampled audio, but this is used
sparsely because of the high CPU usage and storage requirements.
The Game Boy has eight inputs: four directions on the D-pad, and A, B,
Start and Select buttons.
subtract_b_by_12 :
sub b , 12 ; doesn 't work , the CPU has no such command
ret
What this function does is to copy (or load, which is what ld stands for)
the value in the B register to the A register, then perform the subtraction,
and then copy the value back to B. The reason for this is, as mentioned, that
"complex" operations like subtraction only work on the A register. As you
see, we have to think a lot about where data is, as different operations work on
different positions of data. We do not, however, have to worry about the kind
of data we’re dealing with, since the CPU really only supports 8-bit integers,
so it’s implicitly assumed for all commands used in this piece of code that
that’s what we’re working with. The CPU also support 16-bit integers for
some operations, but data types never really get more complicated than that.
Because we are using the A register in the code above, any pre-existing
data there will be overwritten. This is also good to mention in a comment,
to make sure the caller is aware of this. If such behaviour is unwanted, it can
be fixed (see Chapter 2.3, regarding the push and pop commands).
What if we want the number we’re working with to be stored in RAM
instead? ASM doesn’t have automatic memory management like Python, so
we need to know where in RAM the number should be stored, via a memory
address. A memory address is a 16-bit number, and each possible number
refers to a single slot in memory, and each such slot holds a single 8-bit
number.
Each register holds a single 8-bit integer, so to store a memory address
which is 16-bit, we need two registers. When two registers are used as a pair,
2.2. INTRODUCTION TO THE CPU 13
their values are treated like a single 16-bit number and the Game Boy CPU
has some basic operations that work with 16-bit numbers this way, like for
reading from memory from a given address stored in two registers. It should be
stressed that the Game Boy’s CPU doesn’t really operate on 16-bit numbers,
as only very simple operations work on them (that’s why it’s considered an
8-bit CPU) but things like storing a memory address in two registers does
work.
For operations using 16-bit numbers, two registers are used as a pair. Only
certain pairs are allowed: BC, DE, HL and AF (the F register is special, see
Chapter 2.4). One such operation that works with 16-bit numbers is copying
(loading) data from a memory address (to access that stored in RAM or on
the game ROM). If we want to make the same function again to subtract 12
from a number, but this time we want the input number to be defined by a
memory address, we again need to think about where this memory address
would be. A common practice is to provide a single 16-bit input on the H and
L registers, so let’s go with that. The output should be written to the same
memory address so we don’t have to specify an output register.
subtract_by_12 :
; Input and output on RAM , by address in HL
ld a , [ hl ] ; reads memory from address specified
; by the numbers in HL registers
sub 12
ld [ hl ] , a ; write output to RAM
ret
call subtract_by_12
; The number of anteaters in RAM is now 50
What this does is to load data from the memory address defined in the HL
registers and store it in A. Then 12 is subtracted, and the value is “returned”,
14 CHAPTER 2. BASICS OF GAME BOY ASSEMBLY
A final note on the ld operation: When reading and writing from memory
addresses, it is common to read/write many numbers in a row. For such
cases, special syntax allows you to increase of decrease the memory address
after reading/writing, in the same CPU command, for example ld [hl+], a
writes the content of A to whatever address HL is holding, and then increases
HL by one (as a single 16-bit number), and ld a, [hl-] reads from the address
specified in HL, stores the value in A, and finally decreases the value of HL
by one.
Two other common commands are inc and dec, which increment (increase
16 CHAPTER 2. BASICS OF GAME BOY ASSEMBLY
Another use for the stack is to move 16-bit numbers between register pairs.
push bc
pop de
18 CHAPTER 2. BASICS OF GAME BOY ASSEMBLY
Is equivalent to
ld d , b
ld e , c
(The latter is preferred from an optimization standpoint, as it executes
faster).
It is important that every push is followed by a pop at some point, to
avoid stack overflows, and messing with the Game Boy’s program flow (see
Chapter 2.4). It should be noted that when values are pushed to the stack,
they are copied and thus remain on the registers they come from.
some_function :
pop bc ; retrieve the saved data
; do stuff ...
ret
Because the pop bc will actually pop the execution address to BC, and
when the code reaches the ret it will interpret whatever data was stored
initially as an address and try to execute code there, which will result in a
crash or unexpected behaviour.
Another way to control program flow, is through the commands jp and
jr. These are often called goto in modern programming terminology, and are
2.4. PROGRAMMING STRUCTURE 19
used to jump from one place in code to another. Unlike call and ret, the stack
is not used to keep track of where you come from.
When you jump using the commands jp and jr, the position you jump
to should be specified by a label, just like with call. ASM does not require
that every label is connected to one or more ret commands (like a function in
modern programming languages would be) so you can create labels anywhere
you want in your code, and jump to that location. For example, the code
Start :
ld a , 3
jr End
Middle :
add 2
End :
add 5
Will result in A holding the number 8, because the middle is skipped. If
the line jr End was removed, then A would hold 10, as code execution simply
moves from one label to the next unless you tell it otherwise. The labels are
simply names given to the next line in code.
The difference between jp and jr is that the latter is a relative jump,
meaning that you cannot jump further than 127 positions away (since how far
you jump is internally stored as an 8-bit signed integer). From an optimization
standpoint, jr is preferred whenever possible.
All of the operations call, ret, jp and jr support conditional arguments.
This allows something equivalent to simple "if-statements". The syntax is as
follows (in the case of jp):
jp z , some_position_in_code
jp nz , some_position_in_code
jp c , some_position_in_code
jp nc , some_position_in_code
The z, nz, c and nc decide the condition to be fulfilled for the jump com-
mand to work, and those conditions are based on the state of the special F
register (the one that is paired with the A register in 16-bit commands). The
F register does not allow you to write data to it directly, instead it stores some
information about previous commands’ results. Each bit in the F register has
special meaning and these bits are called "flags". The two important bits are
called z and c (the latter should not be confused with the C register!). The
z-flag, which stands for "zero", will be set (meaning it holds a value of 1) if
20 CHAPTER 2. BASICS OF GAME BOY ASSEMBLY
the result of the previous computation was exactly zero, and unset (meaning
it holds a value of 0) if the last operation had some non-zero result. The
c-flag, which stands for "carry", will be set if the last operation had an "over-
flow", meaning that the result was larger than 255 or less than 0 (in which
case the number simply rolls over). For example, if A holds 200, and you run
add 100, the result in A will be 45 and the c-flag will be set, because the result
(200+100=300, which is larger than 255, and since the 8-bit register can only
hold numbers between 0 and 255, when it reaches 256 it will instead have 0,
and continue adding the remaining 45 onto that). The z-flag will be unset,
because the result was not exactly zero. Another example is if A had the value
10, and we execute sub 15, then the result in A would be 250 and the z-flag
would be unset and the c-flag would be set. If A had the value 10, and we
run sub 10, then the result would be 0 in A, and the z-flag would be set, and
the c-flag would be unset. The cp command works the same as sub, except
that it doesn’t actually save the result in A, allowing it to be used to compare
numbers: the z-flag will be set if the numbers are identical, while the c-flag
will be set if the argument is greater than the number in A. For example, the
following code calls do_something if and only if A holds the number 2:
cp 2
call z , do_something
If you want to call do_something if and only if A holds any number except
for 2, do
cp 2
call nz , do_something
Where nz means "not z-flag", that is that the z-flag is unset. Similarly,
nc can be used for checking if the c-flag is unset.
2.5 Loops
The Game Boy doesn’t have any real built-in operations for loops, but they’re
fairly easy to program yourself. If the number of iterations is known in ad-
vance, this number should be stored somewhere, typically in the B register,
and every iteration the number is decremented, and the z-flag is checked to
see if the number reached zero, in which case the loop should end.
For example, let’s say we want to have a function that multiples a number
by 11. Let’s assume, for simplicity, that the input number is small enough so
2.6. OPTIMIZATION 21
that the end result will fit in an 8-bit number and not have to worry about
overflows. A simple loop implementation, which simply adds the number to
itself eleven times, is as follows:
mult_by_eleven :
; Input and output on A
. loop :
add c ; add input onto A
dec b ; decrease loop counter
jr nz , . loop ; if didn ' t reach zero , go back
Notice how the label .loop starts with a dot. This indicates that the label
is local, in the sense that it is only valid until the next line where a new global
label appears (a label without a dot at the start). This allows every function
to have its own sublabel called loop so that you don’t have to come up with
unique names for every time you want to make a loop, for example.
Another thing to note is that if we remove the line ld b, 10 then we have
made a function for multiplying arbitrary numbers, with input on A and B.
Such a function could actually be useful, although it should be noted that the
implementation isn’t always efficient (see Chapter 2.6).
2.6 Optimization
This section will not fully explore all aspects of Game Boy code optimization,
which is a complicated topic, but instead give a basic understanding of what
aspects code can be optimized in, and how it differs from modern program-
22 CHAPTER 2. BASICS OF GAME BOY ASSEMBLY
ming. On the Game Boy, the primary bottleneck is rarely CPU execution time
or RAM usage. Instead, a common issue is that code takes up too much space
on the ROM. All ASM code is divided into sections, and these are stored in
divisions of the ROM called banks. Since each bank has a finite size, there’s
also a limit to how large the sections can be. If sections get too big, they
might need to be split up into smaller sections and/or be placed in different
banks. It is not quite trivial to call or jump to code from one bank inside code
in another bank (how to do this will be covered in Chapter 4.2) so keeping
the code small in size saves some work. In addition, the hardware cartridges
that one could write the game onto also has a finite size, further motivating
keeping the code as small as possible. This is in strong contrast to modern
game development, where the final size of the resulting binary as a result of
changes in the code itself (as opposed to graphical and audio resources) is
rarely a concern.
Often, code that takes up less space also executes quickly. One example
is the jr operation which is both more compact and executes faster than jp.
More examples will be seen in the next chapter.
also means that if you later decide to change the position, you only have to
change a single line.
EQUS works very similarly to EQU but it works with strings (text). See
Chapter 4.3.
A similar symbol is SET, which works exactly the same as EQU except that
multiple values can be set to it, at different times. It can be thought of as a
compile-time variable. For example, the code
xor a ; makes A hold 0
X SET 5
add X
X SET 7
add X
X SET 3
add X
would compile to
xor a
add 5
add 7
add 3
It is possible to use the symbols IF, ELSE and ENDC to create compile-time
if-statements. They can look like so:
X SET 5
IF X > 3
add 5
ELSE
add 3
ENDC
compiles to
sub c
24 CHAPTER 2. BASICS OF GAME BOY ASSEMBLY
sub c
sub c
sub c
sub c
sub c
sub c
sub c
sub c
sub c
If multiple lines are code are inside the loop, this can however be bad from
an optimization standpoint, as the compiled code can become large. A loop
as defined in Chapter 2.5 can be significantly more compact.
Note that compile-time symbols do not have access to runtime variables.
So you cannot, for example do REPT c to try to loop as many times as the
integer stored in the C register, because the value is not known at compile-
time. Loops, as defined in Chapter 2.5, can do such things, however.
The most powerful compile-time command is the macro. It provides a
function-like way of writing code, for example:
AddTwoNumbers : MACRO
ld a , \1
add \2
ENDM
When this is written into the code, no actual code will be placed there
by the compiler. It does however let you write, anywhere after the macro
definition, something like
AddTwoNumbers 5 ,7
which would then compile to
ld a , 5
add 7
This can be extremely useful, for when you do want to repeat code across
multiple places, perhaps with minor changes. Macros allow you to define such
code only once.
Some caution should be taken when using macros; they might look like
they provide a more "modern" programming style, as macros allow for ex-
ample input arguments, but it is important to understand what the macros
actually do, as it can be bad from an optimization standpoint, in some cases.
2.7. COMPILE TIME OPERATIONS 25
Every time a macro is called, it generates new code, which takes up space
in the ROM. If a macro is called many times with the same arguments, it
is probably better to create a run-time function instead, as that would only
exist once in the code, saving space.
A special symbol \@ can be used, for when labels are used inside a macro.
One example would be if one were to write a macro which contains a run-time
loop, like so:
ld a , \1 ; Store X in A
ld b , (\2 -1) ; Store Y -1 in B
ld c , a
. loop \ @ :
add c ; Add X
dec b
jr nz , . loop \@
pop bc
ENDM
Here, the \@ will generate unique label names every time the macro is
used; if we only called the local label .loop there might be a conflict, for
example if the macro is used twice in a row like so:
MultiplyNumbers 5 ,4
MultiplyNumbers 6 ,3
Which would compile to code containing two .loop labels, which would
lead to a compiler error.
This chapter only provides a basic understanding of the more commonly
used symbols and macros. For more detailed information, check the RGBDS
manual available here: https://rednex.github.io/rgbds/ (information about
macros and symbols can be found under "RGBASM language description").
26 CHAPTER 2. BASICS OF GAME BOY ASSEMBLY
dec b
jr nz , . loop
Such tables are used to store graphics, audio, etc. in our ROM files. Note
that the data is stored alongside the code, which means that, if we are not
careful, the CPU might try to execute the data as if it were code, which will
crash or lead to unexpected behaviour. This is easily avoided by making sure
that the command above the data table is something like a jump or return,
so that code execution never moves into the table itself. For example,
ld b , 6 ; some code here
jr MoreCode ; without this line ,
; the CPU will think that the table
; data is code to run , and likely crash
MoreCode :
; Here we want to use the table or whatever
ld a , [ SomeTable ]
The previous chapter focused on the Game Boy CPU and how to control
it. To make a game, you need to control other pieces of hardware as well,
responsible for graphics, audio and interpreting user input. That will be the
subject of this chapter, as well as going through some practical details of how
to work with the RGBDS compiler. By the end of it, you should be able
to compile a Game Boy ROM that can be loaded into an emulator or real
hardware, and start making an actual game.
A good Game Boy emulator for game development is BGB, which is avail-
able here: http://bgb.bircd.org/. It has good features for debugging, and
accurately emulates most of the Game Boy’s quirks.
29
30 CHAPTER 3. MAKING GAME BOY GAMES
Now, we can use the symbol BGPAL to refer to this memory address, when-
ever we want to change the background palette. The background tile palette
basically allows us to swap the four colours that the Game Boy displays, al-
lowing some neat effects like fading the screen to black or white "smoothly"
by changing the colours multiple times, each time making the screen either
darker or brighter. By writing a single 8-bit number to this address, we spec-
ify all four colours at once, each being specified by two bits. The default
palette is %11100100, which means that the colours that is usually the darkest
(the leftmost two bits) should actually be the darkest (%11), while the dark-ish
3.4. SECTIONS 31
colour (the 3rd and 4th bits, from the left) should be dark-ish (%10) and so
on, with %00 being the brightest colour. For example, doing
ld a , %00011011
ld [ BGPAL ] , a
will invert the colours on the background tiles, so that parts that are
usually bright are now dark and vice versa, while
ld a , %11110000
ld [ BGPAL ] , a
will make it so that the background tiles only use two colours, either black
or white (so that the parts that were before dark-ish are now completely dark,
and the bright-ish parts are now at the brightest), like a "high contrast" mode.
Note that the address $FF47 is defined in the GingerBread library as
BG_PALETTE, not BGPAL.
3.4 Sections
Any ASM code you write for RGBDS needs to be placed in some section. A
section is defined by a line that can look like this:
SECTION " Some ␣ stuff " , ROM0
or
SECTION " Some ␣ other ␣ stuff " ,ROMX , BANK [1]
The main difference between the two is where in the ROM the code will
appear, the ROM0 is another name for bank 0, which is a special part of the
game ROM which is always accessible, where the "main code" usually lies, to
some extent, while the second line stores the data in Bank 1. The non-zero
banks can be changed, and only one non-zero bank is accessible at the time.
Each bank can only contain a limited number of bytes of compiled code or
game data. This system allows the game to be quite large (up to something
like 8 MB) even though only a small part of the 16-bit address space is reserved
for pointing at game code. The downside is that the developer needs to take
care of bank switching manually, to make sure the right data is available when
needed. This will be covered in more detail in Chapter 4.2, as it is a slightly
more advanced topic. For now, we will assume that all of our code fits in the
0 and 1 banks.
32 CHAPTER 3. MAKING GAME BOY GAMES
64 kiB, 2 means 128 kiB and so on up until 8, which means 8 MiB. If the
RGBDS compiler complains about there not being enough space for the ROM
file, increase this number. But also make sure than any physical carts used
contain sufficient space for the game.
RAM_SIZE: Specify the size of save RAM on the cartridge, where 0 means
no save RAM, 1 means 2 kiB, 2 means 8 kiB, 3 means 32 kiB, 4 means 128
kiB and 5 means 64 kiB. See Chapter 3.6. Make sure any physical carts used
for the game contain sufficient amounts of save RAM.
An example of setting up a game called SNAKE with Super Game Boy
support, but no Game Boy Color support, with a 128 kiB ROM and 2 kiB of
save RAM is as follows:
GAME_NAME equs " SNAKE "
SGB_SUPPORT equ 1
; GBC_SUPPORT equ 1 ; Note that this line is commented out ,
; setting it to 0 does not disable GBC support
ROM_SIZE equ 2
RAM_SIZE equ 1
INCLUDE " gingerbread . asm "
These lines should be near the top of the .asm file for your game. Make
sure GingerBread is only included once.
3.7 Interrupts
One way the Game Boy handles timing is via something called interrupts. An
interrupt means that, at some special time, the CPU gets interrupted and
stops doing whatever it was doing before, and jumps to a special address in
the game’s code. At the same time, it pushes its the previous position in
code onto the stack, so that it’s possible to return there after dealing with the
interrupt.
Interrupts can be enabled and disabled via the special CPU commands
ei (for "enable interrupts") and di (for "disable interrupts"). After running
a di command, there will be no interruptions in the CPU’s execution until it
runs an ei command, or the special command reti which is a combined ret
and ei command. While one interrupt is being processed, any other inter-
rupts are disabled, so interrupt handling code which does nothing is simply a
reti command; it returns to wherever code was executing before and enabled
interrupts again, in a single call.
34 CHAPTER 3. MAKING GAME BOY GAMES
3.8 RAM
The main place to store your own run time data is in the Game Boy’s RAM.
It occupies the addresses between $C000 and $E000 and can thus store 8 kB of
data. There can also be RAM inside of cartridges, stored in $A000 up to $BFFF
(although the entire space might not be usable, depending on how much RAM
there is in the cart). There is no real data allocation system when coding in
ASM using RGBDS, and it will be up to you to ensure that you read and write
only to addresses within this space. To help with that, you can give specific
positions in RAM names. If you want to store multiple values, simply let the
name point to the first one. The rest can then be accessed via addition. An
example is the following:
Position EQU $C200 ; 2 values
SpeedVector EQU Position +2 ; 2 values
CharacterHP EQU SpeedVector +2 ; 1 value
CharacterMP EQU CharacterHP +1 ; 1 value
Therefore, there exists a special syntax for dividing RAM into sections. For
example,
SECTION " Some ␣ variables ␣ in ␣ RAM " , WRAM0 [ $C200 ]
Position : DS 2
SpeedVector : DS 2
CharacterHP : DS 1
CharacterMP : DS 1
works the same way as above. Just note that a new section has to be
created afterwards for game code, as no code can be placed in RAM sections.
The operation DS simply reserves space, with the number afterwards specifying
the number of bytes to allocate.
Note that GingerBread reserves some space in RAM, for sprite graphics
(see Chapter 3.10) and to accomodate for GBT-Player (see Chapter 4.4), so
usable RAM for your game starts at $C200 (as defined as USER_RAM_START in
GingerBread).
To display the image, you need to first copy the image tile data onto
the tile section of VRAM, and then copy the map data to the map data
section of VRAM. There are GingerBread functions for this. For example, the
function mCopyVRAM can be used for copying data from the game’s ROM onto
VRAM. Note that standard copying functions, simply using ld many times,
will generally not work when writing to VRAM because of timing issues; it’s
only possible to write to VRAM at specific times when the PPU is not busy.
The PPU is the Game Boy’s variant of a GPU. VRAM-safe functions will
wait for these moments to write their data. If you are making a game and the
graphics end up garbled (but you can still see some resemblance of what it was
supposed to look like) this is quite likely the problem. This only applies when
the PPU is turned on; it’s possible to programmatically turn it off temporarily
which might be a good idea if large amounts of data is to be copied to VRAM.
An example of using mCopyVRAM from gingerbread is
ld hl , example_tile_data
ld de , TILEDATA_START
ld bc , example_tile_data_size
call mCopyVRAM
3.9. GRAPHICS: BACKGROUNDS 37
For this function, the 16-bit value in HL should be the address to where the
data should be copied from, and DE should contain the address to where the
data should be copied to (assumed to be somewhere in VRAM, the constant
TILEDATA_START is defined in GingerBread and points to the start of the VRAM
part where tiles should be defined), while BC contains the length of the data
to be copied, as a 16-bit integer.
Map data is copied similarly, but some care must be taken when using
the GBTDG tool if the image is smaller than the 256 × 256 rendering surface
(32 × 32 tiles). The GBTDG writes the map data in order, from left to right,
line by line, and doesn’t mark where one line ends and another begins. If all
the map data is copied straight to the map data storage in VRAM, parts of
the image will be placed outside of view and the graphics will end up garbled
(but all tiles will look correct, they’ll just be in the wrong place). To solve
this, one can right a simple macro like
; Macro for copying a rectangular region into VRAM
; Changes ALL registers
; Arguments :
; 1 - Height ( number of rows )
; 2 - Width ( number of columns )
; 3 - Source to copy from
; 4 - Destination to copy to ( assumed to be on VRAM )
CopyRegionToVRAM : MACRO
I SET 0
REPT \1
ld bc , \2
ld hl , \3+( I *\2)
ld de , \4+( I *32)
call mCopyVRAM
I SET I +1
ENDR
ENDM
The macro simply copies map data line by line. It can be used like
CopyRegionToVRAM 18 , 20 , example_map_data ,
BACKGROUND_MAPDATA_START
38 CHAPTER 3. MAKING GAME BOY GAMES
ld hl , example2_tile_data
ld de , TILEDATA_START + example1_tile_data_size
ld bc , example2_tile_data_size
call mCopyVRAM
To tell the Game Boy PPU which tiles to place where, it’s necessary to
write to VRAM using VRAM-safe writing operations. Because sprite data
is often updated every frame, waiting before writing every single byte can
end up taking too long. Therefore, a technique called DMA (Direct Memory
Access) is used to quickly copy a larger amount of data during the short time
windows when VRAM is writable. This is a bit technical and involved, so it
won’t be described in further detail in this book (a good summary is available
here, for the interested reader: https://gbdev.gg8.se/wiki/articles/OAM_
DMA_tutorial). This technique is implemented in GingerBread. By writing
sprite data to RAM positions starting at the address defined as SPRITES_START,
GingerBread makes sure to copy these values onto VRAM every frame.
The format of the data is quite simple. Each sprite is made up of 4 bytes:
first the Y position, then the X position, then the tile number and finally
an options byte. The positions are from 0-255, allowing the sprites to move
freely across the 256 × 256 rendering surface. The tile number is a bit special
when working with 8 × 16 sprites: all the tiles are divided into pairs of two,
and only a pair can be used as a sprite, with the one on the lower address
appearing above the other one. When selecting a tile number, it doesn’t
matter which one from the pair one chooses. To confirm the tile numbers, a
VRAM viewer in an emulator can be helpful (see Chapter 7.1). Finally, the
options byte contains 8 options, one bit each. The highest four bits correspond
to rendering priority (allowing some sprites to be placed on top of others, in
case they overlap), flipping along the y-axis and flipping along the x-axis,
and finally choosing between two sprite palettes (on non-GBC systems). The
remaining four bits are used only by the Game Boy Color, if the game is
running in GBC mode (see Chapter 6).
; bit 7: unused ,
; bits 6 -4: sweep time ,
; bit 3: sweep frequency increase / decrease ,
; bits 2 -0: number of sweep shifts
SOUND_CH1_START EQU $FF10
; Channel 4 ( noise )
SOUND_CH4_START EQU $FF1F ; Not used
When referring to bit numbers, bit 7 is the most significant bit (the fur-
thest to the left, when written as for example %10101010 ), while bit 0 is the
least significant bit (furthest to the right).
Channel 3 is especially interesting, as it allows custom wave forms to be
played. Some games use this to play some fairly detailed sounds, like voice
samples. Be aware that this requires you to constantly write new wave form
data while the sound is playing, which uses up most of the Game Boy’s CPU
(which is why gameplay is typically inactive while such sounds are playing).
It is however possible to define your own wave form once (or rarely) and use
it for background music or sound effects, to give your game some more unique
audio. The wave form should be written to addresses $FF30-$FF3F (defined as
SOUND_WAVE_TABLE_START and SOUND_WAVE_TABLE_END in GingerBread).
44 CHAPTER 3. MAKING GAME BOY GAMES
Chapter 4
45
46 CHAPTER 4. MORE INVOLVED TOPICS
It can be both read and written, but needs to be activated before use (and
should be deactivated after use, see Chapter 4.5).
$C000-$CFFF contains general purpose RAM which is physically located
inside the Game Boy itself. It is non-switchable, and often called Bank 0
of WRAM. It can both be read and written. Note that GingerBread re-
serves $C000-$C1FF, so usable RAM for your game starts at $C200 (as defined
as USER_RAM_START in GingerBread).
$D000-$DFFF contain bank 1 of WRAM, unless the game runs in GBC mode
in which case it is switchable with banks 1-7. Like $C000-$CFFF, it is general
purpose, and can both be read and written.
$E000-$FDFF is a copy of $C000-$DDFF and is usually not used. Not all emu-
lators emulate this behaviour.
$FE00-$FE9F contains data for sprites, like which tiles they should look like
and where they should be placed, see Chapter 3.10. It can both be read and
written, but only at certain times.
$FEA0-$FEFF does not contain anything meaningful.
$FF00-$FF7F contains many different things related to different hardware,
like graphics, audio, the link cable, buttons and so on. It contains both read-
only parts, write-only parts and parts that can both be read and written.
$FF80-$FFFE contains the so-called High RAM, or HRAM, which is special
because it can be used with DMA transfers (unlike WRAM) which is why it is
often used for keeping sprite data. Therefore it should probably not be used
for general purpose data, unless you know what you are doing. It can both
be read and written.
Finally the address $FFFF is used for specifying which interrupts the game
should use (see Chapter3.7)
number is decimal form to begin with. That means that, for example, the
number 13 should be stored as $13 (which really means 19, but for the purpose
of displaying it on screen, that doesn’t matter). That way, each 8-bit number
contains two decimal numbers, and to display them, we can divide the 8-bit
number into two 4-bit numbers and for each add them to the tile number
of the 0 character, to find which tile number we should draw. The Ginger-
Bread function RenderTwoDecimalNumbers does this. The GingerBread function
RenderFourDecimalNumbers works similarly but for 16-bit numbers, which give
decimal numbers with four digits.
One thing we need to be aware of when working with decimal numbers
like this is that normal numerical operations no longer work as expected. If
you store the decimal number 9 as $09, and then increase it with an add
command, like add 2, then it will become $0B which is not a valid decimal
number. The CPU command daa fixes this, turning $0B into $11 in this case
(working only on the A register). The daa command only works as expected
if used immediately after the commands add, adc, sub or sbc.
Some more examples:
ld a , $39
add $12 ; A now has $4B , but we want $51
daa ; A now has $51
call RenderFourDecimalNumbers
For drawing text, one needs to be aware of how RGBDS handles strings.
If you define a string like
db " This ␣ is ␣ an ␣ ASCII ␣ string "
then by default, RGBDS will store the ASCII values of those characters as
numbers. This might not be what you want, since most likely your character
tiles will not be placed exactly according to the ASCII table (for example,
your character ’A’ might not be at tile number 65) making it cumbersome to
figure out what tiles to draw, in order to draw the string. To fix this, RGBDS
contains a command called CHARMAP which lets you map characters to numbers.
For example
CHARMAP "A", 1
CHARMAP "B", 2
CHARMAP "C", 3
CHARMAP " < heart >" , 4
CHARMAP "␣", 0
AbbaCabText :
DB " ABBA ␣ CAB < heart >"
will write 1,2,2,1,0,3,1,2,4 to the ROM. Note that the CHARMAP commands
need to appear before the text definitions in the code. Also note that it is not
possible to use a single CHARMAP command to specify the entire alphabet; one
CHARMAP command is needed for each character.
If one defines one character to mark the end of a piece of text, and use
the tile number of some non-text tile, like the following:
CHARMAP " <end > " , 150
then one can use the GingerBread function RenderTextToEnd, which will au-
tomatically stop writing when this character appears. Otherwise, RenderTextToLength
can be used, with a pre-set number of characters. For example, the following
code renders the text defined above:
ld b , 10 ; Number of characters
ld c , 0 ; Draw to background
ld d , 5 ; X position
ld e , 6 ; Y position
ld hl , AbbaCarText ; Address of text start
call RenderTextToLength
50 CHAPTER 4. MORE INVOLVED TOPICS
TextWithEndCharacter :
DB " ABA ␣ CACABA < end >"
Note that the multiplication is performed at compile time (the Game Boy
CPU does not support multiplication).
4.4. AUDIO: MUSIC 51
Then, before every halt call in your code, it is necessary to call the function
gbt_update to make sure GBT-Player keeps playing notes and advancing the
music.
Note that every call to functions from GBT-Player (like gbt_play and
gbt_update) changes the current ROM bank (see Chapter 4.2), so you may
need to change the ROM bank back afterwards. This is especially problematic
if the code that calls these functions are not placed in bank 0. The simplest
solution is probably to define a function on bank 0 which calls gbt_update,
executes halt and then switches to a ROM bank depending on the value in
some register. The example game contains the following function for this
purpose:
52 CHAPTER 4. MORE INVOLVED TOPICS
pop af
pop bc
ld [ ROM_BANK_SWITCH ], a
ENDC
ret
The Super Game Boy is a fascinating piece of hardware history, worthy of its
own chapter in this book.
The Super Game Boy cartridge contains the hardware for an actual Game
Boy inside it, except for the screen and speaker, and some electronics for con-
necting it to the Super Nintendo. The Super Nintendo’s CPU and the Game
Boy’s CPU are thus running at the same time, and are mostly independent
of each other. The Game Boy hardware sends images and sound to the Super
Nintendo, which mostly just presents these on the TV. What makes the Super
Game Boy so interesting is that a Game Boy game designed for this hard-
ware configuration can perform specific commands to control how the Super
Nintendo should display the image, create Super Nintendo sounds that play
over the game’s audio and specify a border image to be displayed around the
game play window on the TV.
Using the Super Game Boy can get quite technical. All the details are
in the CPU Manual and Pan Docs, so this chapter will focus on using the
functionality included in GingerBread.
All Game Boy games that run on the original DMG model will also work
on the Super Game Boy, without any additional code, but this results in a
somewhat bland experience with the default borders and color palettes. This
chapter will explain how to customize the experience. In order to activate
Super Game Boy functionality, it is necessary to specify that in the game’s
header. GingerBread takes care of this, as long as SGB_SUPPORT is defined before
GingerBread is loaded.
53
54 CHAPTER 5. SUPER GAME BOY FEATURES
To send commands to the Super Game Boy, the memory address $FF00.
This address is usually used to reading the D-pad status, but by writing to it,
the Super Game Boy can read commands sent from the game. Because many
games will use interrupts that react to button presses, interrupts should be
disabled whenever messages are sent to the Super Game Boy.
Only the bits at positions 4 and 5 of $FF00 are read by the Super Game
Boy. These positions are often called P14 and P15. By setting both P14 and
P15 to zero, the Super Game Boy starts listening to data, and then setting
P14 to zero while P15 is one will send a 0, and setting P14 to one and P15
to zero will send a 1. The GingerBread function SGBSendData implements this,
with reasonable delays to give the Super Game Boy time to read the messages.
Basically, the transfer of data from the game to the Super Game Boy is a bit
cumbersome and quite slow.
When sending larger amounts of data to the Super Game Boy, special
commands allow the Super Game Boy to read graphics from the screen and
interpret as data. This is used for sending border images, for example, which
would take far too long to send over the P14/P15 channel.
Gingerbread contains functionality for the most common uses of the Super
Game Boy, setting up color palettes for the game’s display as well as presenting
a border image around the game’s display. These functions are made to try
to hide the (rather complicated) details of how communication between the
Game Boy and Super Nintendo works, and hopefully make these features
relatively easy to implement.
Note that setting to zero instead of one will still include the SGB support;
to not include it, remove the line altogether (or comment it out).
This will make GingerBread include SGB functions. This takes up addi-
tional space on Bank 0, so if a game compiles fine without SGB support, but
fails to compile with it, try moving code and assets to other banks. Also, on
the topic of banks: Some SGB functionality depends on data and functions
stored on Bank 1. So always change to Bank 1 before calling SGB function-
5.2. PALETTES 55
5.2 Palettes
There are no built-in functions in GingerBread specifically for the purpose
of specifying the palettes to be used for the gameplay visuals on the SGB.
Instead, it is necessary to write the SGB command binaries into the game,
and send them to the SGB with the SGBSendData function.
The SGBSendData function assumes that the HL registers contains a memory
address pointing to a table where the SGB command exists. Each SGB com-
mand is exactly 16 bytes long (some commands need to be split up into several
commands, but SGBSendData does not automagically understand this, so it is
then necessary to use it multiple times for each 16 bytes). The exact specifica-
tion for all possible SGB commands (not limited to those used for defining the
palettes) are available here: https://gbdev.io/pandocs/SGB_Functions.html
The rest of this section will focus on some of the commands that are likely
to be useful for specifying the palettes to be used during gameplay.
The palettes used to colorize the game visuals only apply to whole tiles
of the screen (again, a tile means a non-overlapping 8 × 8 pixel square re-
gion) and this is regardless of what is displayed on those tiles. This means
that the SGB, unlike the Game Boy Color, cannot give sprites different col-
ors than backgrounds. Therefore, games on the SGB tend to not look very
colorful, unless they have many static interface elements like health meters,
score counters and so on. The palettes can change during the course of the
game, but the process is too slow for, for example, dynamically changing the
palettes wherever the main character is, to give it a different palette from the
background.
There are four palettes which can be used to colorize the game window,
numbered from zero to three. To define the colors in the palettes, use the
PAL01 and PAL23 commands. PAL01 defines palettes zero and one, while PAL23
defines palettes two and three. An example of a PAL01 command is as follows:
SGBPalettes01 : ; Specifies the colors of palettes 0 and 1
DB %00000001 ; PAL01 command (%00000) , length one (%001)
DB %11111111 ; Color 0 ( for all palettes ), % gggrrrrr
DB %01111111 ; Color 0 ( for all palettes ), %0 bbbbbgg
56 CHAPTER 5. SUPER GAME BOY FEATURES
%001).
By default, the entire gameplay screen is filled with palette 0. To use
different palettes on different parts of the screen (for example, static visual
elements like a health bar could have a different color scheme from the rest
of the visuals), use the following commands: ATTR_BLK (which specifies a rect-
angular region with different palettes inside, outside and on the border of the
rectangle), ATTR_LIN (which specifies the palettes on a horizontal or vertical
line across the entire screen), ATTR_DIV (which splits the screen in two, either
vertically or horizontally, with different palettes on the left/above the line,
on the line, and to the right/below the line) or the ATTR_CHR (which sets the
palette for only one tile).
An example of ATTR_BLK, which sets the entire screen to palette 2, is as
follows:
SGBPal2Everywhere : ; Tells the SGB to use Palette 2 everywhere
; ( used for title and game over )
DB %00100001 ; ATTR_BLK (%00100) , length one (%001)
DB 1 ; Number of blocks we send
DB %00000100 ; Set the value " outside "
; the block ( doing this with a small
; block means setting the entire screen )
DB %00101010 ; Which palettes to set
58 CHAPTER 5. SUPER GAME BOY FEATURES
; inside (%10) ,
; on the border (%10)
; and outside (%10)
DB 0 ; X1 coordinate
DB 0 ; Y1 coordinate
DB 0 ; X2 coordinate
DB 0 ; Y2 coordinate
DB 0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ; Zero - padding
An example of ATTR_DIV, which sets palette 1 on the top line of the screen
and palette 0 everywhere else, is as follows:
SGBPal01Div : ; Tells the SGB to draw the
; top horizontal line with palette 1,
; and palette 0 everywhere else
DB %00110001 ; ATTR_DIV command (%00110) , length one (%001)
DB %01010100 ; Zero - padding (%0) ,
; horizontal split (%1) ,
; palette on the line (%01) ,
; palette above the line (%01) ,
; palette below the line (%00)
DB 0 ; Y - coordinate
DB 0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ; Zero - padding
They are used as follows:
ld hl , SGBPal2Everywhere
call SGBSendData
and
ld hl , SGBPal01Div
call SGBSendData
respectively.
Basically, if the image does not pass through the script, use fewer unique
tiles or fewer unique colors.
To use the SGB border created by the script, include it into your game
with
INCLUDE " sgb_border . inc "
SGBBorderTransferMacro SGB_VRAM_TILEDATA1 ,
SGB_VRAMTRANS_TILEDATA1 ,
SGB_VRAMTRANS_GBTILEMAP
SGBBorderTransferMacro SGB_VRAM_TILEDATA2 ,
SGB_VRAMTRANS_TILEDATA2 ,
SGB_VRAMTRANS_GBTILEMAP
SGBBorderTransferMacro SGB_VRAM_TILEMAP ,
SGB_VRAMTRANS_TILEMAP ,
SGB_VRAMTRANS_GBTILEMAP
call SGBUnfreeze
ret
60 CHAPTER 5. SUPER GAME BOY FEATURES
The Game Boy Color and Game Boy Advance (for the rest of this chapter,
those models will be, somewhat incorrectly, collectively referred to as "Game
Boy Color" or "GBC") are capable of colorizing games in a more detailed
and natural manner than the Super Game Boy. Most importantly, the GBC
has eight palettes for background tiles, and eight palettes for sprites. Since
the palettes for tiles and sprites are separate, foreground objects can be more
distinct from their backgrounds, something that is difficult to do on the SGB.
Furthermore, the number of palettes allow for many more colors to be visible
on screen at once.
The color palettes use the same format as they do on the SGB (see Chap-
ter 5) but do note that the GBC mixes colors differently from the SGB so
palettes copied from SGB code may need to be altered if they are to look
similar.
In GingerBread, the GBCApplyBackgroundPalettes and GBCApplySpritePalettes
functions can be used to set the palettes. They both take as input an address
on HL, pointing to a table of color palettes, the starting position to write to
on A and the number of bytes to write on B. Each color is two bytes (just
like on the SGB) and there are four colors to each palette. So, for example,
to write the first two palettes (palette 0 and 1), set A to 0 and B to 16. To
write palette 5 only, set A to 40 and B to 8. Make sure HL points to a table
of suitable length.
An example is
61
62 CHAPTER 6. GAME BOY COLOR FEATURES
GBCBackgroundPalettes :
DB %11111111 ; Color 0, palette 0, % gggrrrrr
DB %01111111 ; Color 0, palette 0, %0 bbbbbgg
DB %11100011 ; Color 1, palette 0, % gggrrrrr
DB %00011111 ; Color 1, palette 0, %0 bbbbbgg
DB %11100001 ; Color 2, palette 0, % gggrrrrr
DB %00001001 ; Color 2, palette 0, %0 bbbbbgg
DB %00000000 ; Color 3, palette 0, % gggrrrrr
DB %00000000 ; Color 3, palette 0, %0 bbbbbgg
DB %11111111 ; Color 0, palette 1, % gggrrrrr
DB %01011110 ; Color 0, palette 1, %0 bbbbbgg
DB %01111111 ; Color 1, palette 1, % gggrrrrr
DB %00001100 ; Color 1, palette 1, %0 bbbbbgg
DB %01001111 ; Color 2, palette 1, % gggrrrrr
DB %00000100 ; Color 2, palette 1, %0 bbbbbgg
DB %00000011 ; Color 3, palette 1, % gggrrrrr
DB %00000000 ; Color 3, palette 1, %0 bbbbbgg
SetupGBC :
GBCEarlyExit ; No need to execute pointless code
; if we ' re not running on GBC
ld hl , GBCBackgroundPalettes
xor a ; Start at color 0, palette 0
ld b , 16 ; We have 16 bytes to write
call GBCApplyBackgroundPalettes
ret
The GBCEarlyExit macro, which does a ret if the game is not running on a
GBC, is not technically necessary, as running GBC-specific commands on an
older model does nothing. The colors in the above palettes are green-ish on
palette 0 and red-ish on palette 1.
Sprite palettes work similarly, using GingerBread’s GBCApplySpritePalettes
function. The main difference is that color zero of every palette is ignored, as
that color is always transparent.
Unlike the SGB, the GBC completely ignores the monochrome palette
choices (see Chapter 3.3) and thus any game which is converted to work in
GBC mode needs to add extra code to implement similar effects.
To choose which background tiles should use which palettes, use the
memory address $FF4F, called GBC_VRAM_BANK_SWITCH in GingerBread, to specify
63
SetupGBC :
GBCEarlyExit ; Needed to prevent garbage from
; being drawn on screen on GB
To choose which sprites should use which palettes, the last three bits of
the "options" byte in the sprite table (see Chapter 3.10) are used to define
which of the eight sprite palettes to use for each sprite.
Chapter 7
Practical aspects
65
66 CHAPTER 7. PRACTICAL ASPECTS
differs.
BGB can emulate both GBC and SGB quite accurately, which is useful to
test that the game runs as expected on all hardware. But it is a good idea to
test games on real hardware occasionally because some differences do exist.
Chapter 8
Final notes
Hopefully, this book has given the reader a broad look at the topic of making
Game Boy games in Assembler. For any topics not covered by this book,
the included links should provide to good starting point for further research.
Most importantly, the reader should (hopefully) be familiar enough with the
technology and terminology to easily understand such materials.
67
68 CHAPTER 8. FINAL NOTES
Chapter 9
Version history
69