'( now thoughts )

making of tom's rock

28 May 2022

Last year, I joined a challenge to write a song every week. I started part way through so I only wrote 34 songs, but I learned a lot from the exercise. By having a tight deadline, I was forced to compose and mix a complete, stand-alone song within a generous 7 day time period. Of course, because of my procrastination, I only worked on my submission for two days most weeks.

Even two days is enough time to come up with a main hook, develop it into idea, write accompanying drum and bass parts, record a few guitar tracks, add some pads, re-record the guitar parts, create a rough mix, and upload the track. I had 34 opportunities to program a new drum track, 34 opportunities to come up with a new guitar riff, and, most importantly, 34 opportunities to scope back, accept that it was good enough, and release the dang thing. Any constraint can force creativity, but deadlines forced me to scope. And scoping is a skill I need to practice.

I have a bad habit of getting lost writing game engine code in preparation for the “real” game. Well, it’s time to end my delusions of “productivity.” It’s time to jam. Let’s write some sloppy, ugly, messy, spaghetti-fied game code that runs an actual game.

OGAM#12

The first jam my schedule aligned with was the one game a month jam. I signed up for it, and decided on which tools I would use.

Language

C is fun to write because it’s simple and compiles fast, but I’m not disciplined enough to write large amounts of correct C code. Zig fills the same niche as C, but it’s more explicit. It makes writing code with undefined behavior much more difficult. The metaprogramming features also make it easy to validate behavior at compile time or generate boilerplate code that would otherwise fall out of sync if written by hand. There are definitely missing features and bugs, but I like the philosophy of the language, and enjoy writing it.

One of the killer features that makes adoption simple is @cImport which generates C bindings from a header file with one line of code. It’s trivial to reuse existing C code while leveraging Zig’s additional safety and ergonomics (e.g. stack traces and compiler-generated pretty-printers for structs).

Game framework

Full-fledged game engines make sweeping decisions about state management, core data layout, and scene graph structures. That’s fine if you want to focus on gameplay logic, but I want low level control over memory management, performance, and final binary size (by carefully choosing my dependencies). I prefer lightweight libraries that have few abstractions, immediate mode drawing primitives, and leave most of the resource and scene management logic to me. Love2D was my go-to choice for years, but some of the idiosyncrasies of Lua keep me from using it much anymore. Lua is ideal for embedding, so I understand why it’s used in so many engines, but 1-based indexing, global-by-default variables, and parts of the stdlib frustrate me. Unfortunately, Love2D’s implementation is tightly bound to Lua, and there aren’t bindings for other languages.

While working on game engine code recently, I came across Raylib. Its API is similar to Love2D’s, but it’s written in C, so there are bindings for most languages. Because of Zig’s @cImport, I can include it in my project with the following:

# clone the submodule to my game's repo
$ git submodule add https://github.com/raysan5/raylib
// import the header file in my main source file
pub const raylib = @cImport({
    @cInclude("raylib.h");
});
// build raylib from source in build.zig

// optional, create a libraylib static library
var libraylib = b.addStaticLibrary("raylib", null);
libraylib.defineCMacro("PLATFORM_DESKTOP", "1");
libraylib.addIncludeDir("raylib/src/external/glfw/include/");
// cross platform build!!!
if (target.isWindows()) {
    libraylib.linkSystemLibrary("opengl32");
    libraylib.linkSystemLibrary("gdi32");
    libraylib.linkSystemLibrary("winmm");
} else if (target.isLinux()) {
    libraylib.linkSystemLibrary("X11");
    libraylib.linkSystemLibrary("GL");
    libraylib.linkSystemLibrary("m");
    libraylib.linkSystemLibrary("pthread");
    libraylib.linkSystemLibrary("dl");
    libraylib.linkSystemLibrary("rt");
}
libraylib.linkLibC();

libraylib.addCSourceFile("raylib/src/rglfw.c", &.{"-fno-sanitize=undefined"});
libraylib.addCSourceFile("raylib/src/rcore.c", &.{"-fno-sanitize=undefined"});
libraylib.addCSourceFile("raylib/src/rshapes.c", &.{"-fno-sanitize=undefined"});
libraylib.addCSourceFile("raylib/src/rtextures.c", &.{"-fno-sanitize=undefined"});
libraylib.addCSourceFile("raylib/src/rtext.c", &.{"-fno-sanitize=undefined"});
libraylib.addCSourceFile("raylib/src/rmodels.c", &.{"-fno-sanitize=undefined"});
libraylib.addCSourceFile("raylib/src/utils.c", &.{"-fno-sanitize=undefined"});
libraylib.addCSourceFile("raylib/src/raudio.c", &.{"-fno-sanitize=undefined"});
libraylib.addIncludeDir("raylib/src");

libraylib.setTarget(target);
libraylib.setBuildMode(mode);
libraylib.install();

// actually link it to my game's executable
const game = b.addExecutable("game", "game.zig");
game.addIncludeDir("raylib/src");
game.linkLibrary(libraylib);

You may think that’s a lot of code to import one dependency. Compared to pip install raylib, import raylib, it is. But this code is self-contained. It doesn’t depend on anything other than git and zig. You don’t need make, you don’t dependencies for a bespoke setup.sh script, you don’t need even need clang. It’s the same command whether you’re building for Linux or Windows.

A new developer can clone the repo and be up and running with:

git submodule update --init && zig build

No more messing around trying to build a project with a missing dependency or the wrong version of a library. It’s all right there.

Raylib deserves credit too. It’s implemented in a disciplined and self-contained way that doesn’t require any external dependencies.

Development of Tom’s Rock

The jam lasted for 1 month, and I worked on my submission for 1-2 hours most evenings and 4-5 hours on days that I was off. 3 days before the deadline, I submitted the final Linux and Windows builds on itch.io.

The source can be found here. Be warned, it’s jam code so it’s a bit spaghetti in places and isn’t necessarily idiomatic Zig code.

Here’s a video of some gameplay if you don’t want to download it, although you should if you want to see the final boss fight :)


Snake physics

I started by implementing the snake physics using Verlet integration. The implementation was mostly taken from this blog post, and was surprisingly easy to get running. Gravity is implemented by introducing a force that pulls each joint towards the center of the screen.

Collision was, as always, a pain to get right. Eventually, I settled on a simple implementation that was good enough:

fn blockedCollision(pos: m.Vector2) ?CircleCollision {
    // simplified version that only deals with planet collisions
    if (c.CheckCollisionCircles(pos, player_radius, center, planet_radius)) {
        return CircleCollision{ .center = center, .radius = planet_radius };
    }
    return null;
}

// ... in the main loop ...

while (i < 10) : (i += 1) {
    verletSolveConstraints(state.tail_pos);
    for (state.tail_pos) |*p| {
        if (blockedCollision(p.*)) |col| {
            p.* = m.Vector2Add(col.center, m.Vector2Scale(subNorm(p.*, col.center), col.radius + player_radius));
        }
    }
}

// apply friction if it was colliding last frame too
for (state.tail_pos) |*p, j| {
    if (blockedCollision(state.tail_old_pos[j])) |_| {
        p.* = m.Vector2Add(p.*, m.Vector2Scale(m.Vector2Subtract(state.tail_old_pos[j], p.*), 0.4));
        // clamp movement if it's small enough
        if (m.Vector2Distance(p.*, state.tail_old_pos[j]) < 0.05) {
            p.* = state.tail_old_pos[j];
        }
    }
}

Note that this uses discrete collision detection, so the max speed for each joint needs to be clamped so it doesn’t phase through the planet at high speeds.

The friction code is a hack, but it works for most configurations.

Async

I wanted a coroutine library so I could trigger a worker “thread” to execute an asynchronously function. Coroutines are often used for tweening values, animating sprite sheets, or choreographing gameplay logic. They accommodate more fine-grained control than regular OS threads, since they are explicitly resumed at specific points in the game loop.

A coroutine allows you to represent a state machine in a convenient and compact way. Instead of large switch statements and structs full of miscellaneous variables to track state, they read like procedural code that runs in a parallel thread. Concurrency is still tricky, but at least the behavior is deterministic and simpler to debug.

Since Zig’s async is colorblind, it’s easy to “spawn” a new thread by slapping an async keyword in front of an async function call. To make it a blocking call, remove async, and the caller will automatically become async and will suspend with callee at its suspend point.

I planned to use async throughout my code, but it started causing crashes. I had some misconceptions about how async worked so some of the crashes were my fault. But some of the issues I worked around might be compiler bugs. If they are bugs, I’ll submit some bug reports once I get a better understanding of the root causes.

The Zig async documentation is incomplete at the moment, so here are some details I learned:

var frame: anyframe = async f();

anyframe is a pointer. You can resume execution with resume frame;, assuming the underlying stack frame still exists. Async frames are stack-allocated, so if you save frame and try to resume it after the caller exits, it’ll attempt to access the frame on at the stale stack address and crash.

If you want to reuse an async frame outside of the function it’s called in, you can heap-allocate it. At least, that’s how I think it should work in theory.

I tried writing a simple coroutine library that does the following:

var frame_pointers = std.ArrayList(anyframe).init(gpa.allocator());

fn yield(fr: anytype) void {
    // get the underlying frame type
    const T = @typeInfo(@TypeOf(fr)).Pointer.child;
    // allocate space for it on the heap
    var frame_ptr = gpa.allocator().create(T) catch unreachable;
    // copy it onto the heap
    frame_ptr.* = fr.*;
    // save the pointer to be resumed later
    frame_pointers.append(frame_ptr) catch @panic("couldn't push coroutine");
}

fn resumeCoros() {
    // copy the old frame pointers
    var cos = frame_pointers.clone() catch unreachable;
    defer cos.clearAndFree();

    // clear out the frame pointers for the next batch of `yields`
    frame_pointers.clearRetainingCapacity();

    for(cos.items) |co| {
        resume co;
    }

    for(cos.items) |co| {
        // HACK: cast to convert anyframe to a pointer.
        gpa.allocator().destroy(@ptrCast([*]align(8) const u8, co))
    }
}

It’s used like this:

fn increaseForTime(value: *f32, duration: f32) {
    var start = getTimestamp();
    while(getTimestamp() < start + duration) {
        value.* = value.* + 1;
        suspend {
            yield(@frame());
        }
    }
}


// main loop
while(!shouldExit()) {
    // ...
    if(someButtonWasPressed) {
        // increase background brightness asynchronously for 2 seconds.
        _ = async incrementForTime(background_brightness, 2);
    }
    // ...

    // resume all the active frames here
    resumeCoros();
}

This is similar to the code I wrote for the game and it worked for a while, but started breaking down towards the end of the jam as code became more complex.

I shrunk down the issue to a small failing test case: gist

In the gist, the heap-allocated async frames are leaking. The leak can be fixed by uncommenting lines 41-43, but this causes a segfault. One of the frames that is run, then freed is later accessed by resume somehow!? The segfault can be worked around with one of the following hacks:

  1. Remove the call to fun() on line 63.
  2. Replace fun() with await async fun().

1. makes me think that there’s some reference in the fun stack frame to the level_5 caller. But my understanding is also that both options in 2. should be equivalent ways of making a blocking call to fun(). I don’t know why that prevents the segfault here.

I need to investigate this more…

Math code

In order to keep the language simple and explicit, Zig doesn’t allow operator overloading. Most of the time, I’m fine with that decision, except when it comes to math code. It becomes cumbersome to write code like this:

goal = m.Vector2Add(goal, m.Vector2Scale(subNorm(
    enemy.*.pos,
    diff,
), -(1 - d) * move_force));

When all you wanted to say was:

goal += (enemy.*.pos - diff).norm() * -(1-d) * move_force;

It also would be nice to have swizzling, like in GLSL shaders.

Fortunately, there’s a solution: write a comptime DSL that generates the math code for you. I’ve experimented with this idea briefly:

const r = CompileExpression("(u*v)+u*3-v").eval(.{.u=Vec3{1,2,3}, .v=Vec3{3,4,5}});

Building a library that provides basic vec3/vec4/mat4 math support with infix notation, basic indexing, and swizzling should be doable. I’ve verified that the above expression compiles to efficient assembly code on higher optimization levels, so there shouldn’t be any overhead to using it besides increasing compile times (hopefully not by too much). It might even be possible to expand it into a full-on APL-like DSL for array manipulation, but I don’t want to go too crazy ;)

Web and Windows builds

Traces of web support can be found throughout the project, but I never fully built the web version of the game. Shortly before the jam started, I realized that Zig doesn’t fully support the WASM C ABI. Functions called with register-sized values (int, long, float, double, pointer) and large structs are passed correctly through the C ABI barrier, but small structs that are passed by value and are split up and passed through the registers don’t work yet. This is a problem since many Raylib functions return small structs, like GetMousePosition() which returns a Vector2. Until these functions compile correctly, building the game for web will be difficult. It’s possible to work around the issue by building a shim layer in C that passes and returns pointers to small structs instead of passing them by value. Even with that workaround though, Zig is still missing support for @asyncCall which I used in the coroutine system, so it still isn’t possible for now.

The Zig compiler also lacks lacks small-struct support for the Windows C ABI, so I had to find alternate parts of the Raylib API to make it compile correct code. I implemented one hack by using zig translate-c to convert the raymath.h file to Zig so it would properly pass small Vector2 structs to core math functions. Fortunately, it only took a few minutes to replace the other problematic function calls with alternatives that wouldn’t break.

Linux is still going to remain my main development platform even though I’m usually burned by a Windows porting issue at the end of every game jam. I waste time faffing around with Windows bugs, but I make up for it with faster development time on Linux thanks to the superior tooling.

Raylib

Raylib was great fun to use for this project. Its API is predictable and consistent and provides all the functionality needed for slinging pixels on screen quickly. With builtin support for loading common asset formats, easy-to-use drawing primitives, a developer friendly input API, and some nice helper functions (like a 2D camera), most of the grunt-work was done before I even started. I was able to focus on writing the fun parts: the engine code and the game logic.

I haven’t used the 3D API yet, but I always wanted a lightweight 3D library in C, comparable to ThreeJS. Raylib seems to fill that niche.

The only issue I found with Raylib itself was that it didn’t loop music perfectly. I think it has something to do with the number of samples not lining up with an internal buffer size, but I haven’t had a chance to investigate a fix for it yet. Since looping songs are necessary in game soundtracks, I’ll need to figure out a fix or workaround soon.

Other assets

All of the pixel art was drawn in Aseprite. I tried to keep the designs and key poses simple and readable. The designs look better than I might have predicted, but I could have pushed the use of value and texture more. The results are adequate, but I want to collaborate with a real artist on a future project.

The music is unsophisticated, but I enjoyed creating it, especially the boss theme. I wrote the main theme in about 30 minutes and built the rest of the soundtrack around it. The music and miscellaneous sound effects took around 4 hours to create. I used REAPER for all audio and music production for the project.