-
Notifications
You must be signed in to change notification settings - Fork 12
/
gbasmdev.tex
1490 lines (1079 loc) · 109 KB
/
gbasmdev.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\documentclass[11pt]{book}
\usepackage[a5paper,left=1.5cm,right=1.5cm,top=2.5cm,bottom=2.5cm]{geometry}
\usepackage{url}
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{inconsolata}
\usepackage{listings}
\usepackage[table,xcdraw]{xcolor}
\usepackage[final]{pdfpages}
\usepackage{hyperref}
\usepackage{breakurl}
\title{Game Boy Assembly Programming for the Modern Game Developer}
\author{Martin Ahrnbom}
\begin{document}
\maketitle
\lstdefinelanguage{gbasm}
{
sensitive=false,
morecomment=[l]{;},
morestring=[b]"
}
\lstset{language=gbasm,basicstyle=\footnotesize\ttfamily,commentstyle=\color{purple}}
\lstnewenvironment{code}{}{}
\lstMakeShortInline{\|}
\noindent \small{This book and its source code is released under CC BY-NC-SA 4.0. You can read more about it here: \url{https://creativecommons.org/licenses/by-nc-sa/4.0/}.
~\\
\noindent The book is available in source code here: \url{https://github.com/ahrnbom/gbapfomgd}.
~\\
\noindent
The book is available for download for free here: \url{https://github.com/ahrnbom/gbapfomgd/releases}.
}
\tableofcontents
\chapter{About this book}
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.
\section{Who is it for?}
This book assumes that the reader is familiar with game development on some other platform. It also assumes experience with some modern programming language. When comparing assembler code to "modern" code, the latter will be written in Python, so basic understanding of Python's syntax helps. Such examples will however be quite few.
\section{Why write this?}
There is plenty of documentation on how to make Game Boy games, including Nintendo's own programming manual (available here \url{https://archive.org/download/GameBoyProgManVer1.1/GameBoyProgManVer1.1.pdf}), and the Pan Docs (available here \url{https://gbdev.io/pandocs/}). Such sources however expect the reader to already be familiar with 80s style assembler programming. There are also guides that try to explain the basics of assembler programming, without going into sufficient level of detail to allow the user to make 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 programming 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 disassembling 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 \url{[email protected]}.
There is a project called "Awesome Game Boy Development", available at \url{https://github.com/gbdev/awesome-gbdev} which tries to gather as much resources as possible related to Game Boy development, emulators etc. If you are looking for something that you cannot find in this book, chances are you can find it there.
\section{Assembler vs C}
\label{asmvsc}
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 implementation 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 impossible 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: \url{https://github.com/rednex/rgbds}
It creates Game Boy ROM files that can be played either on an emulator or on real Game Boy hardware (given the right equipment).
\chapter{Basics of Game Boy Assembly}
\label{basics}
\section{Brief description of the hardware}
\label{hardware}
The Game Boy CPU is an 8-bit Sharp LR35902. It is quite similar to the z80 processor which was common in the 1980s, but it's not identical. The z80 has some more advanced instructions, so if you see z80 ASM code you want to use for your game, it might need some modifications. The Game Boy CPU runs at around 4 MHz (or 8 MHz on the Game Boy Color's high speed mode). This is incredibly slow by today's standards, and the CPU has such a limited amount of supported operations that it's quite easy to know most of them by heart, which makes it relatively easy to program for, at least in ASM.
The Game Boy has 8 kB of RAM, and additional RAM may be available inside certain game carts. There's also 8 kB of dedicated VRAM for storing graphics.
The screen has a resolution of 160x144, supporting 4 colors (darkest, dark, light, lightest) on the original Game Boy and the Game Boy Pocket and Light, or many more colors on the Super Game Boy and Game Boy Color (although the way these are colored is completely different, see Chapters~\ref{sgb} and \ref{gbc}).
The graphics are made out of background tiles and foreground sprites, letting the sprites move arbitrarily around over the background. The Game Boy actually renders a 256x256 pixel surface, and then a 160x144 section is 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 pleasant 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.
\section{Introduction to the CPU}
\label{cpu}
One of the most important core concepts in ASM programming is registers. A register is a small memory storage inside the CPU, and in the case of the Game Boy CPU, each register hold one 8-bit number; an integer in the range of 0-255. The operations that the CPU can perform work on the numerical data currently stored in those registers, and the results of the computations are also stored in registers afterwards.
There is one "main" register which is called A. The most "complicated" operations that the CPU supports, like addition and subtraction, work only on the A register. Other registers, like B, C, D, H and L (more on them and their names later) support only simpler operations like incrementing and decrementing (addition and subtraction by one) and nothing more complex than that. Therefore, those registers are used more for temporary storage of data, while the A register contains whatever data you are currently working with, in some sense.
One of the main differences in how you think about programming in ASM compared to modern languages is that in ASM, you need to care a lot about where data is, and less about worrying about what kind of data it is. This is easily shown by example. Let's consider the following python function, which does the simple job of subtracting 12 from an integer number.
\begin{code}
def subtract_by_12(x):
return x-12
\end{code}
It can then be called like this:
\begin{code}
num_anteaters = 62
num_anteaters = subtract_by_12(num_anteaters)
# now the number of anteaters is 50
\end{code}
Here, we don't care at all about where the data (|num_anteaters| and |x|) is stored, but we care about what kind of data it is (it needs to be some kind of number to support subtraction).
Before we can try to translate this extremely simple function to ASM, we need to think about position. The input value |x|, where is it? A common design when a function takes a single input value, is that the input is assumed to be on the A register (the "main" register). The output of the function is similarly also commonly placed on the A register whenever the function only outputs a single number. If we follow these conventions, the equivalent ASM code would be
\begin{code}
; input and output on the A register
subtract_by_12:
sub 12
ret
\end{code}
Which can be called by
\begin{code}
ld a, 62 ; store the number of anteaters in A
call subtract_by_12
; There are now 50 anteaters, as A now holds 50
\end{code}
As one might guess, |sub| means subtraction and |ret| means return. Notice how similar this is to python! The main difference is that there is no |x| anywhere, because the |sub| operation only works on the A register, so it is implied that we work on the A register. |sub| writes its output to the A register, so there is no need to specify a return value; the calling code can simply read the A register after calling this function to access the "return value". Because the code itself doesn't explicitly state where the input and output are, it's a good idea to write this in a comment at the top of a function (comments start with a semicolon |;| in ASM).
If we would, for whatever reason, want the input and output to be on some other place, like the B register, we would need a slightly more complicated function. In modern code, you would expect to be able to just replace |A| with |B| in all places but that doesn't work here, because the |sub| function only works on the A register. So you \textbf{CANNOT} do something like
\begin{code}
subtract_b_by_12:
sub b, 12 ; doesn't work, the CPU has no such command
ret
\end{code}
Instead you have to do something like
\begin{code}
subtract_b_by_12:
; Input and output on B. Destroys any existing data on A.
ld a, b
sub 12
ld b, a
ret
\end{code}
Which is called like
\begin{code}
ld b, 62 ; Store the number of anteaters in B
call subtract_b_by_12
; Now the number of anteaters is 50, because B holds 50
\end{code}
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~\ref{common}, 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, 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~\ref{progstruct}). 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.
\begin{code}
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
\end{code}
Which is called like so, assuming that |NUM_ANTEATERS| is a constant 16-bit number which is a memory address on RAM where we store the number of anteaters:
\begin{code}
; There are 62 anteaters,
; so the number 62 is stored at address NUM_ANTEATERS in RAM
ld hl, NUM_ANTEATERS ; Stores the memory address
; in the H and L registers
call subtract_by_12
; The number of anteaters in RAM is now 50
\end{code}
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”, by writing it to the same memory address.
To summarize:
\begin{itemize}
\item In ASM, we worry more about where data is, and less about what kind of data it is
\item There's a kind of memory called registers, that hold numbers that the CPU operates on
\item The most complex operations work only with the A register, leading to moving data back and forth
\item 16-bit numbers are supported by working with pairs of registers. Can be used for storing memory addresses.
\end{itemize}
\section{The most common commands}
\label{common}
The Game Boy CPU is very limited by today's standards, so there are quite few operations it can do. Therefore, it's quite easy to learn the most important ones by heart. Once you do that, it should become significantly easier to understand how to write ASM code, since that problem boils down to expressing your intent in terms of those operations.
Probably the most common command is |ld|, which stands for "load". It copies integer data from one place to another. It can copy between registers, for example |ld e, b| which copies data from the B register to the E register. It can load constant values, like |ld c, 17| which writes the number 17 to the C register. You could equivalently write |ld c, $11| or |ld C, %00010001| in hex and binary, respectively (hex numbers always start with a dollar sign, and binary numbers always start with a percent sign; |11|, |$11| and |%11| mean 11, 17 and 3 respectively).
|ld| supports 16-bit numbers as well, allowing you to load a number onto a pair of registers. As briefly mentioned, only certain pairs of registers can be used this way: AF, BC, DE and HL. For example, |ld bc, $12AB| writes the number 4779 to the two registers B and C. This means that B would hold |$12| (which is 18 in decimal) and C would hold |$AB| (which is 171 in decimal). Basically, the first 8 bits go into the first register and the last 8 bits go into the last register. You cannot however copy data from a pair of registers to another pair, like |ld bc, de| but the stack can be used for this purpose (see below).
In addition, |ld| can load from memory addresses using special syntax: |ld [de], a| would copy the data from A onto a memory position specified by the address stored on DE. This does not work for other registers; for example, |ld [de], b| does not work. The same goes when the memory address is stored in BC. If the memory address is, however, stored in HL, you have more options for loading the data: |ld [hl], b| and |ld b, [hl]| work, and you can replace |b| with any single register.
A hardcoded address can be used as well, like |ld [$ff83], a| which writes the data stored in A to memory position |$FF83|. Similarly, data can be read from memory positions, for example by |ld a, [$ff83]|. For reading and writing into hardcoded memory addresses, the data must be written to and read from the A register, respectively.
A few things you \textbf{CANNOT} do:
\begin{code}
ld b, $1A2B ; a 16-bit number doesn't fit in a single register
ld ab, $1A2B ; AB is not a valid register pair
ld 5, a ; copying goes from right to left,
; cannot write to a constant
ld [hl], bc ; a memory address points to a single position
; holding a single 8-bit number (BC is 16-bit)
ld [h], 5 ; H register holds a 8-bit value,
; but addresses must be 16-bit
ld hl, bc ; copying 16-bit numbers from
; one register pair to another is not supported
ld [$1234], 255 ; Reading/writing to memory address only works
; to/from A, not some other value like 255
\end{code}
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 by one) and decrement (decrease by one) the value of registers, respectively. These are very flexible, and work on all the commonly used registers (A, B, C, D, E, H and L) as well as 16-bit register pairs (BC, DE and HL). The syntax is intuitive: |inc b| increments the 8-bit number in B while |dec DE| would decrement the 16-bit number in DE.
There’s also |add| and |sub|, which adds and subtracts arbitrary amounts to/from the A register only (for example, |add b| means that the number in A is increased by B). In the case of |add|, 16-bit addition is supported but only to the HL register pair (for example |add hl, bc| increases the 16-bit number on HL by the 16-bit number stored in BC). 16-bit subtraction is not supported, and it is also not supported to add/subtract an 8-bit number to/from a 16-bit number.
The Game Boy CPU also supports bitwise operations (AND, OR, XOR), all of which only work on the A register. These are applied bit-by-bit, with arguments being either another register or an 8-bit constant. For example, if A holds |%10101010|
and B holds |%11110000|,
then |and b| would produce |%10100000|
in A, |or b| would produce |%11111010|
in A, and |xor b| would produce |%01011010|
in A. Another example is |xor a| which always sets A to 0, regardless of previous values. This code is often preferred over the more readable |ld a, 0| for optimization reasons (see Chapter~\ref{optimize}).
The |cpl| command computes the binary complement of A (flips all bits, so that |%11001010| becomes |%00110101|). This command only works on the A register.
The “swap” command swaps the top four bits with the bottom four bits of any register (so |%11001010| becomes |%10101100|).
There are operations for so-called rotations and shifts. The operations |rlc| and |rrc| rotates any register to the left and right respectively, which means that each bit is moved one step left/right, looping around to the other side. For example, if the B register holds |%11001010|
before, then after a |rlc b| it will hold |%10010101|,
but if we ran |rrc b| instead, it would hold |%01100101|.
Note that special, faster commands exist for rotating the A register: |rlca| and |rrca|. These work almost the same, except that they reset the z-flag always, execute twice as fast, and take up half the ROM space (see Chapter~\ref{optimize}).
Shifts are similar to rotations, but do not "wrap around" and instead just pad with zeros. The commonly used commands are |sla| for shifting left, and |srl| for shifting right. For example, if B holds |%11000011|
before, then running |sla b| would result in |%10000110|
while |srl b| would result in |%01100001|.
Note that the shifting operations (sometimes called bit-shifts) are equivalent to multiplying/dividing numbers by two (rounding down, in the case of division).
Several CPU commands can be executed directly on memory addresses, without first loading the data into a register. For this to work, the memory address needs to be stored in the HL registers. Some examples include |swap [hl]|, |sla [hl]|, |sra [hl]|, |inc [hl]| and |dec [hl]|.
There exist a command called |cp| which is used to compare numbers, to see if they're equal or one is bigger than the other. More on this in Chapter~\ref{progstruct}.
The command |halt| puts the CPU to sleep for a while, to save battery life and can be used to keep the timing of the game. The game sleeps until the CPU reaches an interrupt (see Chapter~\ref{weinterruptthisprogramme}). If only VBlank interrupts are used, this means that the CPU sleeps until a 60 Hz timer activates.
The command |nop| does nothing (no operation). Because of a hardware bug, the command |halt| should always be followed by a |nop|.
There exists a built-in data structure inside the Game Boy, called the stack. Unlike "the stack" in languages like C/C++, it is used like an actual stack where you can typically only push data onto the stack and pop the top value, with no easy way to access any data except at the very top. A "pointer" (actually two special registers called SP, for "stack pointer") keep track of where the top of the stack is. The stack stores 16-bit numbers and is therefore often used to store addresses (more on this in Chapter~\ref{progstruct}) but it can also be used to temporarily store arbitrary data. The operations for this are |push| and |pop|, and they use register pairs as their arguments. For example, if we want to implement the |subtract_b_by_12| function from Chapter~\ref{cpu} so that the original value in A is preserved, we can implement it like so:
\begin{code}
subtract_b_by_12:
; Does not affect A. Input and output on B.
push af ; places the 16-bit number from AF onto the stack
ld a, b
sub 12
ld b, a
pop af ; takes the same 16-bit number
; back from the stack onto AF again,
; thus A is restored to its initial value
ret
\end{code}
Another use for the stack is to move 16-bit numbers between register pairs.
\begin{code}
push bc
pop de
\end{code}
Is equivalent to
\begin{code}
ld d, b
ld e, c
\end{code}
(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~\ref{progstruct}). It should be noted that when values are pushed to the stack, they are copied and thus remain on the registers they come from.
\section{Programming structure}
\label{progstruct}
As mentioned, it's possible to write functions in ASM, by writing a label, followed by a colon, and then the indented code. Such functions can be called using the command |call|, and a special command |ret| returns the execution to wherever it was when the |call| happened, to the line after the |call|. The Game Boy keeps track of where to return to by storing an address onto the stack, so every time you run |call|, the address of the next line to execute (a 16-bit number) is pushed to the stack, then the execution is moved to wherever the |call| command pointed, and when a |ret| command is reached, the stack is popped and execution is moved to the location stored on the stack. Because of this, you \textbf{CANNOT} do something like the following:
\begin{code}
push bc ; save this data for later
call some_function
; do more stuff
...
some_function:
pop bc ; retrieve the saved data
; do stuff...
ret
\end{code}
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 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
\begin{code}
Start:
ld a, 3
jr End
Middle:
add 2
End:
add 5
\end{code}
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|):
\begin{code}
jp z, some_position_in_code
jp nz, some_position_in_code
jp c, some_position_in_code
jp nc, some_position_in_code
\end{code}
The |z|, |nz|, |c| and |nc| decide the condition to be fulfilled for the jump command 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 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 "overflow", 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:
\begin{code}
cp 2
call z, do_something
\end{code}
If you want to call |do_something| if and only if A holds any number except for 2, do
\begin{code}
cp 2
call nz, do_something
\end{code}
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.
\section{Loops}
\label{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 advance, 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 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:
\begin{code}
mult_by_eleven:
; Input and output on A
push bc ; lets us use B and C registers
; without messing up existing data,
; as it is restored before we return
ld b, 10 ; the number of times we loop
; Since input is already on A, we only
; need to add it 10 times
ld c, a ; store the input in C
.loop:
add c ; add input onto A
dec b ; decrease loop counter
jr nz, .loop ; if didn't reach zero, go back
; If we got here, the loop is finished
pop bc ; restore whatever were on BC before
ret
\end{code}
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~\ref{optimize}).
\section{Optimization}
\label{optimize}
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 programming. 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~\ref{rombanks}) 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.
\section{Compile time operations}
\label{compiletime}
RGBDS supports symbols and macros, which allow the programmer to avoid repeating code, and in the best case can make code more readable. The simplest symbol is probably the |EQU| command, which allows the creation of compile-time constants. This is useful for, among other things, not having to remember memory addresses. For example, in this example back in Chapter~\ref{cpu}:
\begin{code}
ld hl, NUM_ANTEATERS ; Stores the memory address in HL registers
call subtract_by_12
\end{code}
In order for the compiler to know which address |NUM_ANTEATERS| is, it needs to be defined somewhere above that line. This is done by writing something like
\begin{code}
NUM_ANTEATERS EQU $FF13
\end{code}
Where |$FF13| is a position in the Game Boy's RAM. It is your job to make sure you don't use the same memory location for anything else (unless you know what you're doing), but at least you only have to define the position once, and can just refer to it as |NUM_ANTEATERS| anywhere else in the code. It 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~\ref{text}.
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
\begin{code}
xor a ; makes A hold 0
X SET 5
add X
X SET 7
add X
X SET 3
add X
\end{code}
would compile to
\begin{code}
xor a
add 5
add 7
add 3
\end{code}
It is possible to use the symbols |IF|, |ELSE| and |ENDC| to create compile-time if-statements. They can look like so:
\begin{code}
X SET 5
IF X > 3
add 5
ELSE
add 3
ENDC
\end{code}
which would compile to just |add 5|.
It is also possible to make repetitions, using the |REPT| and |ENDR| symbols. For example,
\begin{code}
REPT 10
sub c
ENDR
\end{code}
compiles to
\begin{code}
sub c
sub c
sub c
sub c
sub c
sub c
sub c
sub c
sub c
sub c
\end{code}
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~\ref{loops} 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~\ref{loops}, 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:
\begin{code}
AddTwoNumbers: MACRO
ld a, \1
add \2
ENDM
\end{code}
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
\begin{code}
AddTwoNumbers 5,7
\end{code}
which would then compile to
\begin{code}
ld a, 5
add 7
\end{code}
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 example 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. 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:
\begin{code}
; Multiply two numbers X and Y
; By adding X onto X, Y-1 times
MultiplyNumbers: MACRO
push bc
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
\end{code}
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:
\begin{code}
MultiplyNumbers 5,4
MultiplyNumbers 6,3
\end{code}
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: \url{https://rednex.github.io/rgbds/} (information about macros and symbols can be found under "RGBASM language description").
\section{Defining data}
\label{defdata}
When you write code, it will be compiled into simple numbers to be stored in the ROM file. It is possible to generate arbitrary numbers into the ROM file, which can be used for many things that are not necessarily game code to be executed on the CPU. Since the Game Boy CPU is so limited, it is often a good idea to precompute any complicated calculations, if possible, and store the results in a table in the ROM, where the Game Boy can simply read the results instead of trying to compute them. This can be useful for things like trigonometric functions (to have an object move like a sine-wave across the screen, for example). Computing a sine-wave in real-time on the Game Boy CPU would probably be a lot slower than just having a pre-computed one which can be read from the ROM.
To define such a table, it is a good idea to create a label at the top, so that the table can be referred to in code. There exist compiler commands for defining such data: |db| (to define data as bytes, 8-bit numbers) and |dw| (to define 16-bit numbers, called "words"; this has nothing to do with human words, typically stored as strings). For example
\begin{code}
SomeTable:
db 5,3,7
dw 1337
db "Hello"
\end{code}
will place the following 8-bit numbers in the ROM at that position: 5,3,7,5,57,72,101,108,108,111, because |1337|, as a 16-bit number, is written as |%0000010100111001|
which is divided into |%00000101|
(which is 5) and |%00111001|
(which is 57), and strings are simply stored as their ASCII-numbers (‘H’ is 72 in ASCII, and so on). There is no real distinction between data defined in different lines, the data is simply placed after the data from the previous line.
If we would want to use this table in our game, we could load the values like so:
\begin{code}
ld hl, SomeTable
ld b, 10
.loop:
ld a, [hl+]
; do something here... The first time,
; we will get 5 in A,
; the second 3 in A, and so on
dec b
jr nz, .loop
\end{code}
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,
\begin{code}
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
; Here we define some table
SomeTable:
db 1,2,3,4,5
MoreCode:
; Here we want to use the table or whatever
ld a, [SomeTable]
\end{code}
If you find yourself in a situation where an emulator complains that you are executing "invalid opcodes", it means that it is trying to execute some data as if it were code, but it isn't. One way this could happen is if there is no |jump| command before a data table. Another possibility is a bank switching error (see Chapter~\ref{rombanks}).
\chapter{Making Game Boy games}
\label{gbgames}
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 available here: \url{http://bgb.bircd.org/}. It has good features for debugging, and accurately emulates most of the Game Boy's quirks.
\section{About RGBDS}
\label{rgbds}
RGBDS is a Game Boy development system, and probably the compiler most commonly used today for making Game Boy games. It contains an assembler (which converts assembly code to binary), a linker (which converts one or more binaries into a Game Boy ROM file) as well as a "fixer" which fixes some common mistakes in the ROM file's header (like the checksum). You can find RGBDS and up-to-date install instructions for several operating systems are available here: \url{https://rgbds.gbdev.io}
\section{The GingerBread library}
\label{gingerbread}
The GingerBread library is an attempt to make a kind of standard library for developing Game Boy games using RGBDS assembler. It is written by the author of this book, to make game development a bit easier, and allows this book to skip some boring technical details. By relying on this library, this book can focus on understanding the important things and getting games up and running quicker. The library is open source (with the extremely permissive Unlicense license) and available here: \url{https://github.com/ahrnbom/gingerbread}.
Many examples in the chapter will make use of constants and functions defined in this library, and this will be pointed out every time, to make sure the distinction between RGBDS standard functionality and GingerBread is clear.
\section{Memory mapping}
\label{memmapping}
There is no official set of functions to control the various hardware inside the Game Boy, something like an API. Instead, memory mapping is used to directly control the hardware. When reading and writing to memory addresses, using the |ld| command, those addresses can point to plenty of different things, not just positions in RAM. By reading and writing special numbers to and from special addresses, we create graphics, interpret user input and produce sounds.
An easy example is the memory address |$FF47|, which controls the palette of background tiles. Since |$FF47| might be difficult to remember, it is usually given a name, like so:
\begin{code}
BGPAL equ $FF47
\end{code}
Now, we can use the symbol |BGPAL| to refer to this memory address, whenever we want to change the background palette. The background tile palette basically allows us to swap the four colours that the Game Boy displays, allowing 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 specify 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 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
\begin{code}
ld a, %00011011
ld [BGPAL], a
\end{code}
will invert the colours on the background tiles, so that parts that are usually bright are now dark and vice versa, while
\begin{code}
ld a, %11110000
ld [BGPAL], a
\end{code}
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|.
\section{Sections}
\label{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:
\begin{code}
SECTION "Some stuff",ROM0
\end{code}
or
\begin{code}
SECTION "Some other stuff",ROMX,BANK[1]
\end{code}
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~\ref{rombanks}, 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.
\section{The starting point}
\label{begin}
GingerBread contains very basic code for starting up the game. The library expects that the game defines a label called |begin| which is where the game's logic can start. Some things are necessary to do there, like calling |StartLCD| and |EnableAudio| (both GingerBread functions) but it is probably a good idea to load some graphics first (see Chapter~\ref{bakcgrounds}).
\section{The header}
\label{header}
A special part of the game ROM is called the header. This part contains information about the game, that the Game Boy needs to know about before it even begins running any code. It is used to specify what kind of cart the game runs on (and if you write the game onto a real physical cart, that cart must support the cart type specified in the ROM file), if any GBC/SGB features are used, etc. If any changes are made to the ROM header, it’s a good idea to run the game ROM in BGB (which will complain if there are any header errors) and then look at Right Click $\rightarrow$ Other $\rightarrow$ Cart Info, which will (hopefully) confirm how a Game Boy would interpret the header.
GingerBread will provide your game with a sensible default game header. There are some options which can be used to adjust the header settings. These options are changed by defining compile-time constants prior to importing the GingerBread library. The options are as follows:
|GAME_NAME|: Sets the name of the game. This is mostly for cosmetic reasons when running in emulators that may display it. It should be up to 15 characters, in capital letters. Since |GAME_NAME| is a string (text) the |EQUS| command should be used for defining it, while the other options are specified with |EQU| which works with numerical values.
|SGB_SUPPORT|: Makes the game support Super Game Boy functionality. By setting this to 1, GingerBread functions related to the Super Game Boy become available (see Chapter~\ref{sgb}) but this also makes the GingerBread library take up more space on Bank 0.
|GBC_SUPPORT|: Makes the game support Game Boy Color functionality. By setting this to 1, GingerBread functions related to the Game Boy Color become available (see Chapter~\ref{gbc}) but this also makes the GingerBread library take up slightly more space on Bank 0.
|ROM_SIZE|: Specify the size of the ROM file, where 0 means 32 kiB, 1 means 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~\ref{header}. 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:
\begin{code}
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"
\end{code}
These lines should be near the top of the .asm file for your game. Make sure GingerBread is only included once.
\section{Interrupts}
\label{weinterruptthisprogramme}
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 interrupts 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.
The GingerBread library listens to VBlank interrupts (every time the entire screen finishes drawing) and uses this to update the positions of sprites. If other interrupts are desired, the GingerBread library needs to be modified, under "boot process and interrupts" to both jump somewhere when that interrupt happens (similar to the |jp DMACODE_START| that happens on VBlank) and also the GingerBreadBegin routine needs to be modified so that more interrupts are fed into rIE, so that the Game Boy knows that those other interrupts should be listened to. Make sure that the code you jump to ends with |reti| or otherwise re-enable interrupts.
\section{RAM}
\label{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:
\begin{code}
Position EQU $C200 ; 2 values
SpeedVector EQU Position+2 ; 2 values
CharacterHP EQU SpeedVector+2 ; 1 value
CharacterMP EQU CharacterHP+1 ; 1 value
\end{code}
Then, to write the vector (5, 3) to the SpeedVector, we can do something like
\begin{code}
ld a, 5
ld [SpeedVector], a
ld a, 3
ld [SpeedVector+1], a
\end{code}
This syntax is easy to understand but somewhat error-prone, as the word "Position" appears twice and without the comments, it's not super obvious how many values we want (especially for the last named place in the list). Therefore, there exists a special syntax for dividing RAM into sections. For example,
\begin{code}
SECTION "Some variables in RAM",WRAM0[$C200]
Position: DS 2
SpeedVector: DS 2
CharacterHP: DS 1
CharacterMP: DS 1
\end{code}
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~\ref{sprites}) and to accomodate for GBT-Player (see Chapter~\ref{music}), so usable RAM for your game starts at |$C200| (as defined as |USER_RAM_START| in GingerBread).
\section{Graphics: backgrounds}
\label{bakcgrounds}
The graphics on the Game Boy are divided into three separate layers, which can all display different visuals, and they are placed over each other. The first layer is the background layer, which is always drawn the furthest "back" and can use all four "colors" on the Game Boy. This layer is usually used to draw the game world. The second layer is used for drawing sprites, which are movable objects like characters, enemies, projectiles, power ups etc. This layer has one "color" reserved for transparency so that the background can be visible behind objects. The third layer is called the "window" and works a lot like the background layer, but does not necessarily cover the entire screen and is drawn above the background layer (the drawing order of the sprite layer and window layer can be controlled). The window layer is usually used for things that should have a static position on screen, regardless of the scrolling of the background and sprites, like a health meter, menus and score counts, because it does not support scrolling. The rest of this section will focus on the background layer.
If you draw a $160\times144$ image using an image editing program on your computer, and make sure to only use four colors, like four shades of gray, green or some other color (similar to what you'd find on a Game Boy screen). The image should be saved as a lossless .PNG. Then, a tool called Game boy Tile Data Generator (available at \url{http://chrisantonellis.github.io/gbtdg/}, source code here \url{https://github.com/chrisantonellis/gbtdg}) lets you select an image and convert it to assembly code for RGBDS. At the time of writing, this tool is not hosted, but if the source code is downloaded then the index.html file can be opened in a browser locally and the tool still works.
Such assembly code mainly contains |DB| commands for defining the binary code of the graphics. It's divided into two main parts: the tile data that describes every tile used in the image, and the map data that explains how the tiles should be placed on screen. Because of this division, reusing the same tiles across multiple places results in significant savings in the amount of storage needed for the visuals. Because there's a limit to how many different tiles that the Game Boy can keep in its VRAM, it can even be necessary to reuse tiles as much as possible. A classic example of a similar strategy is in Super Mario Bros. on the NES, where the bushes and clouds are actually the same image (but colourized differently).
To be able to use the graphics in your ASM code the .inc file needs to be included, by a command like
\begin{code}
INCLUDE "your_image.inc"
\end{code}
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
\begin{code}
ld hl, example_tile_data
ld de, TILEDATA_START
ld bc, example_tile_data_size
call mCopyVRAM
\end{code}
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\times256$ rendering surface ($32\times32$ 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
\begin{code}
; 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
\end{code}
The macro simply copies map data line by line. It can be used like
\begin{code}
CopyRegionToVRAM 18, 20, example_map_data,
BACKGROUND_MAPDATA_START
\end{code}
where |BACKGROUND_MAPDATA_START| is the start of the map data storage in VRAM (|$9800|), a constant defined in GingerBread.
Note that the map data that GBDTG creates is based on the assumption that the tile data will be placed starting from the beginning of the tile data region of VRAM, as the map data starts with 0, 1 and so on. If you load multiple images into VRAM at once, one of them will have to be loaded after another, for example
\begin{code}
ld hl, example1_tile_data
ld de, TILEDATA_START
ld bc, example1_tile_data_size
call mCopyVRAM
ld hl, example2_tile_data
ld de, TILEDATA_START+example1_tile_data_size
ld bc, example2_tile_data_size
call mCopyVRAM
\end{code}
In such a case, the map data need to be modified by adding a constant to all the values in it (either at run-time or at compile-time, or perhaps before compilation, via some script).
\section{Graphics: sprites}
\label{sprites}
Sprites are graphical objects that appear on top of the background tiles. Unlike background tiles, they can move around freely (including positions that aren't divisible by 8). Their background color is transparent, allowing the background tiles to be seen behind them. The Game Boy can handle up to 40 sprites at once, and at most 10 sprites per horizontal line. Each sprite is either $8\times8$ (a single tile) or $8\times16$ (two tiles, one on top of the other), depending on which mode the PPU runs in. The latter mode is enabled by default in GingerBread, as it allows a larger number of sprite tiles to be on screen at once. The tile data used for sprites are shared with background tiles and window tiles. Because most games use objects that are larger than $8\times16$, objects are often made up of multiple sprites placed next to each other. This does typically not result in any visual artefacts or seams, as the resolution of the screen is low, and sprites are always placed at integer pixel values, giving the impression of larger sprites moving across the screen.
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: \url{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\times256$ rendering surface. The tile number is a bit special when working with $8\times16$ 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~\ref{debug}). 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~\ref{gbc}).
\section{Reading user input}
\label{userinput}
Reading values from the D-pad and buttons is actually a bit involved, as the hardware is directly controlled from the Game Boy CPU. The GingerBread library has a function called |ReadKeys| which writes a byte to the A register, where each bit represents a single input.
The first (highest, leftmost) bit represents the Start button, followed by Select, B, A, down, up, left and right. For example, the value in A after running |ReadKeys| will be |%00010010| if the player is pressing both left on the D-pad and the A button.
For example, you can do
\begin{code}
call ReadKeys
and KEY_A
cp 0
call nz, doSomethingIfAIsPressed
\end{code}
Which will call |doSomethingIfAIsPressed| if A is pressed, and not otherwise. The call will be made even if the user is also pressing some other button; it only checks whether the A button is pressed or not. Note that the |ReadKeys| command does not wait for user input, so you may want to run such code in a loop, to give the user more than one opportunity to perform the given action.
\section{Audio: Sound effects}
\label{soundfx}
The Game Boy produces sounds from four audio channels: two square wave channels, of which the first supports both "sweep" and "envelope" effects, while the second only supports "envelope" effects, and one customizable wave channel, and finally a white noise channel, also supporting an "envelope" effect.
The "sweep" effect means that the frequency of the square wave changes while the sound is playing, while the "envelope" effect means the output volume changes. Both of these effects are commonly used for making a large variety of sound effects.
The GingerBread library supports playing basic sounds. The "EnableAudio" and "DisableAudio" functions turn audio on and off, with sensible default settings (maximum output volume, all channels active, and all channels output to both speakers in case stereo headphones are connected). If other settings are desired, you need to implement this yourself (the relevant addresses are |$FF24|-|$FF26|, defined in GingerBread as |SOUND_VOLUME|, |SOUND_OUTPUTS| and |SOUND_ONOFF|).
GingerBread has a function called |PlaySoundHL| which is designed to play sound effects. First, a sound effect needs to be specified as a data table, containing first a 16-bit "word" specifying which channel to be used, using one of the four |SOUND_CH1_START| through |SOUND_CH4_START|. This should then be followed by five bytes, containing the data to be written to that channel's addresses. For each channel, the meaning of each bit is a bit different, which will be described right after this example of a sound effect:
\begin{code}
Sound_ball_bounce: ; give the sound a name in the code
DW SOUND_CH4_START ; specify which channel
; in this case channel 4 (noise)
DB %00000000 ; Data to be written to SOUND_CH4_START
DB %00000100 ; Data to be written to SOUND_CH4_LENGTH
DB %11110111 ; Data to be written to SOUND_CH4_ENVELOPE
DB %01010101 ; Data to be written to SOUND_CH4_POLY
DB %11000110 ; Data to be written to SOUND_CH4_OPTIONS
\end{code}
which can be played like so:
\begin{code}
ld hl, Sound_ball_bounce
call PlaySoundHL
\end{code}
The comments to GingerBread’s definition to the sound addresses explain what each bit does, so it is included below for reference. For more details on what the bits do, there’s more detailed information at \url{http://gbdev.gg8.se/wiki/articles/Sound_Controller} and \url{http://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware}.
\begin{code}
; Channel 1 (square with sweep and envelope effects)
; 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
; bits 7-6: wave duty,
; bits 5-0: length of sound data
SOUND_CH1_LENGTH EQU $FF11
; bits 7-4: start value for envelope,
; bit 3: envelope decrease/increase,
; bits 2-0: number of envelope sweeps
SOUND_CH1_ENVELOPE EQU $FF12
; bits 7-0: lower 8 bits of the sound frequency
SOUND_CH1_LOWFREQ EQU $FF13
; bit 7: restart channel,
; bit 6: use length,
; bits 5-3: unused,
; bits 2-0: highest 3 bits of frequency
SOUND_CH1_HIGHFREQ EQU $FF14
; Channel 2 (square with envelope effect,
; with no sweep effect)
SOUND_CH2_START EQU $FF15 ; Not used
; bits 7-6: wave duty,
; bits 5-0: length of sound data
SOUND_CH2_LENGTH EQU $FF16
; bits 7-4: start value for envelope,
; bit 3: envelope decrease/increase,
; bits 2-0: number of envelope sweeps
SOUND_CH2_ENVELOPE EQU $FF17
; bits 7-0: lower 8 bits of the sound frequency
SOUND_CH2_LOWFREQ EQU $FF18
; bit 7: restart channel,
; bit 6: use length,
; bits 5-3: unused,
; bits 2-0: highest 3 bits of frequency
SOUND_CH2_HIGHFREQ EQU $FF19
; Channel 3 (custom wave)
; bit 7: on/off, bits 6-0: unused
SOUND_CH3_START EQU $FF1A
; bits 7-0: length of sound
SOUND_CH3_LENGTH EQU $FF1B
; bits 6-5: audio volume
; %00 is mute, %01 is loudest,
; %10 is pretty quiet and %11 is very quiet
SOUND_CH3_VOLUME EQU $FF1C
; bits 7-0: lower 8 bits of the sound frequency
SOUND_CH3_LOWFREQ EQU $FF1D
; bit 7: restart channel,
; bit 6: use length,
; bits 5-3: unused,
; bits 2-0: highest 3 bits of frequency
SOUND_CH3_HIGHFREQ EQU $FF1E
; Channel 4 (noise)
SOUND_CH4_START EQU $FF1F ; Not used
; bits 5-0: length of sound
SOUND_CH4_LENGTH EQU $FF20
; bits 7-4: start value for envelope,
; bit 3: envelope decrease/increase,
; bits 2-0: number of envelope sweeps
SOUND_CH4_ENVELOPE EQU $FF21
; bits 7-4: polynomial counter,
; bit 3: number of steps (15 or 7),
; bits 2-0: ratio of frequency division
; (%000 highest frequency, %111 lowest)
SOUND_CH4_POLY EQU $FF22
; bit 7: restart channel,
; bit 6: use length
SOUND_CH4_OPTIONS EQU $FF23
\end{code}
When referring to bit numbers, bit 7 is the most significant bit (the furthest 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).
\chapter{More involved topics}
\section{Memory map}
\label{memmap}
As mentioned, the Game Boy has a single 16-bit address space, where each possible address between |$0000| and |$FFFF| pointing to something in the hardware. The space is divided as follows:
|$0000|-|$3FFF| contains the game code for bank 0 which is located in the ROM on the cartridge. It is read-only. Some parts of this range have specific purposes, like |$0040| where code execution jumps during vblank interrupts (see Chapter~\ref{weinterruptthisprogramme}), and |$0104| which is where the ROM header starts (see Chapter~\ref{header}).
|$4000|-|$7FFF| contains the game code for the switchable bank, which at the start of the game will be bank 1 (see Chapter~\ref{rombanks}). When the chosen bank changes, the same addresses here will point to a different part of the game's ROM code. It is read-only.
|$8000|-|$9FFF| contains VRAM, or graphics memory. In Game Boy Color mode, this section is switchable with two different banks, otherwise it will only have a single bank. It is where the data for what the tiles should look like (from |$8000|-|$97FF|) and where the tiles should be placed in the background (|$9800|-|$9BFF|) and window (|$9C00|-|$9FFF|). See chapter~\ref{bakcgrounds}. It can both be read and written, but only at certain times (see Chapter~\ref{bakcgrounds})
|$A000|-|$BFFF| contains cartridge RAM, in case the cartridge has such RAM in it. This RAM is sometimes called SRAM and is usually used for save data. It can be both read and written, but needs to be activated before use (and should be deactivated after use, see Chapter~\ref{savedata}).
|$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 reserves |$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 emulators emulate this behaviour.
|$FE00|-|$FE9F| contains data for sprites, like which tiles they should look like and where they should be placed, see Chapter~\ref{sprites}. 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 Chapter\ref{weinterruptthisprogramme})
\section{ROM banks, bank switching}