Game Boy ASM style guide
Written by ISSOtm
This style guide aims to formalize a style that most Game Boy ASM programmers agree on, and provide a good baseline for new programmers just starting in this field. (If that's you, welcome! 😄)
To quote the Linux kernel style guide:
Coding style is very personal, and I won't force my views on anybody, but this is what goes for anything that I have to be able to maintain, and I'd prefer it for most other things too. Please at least consider the points made here.
Many people follow alternate style guides, and that's fine; but if you're starting to code in ASM, a clean style goes a long way to keep your code organized. Again: you don't have to do everything listed here, but please at least consider the reasons behind each bullet point.
Oh, by the way, you're free to contribute to this document and/or chat with us about it!
Naming
RGBASM accepts a lot of symbol names:
Symbol names can contain letters, numbers, underscores ‘_’, hashes ‘#’ and at signs ‘@’. However, they must begin with either a letter, or an underscore.
However, naming conventions make code easier to read, since they help convey the different semantics between each symbol's name.
Labels use PascalCase:
DrawNPCs
,GetOffsetFromCamera
.Labels in RAM (VRAM, SRAM, WRAM, HRAM; you shouldn't be using Echo RAM or OAM) use the same convention but are prefixed with the initial of the RAM they're in, in lowercase:
wCameraOffsetBuffer
,hVBlankFlag
,vTilesetTiles
,sSaveFileChecksum
. Rationale: to know in which memory type the label is; this is important because VRAM and SRAM have special access precautions and HRAM can (should (must)) be accessed using theldh
instruction.Local labels use camelCase, regardless of memory type:
.waitVRAM
,wPlayer.xCoord
.Macro names use snake_case:
wait_vram
,end_struct
.Constants use CAPS_SNAKE:
NB_NPCS
,OVERWORLD_STATE_LOAD_MAP
.Exception: constants that are used like labels should follow the label naming conventions. For example, see hardware.inc's
rXXX
constants.
Best practices
Avoid hardcoding things. This means:
No magic numbers.
ld a, CURSOR_SPEED
is much more obvious thanld a, 5
. In addition, if you ever change your mind and decide to change the cursor speed, you will only need to do so in one location (CURSOR_SPEED equ 5
→CURSOR_SPEED equ 4
) instead of at every location you're using it, potentially missing some.Unless absolutely necessary, don't force a
SECTION
's bank or address. This puts the burden of managing ROM space on you, instead of offloading the job to RGBLINK, which performs very well in typical cases. Exceptions:- Your ROM's entry point must be at $0100, however the jump does not have to be to $0150 (example).
rst
vectors and interrupt handlers obviously need to be at the corresponding locations.- RGBDS presently does not allow forcing different sections to be in the same bank. If you need to do so, the ideal fix is to merge the two sections together (either by moving the code, or using
SECTION FRAGMENT
), but if that option is unavailable, the only alternative is to explicitly declare them with the sameBANK[]
attribute. (In which case it's advisable to add anassert BANK("Section A") == BANK("Section B")
line.)
If you need some alignment, prefer
ALIGN[]
to forcing the address. A typical example is OAM DMA; for that, preferSECTION "Shadow OAM", WRAM0,ALIGN[8]
over e.g.SECTION "Shadow OAM", WRAM0[$C000]
.
Allocate space for your variables using labels +
ds
& co instead ofequ
. This has several benefits:- Removing, adding, or changing the size of a variable that isn't the last one doesn't require updating every variable after it.
- The size of each variable is obvious (
ds 4
) instead of having to be calculated from the addresses. equ
allocation is equivalent to hardcoding section addresses (see above), whereas labels are placed automatically by RGBLINK.- Labels support
BANK()
and many cool other features! - Labels are output in
map
andsym
files.
If a file gets too big, you should split it. Files too large are harder to read and navigate. However, the splitting should stay coherent and consistent; having to jump around files constantly is equally as hard to read and navigate.
Unless you're making a 32k ROM, put things in
ROMX
by default.ROM0
space is precious, and can deplete quickly; and when you run out, it's difficult to move things to ROMX.However, if you have code in ROM bank A refer to code or data in ROM bank B, then either should probably be moved to ROM0, or both be placed in the same bank (options for that are mentioned further above).
farcall
is a good way to make your code really spaghetti.Don't clear RAM at init! Good debugging emulators will warn you when you're reading uninitialized RAM (BGB has one in the option's Exceptions tab, for example), which will let you know that you forgot to initialize a variable. Clearing RAM does not fix most of these bugs, but silences the helpful warnings.
Also, a lot of the time, variables need to get initialized to values other than 0, so clearing RAM is actually counter-productive in these cases.
Recommendations
The difference between these and the "best practices" above is that these are more subjective, but they're still worth talking about here.
Historically, RGBDS has required label definitions to begin at "column 1" (i.e. no whitespace before them on their line). However, later versions (with full support added in 0.5.0) allow indenting labels, for example to make loops stand out like in higher-level languages. However, a lot of people don't do this, so it's up to you.
Please use the
.asm
(or.s
) file extensions, not.z80
. The GB CPU isn't a Z80, so syntax highlighters get it mostly right, but not quite. And it helps spreading the false idea that the GB CPU is a Z80. 😢Compressing data is useful for several reasons; however, it's not necessary in a lot of cases nowadays, so you may want to only look at it after more high-priority aspects.
Avoid abusing macros. Macros tend to make code opaque and hard to read for people trying to help you, in addition to having side effects and sometimes leading to very inefficient code.
Never let the hardware draw a corrupted frame even if it's just one frame. If it's noticeable by squinting a bit, it must go.
Makefiles are bae; they speed up build time by not re-processing what hasn't changed, and they can automate a lot of tedium. Writing a good Makefile can be quite daunting, but gb-boilerplate and gb-starter-kit can help you get started faster.