Writing Game Boy programs in Nim
ok but why Nim specifically
I like it. Sure, it may have some surface-level goofiness, but I'd say it's been pleasant so far. A syntax as approachable as Python while remaining absolutely performant. It sounds too good to be true, but it really is that good.
The reference implementation of Nim compiles to C. "Hold on, but doesn't that make Nim Not a Real Compiler™?", you say. I like to think that Nim is cheating, but it's a good kind of cheating—piggybacking off of the many C compilers makes it possible to build things with it to all kinds of systems. And you get smooth interop!
Aside from the obvious "we made it work on microcontrollers", someone made it work on a GBA and developed a whole SDK for it. That made me wanna try it with the one (widely-used) C compiler that compiles to Game Boy, that is GBDK-2020. It's very specialized, and it does take a bit of muscle to get there, but it surprisingly works pretty well.
A bit of a disclaimer here, I've been using Nim for like a week at the time of writing, so I don't know everything there is to it. This is just my noobie take on doing a Thing™.
but how
Since Nim compiles to C, it needs a nimbase.h
to mostly help out with compiling for different platforms. Now there's no way in hell the default nimbase.h
would ever work with SDCC (therefore GBDK), so I made a pretty minimal one that contains exactly only what's needed for the generated C code to compile.
First, I tried to echo
something, thinking that it would magically work. Well, since Nim strings are not the same as strings in C, that doesn't work—when that's encountered, the Nim compile will bring in its entire standard library with it. Of course, that won't fly, considering the Game Boy isn't POSIX (or any kind of OS, really), and there's probably not enough useful memory to store all the necessary "safe" structs.
I then made a wrapper around printf
. I referenced the Nim manual among other sources for some pointers on making C bindings. After taking a sample Makefile from the basic GBDK template and modifying it with some notes from Natu:
Okay, turns out it was simple enough. I then tried some basic constructs like this small routine:
proc printf*(s: cstring) {.importc, varargs, header:"<stdio.h>".}
proc delay*(s: uint16) {.importc, header:"<gb/gb.h>".}
proc addNumbers(): int =
result = 0
for i in 1..32:
result += i
printf "%d\n", result
delay 43
Of course, that works as well:
You'll notice that it wraps around. That's because at this point (for whatever reason) I set NI
(Nim's integer typedef in C) to signed char
. It's a typedef anyway, can be changed at any time. So I continued.
Now, as I was trying out Nim's features, one that doesn't work well is sequences. Unlike arrays, sequences can be appended to. That means yet another special struct to deal with, so you'll have to provide ways for Nim to make one that fits the hardware (by defining newSeq
), if you're using the os:standalone
and mm:none
compiler options, as I'm using here. I'm sure it can be done (GBDK's stdlib actually contains malloc
among other things), and GC/RC can probably be enabled, but I don't really feel like it lol. Especially considering the small size of the Game Boy's WRAM... yeah.
One of my personal highlights is Nim's ability to define bit fields using sets. I really couldn't believe the following works, but it does:
proc printf*(s: cstring) {.importc, varargs, header:"<stdio.h>".}
proc joypad*(): set[JoypadKey] {.importc, header:"<gb/gb.h>".}
type
JoypadKey* {.size:1.} = enum
jRight,jLeft,jUp,jDown,jA,jB
jSelect,jStart
JoypadKeys = set[JoypadKey]
# ------------------------
while true:
if jStart in joypad():
printf "Start!\n"
It's beautifully self-explanatory. "Repeat this forever: if it detects that Start is among the buttons currently pressed, then do the following". And that's exactly what it looks like! This works also for detecting key combos: if joypad() == {jStart, jA, jUp}
.
...Okay, maybe for that one you'd expect something like "START + A + UP", in which case, yes: if joypad() == ({jStart} + {jA} + {jUp})
. It's a bit more noisy, but that'll do.
what else
To help me aim while making more GBDK bindings, I decided to port some of the GBDK examples. I started out with the phys
example, which is just moving a sprite around the screen with some momentum to make it not-so-robotic.
It was initially a straight port, including defining spriteData
exactly like the C version. Again, making bindings were really straightforward, just need to map the names to the corresponding header file. c2nim exists for this exact purpose, but the thing is that GBDK's header files are very specialized towards SDCC. What with the BANKED
, NONBANKED
, CRITICAL
and the rest of the keywords that c2nim doesn't recognize (if there's a way to set it then I just don't feel like finding out), I had to port them over manually.
One of the things I stumbled on was when I was creating the bindings for joypad_init
and joypad_ex
. At first, I thought that having the correct return type of joypad_init
—which makes discard
ing the results of joypad_init
necessary, since the phys
demo calls it standalone—makes it not work, somehow. Nim doesn't like it when you call a proc (a function, really) that returns something, but then you don't use it.
I had a look at the C that it generated. Turns out it was wrapping joypad_init
and joypad_ex
into their own functions. This adds overhead that both of those functions weren't meant to sustain, and so it can't poll the joypad in time. What gives? Well... I soon realized it wasn't the return type that was the problem:
proc joypadInit*(npads: uint8, joypads: ptr Joypads): uint8 {.importc: "joypad_init", gb.}
proc joypadInit*(joypads: Joypads, npads: uint8): uint8 =
joypadInit(npads, unsafeAddr joypads)
See that second proc
? I was attempting to overload joypadInit
so that I can use it Nim-style, like so: myJoypads.joypadInit(1)
. But in doing so, what making that a proc
is doing is... well, exactly how the C outputted: wrapping the inner function. That's the problem right here.
What I should have used is template
. It's like a more powerful version of #define FUNCTION(a,b)
used in C and C++. You can use templates like a function, but Nim substitutes it at compile time so that it's actually the thing inside the template.
proc joypadInit*(npads: uint8, joypads: ptr Joypads): uint8 {.importc: "joypad_init", gb.}
template joypadInit*(joypads: Joypads, npads: uint8): uint8 =
# now it works again
joypadInit(npads, unsafeAddr joypads)
Emulating an incbin
is also very much useful. This was my initial implementation:
{.emit:"#include <gbdk/incbin.h".}
{.emit:""""INCBIN(my_gfx, "res/my_gfx.2bpp")""".}
var myGfx {.importc:"my_gfx".}: uint8
Yeah... Hooking it up to GBDK's INCBIN
define... not the best idea. I checked the Natu repo again, and found this. Now that's more like Nim! But... where does it read the file from? Apparently, the file where the template is defined, that's where. However, it's an easy fix: prepend the path of the currently compiling source file. So the new implementation is a template:
# adapted from Natu
template incBin*(filename: static string): untyped =
const data = static:
const str = staticRead(getProjectPath() & "/" & filename)
var arr: array[str.len, uint8]
for i, c in str:
arr[i] = uint8(c)
arr
data
And I get to use it like incBin("../res/my_gfx.2bpp")
. Sweet! BUT! There's a catch.
If I use let myGfx = incBin("../res/my_gfx.2bpp")
, what that does is copy the entire file to memory! I've already mentioned the small size of the Game Boy's RAM, so yeah that's definitely a no-go. (That being said, I've dealt with cases like this, it's definitely a weird GBDK thing)
Nononono, what you wanna do is use const myGfx = incBin("../res/my_gfx.2bpp")
instead.
In that same kinda vein, I'm debating whether or not I should also decouple constants in gbdk/io
so that it's just raw addresses instead. But I've got a feeling that GBDK can optimize if it's given in its native define, so... I don't know.
Next up is the scroller
example. Now, this demo uses a interrupt handler, and adding that handler requires the necessary code to be inside of a CRITICAL {}
block, which SDCC interprets it as "enables should be disabled before running this, and then re-enabled afterwards". As you saw before, Nim has an emit
pragma that can be used to manually insert stuff into the generated C code. But... where is it placing it, exactly? There's three options documented: type section, var section, and include section. None of them can be used, however, since I'm using it to wrap a function. I managed to implement a critical:
macro, but as it says, there's a catch—have to use it inside a function that only has the critical
block inside it. Everywhere else, you'll find a lone CRITICAL{}
nowhere near your function. :/
ok but i'm tired of the gbdk examples
Same. How about I try porting the GB ASM tutorial example instead? It looks kinda nice, I think. For my port, I called it "ReBRICK'D" because I'm so fuckin original. :p
Anyway, it's got all the things you'd expect, control a paddle, destroy bricks with the ball, earn the high score. I find enums to be very handy for organizing graphics:
type
BgGfxParts = enum
bgBorderTopLeft = 0'u8, bgBorderTop, bgBorderTopRight
bgBlack, bgBorderLeft, bgPaddleLhalf, bgPaddleRhalf,
bgBorderRight, bgWhite, bgBorderBottom
Which means I can write code like:
# check for collision with the top of the screen
if currentCrossedTile.BgGfxParts in [
bgBorderTopLeft, bgBorderTop, bgBorderTopRight
]:
# ...
In assembly or even GBDK, you could pretty much do the same. It's just that I find it "friendlier" here (probably attributed more to the fact that I'm familiar with Python...)
Another useful feature is the ability to overload operators in Nim. That means I can hook up the score to automatically update it on the screen when it's being changed:
proc `+=`(a: var Score, b: int) =
a = Score(a.int + b)
updateScore()
So, adding 1 to a Score
makes it call updateScore
as well. As a side note, Python lets you overload existing operators as well, but it's not quite as "direct", as to overload +
you'd have to define __add__
.
I've made a main-game-loop sorta model here, where different parts of the game run in different modes. The game runs in a single loop that hands it off to the update routine of the currently-running game mode. Switching between them should automatically call the respective init function, before handing it off to the main loop again. I've got it handled with the switchGameMode
proc, which is:
proc switchGameMode(mode: GameMode) =
gameModeInits[mode.ord]()
gameMode = mode
Which brings me to another feature of Nim. Unlike in Python, you can actually define custom operators! I wanted gameMode -> gmGame
to mean exactly the proc above, so I replaced it with:
proc `->`(gameMode: var GameMode, newMode: GameMode) =
gameModeInits[newMode.ord]()
gameMode = newMode
I switched it back to switchGameMode
, however—so, no custom operator. I felt like having this as an operator might reduce readability. But still, it's really cool that it's possible in Nim.
You'll notice that I used the movement code from GBDK's phys
demo, which gives it a bit of a smooth-looking control (if not slippery). But that's the good news. The bad news is that this demo still has major bugs, notably in my really lazy eyeballed paddle collision (instead of just following the tutorial), and also the really wonky wall/brick collision. Out of 33 bricks, the highest I've managed to achieve is 29, before promptly getting stuck inside a wall. :p
Outside of playing, you'll find .2bpp files instead of neat little PNGs. At this point the Makefile is getting a bit crowded, and I'm too lazy to work in an asset conversion tool down the chain (png2asset
converts it to .h files which I don't wanna have to import). Maybe for next time, when I convert the Makefile into Nimble tasks.
what are the catches?
Besides the obvious performance hit you might get with GBDK, you might find yourself using ptr
, addr
and cast
very often. Maybe a bit of type juggling, and looking at the generated C (or ASM) code when things go south (which is not pretty at all).
Also, the GBDK bindings aren't even complete. Maybe it'll be useable one day, but right now I don't think it's ready to rumble Nimble. I just ported over what I needed, and the documentation is near verbatim to the original GBDK (for convenience, should someone run nim doc --project docs.nim
on the gbdk folder)
Not to mention, I haven't checked if banking is possible. It might be as simple as adding a codegenDecl
pragma to mark it as BANKED
, and then somehow adjusting lcc
to make it compile at some bank, I don't even know.
But overall, it's been a pretty fun and satisfying experiment.