Devlog: building a 3D runner that loads in a browser tab
Servo Sprint started from one stubborn constraint: it had to open from a link. No store page, no installer, no "download our launcher". Someone taps a link in a group chat and is running down a factory line a few seconds later, on whatever they happen to be holding. That single rule shaped almost every technical decision in the game, and this post walks through the ones that mattered most.
Why three.js, and why no model files
The game is built on three.js, the long-running WebGL library. It is not a game engine, which is the point: there is no editor, no runtime to download, just a JavaScript module that draws triangles. The whole game is plain ES modules served as static files.
The less obvious choice is that there are no 3D model files. Every robot, crate, press, crane and roof truss is built in code from primitive shapes: tapered limb shells, joint rings, extruded panels. A robot is a JavaScript file that assembles itself. That sounds like extra work, and at first it is, but it pays back in three ways. Downloads stay small because geometry is generated on the device. Paint is trivial because every surface already knows whether it is a body panel or an accent. And because every robot shares one parts bin, a new robot is mostly a new arrangement of existing parts rather than a new art pipeline.
Loading fast on a bad connection
A runner that takes twenty seconds to load is a runner nobody plays. A few habits keep the first load short:
- One round trip for the module graph. The page lists every module up front with
modulepreload, so the browser fetches them in parallel instead of discovering imports one level at a time. - A pinned, minified three.js. The library comes from a CDN, mapped to its minified build by an import map and locked with Subresource Integrity, so the browser refuses any file that doesn't match the expected hash.
- Cache forever, invalidate by name. Every script, stylesheet and font URL carries a build token. Those files are cached for a year, and a new release simply changes the token. The HTML page itself always revalidates, so players get a new build the moment it ships.
- Music only when it plays. The soundtrack is about 1.5 MB, so it isn't downloaded at all until music actually starts, and never if you have it turned off.
A fixed timestep, so a course is a course
The most important line in the codebase is probably the one that says the simulation runs at a fixed 120 steps per second. Rendering happens whenever the screen refreshes, at 60, 90, 120 or 144 Hz, but the physics always advance in exact 1/120-second slices.
That matters because of the Daily Run and challenge links. Both depend on a seed: a number that generates the same zones, turns and obstacles for everyone. If physics depended on frame rate, a jump on a 144 Hz monitor would land in a slightly different place than on a 60 Hz phone, and "the same course" would quietly stop being the same. With a fixed step, a given seed and a given set of inputs produce the same run on every device. That is also what makes ghosts possible: a recorded run can be replayed beside you and stay honest.
Real units make tuning easier
The world uses one unit to 0.85 metres, and gravity is real gravity, 9.81 m/s², converted into those units. The starter robot stands about 1.6 m tall and jumps about 1.4 m. Lane changes use a critically damped spring, which settles in about 0.2 seconds without overshooting. Working in real units sounds pedantic, but it means a design note like "this robot should feel a bit heavier" turns into a concrete change (a 10% stronger gravity multiplier on its jump) rather than a magic number nobody remembers the meaning of.
Phones are the hard part
Desktop GPUs are forgiving. Phones are not. A few lessons that cost real time:
- Cap the pixels. Modern phones report device pixel ratios of 3 or more, and rendering a full-screen canvas at that resolution melts batteries. The canvas is capped at about 3.2 megapixels, which looks sharp and keeps frame rates steady.
- Rest when nobody is steering. On the title and game-over screens, phones draw the background at about 30 frames per second instead of full rate. Runs, countdowns and pauses are untouched.
- Survive a lost GPU. Mobile browsers can drop the WebGL context when memory runs short, especially iOS when you switch apps. Early builds showed a black screen. Now the game notices, pauses, shows a short message and rebuilds the renderer when the context comes back.
- Make swipes forgiving. A thumb rarely moves in a straight line. Swipes trigger after about 10 pixels, and a vertical swipe that drifts sideways still counts as vertical unless it is clearly horizontal.
A small server for the parts that must be trusted
Everything that can live on the device does: your save, missions, ghosts and settings. The parts that need to be shared or trusted (leaderboards, saved names, Crews and badges) run on a small serverless API backed by a SQL database. The server never trusts a score blindly: there is a ceiling on how fast points can arrive in any second and over a whole run, so an honest run never gets near it while an edited one stands out. Saving your name uses passkeys, the same Face ID or fingerprint unlock you already use elsewhere, with a short backup code as a fallback. There are no passwords and no email addresses to leak.
How it was built
Servo Sprint is a small, independent project. Much of its code was written with AI coding agents under close direction, while the design calls, the playtesting and the long tail of fixes were done by hand, one release at a time. Releases are small and frequent, each one tested on desktop and phone before it goes live, and each with a way to roll back.
What's next
The newest piece is the Workshop, where you rebuild a robot from real kinds of parts and see why it stalls, overheats or tips over. It is the most technically ambitious part of the game so far, and there is a separate post on the real robots that inspired the bots. If you want to see what all of this adds up to, the best way is still the simplest one: open the game and run.