
When I posted about Goodboy Galaxy on the Nim forums, enthus1ast asked:
Can you give a little insight what technologies you've used?
Here's a repost of my answer:
Our Nim GBA library is called Natu, which includes bindings for various C/asm libraries such as:
- libtonc for interacting with the hardware and rendering text
- posprintf for efficient string formatting
- maxmod for music and sound.
We used the devkitARM1 cross compiler toolchain (though plain old embedded ARM GCC can work fine too, so we'll probably switch to that in the future).
The library itself started as a straight-up C wrapper, but has become more idiomatic since then (more type safe, better naming, using sets instead of ORing together constants, etc.). I'm still trying to clean it up and make it more usable for others, when I can. While a bit outdated already, you can check out my NimConf video from last year if you'd like to see it in action. :)
Natu works with both --gc:none and --gc:arc, though arc didn't exist when I started working on Goodboy Galaxy, but I've had success with it for other small projects. Right now everything in the game is on the stack or statically allocated2, which is nice for performance, so the choice of memory management strategy doesn't matter all that much for me. However, I'm envious of the benefits that destructors could bring if I'd written the engine with arc in mind… No more leaks would be great!
While building the game we implemented many of the systems described in Game Boy Advance Resource Management, such as a tile engine and Obj (sprite) VRAM allocator. This article is absolute gold, I don't know where I'd be without it!
For level design we use Tiled. But we can't just parse the XML files at runtime - custom tooling is needed. We devised a level format (in Nim types), and wrote a tool which takes the levels, processes and converts all the maps, tileset graphics etc. and spits out C code and Nim code, allowing that data to be used in the game. Earlier this year I discovered Nim's stdtmpl filter which makes this kind of codegen much more pleasant, so I've started using that where I can.
One aspect of the game I'm really happy with is the dialogue and cutscene system. I really wanted some sort of coroutines3 so that I could sequence a cutscene using simple procedural code:
player.runLeft() wait 120 # give control back to the main thread # then resume from here in 2 seconds. player.stopRunning() say chMaxwell, "Oh... Heck." # show a speech box.
To achieve this initially I made my own hacked-together version of CPS - a macro that splits a block of code into a chain of functions. It was ok at first, but ended up being a buggy nightmare to maintain. Then, by chance a member of the GBAdev community went and implemented context switching for the GBA, which allowed me to adapt Zevv's nimcoro to get true Lua-style coroutines. After getting this working, I never looked back. it's perfect!
Oh yeah, I guess I should mention some pain points:
- Nim's methods are too heavy for use on embedded platforms - RTTI eats up a ton of RAM and the performance degrades as more subclasses are added. So we had to roll our own VTables instead for things like the entity system.
- Addressable contant data in Nim is still pretty limited. The improvements to let were nice but only work for arrays of ordinal types. So we've mostly resorted to generating all our data as C code to work around this, which is fine once you get used to it.
- Lack of circular imports is super painful, as we need things to talk to each other and can't afford to use event busses and other high-level decoupling techniques that would usually solve this. We often have to resort to hacks involving {.exportc.} and {.importc.} to work around this problem.
That said, if you sent me back in time by 2 years I would absolutely choose Nim for this project again! It's a joy to work with and I can't think of any other language I'd rather be using today.
- [1]
- Later we switched to vanilla embedded ARM GCC (available on most package managers), with the help of gba-bootstrap by AntonioND. I'm so happy with this change, because it simplified installation and allowed us to distance ourselves from devkitPro, who are too gatekeepy for my taste nowadays.
- [2]
- This quickly changed after the demo, as we kept running out of RAM. So we started to allocate data for minigames, menus etc. on the heap so they'd take up zero memory when not in use. Newlib's malloc was too heavy so we used ACSL's instead.
- [3]
- Check my old post using Lua coroutines to create an RPG dialogue system to see why this is so awesome.