Game Boy ASM style guide
Written by ISSOtmopen in new window
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 guideopen in new window:
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 documentopen in new window and/or chat with us about itopen in new window!
Naming
RGBASM accepts a lot of symbol namesopen in new window:
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.incopen in new window'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 bankopen in new window 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 $0100open in new window, however the jump does not have to be to $0150 (exampleopen in new window).
rst
vectors and interrupt handlersopen in new window obviously need to be at the corresponding locations.- RGBDS presently does not allow forcing different sections to be in the same bankopen in new window. 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
open in new window), 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[]
open in new window to forcing the address. A typical example is OAM DMAopen in new window; for that, preferSECTION "Shadow OAM", WRAM0,ALIGN[8]
over e.g.SECTION "Shadow OAM", WRAM0[$C000]
.
Allocate space for your variables using labelsopen in new window +
ds
& coopen in new window instead ofequ
open in new window. 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()
open in new window and many cool other features! - Labels are output in
map
andsym
open in new window 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 ROMopen in new window, put things in
ROMX
open in new window 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
open in new window is a good way to make your code really spaghettiopen in new window.Don't clear RAM at init! Good debugging emulators will warn you when you're reading uninitialized RAM (BGBopen in new window 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 labelsopen in new window, for example to make loops stand out like in higher-level languages. However, a lot of people don't do thisopen in new window, 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 baeopen in new window; 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-boilerplateopen in new window and gb-starter-kitopen in new window can help you get started faster.