mattalcock.com/labs

Labs · Library

Concepts

A pattern is a pure function of position

Strudel looks like it is about time, but the idea underneath is more general. A pattern is a pure function: give it a position and it tells you what happens there.

events = pattern(clock, context)

In this repository a position is measured in cycles, and one cycle is one bar. mini("x ~ x x") doesn't know about seconds, tempo, audio or games. Ask it what starts between bar 3 and bar 3.5 and it answers.

Because patterns are pure you get a lot for free:

The clock decides what position means

The pattern core (src/pattern in Jump) has one Clock class. It converts between seconds on some timebase (usually an AudioContext's currentTime) and beats and bars. Any clock that can do that can drive any pattern.

Clock Context
Strudel tempo time almost none: sliders, MIDI
Chop the track's playhead and beat grid the sound itself: stems, energy, key, drops
Jump musical time, shared by game and music game state: danger, lives, thrust

Predictability decides the engineering

The real difference between these clocks is how predictable they are.

That is why state drives context, not the clock. If game state were a clock it would lurch around. Instead state selects and bends patterns, and the clock stays steady.

Jump makes quantisation free. Music layers are functions of state, and the scheduler evaluates them only when it books their next steps. A state change therefore lands on the next step that hasn't been booked yet. Nothing has to be cut.

One rule for games

The player's actions are instant. The world's actions land on the beat.

A bullet leaves the ship the moment you tap. Its sound lands on the next sixteenth. Rocks spawning, the saucer arriving, waves starting and respawns all happen on the same clock as the music.

Planning backwards from a predictable clock

A track is a predictable clock, so a game can work backwards from the music. Chop knows when the drop is. Jump can launch rocks off-screen early so that they arrive on the drop. Every spawn is worked out from the moment it should be seen:

launch = arrive − time to travel from off-screen to the screen edge

The travel time is integrated backwards through any slow-motion sections (breakdowns run at 45% speed), so a rock launched before a breakdown and arriving after it still lands on its beat. In the headless playtests, rocks cross onto the screen within one frame of their beat.

Separate libraries, shared core

Chop and Jump are separate projects with no dependency on each other. The pattern core lives in Jump; Chop keeps its own small copy of the Clock and key helpers so it stands alone. They meet at the clock: trackClock(map, ...) from Chop is a Clock, and Jump accepts any Clock. The Asteroids demo is the place where they are combined.