One codebase, three releases: how Cyber Surfer is built
Cyber Surfer runs in a browser tab, as an iPhone and iPad app, and as a YouTube Playables build. All three come from the same TypeScript code. Here's how that works, and a few decisions that kept the game small and fast.
The stack
- three.js for the 3D: the city, the traffic, the car, the lighting and the post-processing glow.
- TypeScript for everything, built with Vite.
- Capacitor wraps the same web build into the native iPhone and iPad app.
- Vercel hosts the website and a few small serverless functions for the leaderboards and cloud saves, backed by a Neon Postgres database.
Three targets, one switch
The build has one switch: the target. vite build --mode web, --mode mobile or
--mode youtube picks a small platform module for that release and leaves the others out. The game code
never talks to YouTube or Capacitor directly; it asks the platform module to save, pause, show an ad or vibrate, and
each target answers in its own way.
- Web (the version on this site) is an installable app with a service worker, so once you've played it, it loads offline. Saves go to your browser's storage and the cloud.
- Mobile uses native storage, pauses the game when the app goes to the background, and backs up your player in iCloud Keychain so a reinstall brings it back.
- YouTube Playables loads YouTube's SDK first and lets YouTube own pause, mute, saves and ads.
Living with YouTube's rules
YouTube Playables has strict rules for what a game bundle can contain, and they shaped the whole project. The build checks every one of them before it packages the game:
- Every file should stay under 512 KiB, so three.js and the world's scenery are split into their own chunks.
- Only relative paths, no outside URLs, and file names limited to plain letters, digits, dots, dashes and underscores.
- No browser storage, cookies or page-visibility events in the game code: YouTube handles saving and pausing.
- YouTube's SDK must be the very first script on the page.
Designing for the strictest platform first made the other two easy. A game that fits YouTube's rules loads fast on a phone browser too.
No physics engine
An endless runner doesn't need real physics, and a physics engine would make the controls feel loose. Instead, the car lives on three lanes along a curving track. A lane change is a timed slide, a hop is a jump arc with a set height, and collisions are simple box checks along the track. That's why the controls are exact: a hop always clears a car and always reaches a truck roof.
The road itself is generated as you drive: a spline that bends, climbs and dips, with curves that tighten and hills that steepen the further you get. The traffic, ramps, stunt sections and hazards are placed along it ahead of you, with a few fairness rules, like never letting traffic block all three lanes unless there's a truck to ride over.
The HUD is plain HTML
Everything you read on screen, from the score and stars to the menus and the garage, is HTML and CSS layered over the 3D canvas, not drawn in WebGL. It's sharper on every screen, easier to make readable, and it keeps the 3D side focused on the road.
Art and sound
The 3D world is built from Kenney's free CC0 asset packs: cars, city blocks, nature, boats and more, lit for the neon look. The hero car is modeled in code. Many of the sounds are synthesized at build time (the siren, the EMP, the laser, the near-miss whoosh), and the music and the rest of the sound effects come from Kenney's audio packs too. The 2D art (logo, icons and tutorial pictures) was generated for this project.
Shipping
A GitHub Actions workflow builds the apps and uploads the iPhone build to App Store Connect, so a new version for testers is one push away. The website deploys separately. The iPhone and iPad app went live on the App Store as Cyber Surfer: Neon Run (plain "Cyber Surfer" was already taken).
Next in the devlog: how the game changed from the first prototype to launch.