← back to jackcoldrick.com
city-slinger — project detail
City Slinger icon

Side projectCity Slinger

Click a building, swing on the rope, let go and keep the arc. A side-scrolling web-swinger built in three.js — partly to see whether a rope can be made to feel right, and partly to answer a narrower question left over from two previous games: what does a 3D engine actually buy you?

The City Slinger start screen: the title in red over a dark city, the instructions beneath it, three mode buttons reading DAY, NIGHT and CYCLE, and a leaderboard panel on the right.
The start screen. Picking a light mode repaints the city behind the overlay immediately — the button is a preview of the choice, not a switch thrown after it.

What it is

You fall to the right through a city that never ends. Click a building and a web fires at it; hold to reel yourself in and gain speed; release and you keep exactly the arc you let go on. Pizza slices are worth a point, web fluid tops you back up, and you carry eight webs before you need to find more. Drones fly in against your direction of travel — touch one and the run is over. Distance goes to a global leaderboard.

Everything you can see and hear is generated as the page loads. There is no model file, no animation clip, no texture on disk and no audio file anywhere in the project.

How it's built

The game runs on three.js and WebGL, vendored into the project rather than fetched from a CDN — so it works offline, has no third-party dependency at runtime, and the version can never move underneath it. The parts that decide how the game feels — the rope, the character's posing, the lighting model — are plain JavaScript modules that import no engine types at all, which keeps them independent of the parts that draw.

Choosing a real 3D engine for a side-scrolling game buys three things outright:

  • Parallax is not computed. There are no per-layer scroll factors anywhere in the codebase. Buildings sit at ten different depths and a perspective camera does the arithmetic.
  • Occlusion is free. Foreground buildings pass in front of the player because they are in front of the player.
  • The character is posed, not animated. He is eight boxes, and every joint angle is computed from the physics each frame — so he leans correctly into arcs nobody keyframed.

The city itself is instanced rather than modelled: about a hundred buildings are in shot at any moment, drawn in 64 draw calls at 60fps, because every instance carries its own window grid in a single shared texture. A 78-unit tower gets 24 floors of windows and a walk-up gets 6, and both cost the same one draw call.

Mid-swing in City Slinger: the player hanging from a web between two towers, an avenue receding into haze below, buildings in several depth lanes fading into fog.
Mid-swing at dawn. Ten depth lanes reaching back half a kilometre, a hundred-odd buildings in shot, and an avenue running down into the haze.

Features

  • A rope that behaves like a rope. While the web is taut the player is integrated as a true pendulum, so momentum carries from one arc into the next. Reel in at the bottom of a swing and you come out of it faster, the way a child pumps a swing — and the line goes slack and snaps taut again exactly when the physics says it should.
  • An endless city that stores nothing. Every building's height, window grid, tint and whether the block is built on at all are derived from its position, so the city is consistent in both directions forever without remembering a byte. Block 400 is always the same building as block 400.
  • A four-minute day and night. A single value drives the sun and moon, the sky gradient, fog, shadows, exposure, bloom, whether the windows are lit and whether the street lamps and headlights are on. Day and Night pin the clock so the look never drifts; Cycle lets it run.
  • Facades that catch the light. Each architectural type gets matching colour, emissive and normal maps generated at startup, so a window reads as a recess rather than a square painted on, and a whole facade responds as the moonlight moves across it.
  • A city that reads as a city. Cross avenues open every hundred units so you can look straight down a side street: roads, kerbs, pavements, street lamps, planted medians, traffic, overpasses and sign gantries all recede into the haze, and every one of them is placed by the same hash, so nothing has moved when you swing back past it.
  • Speed you can see. Streaks in the near air give the eye something to measure against, and a directional blur smears the city behind the player while he stays sharp — so eighty units per second looks nothing like fifteen.
  • Aim assist you never notice. A web tries the exact ray you clicked, then a small fan around it, then the roofline of whichever reachable building best matches where you pointed — so a click a few pixels above a roof still catches.
  • Sound with no sound files. Wind, thwips and impacts are all synthesised in the browser through the Web Audio API, mixed through a single master gain.
  • A global leaderboard. Runs are submitted at the end and ranked by distance, so the start screen always opens on a score to beat.

The stack

Engine
three.js and WebGL, vendored into the project — no runtime dependency on a CDN
Rendering
Instanced geometry throughout: ~100 buildings in 64 draw calls, with a whole fleet of traffic in one more
Assets
None. Every texture, mesh and sound is generated in the browser at startup
Physics
A symplectic pendulum in a pure module with no engine types, verified headlessly
Audio
Synthesised through the Web Audio API
Leaderboard
A Vercel Function over Postgres (Neon), with the connection pool drained on instance freeze so pooling and scale-to-zero don't fight
Input
Pointer events throughout, so a finger and a mouse are one code path
Hosting
Vercel

A detail I like

The sun and the moon carry a deliberate, very old cheat. The visible disc is not where the light is. The key light needs to sit behind the camera so it rakes across the building faces you can actually see — but anything behind the camera is never once in shot. So the disc is drawn on the visible side of the sky, sharing the light's left-right and up-down position: the sun is on the right when the light comes from the right, and the mismatch is confined to the one axis the eye cannot read without cast shadows. It is depth-tested but does not write depth, so the skyline rises in front of it and the sun genuinely sets behind a tower.