Building Pong in Zig with raylib (part 1 – paddles and a ball)

I’ve been working on a bigger project in Zig for a while, but before diving back into it, I wanted to build something small and self-contained to get back into the rhythm - so I’m building Pong.

This is a build-along style series - figuring things out as I go. In part 1 I get the project set up using raylib-zig, draw the paddles, the ball, and lay out the playground.

Video: https://youtu.be/ICq2D_na6zc
Blog:
Building Pong in Zig with Raylib – Part 1: Setup, Paddles, and Ball - words on sand
Code:
wordsonsand/games/pong at main · drone-ah/wordsonsand · GitHub

I’ll be posting more parts soon - next up is ball movement and paddle collision.
It’s been fun so far. Happy to hear thoughts if anyone’s tried similar or is
doing anything raylib-y in Zig.

I welcome any feedback you might have - learning as I’m going - only been doing zig for a couple of months.

8 Likes

Thanks for sharing! I looked over your code, and have a few points of feedback.

Structural things:

  • Your collision algorithm will cause noticeable clipping, as it only checks the center of the ball in the y-axis. You’ll need to either make the ball have a square hitbox by extending the checked area in the y-axis, or use a more involved circle-rectangle overlap algorithm.
  • A paddle method called isColliding shouldn’t be implicitly responsible for the paddle’s coloring. I would rename it to something like updateCollision, or ideally restructure things so that isColliding simply checks if the paddle is colliding without mutating it.
  • You set some position values based on the window size, but not all, which kind of defeats the purpose. You mentioned in your blog that you wanted to keep the logic in Paddle simple, which is understandable, but I feel like it would be more consistent to have everything be hardcoded or have everything be based on window size.

Stylistic things:

  • paddle.zig and ball.zig should be Paddle.zig and Ball.zig: https://ziglang.org/documentation/master/#Names.
  • Structs that aren’t used as config arguments shouldn’t have default field initializers. If the initial state of a struct is static (doesn’t need an init function), make a public constant called init that represents the initial state of the struct.
  • You should be using RLS in more places:
// Original:
var left_paddle = Paddle.init( ... );
var right_paddle = Paddle.init( ... );
var ball = Ball{ ... };

// Revised:
var left_paddle: Paddle = .init( ... );
var right_paddle: Paddle = .init( ... );
var ball: Ball = .{ ... };
3 Likes

Thank you so much for the feedback! I really appreciate it - and value the learnings.

I have already recorded a couple more videos which I will publish twice a week (because that’s what YouTube wants). I will update with these suggestions (all of which sounds like the right thing to do) in the next video I do.

On a side note, when I publish more videos - can I ask for more feedback? should it be as additional replies to this post, or create new threads?

Would it be better if I did them as pull requests, but I am not sure how that could work with the logistics of the videos - maybe I should implement the changes as well in the video before publishing… Maybe that is also a valuable aspect to capture?

I’d be happy to give more feedback as you continue to work on this.

It would probably be better to keep incremental updates as replies rather than new posts, as that could get spammy.

Since it’s a devlog, I think it makes sense to show the revision process.

1 Like

That’s fantastic and I really appreciate your support.

I’ve rejigged parts 2 and 3 as pull requests:

At your leisure, of course. You can ignore any .md files. Also, please bear in mind that I did these before your original bits of feedback, and there is no need to repeat those - I’ll apply those suggestions across the board when I do part 4, possibly along with feedback from these PR’s depending on timings.

Thank you :slight_smile:

1 Like

So, part 2 looks like it just covers the code I saw before, so I don’t have any additional comments on it.

As for part 3, the main thing I’d do is put the paddle movement logic in moveUp and moveDown methods, and call them based on user input. This would be more consistent, as your other movement logic is mostly in the ball+paddles themselves, with the main loop just doing orchestration.

1 Like

Good point, and thank you :smiley:

This is funny I actually have made a server side pong game in Zig for a school project, and I was using a Raylib client that I’ve made for local testing for a while

Both your code and your blog looks really cool :slight_smile:

1 Like

Hey folks, videos for parts 2 & 3 are now up.

Part 2 covers:

  • Ball movement
  • Paddle collision

Part 3 adds:

  • Edge collisions
  • Scoring
  • Player input

It’s effectively playable now - though the shortcuts are a little weird and the scores are not shown on the game screen

Part 2: https://youtu.be/IoOLH1O_a7M
Part 3: https://youtu.be/9TmoiLjtWrg

Open to any feedback you might have, and hope that these add value :slight_smile:

Looking forward to looking properly at these. I’m not really that interested in games, and it certainly isn’t what I planned on programming in zig, but your first post impressed me with how easy it can be to write one. I’ve been playing about with raylib since and I’m looking forward to more inspiration from parts 2 and 3.

1 Like

Hi there,

The fixes you suggested have now been implemented. Thanks again.

I’ve now got part 5 ready if you wouldn’t mind taking a look, at your leisure:

Thank you so much!
Shri

1 Like

I looked over this PR, and also tried building/running the code on my machine for the first time. If you haven’t tested it on Windows yet, I can confirm that it runs great!

My feedback:

  • std.heap.GeneralPurposeAllocator is deprecated, use std.heap.DebugAllocator (it’s basically the same thing under a different name).
  • You are initializing the allocator correctly, nothing to change there.
  • The proper way to deinit your allocator is to just do defer _ = gpa.deinit(). deinit will already print memory leak info if any leaks occur, so there is no need to @panic if leaks occur.
  • The showScore logic could be part of Paddle since the paddles own the scores, but that feels kinds unintuitive. I think that if you want a super ‘correct’ structure/hierarchy, you should either move the scores into the Game struct and keep showScore in Game, or make a new Player struct that contains a score, a paddle, and showScore. Either way, moving the score out of Paddle seems like the right call.
  • Your score rendering is fine, but just in case you want to make the font larger, I wrote an example of how you could do that in your code. I wanted to point this out because setting font sizes in dvui is a little unintuitive, and I wasn’t sure if you were happy with the font size, or if you just settled for the largest default option:
const font_size: f32 = 64;
var label_options: dvui.Options = .{
    .color_text = .white,
    .font_style = .title,
};
label_options.font = label_options.fontGet().resize(font_size);
dvui.label(@src(), "{d}", .{score}, label_options);
  • A ball may still be colliding with a paddle after bouncing off it, triggering an additional bounce. This can lead to a glitchy state where the ball is permanently colliding, constantly switching direction while embedded in the paddle. You can resolve this by making your crossing_x logic check if the ball is moving towards the paddle:
const crossing_x = switch (self.which) {
    .right => ball.vel.x > 0 and
        ball.pos.x + ball.r >= self.pos.x,
    .left => ball.vel.x < 0 and
        ball.pos.x - ball.r <= self.pos.x + size.x,
};