Building Pong with Zig and Raylib - Part 4: Smarter Collisions, Cleaner Code
Change of Plans
I was going to dive into menus and UI, but after sharing the early version of Pong on ziggit.dev , I got a bunch of helpful feedback. The feedback was the kind that makes you stop and think, ah, right… I should probably fix that before carrying on.
So that’s what this episode became: a collection of fixes, tweaks, and small refactors that clean up the code and align things more closely with how things should be done in Zig (or at least, better than I had them before).
🧼 Naming Matters
I’d originally named my files paddle.zig
and ball.zig
- lowercase,
snake-case. I had tried to find the guidelines around this, but turns out I only
got half the story. If a file implicitly defines a struct via top-level fields,
it should be named in PascalCase. So, Paddle.zig
, not paddle.zig
.
It’s a small thing, but one that helps be a bit more idiomatic - and avoids confusion when others are reading it.
(now to make the same change across many more files in triangle )
🛠️ Default Field Initializers (and When Not to Use Them)
Another thing I learned: structs that aren’t used as config objects shouldn’t
use default field values. Instead, they should have an init
constant that
represents their starting state.
I’d missed this distinction, and both of my types were using default values
incorrectly. So I cleaned that up and added the values into the init
method.
It’s a subtle change, but it keeps config objects and plain data objects
conceptually separate - and makes it clearer which parts of a struct are
supposed to be overridden.
✨ RLS, Please
One of the comments suggested I lean more into Zig’s Result Location Syntax - where you define the type on the left-hand side and let Zig figure out the rest.
I’d been using a mix of styles. Nothing broke, but consistency helps. So I swept through the code and updated those as well.
|
|
🎯 Fixing Paddle Collisions on the Y Axis
Now for something more visible: my collision detection logic only checked the center point of the ball along the y-axis. That meant if the ball clipped the paddle at the edge, it was sneaking through.
The fix was simple: add/subtract the ball’s radius in the y-axis check. Much better.
|
|
You can actually see the difference in-game - that satisfying little thock now triggers when it should, even on corner hits.
(now I just need add some sounds - I’d forgotten about that)
🧽 isColliding
Should Only Collide
Previously, isColliding
also handled coloring the paddle red when it detected
a hit - a debug leftover that had no place in the final function.
I stripped that out and left isColliding
to do just one thing: return whether
there was a collision. If I want debug visuals again later, I’ll wrap this in
another function.
|
|
🔀 Movement Logic Encapsulation
Paddle movement was scattered, and the logic for moving up/down lived inside
main.zig
. I pulled that out into proper moveUp
and moveDown
methods on
Paddle
.
It reads cleaner now:
|
|
|
|
…and it keeps the input logic in main
, but the movement logic in the paddle -
where it belongs.
📐 Resolution Independence
Some values were hardcoded (like setting y = 200
for paddle start position),
while others used getScreenHeight()
and getScreenWidth()
. I refactored to
make everything use actual screen dimensions, converting i32
screen height
values to f32
where needed.
It was a bit fiddly, but worth it. Now Pong should behave properly regardless of window size.
Closing
So, yeah - not the flashiest episode, but a satisfying one. Lots of small things that feel better now that they’re fixed. And it’s a reminder that sharing early (even rough work) is usually a good idea. You never know what you’ll learn.
Next time, we will get into UI. I’m planning to bring in DVUI and show a basic score display, a pause menu, and maybe some options for reset and quit.
Until then, thanks for reading (and watching) - see you in the next one.