⚡ Zig Guide LiveUnofficialbut fully verified
✓ Zig 0.17.0-dev.1941+71115f0abWhat's newOn an older Zig?

Input

The simulation runs at a fixed 120 Hz. The window runs at whatever the display does. Those do not divide evenly, so a rendered frame may owe the simulation zero ticks, or one, or three.

A press has to reach exactly one of them. Drop it and the game eats inputs. Repeat it and one tap crosses three lanes.

examples/lane-dodger/src/platform/input.zigLatch
pub const Latch = struct {
    left: bool = false,
    right: bool = false,
    confirm: bool = false,

    /// Called once per rendered frame, before the tick loop.
    pub fn poll(self: *Latch) void {
        if (rl.IsKeyPressed(rl.KEY_LEFT) or
            rl.IsKeyPressed(rl.KEY_A)) self.left = true;
        if (rl.IsKeyPressed(rl.KEY_RIGHT) or
            rl.IsKeyPressed(rl.KEY_D)) self.right = true;
        if (rl.IsKeyPressed(rl.KEY_SPACE) or
            rl.IsKeyPressed(rl.KEY_ENTER)) self.confirm = true;

        // Touch and mouse: tap a side of the screen to steer, which is how the
        // game is actually played on a phone.
        if (rl.IsMouseButtonPressed(rl.MOUSE_BUTTON_LEFT)) {
            const x = rl.GetMousePosition().x;
            if (x < @as(f32, @floatFromInt(rl.GetScreenWidth())) * 0.5) {
                self.left = true;
            } else {
                self.right = true;
            }
            self.confirm = true;
        }
    }

    /// Hand the latched presses to one tick and forget them.
    pub fn take(self: *Latch) sim.Input {
        const input: sim.Input = .{
            .left = self.left,
            .right = self.right,
            .confirm = self.confirm,
        };
        self.* = .{};
        return input;
    }

    pub fn pending(self: *const Latch) bool {
        return self.left or self.right or self.confirm;
    }
};

poll runs once per rendered frame, before the tick loop. take hands the latched presses to one tick and clears them.

Why the fields are booleans and not key states

IsKeyPressed is true on the frame a key goes down and false while it is held. That is the behaviour the game wants. A held D should move one lane, not slide across the board at a hundred lanes a second.

The simulation says so in its own type:

examples/lane-dodger/src/sim/sim.zigInput
/// Edge-triggered: each field means "pressed during this tick", not "held".
/// A held key must not slide the player across the board.
pub const Input = struct {
    left: bool = false,
    right: bool = false,
    confirm: bool = false,

    pub const none: Input = .{};
};

And a test holds the compiler to it:

examples/lane-dodger/src/sim/tests.ziga held direction moves exactly one lane
test "a held direction moves exactly one lane" {
    // Input is edge-triggered. If a held key were read as a press every tick,
    // the player would cross the board in two frames.
    var w: World = .init(1);
    w.start();
    const start_lane = w.player.lane;
    w.step(.{ .right = true });
    try std.testing.expectEqual(start_lane + 1, w.player.lane);
    for (0..60) |_| w.step(.none);
    try std.testing.expectEqual(start_lane + 1, w.player.lane);
}

A frame that owes no ticks

If a frame produces no ticks, take is never called and the latch keeps its presses for the next frame. A tap between two ticks is remembered rather than lost.

If a frame produces three ticks, the first gets the press and the other two get nothing. That is what take clearing itself buys.

Touch

The same poll reads the mouse, and splits the window down the middle.

Tapping the left half steers left, tapping the right half steers right, and either counts as the confirm that starts a run. That is the whole control scheme on a phone, and it needed four lines because the simulation already thought in lanes rather than in keys.

What the platform layer does not do

Mute is a keypress, and it never reaches the simulation:

if (rl.IsKeyPressed(rl.KEY_M)) self.audio.toggleMute();

Muting is not a rule of the game. Nothing in sim/ knows sound exists, and adding a key for it would have been the first crack in that.