Of butterflies, WASM and Zig
In December last year, I became interested in WebAssembly, but at the same time I wanted to re-write one of my old project games (it’s not finished yet, so I can’t talk about it), all I want to say is that it builds on top of, and uses the same engine as the old Butterfly Catching Game that I finished in Nov 2020.
Because this was my spare time and I was destracted by other personal things, I started the work at the Butterfly game at the end of March.
I was already trying to re-write another old project: img-DB in Rust, the most natural choice was to just write the game logic in Rust, because the WASM export from Rust is good.
I tried. And I tried harder. And even harder. And the hardest… And the most hardest I possibly could… And when I ran out of worthy sacrifices to make to the BorrowChecker God, I quit.
I’m trying not to remember, it was a painful, depressing and demoralizing experience. But I learned many things that I shouldn’t do.
I was demotivated for a while, tried and failed a few more ways of organizing the structure of the WASM bits, and while searching randomly on the internet, I found AssemblyScript.
It sounded like the Holy Grail of WASM. Easy to learn, easy to write, super performant. My old game was already written in TypeScript, so I started porting the code immediately.
Even from the start, I was surprised by the small size of the WASM files, compared to what was generated from Rust (from 30k down to around 8k from what I remember).
I made a lot of progress for some time, then the game started to freeze randomly. I would see random unknown unicode characters in the console log. Sometimes Firefox was freezing completely and the CPU was 100% used, all 4 cores of my Intel i7!!
The game logic was crystal clear, because I already made it in TypeScript and in Rust, I knew exactly what I had to to, I checked over and over and over again, but after many tries, debug over debug, prayers… I just couldn’t trace the random crashes and bugs, so I quit again.
I did learn a lot of things about the way “strings” are sent from WASM to JS, by looking at probably tens of issues and PRs, and I learned that it’s not so performant to use 2D arrays in WASM, so I started using a flat array for my game map and viewport window.
I have to mention, if I was doing this task for work, I would have made all the efforts to trace, debug, raise issues, or whatever was needed to make it move, but because I didn’t have any hard constrains, I just didn’t need to continue, instead I searched for something that “sparks joy”.
I was pretty close to wrap up the project and collect all the notes and knowledge I gathered so far and stop, when I randonly discovered this article about a GameJam from 2022: https://wasm4.org/blog/
In it, it shows that ZIG was the main language used for that particular game jam. I heard about ZIG some time ago, but I never played with it, so I thought: “Why not? Can’t be harder than Rust!”
I checked it out for short time and went straight into it. I LOVED IT.
Again, the generated WASM were very small, and once the game was compiled, I never had any random faults, crashes, or CPU spikes.
In ZIG I simplified the game logic by a lot. Only 3 functions are exposed: “init(seed)”, “turn(direction)” and “inspectAt(idx)”. That’s it.
Well, some config numbers are also exposed, the map size and viewport size, but they are optional, they can be hardcoded.
Also “init” is not strictly needed, but I decided to expose it just to allow a random seed into the game, so I can rebuild the map in case of bugs or whatever.
How I reduced the WASM size from 32k, to 7k:
I replaced the pre-allocated game grid:
grid: [cfg.viewSize]u16 = [_]u16{32} ** cfg.viewSize
to:
grid: [cfg.viewSize]u16 = undefined
and replaced area tiles from:
tiles: [cfg.mapSize]u16 = [_]u16{'\''} ** cfg.mapSize
to:
tiles: [cfg.mapSize]u16 = undefined
It’s surprising how many bugs can be squeezed in such a basic game…
The hardest bug to catch (no pun intented) was when the butterflies were “teleporting” from the right of the screen, to the left. It was a problem with the area between two points, I was including the bottom right coordinates in the area… took me forever to find.
In the end, I really want to highlight an important lesson that I learned: WASM is a restrictive environment and if you’re obsessed about size or performance, you can’t build a whole app without seriously thinking about each and every function and data structure you want to use.
And from this lesson, another one: I could have used Rust or AssemblyScript just as well, instead of ZIG and probably squeeze the same performance and WASM file size. But I had to go through all that long process with lots of mistakes to learn this.
In the end, I learned MUCH MORE using ZIG, because I was forced to learn a lot of low level concepts about the memory layout of WASM and because ZIG feels like a low level programming language, I used very simple lists and structures.
And I used the lessons learned while banging my head in Rust and AssemblyScript, probably without those lessons, I wouldn’t have finished the project with ZIG. I will never know.
The source code of the game can be found at: https://github.com/croqaz/zigzag-butterflies
You can play it on this website: Zig-zag Butterflies 🦋.
Enjoy!