<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Raylib on despatches</title><link>https://icle.es/tags/raylib/</link><description>Recent content in Raylib on despatches</description><generator>Hugo</generator><language>en</language><lastBuildDate>Thu, 19 Mar 2026 20:01:29 +0000</lastBuildDate><atom:link href="https://icle.es/tags/raylib/index.xml" rel="self" type="application/rss+xml"/><item><title>Building Pong with Zig and Raylib #6: Font Size, Collision Bugs, and Refactors</title><link>https://icle.es/2025/07/19/building-pong-with-zig-and-raylib-%236-font-size-collision-bugs-and-refactors/</link><pubDate>Sat, 19 Jul 2025 08:00:00 +0100</pubDate><guid>https://icle.es/2025/07/19/building-pong-with-zig-and-raylib-%236-font-size-collision-bugs-and-refactors/</guid><description>&lt;p>In this one, we’ll tidy up our Pong implementation by addressing three key
issues:&lt;/p>
&lt;ol>
&lt;li>The score font is too small&lt;/li>
&lt;li>The ball can get stuck inside a paddle&lt;/li>
&lt;li>Trigger score on edge (not the middle) of the ball going past.&lt;/li>
&lt;/ol>
&lt;p>We’ll also refactor score tracking to better match game logic structure.&lt;/p>
&lt;h2 id="-make-the-score-font-more-readable">🖋️ Make the Score Font More Readable&lt;/h2>
&lt;p>The score text was a little too small. If you&amp;rsquo;re using &lt;code>dvui&lt;/code>, here&amp;rsquo;s how to
adjust it:&lt;/p></description><content:encoded><![CDATA[<p>In this one, we’ll tidy up our Pong implementation by addressing three key
issues:</p>
<ol>
<li>The score font is too small</li>
<li>The ball can get stuck inside a paddle</li>
<li>Trigger score on edge (not the middle) of the ball going past.</li>
</ol>
<p>We’ll also refactor score tracking to better match game logic structure.</p>
<h2 id="-make-the-score-font-more-readable">🖋️ Make the Score Font More Readable</h2>
<p>The score text was a little too small. If you&rsquo;re using <code>dvui</code>, here&rsquo;s how to
adjust it:</p>
```zig
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);
```
<p>Thanks to
<a href="https://ziggit.dev/t/building-pong-in-zig-with-raylib-part-1-paddles-and-a-ball/10768/12">code sample from milogreg</a></p>
<p>You may also need to adjust the width and height of the container if the larger
font gets clipped. In our case, font size of 64 felt about right.</p>
<h2 id="-fix-the-ball-getting-stuck-in-the-paddle">🧱 Fix the Ball Getting Stuck in the Paddle</h2>
<p>When the ball moves too far in one frame, it can land <em>inside</em> the paddle and
bounce back and forth infinitely.</p>
<p>This trap happens because we just multiply the x-velocity with <code>-1</code> to reverse
direction.</p>
<p>One way to fix this is to only trigger the bounce if the ball is moving <em>toward</em>
the paddle. For example:</p>
```zig
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,
};
```
<p>Thanks again to
<a href="https://ziggit.dev/t/building-pong-in-zig-with-raylib-part-1-paddles-and-a-ball/10768/12">code sample from milogreg</a></p>
<p>This ensures we don’t apply the bounce logic when the ball is already inside the
paddle.</p>
<p>Another (potential) way to fix this would be to change the collision logic to
fix the x direction based on whether it&rsquo;s the left or right paddle.</p>
<h2 id="-trigger-a-score-when-the-balls-edge-crosses-the-screen">🧮 Trigger a Score When the Ball’s Edge Crosses the Screen</h2>
<p>Previously, we checked whether the ball’s <strong>center</strong> (<code>ball.x</code>) crossed the
screen edge. Particularly with the ball being bigger than the paddle, this
caused issues when the top/bottom of the paddle hit the ball.</p>
<p><a href="https://icle.es/games/pong/src/Game.zig">Game.zig</a></p>
```zig
if (self.ball.pos.x + self.ball.r > self.screen_width) {
    self.left_score += 1;
    self.ball.reset();
}

if (self.ball.pos.x < self.ball.r) {
    self.right_score += 1;
    self.ball.reset();
}
```
<p>This triggers the score as soon as the edge of the ball crosses the screen
bounds.</p>
<h2 id="-refactor-move-scores-out-of-the-paddle-struct">🔄 Refactor: Move Scores Out of the Paddle Struct</h2>
<p>Storing the score inside the <code>Paddle</code> struct is convenient but semantically odd</p>
<ul>
<li>paddles shouldn’t own scores. Instead, let&rsquo;s move them into your <code>Game</code>
struct:</li>
</ul>
<p>Then, pass the score explicitly to the rendering function.</p>
<p>You can find the updated code in
<a href="https://icle.es/games/pong/src/Game.zig">Game.zig</a>.</p>
<h2 id="-bonus-fix-make-score-display-use-paddle-play-area">✅ Bonus Fix: Make Score Display Use Paddle Play Area</h2>
<p>We now compute each paddle’s <em>play area</em> (a <code>dvui.Rect</code>) and use it to position
the score label, keeping layout logic more re-usable.</p>
<p>We add a <code>play_area</code> field or method to our <code>Paddle</code> struct that returns its
side of the screen. This makes the rendering logic clearer and more flexible.</p>
<h2 id="-whats-next">⏭️ What’s Next?</h2>
<p>Next episode: adding a <strong>pause menu</strong> to Pong, based on what I just built for my
other game, <em>triangle</em>. We’ll add basic Resume/Quit options and freeze game
state mid-play.</p>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://icle.es/youtube/shri-codes/pong/pong-6.md">Watch Video</a></li>
<li><a href="https://icle.es/games/pong/">Source Code (at this point)</a></li>
<li>Prev: <a href="https://icle.es/5-ui.md">Smarter Collisions &amp; Cleaner Code</a></li>
<li>Next: Pause Menu</li>
</ul>
]]></content:encoded></item><item><title>Building Pong with Zig and Raylib #5: Show Score with dvui</title><link>https://icle.es/2025/07/15/building-pong-with-zig-and-raylib-%235-show-score-with-dvui/</link><pubDate>Tue, 15 Jul 2025 08:00:00 +0100</pubDate><guid>https://icle.es/2025/07/15/building-pong-with-zig-and-raylib-%235-show-score-with-dvui/</guid><description>&lt;p>In this episode, I finally add a score display to Pong using DVUI, a native Zig
UI framework. The scoring logic was already in place - now it&amp;rsquo;s time to show it
on screen.&lt;/p>
&lt;h2 id="extract-out-game-struct">Extract out &lt;code>Game&lt;/code> struct&lt;/h2>
&lt;p>One of the structural changes I wanted to make to tidy up the code was to pull
out the game logic into its own struct. This change helps to declutter the
&lt;code>main.zig&lt;/code> file, leading the way to add in the dvui scaffolding.&lt;/p></description><content:encoded><![CDATA[<p>In this episode, I finally add a score display to Pong using DVUI, a native Zig
UI framework. The scoring logic was already in place - now it&rsquo;s time to show it
on screen.</p>
<h2 id="extract-out-game-struct">Extract out <code>Game</code> struct</h2>
<p>One of the structural changes I wanted to make to tidy up the code was to pull
out the game logic into its own struct. This change helps to declutter the
<code>main.zig</code> file, leading the way to add in the dvui scaffolding.</p>
<p>This refactor pulls the update and render logic into a <code>Game</code> struct, which
helps declutter <code>main.zig</code> and sets up a better foundation for UI work.</p>
<p><a href="https://icle.es/games/pong/src/Game.zig">Game.zig</a></p>
```zig
pub fn update(self: *Game, dt: f32) void {
    self.ball.checkEdgeCollisions(self.screen_height);
    self.ball.update(dt);
    self.ball.checkPaddleCollision(&self.left_paddle);
    self.ball.checkPaddleCollision(&self.right_paddle);
    if (self.ball.pos.x > self.screen_width) {
        self.left_paddle.score += 1;
        std.debug.print("scores: l: {d}, r: {d}\n", .{ self.left_paddle.score, self.right_paddle.score });
        self.ball.reset();
    }

    if (self.ball.pos.x < 0) {
        self.right_paddle.score += 1;
        std.debug.print("scores: l: {d}, r: {d}\n", .{ self.left_paddle.score, self.right_paddle.score });
        self.ball.reset();
    }

    if (rl.isKeyDown(.w)) {
        self.left_paddle.moveUp(dt);
    }

    if (rl.isKeyDown(.s)) {
        self.left_paddle.moveDown(dt);
    }

    if (rl.isKeyDown(.e)) {
        self.right_paddle.moveUp(dt);
    }

    if (rl.isKeyDown(.d)) {
        self.right_paddle.moveDown(dt);
    }
}

pub fn render(self: *const Game) void {
    self.left_paddle.render();
    self.right_paddle.render();
    self.ball.render();

    showScore(self.screen_width * 0.25, self.left_paddle.score);
    showScore(self.screen_width * 0.75, self.right_paddle.score);
}
```
<h2 id="add-dvui-dependency">Add dvui dependency</h2>
<p>Let&rsquo;s fetch the dependency, adding it to <code>build.zig.zon</code>:</p>
<p><code>zig fetch --save git+https://github.com/david-vanderson/dvui.git</code></p>
<p>Then, in <code>build.zig</code>, we also need to declare it:</p>
```zig
const dvui_dep = b.dependency("dvui", .{
    .target = target,
    .optimize = optimize,
});

const dvui = dvui_dep.module("dvui_raylib");
```
<p>and then add it as a dependency:</p>
```zig
exe.root_module.addImport("dvui", dvui);
```
<h2 id="add-dvui-to-game-loop">Add <code>dvui</code> to game loop</h2>
<p>We also need to initialise dvui in the main loop.</p>
<p><a href="https://icle.es/games/pong/src/main.zig">main.zig</a></p>
<h3 id="import">Import</h3>
```zig
const dvui = @import("dvui");

const RaylibBackend = dvui.backend;
comptime {
    std.debug.assert(@hasDecl(RaylibBackend, "RaylibBackend"));
}
const ray = RaylibBackend.c;
```
<h3 id="initialise">Initialise</h3>
```zig
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
const allocator = gpa.allocator();
defer _ = gpa.deinit();

//--------------------------------------------------------------------------
// init Raylib backend
// init() means the app owns the window (and must call CloseWindow itself)
var backend = RaylibBackend.init(allocator);
defer backend.deinit();
backend.log_events = true;

// init dvui Window (maps onto a single OS window)
// OS window is managed by raylib, not dvui
var win = try dvui.Window.init(@src(), allocator, backend.backend(), .{});
defer win.deinit();
```
<h3 id="pre-render">Pre-render</h3>
```zig
// marks the beginning of a frame for dvui, can call dvui functions after this
try win.begin(std.time.nanoTimestamp());

// send all Raylib events to dvui for processing
_ = try backend.addAllEvents(&win);
```
<h3 id="post-render">Post-render</h3>
```zig
_ = try win.end(.{});

// cursor management
if (win.cursorRequestedFloating()) |cursor| {
    // cursor is over floating window, dvui sets it
    backend.setCursor(cursor);
} else {
    // cursor should be handled by application
    backend.setCursor(.arrow);
}
```
<h2 id="show-score">Show Score</h2>
<p>We can now show the score using <code>dvui.label</code> with positioning hardcoded to about
25% and 75% across the screen. There may be a better way to position it but it
works well enough for now.</p>
<p>I had to generate an id for the label so that they were unique. I generated it
the x-position using <code>@intFromFloat()</code>.</p>
<p><a href="https://icle.es/games/pong/src/Game.zig">Game.zig</a></p>
```zig
pub fn render(self: *const Game) void {
    self.left_paddle.render();
    self.right_paddle.render();
    self.ball.render();

    showScore(self.screen_width * 0.25, self.left_paddle.score);
    showScore(self.screen_width * 0.75, self.right_paddle.score);
}

fn showScore(xpos: f32, score: u8) void {
    const id: usize = @intFromFloat(xpos);
    var right = dvui.box(@src(), .horizontal, .{ .rect = .{ .x = xpos, .y = 50, .w = 50, .h = 50 }, .id_extra = id });
    defer right.deinit();

    dvui.label(@src(), "{d}", .{score}, .{ .color_text = .white, .font_style = .title });
}
```
<h2 id="closing">Closing</h2>
<p>As always, things took a bit longer than expected, but by the end:</p>
<ul>
<li>The game&rsquo;s structure is cleaner</li>
<li>DVUI is wired up properly</li>
<li>Scores now show up on screen</li>
</ul>
<p>Shoutout to <a href="https://ziggit.dev/u/milogreg/summary">milo_greg</a> on
<a href="https://ziggit.dev/">ziggit.dev</a> for loads of really valuable feedback and
tips. That kind of thoughtful review really helps.</p>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://icle.es/youtube/shri-codes/pong/pong-5.md">Watch Video</a></li>
<li><a href="https://icle.es/games/pong/">Source Code (at this point)</a></li>
<li>Prev: <a href="https://icle.es/4-refactor.md">Smarter Collisions &amp; Cleaner Code</a></li>
<li>Next: <a href="https://icle.es/6-refactor.md">Font Size, Collision Bugs, and Refactors</a></li>
</ul>
]]></content:encoded></item><item><title>Building Pong with Zig and Raylib - Part 4: Smarter Collisions, Cleaner Code</title><link>https://icle.es/2025/07/10/building-pong-with-zig-and-raylib-part-4-smarter-collisions-cleaner-code/</link><pubDate>Thu, 10 Jul 2025 15:33:34 +0100</pubDate><guid>https://icle.es/2025/07/10/building-pong-with-zig-and-raylib-part-4-smarter-collisions-cleaner-code/</guid><description>&lt;p>Change of Plans&lt;/p>
&lt;p>I was going to dive into menus and UI, but after sharing the early version of
Pong on &lt;a href="https://ziggit.dev/">ziggit.dev&lt;/a>, I got a bunch of helpful feedback.
The feedback was the kind that makes you stop and think, &lt;em>ah, right&amp;hellip; I should
probably fix that before carrying on.&lt;/em>&lt;/p>
&lt;p>So that&amp;rsquo;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
&lt;em>should&lt;/em> be done in Zig (or at least, better than I had them before).&lt;/p></description><content:encoded><![CDATA[<p>Change of Plans</p>
<p>I was going to dive into menus and UI, but after sharing the early version of
Pong on <a href="https://ziggit.dev/">ziggit.dev</a>, I got a bunch of helpful feedback.
The feedback was the kind that makes you stop and think, <em>ah, right&hellip; I should
probably fix that before carrying on.</em></p>
<p>So that&rsquo;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
<em>should</em> be done in Zig (or at least, better than I had them before).</p>
<h2 id="-naming-matters">🧼 Naming Matters</h2>
<p>I’d originally named my files <code>paddle.zig</code> and <code>ball.zig</code> - 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, <code>Paddle.zig</code>, not <code>paddle.zig</code>.</p>
<p>It’s a small thing, but one that helps be a bit more idiomatic - and avoids
confusion when others are reading it.</p>
<p>(now to make the same change across many more files in
<a href="https://icle.es/endeavours/triangle.md">triangle</a>)</p>
<h2 id="-default-field-initializers-and-when-not-to-use-them">🛠️ Default Field Initializers (and When <em>Not</em> to Use Them)</h2>
<p>Another thing I learned: structs that aren’t used as config objects shouldn’t
use default field values. Instead, they should have an <code>init</code> constant that
represents their starting state.</p>
<p>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 <code>init</code> 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.</p>
<h2 id="-rls-please">✨ RLS, Please</h2>
<p>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.</p>
<p>I’d been using a mix of styles. Nothing broke, but consistency helps. So I swept
through the code and updated those as well.</p>
```zig
//main.zig
var left_paddle: Paddle = .init(Paddle.size.x * 0.5, .left, screen_height);
var right_paddle: Paddle = .init(screen_width - Paddle.size.x * 1.5, .right, screen_height);
var ball: Ball = .init(.{ .x = screen_width * 0.5, .y = screen_height * 0.5 });
```
<h2 id="-fixing-paddle-collisions-on-the-y-axis">🎯 Fixing Paddle Collisions on the Y Axis</h2>
<p>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.</p>
<p>The fix was simple: add/subtract the ball’s radius in the y-axis check. Much
better.</p>
```zig
const colliding = ball.pos.y + ball.r >= self.pos.y and ball.pos.y - ball.r <= self.pos.y + size.y;
```
<p>You can actually see the difference in-game - that satisfying little <em>thock</em> now
triggers when it should, even on corner hits.</p>
<p>(now I just need add some sounds - I&rsquo;d forgotten about that)</p>
<h2 id="-iscolliding-should-only-collide">🧽 <code>isColliding</code> Should Only Collide</h2>
<p>Previously, <code>isColliding</code> also handled coloring the paddle red when it detected
a hit - a debug leftover that had no place in the final function.</p>
<p>I stripped that out and left <code>isColliding</code> 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.</p>
```zig
// Paddle.zig
pub fn isColliding(self: *const Paddle, ball: *const Ball) bool {
    // which edge do we need to check
    const crossing_x: bool = switch (self.which) {
        .right => ball.pos.x + ball.r >= self.pos.x,
        .left => ball.pos.x - ball.r <= self.pos.x + size.x,
    };

    if (!crossing_x) {
        return false;
    }

    const colliding = ball.pos.y + ball.r >= self.pos.y and ball.pos.y - ball.r <= self.pos.y + size.y;

    return colliding;
}
```
<h2 id="-movement-logic-encapsulation">🔀 Movement Logic Encapsulation</h2>
<p>Paddle movement was scattered, and the logic for moving up/down lived inside
<code>main.zig</code>. I pulled that out into proper <code>moveUp</code> and <code>moveDown</code> methods on
<code>Paddle</code>.</p>
<p>It reads cleaner now:</p>
```zig
//Paddle.zig
pub fn moveUp(self: *Paddle, dt: f32) void {
    self.move(-100, dt);
}

pub fn moveDown(self: *Paddle, dt: f32) void {
    self.move(100, dt);
}
```
```zig
// main.zig
if (rl.isKeyDown(.w)) {
    left_paddle.moveUp(dt);
}

if (rl.isKeyDown(.s)) {
    left_paddle.moveDown(dt);
}

if (rl.isKeyDown(.e)) {
    right_paddle.moveUp(dt);
}

if (rl.isKeyDown(.d)) {
    right_paddle.moveDown(dt);
}
```
<p>…and it keeps the input logic in <code>main</code>, but the movement logic in the paddle -
where it belongs.</p>
<h2 id="-resolution-independence">📐 Resolution Independence</h2>
<p>Some values were hardcoded (like setting <code>y = 200</code> for paddle start position),
while others used <code>getScreenHeight()</code> and <code>getScreenWidth()</code>. I refactored to
make everything use actual screen dimensions, converting <code>i32</code> screen height
values to <code>f32</code> where needed.</p>
<p>It was a bit fiddly, but worth it. Now Pong should behave properly regardless of
window size.</p>
<h2 id="closing">Closing</h2>
<p>So, yeah - not the flashiest episode, but a satisfying one. Lots of small things
that feel better now that they’re fixed. And it&rsquo;s a reminder that sharing early
(even rough work) is usually a good idea. You never know what you&rsquo;ll learn.</p>
<p>Next time, we <em>will</em> get into UI. I’m planning to bring in
<a href="https://github.com/david-vanderson/dvui">DVUI</a> and show a basic score display,
a pause menu, and maybe some options for reset and quit.</p>
<p>Until then, thanks for reading (and watching) - see you in the next one.</p>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://icle.es/youtube/shri-codes/pong/pong-4.md">Watch Video</a></li>
<li><a href="https://icle.es/games/pong/">Source Code (at this point)</a></li>
<li>Prev: <a href="https://icle.es/3-scoring.md">Ball Movement &amp; Paddle Collisions</a></li>
<li>Next: <a href="https://icle.es/5-ui.md">Integrate DVUI &amp; Show Score</a></li>
</ul>
]]></content:encoded></item><item><title>Building Pong in Zig with Raylib – Part 3: Edge Collisions, Scoring &amp; Player Input</title><link>https://icle.es/2025/07/07/building-pong-in-zig-with-raylib-part-3-edge-collisions-scoring-player-input/</link><pubDate>Mon, 07 Jul 2025 12:01:00 +0100</pubDate><guid>https://icle.es/2025/07/07/building-pong-in-zig-with-raylib-part-3-edge-collisions-scoring-player-input/</guid><description>&lt;p>In this part, we round out the core gameplay of our Pong clone in Zig using
raylib. By the end, we&amp;rsquo;ve got a working game - with a bouncing ball, paddle
controls, and basic scoring.&lt;/p>
&lt;h2 id="-edge-collisions">🧱 Edge Collisions&lt;/h2>
&lt;p>We already had paddle collision working, but we needed the ball to bounce off
the top and bottom edges of the screen. That meant checking if the ball&amp;rsquo;s
y-position was outside the visible range and inverting its vertical velocity
accordingly:&lt;/p></description><content:encoded><![CDATA[<p>In this part, we round out the core gameplay of our Pong clone in Zig using
raylib. By the end, we&rsquo;ve got a working game - with a bouncing ball, paddle
controls, and basic scoring.</p>
<h2 id="-edge-collisions">🧱 Edge Collisions</h2>
<p>We already had paddle collision working, but we needed the ball to bounce off
the top and bottom edges of the screen. That meant checking if the ball&rsquo;s
y-position was outside the visible range and inverting its vertical velocity
accordingly:</p>
```zig
pub fn checkEdgeCollisions(self: *Ball, screen_height: f32) void {
    if (self.pos.y < self.r or self.pos.y > screen_height - self.r) {
        self.vel.y *= -1;
    }
}
```
<p>There was a small detour where I realised <code>screen_height</code> wasn’t giving the
expected value - using <code>GetScreenHeight()</code> directly from raylib helped resolve
that.</p>
<h2 id="-scoring-system">🏁 Scoring System</h2>
<p>Once edge collisions were in place, it was time to detect goals. If the ball
passed the left or right edge of the screen, the opposing player got a point. We
also need a <code>reset()</code> method to the <code>Ball</code> struct to return it to the centre
after each goal.</p>
```zig
pub fn reset(self: *Ball) void {
    self.pos = self.home;
    self.vel = .{ .x = 250, .y = -50 };
}
```
<p>For now, let&rsquo;s print the scores via <code>std.debug.print</code>, but this sets the stage
for UI integration.</p>
```zig
if (ball.pos.x > screenWidth) {
    left_paddle.score += 1;
    std.debug.print("scores: l: {d}, r: {d}", .{ left_paddle.score, right_paddle.score });
    ball.reset();
}

if (ball.pos.x < 0) {
    right_paddle.score += 1;
    std.debug.print("scores: l: {d}, r: {d}", .{ left_paddle.score, right_paddle.score });
    ball.reset();
}
```
<h2 id="-player-input">⌨️ Player Input</h2>
<p>Finally, we wired up keyboard input to allow paddle movement. We used
<code>IsKeyDown</code> from raylib, and made sure movement was frame-rate independent by
scaling it with <code>dt</code>:</p>
```zig
pub fn move(self: *Paddle, y: f32, dt: f32) void {
    self.pos.y += y * dt;
}
```
<p>Left paddle uses <code>W/S</code>, right paddle uses <code>E/D</code>. Simple and responsive.</p>
```zig
if (rl.isKeyDown(.w)) {
    left_paddle.move(-100, dt);
}

if (rl.isKeyDown(.s)) {
    left_paddle.move(100, dt);
}

if (rl.isKeyDown(.e)) {
    right_paddle.move(-100, dt);
}

if (rl.isKeyDown(.d)) {
    right_paddle.move(100, dt);
}
```
<h2 id="-whats-working">✅ What&rsquo;s Working</h2>
<p>By the end of this episode, we’ve got:</p>
<ul>
<li>Ball bounces off top and bottom edges</li>
<li>Scoring when the ball passes a paddle</li>
<li>Ball reset after each goal</li>
<li>Both paddles are fully controllable</li>
</ul>
<p>All that’s missing now is a visible score, a win condition, and maybe a simple
menu.</p>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://icle.es/youtube/shri-codes/pong/pong-3.md">Watch Video</a></li>
<li><a href="https://icle.es/games/pong/">Source Code (at this point)</a></li>
<li>Prev: <a href="https://icle.es/2-collisions.md">Ball Movement &amp; Paddle Collisions</a></li>
<li>Next: <a href="https://icle.es/4-refactor.md">Code Improvements from Review</a></li>
</ul>
]]></content:encoded></item><item><title>Building Pong in Zig with Raylib – Part 2: Ball Movement &amp; Paddle Collisions</title><link>https://icle.es/2025/07/04/building-pong-in-zig-with-raylib-part-2-ball-movement-paddle-collisions/</link><pubDate>Fri, 04 Jul 2025 12:01:00 +0100</pubDate><guid>https://icle.es/2025/07/04/building-pong-in-zig-with-raylib-part-2-ball-movement-paddle-collisions/</guid><description>&lt;p>In &lt;a href="https://icle.es/1-setup.md">Part 1&lt;/a> we set up the basics: a window, paddles, and a ball.
In this episode, we go one step further and get the ball moving 😉, add paddle
collisions, and make everything frame-rate independent.&lt;/p>
&lt;h2 id="ball-movement">Ball Movement&lt;/h2>
&lt;p>The first step was to give the ball some velocity and update its position every
frame.&lt;/p>
```zig
self.pos = rl.math.vector2Add(self.pos, self.vel);
```
&lt;p>We also fixed a small oversight: the ball and paddles were being recreated every
frame inside the game loop. Moving their initialization outside meant we could
actually observe state changes between frames.&lt;/p></description><content:encoded><![CDATA[<p>In <a href="https://icle.es/1-setup.md">Part 1</a> we set up the basics: a window, paddles, and a ball.
In this episode, we go one step further and get the ball moving 😉, add paddle
collisions, and make everything frame-rate independent.</p>
<h2 id="ball-movement">Ball Movement</h2>
<p>The first step was to give the ball some velocity and update its position every
frame.</p>
```zig
self.pos = rl.math.vector2Add(self.pos, self.vel);
```
<p>We also fixed a small oversight: the ball and paddles were being recreated every
frame inside the game loop. Moving their initialization outside meant we could
actually observe state changes between frames.</p>
<h2 id="frame-rate-independence">Frame-Rate Independence</h2>
<p>Raylib provides <code>GetFrameTime()</code> which returns the time in seconds since the
last frame. Multiplying the velocity by this <code>dt</code> value ensures that the ball
movement stays consistent across different frame rates:</p>
```zig
const vel_this_frame = rl.math.vector2Scale(self.vel, dt);
self.pos = rl.math.vector2Add(self.pos, vel_this_frame);
```
<p>With that, the ball now moves at a steady speed, no matter the frame rate.</p>
<h2 id="paddle-collision-x-axis">Paddle Collision (X-Axis)</h2>
<p>Next up: detecting collisions between the ball and paddles. I considered writing
a standalone collision checker, but ended up keeping the logic within the
<code>Paddle</code> struct itself.</p>
<p>To simplify which edge to check (left or right), I added a <code>which</code> field to
paddles - an enum with values <code>left</code> and <code>right</code>. That made the conditional
logic much cleaner.</p>
<p>To debug collisions, I added color switching: when a paddle detects a collision
on the x-axis, it flashes red.</p>
```zig
pub fn isColliding(self: *Paddle, ball: *const Ball) bool {
    // which edge do we need to check
    const crossing_x: bool = switch (self.which) {
        .right => ball.pos.x + ball.r >= self.pos.x,
        .left => ball.pos.x - ball.r <= self.pos.x + size.x,
    };

    self.colour = if (crossing_x) .red else .white;
    return crossing_x;
}
```
<h2 id="paddle-collision-y-axis">Paddle Collision (Y-Axis)</h2>
<p>After confirming horizontal collision detection, I added vertical bounds
checking. This just involved verifying the ball&rsquo;s y-position is within the
paddle’s vertical range.</p>
```zig
pub fn isColliding(self: *Paddle, ball: *const Ball) bool {
    // which edge do we need to check
    const crossing_x: bool = switch (self.which) {
        .right => ball.pos.x + ball.r >= self.pos.x,
        .left => ball.pos.x - ball.r <= self.pos.x + size.x,
    };

    if (!crossing_x) {
        self.colour = .white;
        return false;
    }

    const colliding = ball.pos.y >= self.pos.y and ball.pos.y <= self.pos.y + size.y;

    self.colour = if (colliding) .red else .white;
    return colliding;
}
```
<h2 id="bounce-logic">Bounce Logic</h2>
<p>With detection in place, we added bounce logic to the ball. If a collision with
a paddle is detected, we flip the x-component of the velocity vector:</p>
```zig
pub fn checkPaddleCollision(self: *Ball, paddle: *Paddle) void {
    if (paddle.isColliding(self)) {
        self.vel = rl.math.vector2Scale(self.vel, -1);
    }
}
```
<h2 id="whats-next">What’s Next</h2>
<p>That wraps up part 2. In the next episode, we’ll handle edge collisions (top and
bottom), scoring, and input management.</p>
<p>Thanks for following along!</p>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://youtu.be/IoOLH1O_a7M">Watch Video</a></li>
<li><a href="https://github.com/drone-ah/wordsonsand/tree/shri-codes/pong/part-2/games/pong">Source Code (at this point)</a></li>
<li>Prev: <a href="https://icle.es/1-setup.md">Place Paddles &amp; Ball</a></li>
<li>Next: <a href="https://icle.es/3-scoring.md">Edge Collisions, Scoring &amp; Inputs</a></li>
</ul>
]]></content:encoded></item><item><title>Building Pong in Zig with Raylib – Part 1: Setup, Paddles, and Ball</title><link>https://icle.es/2025/06/28/building-pong-in-zig-with-raylib-part-1-setup-paddles-and-ball/</link><pubDate>Sat, 28 Jun 2025 20:38:29 +0100</pubDate><guid>https://icle.es/2025/06/28/building-pong-in-zig-with-raylib-part-1-setup-paddles-and-ball/</guid><description>&lt;p>Before continuing development on Triangle - my larger, arcade ARPG, factory
game, I wanted to take a step back and build something small and familiar. Pong
felt like the perfect choice: quick to prototype, easy to understand, and a good
warm-up before diving deeper into raylib again.&lt;/p>
&lt;p>This post goes alongside &lt;a href="https://youtu.be/ICq2D_na6zc">my video on YouTube&lt;/a>,
and walks through the early steps of the project: setting up the game, getting
something on screen, and implementing the paddles and the ball.&lt;/p></description><content:encoded><![CDATA[<p>Before continuing development on Triangle - my larger, arcade ARPG, factory
game, I wanted to take a step back and build something small and familiar. Pong
felt like the perfect choice: quick to prototype, easy to understand, and a good
warm-up before diving deeper into raylib again.</p>
<p>This post goes alongside <a href="https://youtu.be/ICq2D_na6zc">my video on YouTube</a>,
and walks through the early steps of the project: setting up the game, getting
something on screen, and implementing the paddles and the ball.</p>
<h2 id="why-pong">Why Pong?</h2>
<p>Sometimes, before getting deeper into a project, it helps to do something simple
just to get your hands moving again. I hadn’t written Zig in a couple of weeks,
and wanted a fast feedback loop to:</p>
<ul>
<li>Get back into using <code>raylib-zig</code></li>
<li>Have a bit of fun</li>
<li>Remind myself why I made certain decisions in the first place</li>
</ul>
<h2 id="project-setup">Project Setup</h2>
<p>I&rsquo;m using <a href="https://github.com/Not-Nik/raylib-zig"><code>raylib-zig</code></a> for this. You
can scaffold a new project with:</p>
```bash
./project_setup.sh <project-name>
```
<p>You could also use <code>zig init</code> and then add the dependency in manually if you
prefer.</p>
<h2 id="drawing-the-playground">Drawing the Playground</h2>
<p>The Pong &ldquo;arena&rdquo; is simple:</p>
<ul>
<li>Two paddles</li>
<li>A ball (currently static)</li>
<li>A center dividing line</li>
<li>Placeholder for scores</li>
</ul>
<h2 id="implementing-paddles">Implementing Paddles</h2>
<p>The paddles have:</p>
<ul>
<li>A position (top-left corner)</li>
<li>A fixed size</li>
<li>A simple render function</li>
</ul>
```zig
const std = @import("std");
const rl = @import("raylib");

const Paddle = @This();

pos: rl.Vector2,

pub fn init(x: f32) Paddle {
    return .{ .pos = .{
        .x = x,
        .y = 200,
    } };
}

pub const size = rl.Vector2{ .x = 25, .y = 100 };

pub fn render(self: Paddle) void {
    rl.drawRectangleV(self.pos, size, .white);
}
```
<p>It might have been nice to be able to have the paddle calculate more of its own
values, but the more I think about it, the more it makes sense to keep the logic
in paddle simpler.</p>
<h2 id="ball-placeholder">Ball Placeholder</h2>
<p>To wrap things up for this session, I added a static ball in the center of the
screen. It has a radius, position, and velocity fields ready to go. Rendering is
straightforward:</p>
```zig
const std = @import("std");
const rl = @import("raylib");

const Ball = @This();

pos: rl.Vector2,
r: f32 = 16,
vel: rl.Vector2 = .{ .x = 0, .y = 0 },

pub fn render(self: Ball) void {
    rl.drawCircleV(self.pos, self.r, .white);
}
```
<h2 id="whats-next">What&rsquo;s Next</h2>
<p>This was mostly about warming up, but the next episode will tackle:</p>
<ul>
<li>Adding velocity to the ball</li>
<li>Basic collision detection with paddles</li>
</ul>
<p>I&rsquo;ll try and think about simplifying paddle logic, especially the awkward
symmetry between left and right.</p>
<p>You can find the full source code
<a href="https://github.com/drone-ah/wordsonsand/tree/main/games/pong">on GitHub</a>.</p>
<p>See you in part 2!</p>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://icle.es/youtube/shri-codes/pong/pong-1.md">YouTube Video</a></li>
<li><a href="https://github.com/drone-ah/wordsonsand/tree/shri-codes/pong/part-1/games/pong">Full Source Code (at this point)</a></li>
<li>Next: <a href="https://icle.es/2-collisions.md">Ball Movement &amp; Paddle Collisions</a></li>
</ul>
]]></content:encoded></item><item><title>Under the Hood of triangle</title><link>https://icle.es/2025/06/27/under-the-hood-of-triangle/</link><pubDate>Fri, 27 Jun 2025 08:01:28 +0100</pubDate><guid>https://icle.es/2025/06/27/under-the-hood-of-triangle/</guid><description>&lt;p>In &lt;a href="https://youtu.be/8nA-a5Z1IDc">this Let&amp;rsquo;s Code video&lt;/a> on
&lt;a href="http://www.youtube.com/@ShriCodesHere">&lt;strong>shri codes&lt;/strong>&lt;/a>, I walk through some of
the early systems in place for &lt;a href="https://icle.es/tags/triangle/">&lt;em>Triangle&lt;/em>&lt;/a> - my space
ARPG built in &lt;strong>Zig&lt;/strong> with &lt;strong>raylib&lt;/strong>.&lt;/p>
&lt;p>For the broader story behind the project - what &lt;em>Triangle&lt;/em> is, where it&amp;rsquo;s going,
and why I’m making it - check out the
&lt;a href="https://icle.es/endeavours/triangle.md">triangle endeavour&lt;/a> or the
&lt;a href="https://icle.es/tags/triangle/">devlog archive&lt;/a>. This post is about the &lt;em>how&lt;/em>.&lt;/p>
&lt;h3 id="-core-systems-so-far">🧩 Core Systems So Far&lt;/h3>
&lt;p>At this stage, Triangle supports:&lt;/p>
&lt;ul>
&lt;li>Basic player movement and shooting (Asteroids-style).&lt;/li>
&lt;li>Procedurally generated asteroid fields per &lt;em>sector&lt;/em>.&lt;/li>
&lt;li>Material drops (iron ore) from destroyed asteroids.&lt;/li>
&lt;li>Automatic recipe unlocking when new items are picked up.&lt;/li>
&lt;li>Smelting system that converts ore to ingots.&lt;/li>
&lt;li>Unlocking and queuing up &lt;em>constructors&lt;/em> using the crafting menu.&lt;/li>
&lt;li>A basic UI for crafting and inventory.&lt;/li>
&lt;li>Configurable keyboard mappings using TOML.&lt;/li>
&lt;/ul>
&lt;p>That’s the visible layer. Below the surface, the codebase has carved out some
space for more ambitious systems.&lt;/p></description><content:encoded><![CDATA[<p>In <a href="https://youtu.be/8nA-a5Z1IDc">this Let&rsquo;s Code video</a> on
<a href="http://www.youtube.com/@ShriCodesHere"><strong>shri codes</strong></a>, I walk through some of
the early systems in place for <a href="https://icle.es/tags/triangle/"><em>Triangle</em></a> - my space
ARPG built in <strong>Zig</strong> with <strong>raylib</strong>.</p>
<p>For the broader story behind the project - what <em>Triangle</em> is, where it&rsquo;s going,
and why I’m making it - check out the
<a href="https://icle.es/endeavours/triangle.md">triangle endeavour</a> or the
<a href="https://icle.es/tags/triangle/">devlog archive</a>. This post is about the <em>how</em>.</p>
<h3 id="-core-systems-so-far">🧩 Core Systems So Far</h3>
<p>At this stage, Triangle supports:</p>
<ul>
<li>Basic player movement and shooting (Asteroids-style).</li>
<li>Procedurally generated asteroid fields per <em>sector</em>.</li>
<li>Material drops (iron ore) from destroyed asteroids.</li>
<li>Automatic recipe unlocking when new items are picked up.</li>
<li>Smelting system that converts ore to ingots.</li>
<li>Unlocking and queuing up <em>constructors</em> using the crafting menu.</li>
<li>A basic UI for crafting and inventory.</li>
<li>Configurable keyboard mappings using TOML.</li>
</ul>
<p>That’s the visible layer. Below the surface, the codebase has carved out some
space for more ambitious systems.</p>
<h3 id="-code-structure-overview">🗂️ Code Structure Overview</h3>
<p>Here’s a quick tour of the key directories and their purpose:</p>
<h4 id="blit"><code>blit/</code></h4>
<p>Handles all the 2D rendering abstractions - bounding boxes, shapes (including
triangle fans 😉 and circles), and canvas drawing. Wraps <code>raylib</code> for better
namespacing and type control.</p>
<h4 id="phys"><code>phys/</code></h4>
<p>Physics system. <code>body.zig</code> implements basic movement, collisions, and inertia.
Still minimal, but usable.</p>
<h4 id="combat"><code>combat/</code></h4>
<p>Still early - only <code>bullet</code> is implemented, and is used to destroy asteroids.
Placeholder for upcoming systems like health, damage, and targeting.</p>
<h4 id="items"><code>items/</code></h4>
<p>This module forms the backbone of the crafting system. Items are tied to
recipes, and recipes are triggered via pickups or crafting actions. There&rsquo;s a
lot more to unpack here - likely its own devlog soon.</p>
<h4 id="notifier"><code>notifier/</code></h4>
<p>Manages in-game UI feedback. When you pick something up or unlock a new recipe,
this is what shows it. Notifiers are scoped - one for inventory, one for
crafting - and show up on either side of the screen.</p>
<h4 id="ship"><code>ship/</code></h4>
<p>This module contains the ship related logic, including movement, and the logic
for constructing smelters and other buildables. Eventually this will handle
assembly chains.</p>
<h4 id="asteroid"><code>asteroid</code></h4>
<p>These files should be refactored into its own module, and should contain the
asteroid itself, as well as sector, a chunk of space - asteroids, entities,
bullets, etc. This is the closest thing to a “level” in Triangle. It’s
procedurally generated, and you can eventually travel from one sector to the
next.</p>
<h4 id="diag"><code>diag/</code></h4>
<p>Code to support diagnostics &amp; logging</p>
<h4 id="game"><code>game/</code></h4>
<p>This module should get a bit of refactoring as well, moving some of the files
from the root into its own module</p>
<ul>
<li><code>config</code>: Takes care of user control bindings and input overrides via a TOML
config file.</li>
<li><code>notifier</code>: supports collecting notifications across multiple channels.</li>
<li><code>save</code>: Just scaffolding for now - no save/load logic yet, but it’s a
placeholder to group serializable components together.</li>
<li><code>context</code>: container struct to support dependency injection.</li>
</ul>
<h4 id="ui"><code>ui/</code></h4>
<p>Holds in-game panels like crafting and inventory. Needs cleanup and
consolidation.</p>
<h3 id="-whats-missing">🧪 What’s Missing</h3>
<p>A lot of the systems are stubbed out, but not yet wired in. For instance:</p>
<ul>
<li><code>power</code> is empty (future energy system)</li>
<li>The <em>canvas</em> currently wraps <code>raylib</code> without adding much - that will likely
change as UI complexity grows.</li>
</ul>
<h3 id="-threads-ill-pull-next">🧶 Threads I’ll Pull Next</h3>
<p>These are the next things I’ll either build or record:</p>
<ul>
<li>Improving the coding structure. I&rsquo;d like to improve organisation and am
considering extracting a re-usable library out of it, mainly so I can tinker
with other smaller game projects.</li>
<li>A <strong>rudimentary menu system</strong></li>
<li>A <strong>build+deploy system</strong> to generate early test builds</li>
<li>Possibly a <strong>contact/feedback</strong> form directly in-game or on the site</li>
</ul>
<p>I’ll be releasing this in what I’m calling a <a href="https://icle.es/sprout.md"><strong>&ldquo;Sprout&rdquo; build</strong></a> -
a playable seed, a form of extremely early access. Feedback and collaboration is
really important to me and I want to get this out there as soon as there is the
tiniest spark of &ldquo;life.&rdquo;</p>
<p>If you’d like to follow the journey, you can
<a href="https://youtu.be/8nA-a5Z1IDc">watch the video devlog</a></p>
]]></content:encoded></item><item><title>Basic Gameplay</title><link>https://icle.es/2025/05/08/basic-gameplay/</link><pubDate>Thu, 08 May 2025 10:07:08 +0000</pubDate><guid>https://icle.es/2025/05/08/basic-gameplay/</guid><description>&lt;p>The first goal was to get it working as far as the coding challenge itself.&lt;/p>
&lt;h2 id="the-ship">The Ship&lt;/h2>
&lt;p>Spawning the ship itself was relatively straightforward. I only needed three
points and &lt;code>drawTriangle&lt;/code> from &lt;code>raylib&lt;/code>.&lt;/p>
&lt;p>In the coding challenge, the ship was rotated with the keyboard, but I wanted
the ship to point to the mouse so that aiming was straightforward. Rotating to
the mouse was trickier — it involved &lt;code>atan2&lt;/code> and ChatGPT got me started.&lt;/p>
&lt;p>The coding challenge video did not worry about time lapsed between each frame,
but I wanted triangle to be framerate independent. That involved a bit of
jiggery pokery to get working, including determining how much the ship can move
within a time frame.&lt;/p>
&lt;p>Moving the ship was a bit easier, but based on the many videos of the Coding
Train that implemented force, velocity and dampening, it was pretty
straightforward. Dampening took a bit of trial and error. ChatGPT sent me down
the garden path initially with an over-complicated formula, but I was able to
simplify it later.&lt;/p>
&lt;p>Integrating the physics for linear momentum with the one for rotational momentum
was also quite nice — it meant that the ship could overshoot the aim when
rotating and will correct back.&lt;/p></description><content:encoded><![CDATA[<p>The first goal was to get it working as far as the coding challenge itself.</p>
<h2 id="the-ship">The Ship</h2>
<p>Spawning the ship itself was relatively straightforward. I only needed three
points and <code>drawTriangle</code> from <code>raylib</code>.</p>
<p>In the coding challenge, the ship was rotated with the keyboard, but I wanted
the ship to point to the mouse so that aiming was straightforward. Rotating to
the mouse was trickier — it involved <code>atan2</code> and ChatGPT got me started.</p>
<p>The coding challenge video did not worry about time lapsed between each frame,
but I wanted triangle to be framerate independent. That involved a bit of
jiggery pokery to get working, including determining how much the ship can move
within a time frame.</p>
<p>Moving the ship was a bit easier, but based on the many videos of the Coding
Train that implemented force, velocity and dampening, it was pretty
straightforward. Dampening took a bit of trial and error. ChatGPT sent me down
the garden path initially with an over-complicated formula, but I was able to
simplify it later.</p>
<p>Integrating the physics for linear momentum with the one for rotational momentum
was also quite nice — it meant that the ship could overshoot the aim when
rotating and will correct back.</p>
<h2 id="the-asteroids">The Asteroids</h2>
<p>The coding challenge worked with a fixed number of on screen asteroids and they
wrapped around. I needed to expand this to:</p>
<ul>
<li>A potentially infinite vertical scroller.</li>
<li>Collisions between the asteroids (currently the same as from the challenge,
only checks full circle collision)</li>
<li>Figure out how to handle asteroids moving out of the screen</li>
</ul>
<p>The above will be covered in a bit more depth in the
<a href="https://icle.es/2025-05-10-asteroid-field.md">next devlog</a>.</p>
<h2 id="the-camera">The Camera</h2>
<p>I also needed a follow camera. Unlike the original asteroids, the ship can move
up/down and the camera should follow it.</p>
<p>The current code looks something like this:</p>
```zig
/// ship_y: ship's current y position
/// ship_vy: ship's y velocity
/// dt: time elapsed since last update
/// margin: the zone out of which camera is moved
/// speed: maximum camera movement speed
fn updateCameraY(self: *Self, ship_y: f32, ship_vy: f32, dt: f32, margin: f32, speed: f32) void {
    const cam_y = self.camera.target.y;
    const inner_margin = margin * 0.8;

    // Predict future ship position
    // TODO: at high speeds, when the clamp goes off, the camera does a little shuffle
    // It would be nice to fix that
    const screen_height: f32 = @floatFromInt(self.screen.height);
    const predicted_y = ship_y + std.math.clamp(ship_vy * 30.0, -screen_height, screen_height);

    var target_y = self.camera.target.y;
    var target_clr: rl.Color = .red;

    const tmargin = cam_y - inner_margin;
    const bmargin = cam_y + inner_margin;
    if (predicted_y < tmargin) {
        target_y = predicted_y + margin;
        target_clr = .blue;
    } else if (predicted_y > bmargin) {
        target_y = predicted_y - margin;
        target_clr = .green;
    }

    if (target_y > screen_height / 2) target_y = screen_height / 2;
    const t = std.math.clamp(dt * speed, 0.0, 1.0);
    self.camera.target.y = lerp(cam_y, target_y, t);
}
```
<p>It predicts the position of the ship, based on its y velocity. If that&rsquo;s outside
the middle zone, it&rsquo;ll move the camera at up to the maximum speed. There is some
clamping to prevent things flying off at high speeds. It also prevents the
camera from overtaking the ship.</p>
<p>This code is mostly hacked together with some help from ChatGPT. It&rsquo;ll be
cleaned up later, once I have a better indication of possible ship speeds, and
how/if we&rsquo;ll zoom in and out as well.</p>
<p>Another important facet to consider with the camera was what to do with the
edges. Out of the four edges, only one is infinite.</p>
<p>The current solution is that the camera stops tracking when it gets to the
&ldquo;bottom.&rdquo; It also does not move to the left or right. The ship, on the other
hand is free to move out of the range of the camera. Currently, nothing changes
except that the camera does not follow.</p>
<p>Since the thrust works in the direction of the mouse, it&rsquo;s pretty easy to bring
the ship back on to the screen. This feels like the world is big and still out
there, we just don&rsquo;t track what happens out there.</p>
<p>I considered wrapping around on the left and right, but that felt more like the
ship was trapped in that zone. I want the feeling of
<a href="https://icle.es/2025-04-26-a-lonely-triangle.md#story">being trapped in vengeance</a> to be more
subtle ;)</p>
<h2 id="combat">Combat</h2>
<p>Combat is pretty much a mirror image of what happens in the coding challenge,
though I didn&rsquo;t have some of the helper functions. I learned some math :)</p>
<p>Initially, the update loop only handled asteroid collisions. I added bullets as
a separate field in the update loop. It then checks each bullet with every
asteroid in the active chunks (covered in
<a href="https://icle.es/2025-05-10-asteroid-field.md">devlog #2</a>).</p>
<p>If there is collision, the asteroid is split into two, moved apart a bit, and
given opposite linear momentum. The bullet also takes &ldquo;damage&rdquo; at this point,
and can be removed. The damage system is designed to support penetration, which
is currently not active.</p>
<p>If the spawned asteroids would be too small, nothing is spawned. Later on, this
would be the trigger to spawn a material drop.</p>
<h2 id="zig--raylib">Zig / Raylib</h2>
<p>Learning <a href="https://ziglang.org">zig</a> and <a href="https://www.raylib.com/">raylib</a> was a
big part of this. Fortunately, both were a lot of fun to learn and work with.</p>
<p>One thing I found annoying was that the vector functionality in raylib was
scattered around as individual functions instead of on the vector struct. While
this was understandable, with raylib being written in C, I found it a bit
frustrating.</p>
<p>I ended up writing my own Vector struct in zig and the functions that I used as
methods on that struct. It was an opportunity for me to learn some vector math
as well.</p>
<p>I also encapsulated raylib inside a <code>Canvas</code> struct. I probably still have some
<code>rl.</code> calls other places in the code, but the idea is that a canvas is passed
into any bits of code that needs it. The main help is that it&rsquo;ll convert our
version of <code>Vector2</code> to the one that raylib wants.</p>
<p>I am also thinking about how I want input handling to work. I would like to
encapsulate that into a separate struct. Right now, I mainly access raylib
directly.</p>
<h2 id="manual-testing">Manual testing</h2>
<p>While I am a big proponent and fan of Test Driven Development, I was happy
enough with manual testing for triangle. I found joy in seeing it work each time
I manage to get something working.</p>
<p>I do end up writing tests later, particularly when I got to bits of code that
was harder to test manually.</p>
<h2 id="feel">Feel</h2>
<p>triangle already feels fun and light. There are some clunky elements to be
ironed out, but so far, it feels good :)</p>
<h2 id="other-posts">Other posts</h2>
<ul>
<li><a href="https://youtu.be/F2ITT2-uKso">Companion vlog for this post</a></li>
<li><a href="https://icle.es/2025-04-26-a-lonely-triangle.md">Prev: A lone triangle vs the universe</a></li>
<li><a href="https://icle.es/2025-05-10-asteroid-field.md">Next: Procedural Asteroid Field</a></li>
</ul>]]></content:encoded></item></channel></rss>