Zig with Raylib

I’ve been experimenting with using Zig for simple games and graphics lately. Raylib is a great library for 2D graphics, ut it took me a while to figure out how to setup with Zig. So I thought I’d write up a quick guide.

This will essentially be re-iterating the information found in in the raylib-zig README. But I everytime I’ve gone to setup the raylib with Zig I had to re-figure out how to do this. So this is more of a guide for my future self then it being useful for others 😅.

Install Zig

Install the latest stable version of Zig by following the instructions on the Zig website.

I recommend using something like zigup to install Zig. This makes switching between versions easy.

As of time of writing, Zig 0.13.0 is the latest version.

Create a new Zig project

Create a new directory for your project and navigate into it.

mkdir zig-with-raylib
cd zig-with-raylib

Initialize a new Zig project.

zig init

Add Raylib

We will be using the raylib-zig bindings. Fetch the bindings with:

zig fetch --save https://github.com/Not-Nik/raylib-zig/archive/devel.tar.gz

Configure build.zig

After installing the exe artifact, configure the raylib dependency in build.zig.

b.installArtifact(exe); // already exists

const raylib_dep = b.dependency("raylib-zig", .{
    .target = target,
    .optimize = optimize,
});

const raylib = raylib_dep.module("raylib");
const raylib_artifact = raylib_dep.artifact("raylib");

exe.linkLibrary(raylib_artifact);
exe.root_module.addImport("raylib", raylib);

Render something

Now you can just copy the code in the README from raylib-zig. But lets get setup with something a little more structured.

We are going to make a little program where a bunch of balls bounce around the screen.

Imports and constants

Lets define some imports and constants in the main.zig file.

const std = @import("std");
const rl = @import("raylib");

const num_balls = 10;

const screen_width = 800;
const screen_height = 450;

const frame_rate = 60.0;
const tick_rate = 30.0; // 30 ticks per second

Ball and game state

Lets define a Ball and a GameState.

The Ball struct will hold the position and velocity of a ball. The GameState struct will hold a list of balls and the init, update, and draw methods for the game state.

I like this pattern for simple games because it keeps things pretty organized. Each object is in control of its own state and behaviour. The GameState object controls the game loop and the Ball objects are responsible for their own movement and drawing.

const Ball = struct {
    pos: rl.Vector2,
    vel: rl.Vector2,

    pub fn random_ball(rng: *std.rand.DefaultPrng) Ball {
        return Ball{
            .pos = .{
                .x = rng.random().float(f32) * screen_width,
                .y = rng.random().float(f32) * screen_height,
            },
            .vel = .{
                .x = rng.random().float(f32) * 5 + 5,
                .y = rng.random().float(f32) * 5 + 5,
            },
        };
    }

    pub fn update(self: *Ball) void {
        self.pos.x += self.vel.x;
        self.pos.y += self.vel.y;

        // Bounce off walls
        if (self.pos.x <= 0 or self.pos.x >= screen_width) {
            self.vel.x *= -1;
        }
        if (self.pos.y <= 0 or self.pos.y >= screen_height) {
            self.vel.y *= -1;
        }

        // Ensure the ball stays within the screen bounds
        self.pos.x = std.math.clamp(self.pos.x, 0, screen_width);
        self.pos.y = std.math.clamp(self.pos.y, 0, screen_height);
    }

    pub fn draw(self: Ball) void {
        rl.drawCircle(@intFromFloat(self.pos.x), @intFromFloat(self.pos.y), 10, rl.Color.pink);
    }
};

const GameState = struct {
    balls: []Ball,

    pub fn init(allocator: std.mem.Allocator, rng: *std.rand.DefaultPrng) !GameState {
        var balls = try allocator.alloc(Ball, num_balls);
        errdefer allocator.free(balls);

        var i: usize = 0;
        while (i < num_balls) : (i += 1) {
            balls[i] = Ball.random_ball(rng);
        }

        return GameState{ .balls = balls };
    }

    pub fn update(self: *GameState) void {
        for (self.balls) |*ball| {
            ball.update();
        }
    }

    pub fn draw(self: *GameState) void {
        for (self.balls) |ball| {
            ball.draw();
        }
    }
};

Main

Now we can write our main function. This will handle initializing Raylib and our game state. It will also handle the game loop and exit when the window is closed.

pub fn main() anyerror!void {
    var rng = std.rand.DefaultPrng.init(@intCast(std.time.milliTimestamp()));

    rl.initWindow(screen_width, screen_height, "pong");
    defer rl.closeWindow();

    const target_tick_rate = 1.0 / tick_rate;
    var accumulated_time: f32 = 0.0;

    rl.setTargetFPS(frame_rate);

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Create balls
    var game_state = try GameState.init(allocator, &rng);
    defer game_state.deinit(allocator);
    // Main game loop
    while (!rl.windowShouldClose()) {
        const deltaTime = rl.getFrameTime();
        accumulated_time += deltaTime;

        while (accumulated_time >= target_tick_rate) {
            game_state.update();

            accumulated_time -= target_tick_rate;
        }

        rl.beginDrawing();
        rl.clearBackground(rl.Color.black);

        // Draw the game state
        game_state.draw();

        rl.endDrawing();
    }
}

Run the program

Lets run the program and see what we get.

zig build run

And there we have it! A bunch of pink balls bouncing around the screen.

Zig with Raylib final result

Repo

You can find all the code for this post here.

If interested, you can also take a look at a pong game I made using raylib and zig.