a weekend love story - raylib/zig

August 15, 2024

gamedevraylibzig

raylib-zig-emscripten

Zig Psyop

Before this weekend, I was a plebeian who used JavaScript for 80% of my tasks. Now I am an esoteric plebeian who has used zig once. Anyway, I decided to give zig a shot and try to build a game with it. There was really only two libraries I wanted to learn (sokol and raylib), I went with raylib.

Initial References

I went through the official raylib docs to get a feel for the API, then I searched for zig bindings and found two, Not-Nik/raylib-zig and ryupold/raylib-zig

The second one hasn't been updated in 6 months, and it isn't using the zig package manager. Ergo, leaving me no choice but to use the first one.

Setup

Fairly easy to set things up, I just needed to install zig and raylib-zig.

NOTE : We will be using zig v0.12.0 instead of v0.13.0, we will see why later.

I used zvm to make life easier mangaging various zig versions, but you can install zig however you want.

Follow this part of the docs to install raylib-zig. Do this inside your game directory. This adds raylib-zig as a dependency to our project. You can check the build.zig.zon file to verify.


Now the most important part if you want to compile your game for the web with emscripten. IF YOU'RE NOT BUILDING FOR THE WEB, PLEASE SKIP.


Install emsdk as mentioned here: emscripten installation guide and make sure to do the source ./emsdk_env.sh command.

NOTE : Just make sure not to clone the emsdk into $HOME/.emscripten, because emscripten uses that as the default cache directory. It will fuck your build up. (I didn't do this at all)

If you followed the raylib-zig installation guide, your build.zig should look something like this:


 1const std = @import("std");
 2
 3pub fn build(b: *std.Build) !void {
 4    const target = b.standardTargetOptions(.{});
 5
 6    const optimize = b.standardOptimizeOption();
 7
 8    const exe = b.addExecutable(.{
 9        .name = "steroids.zig",
10        .root_source_file = b.path("src/main.zig"),
11        .target = target,
12        .optimize = optimize,
13    });
14
15    const raylib_dep = b.dependency("raylib-zig", .{
16        .target = target,
17        .optimize = optimize,
18    });
19
20    const raylib = raylib_dep.module("raylib");
21    const raylib_artifact = raylib_dep.artifact("raylib");
22
23    exe.linkLibrary(raylib_artifact);
24    exe.root_module.addImport("raylib", raylib);
25
26    b.installArtifact(exe);
27
28    const run_cmd = b.addRunArtifact(exe);
29
30    run_cmd.step.dependOn(b.getInstallStep());
31
32    if (b.args) |args| {
33        run_cmd.addArgs(args);
34    }
35
36    const run_step = b.step("run", "Run the app");
37    run_step.dependOn(&run_cmd.step);
38}

If it isn't, it should be now! Now we need to add a block specific to the emscripten target.


 1// add these two imports
 2const fs = std.fs;
 3const rlz = @import("raylib-zig");
 4
 5pub fn build(b: *std.Build) !void {
 6    // ... other code
 7
 8    const raylib = raylib_dep.module("raylib");
 9    const raylib_artifact = raylib_dep.artifact("raylib");
10
11    if (target.query.os_tag == .emscripten) {
12        const exe_lib = rlz.emcc.compileForEmscripten(b, "steroids.zig", "src/main.zig", target, optimize);
13
14        exe_lib.linkLibrary(raylib_artifact);
15        exe_lib.root_module.addImport("raylib", raylib);
16
17        const include_path = try fs.path.join(b.allocator, &.{ b.sysroot.?, "cache", "sysroot", "include" });
18        defer b.allocator.free(include_path);
19        exe_lib.addIncludePath(.{ .path = include_path });
20        exe_lib.linkLibC();
21
22        // linking raylib to the exe_lib output file.
23        const link_step = try rlz.emcc.linkWithEmscripten(b, &[_]*std.Build.Step.Compile{ exe_lib, raylib_artifact });
24
25        // Use the custom HTML template
26        // This will be the index.html where the game is rendered.
27        // You can find an example in my repository.
28        link_step.addArg("--shell-file");
29        link_step.addArg("shell.html");
30
31        // Embed the assets directory
32        // This generates an index.data file which is neede for the game to run.
33        link_step.addArg("--preload-file");
34        link_step.addArg("assets");
35
36        link_step.addArg("-sALLOW_MEMORY_GROWTH");
37        link_step.addArg("-sWASM_MEM_MAX=16MB");
38        link_step.addArg("-sTOTAL_MEMORY=16MB");
39        link_step.addArg("-sERROR_ON_UNDEFINED_SYMBOLS=0");
40        // Add any other flags you need
41
42        b.getInstallStep().dependOn(&link_step.step);
43        const run_step = try rlz.emcc.emscriptenRunStep(b);
44        run_step.step.dependOn(&link_step.step);
45        const run_option = b.step("run", "Run the game");
46        run_option.dependOn(&run_step.step);
47        return;
48    }
49
50    // rest of the build script
51}

I missed this code block and was stuck trying to figure out how to get the emscripten run step to work. :(

Write some code!

I wrote an asteroids clone following along with this video: zig space rocks - jdh. Go write whatever game you want!


Here is a screenshot of the game in action:

space rocks


Here are some other cool games made in zig: tetris, terrain-zigger


Below is some cool zig code I wrote that I quite like:


 1rl.initAudioDevice();
 2defer rl.closeAudioDevice();
 3sound = Sound{
 4    .asteroid = rl.loadSound("assets/asteroid.wav"),
 5    .bloop_lo = rl.loadSound("assets/bloop_lo.wav"),
 6    .bloop_hi = rl.loadSound("assets/bloop_hi.wav"),
 7    .explode = rl.loadSound("assets/explode.wav"),
 8    .shoot = rl.loadSound("assets/shoot.wav"),
 9    .thrust = rl.loadSound("assets/thrust.wav"),
10};
11
12rl.setSoundVolume(sound.explode, 0.5);
13
14// this unwraps the loop at compile time for each field in the Sound struct
15// and frees the memory allocated for each sound
16defer inline for (std.meta.fields(Sound)) |f| {
17    rl.unloadSound(@field(sound, f.name));
18};
19
20defer state.asteroids.deinit();
21defer state.asteroids_q.deinit();
22defer state.particles.deinit();
23defer state.projectiles.deinit();
24defer state.aliens.deinit();

This defer syntax is so fucking cool, I will never forget to free again.


 1const bloop_intensity: usize = @min(@as(usize, @intFromFloat(state.now - state.stage_start)) / 16, 4);
 2const bloop_mod: usize = 60;
 3const adjusted_bloop_mod: usize = @max(1, bloop_mod >> @intCast(bloop_intensity));
 4
 5if (state.frame % adjusted_bloop_mod == 0) {
 6    state.bloop += 1;
 7}
 8
 9if (!state.ship.isDead() and state.bloop != state.last_bloop) {
10    rl.playSound(if (state.bloop % 2 == 0) sound.bloop_hi else sound.bloop_lo);
11}
12state.last_bloop = state.bloop;

This code is for deciding when to play the low/high bloop sound. It increases in intensity the longer a player is alive. Makes it feel like the game is getting harder. Pretty cool!
Also avoided for loops with bit shifting. absolutely unnecessary. But I did it anyway.

Memory management

One thing I think is important is for some reason I wasn't able to just use a std.heap.GeneralPurposeAllocator for the heap allocations. So i reused some code from the ryupold/examples-raylib.zig repo and made a custom emscripten allocator.


1// i modified only one line
2// line 50
3const c = @cImport({
4    @cDefine("__EMSCRIPTEN__", "1");
5    @cInclude("emscripten.h"); // <- I changed it to just emscripten.h
6    @cInclude("stdlib.h");
7});

Without this, I was getting Out of Memory issues. I also increased the memory limit to 16MB. Idk which of those fixed it, but it works now! After that, I was sitting at a solid 1.23 MB of memory usage, so I think I was good.


memory usage


Build the game

You can build the game for desktop with zig build run. For the web, you need to run the following command:


1zig build -Dtarget=wasm32-emscripten --sysroot "/absolute/path/to/emsdk/upstream/emscripten"
2# the absolute path is crucial, otherwise it will not work.
3# now to run the game, you can use the following command:
4emrun ./zig-out/htmlout/index.html

Now, as to why we used v0.12.0. For some reason the build command above fails for the emscripten target with v0.13.0 as mentioned in this open issue. If someone can figure out why, please mention it in the issue!


You can drop everything in the zig-out/htmlout folder and host it wherever.

Fin!

Was a fun start to my zig arc! I hope you guys don't waste time debugging shit like me lol.

May we zig harder every day.


Source Code