← Back to the game

Workshop Notes

How Bangers and Smash! is put together, and the numbers we have tuned along the way. Written for anyone who wants to download the game from GitHub, poke at it and make it their own.

Start here

It is a plain browser game: JavaScript modules and Three.js (r180, loaded from a CDN). There is no build step and no bundler. Edit a file, refresh the page.

git clone https://github.com/gunpowderstudios/car-racer.git
cd car-racer
python3 -m http.server 8080     # or: npm start
# open http://localhost:8080/
npm test                        # node --test tests/*.test.mjs  (Node 20 or newer)
The tests run in plain Node with no browser. The game logic (physics, damage, AI, multiplayer, track links) has no DOM in it, so it can be tested; the drawing code cannot, so changes to looks have to be checked by eye in a real browser.
Licence. The source is public so people can read it, learn from it and experiment. A formal licence file has not been added yet (see the README), so please check the repository before you publish a modified copy.

Every number on this page comes from the source and is the value at version 17.258. Where a value lives in code, its file is named so you can change it.

The clock

The whole game runs a little faster than real time. Handling, jumps and tracks are unchanged; races just finish quicker. The speedometer and lap clock convert back to real mph and seconds.

SettingValueWhere
Game speed1.1GAME_SPEED in src/gameSpeed.js
Longest frame the game will believe0.05 sMath.min(0.05, rawDt) in src/main.js
Physics steps per frame, at most6stepPhysics in src/main.js
Physics step1/120 s (1/60 on Low)physicsHz in src/quality.js

The clock is the same on every device. A phone at 60 fps plays at exactly the speed of a desktop. At 30 fps (Low Power Mode) it plays at the same speed but looks choppier, and only below about 20 fps does the game actually slow down, because of the 0.05 s frame cap and the 6-step limit.

Frame time is smoothed by interpolating the drawn car between the last two physics steps, so a 120 Hz physics step on a 60 fps screen still looks smooth.

Graphics tiers

Three tiers live in one table, TIERS in src/quality.js. Pick one in the menu (Graphics), force one with ?q=low, ?q=medium or ?q=high, or leave it on Auto.

SettingLowMediumHigh
Pixel ratio cap11.52
Anti-aliasing (MSAA)offoffon
Shadowsoff1024 map2048 map, soft
Physics rate60 Hz120 Hz120 Hz
Rivals by default356
Dents a car can show068
Flying debris pieces04896
Smoke sprites at once60110180
Mirror redrawevery 3rd frameevery 2ndevery frame
Ground texture sharpness (anisotropy)248
Lowest adaptive resolution0.550.600.75

Adaptive resolution

While you drive, the game watches the frame rate in one-second windows (AdaptiveRes in src/quality.js):

Add ?perf to the address for a live overlay: tier, fps, resolution scale, physics Hz, draw calls, triangles, geometries and textures.

Phones

Which tier a device starts on

detectTier in src/quality.js, in this order. A forced tier (?q= or the menu) always wins.

What we measured

DeviceResult
Desktop (Mac, Chrome), High60 fps, about 680k triangles and 109 draw calls
iPhone 13, plugged in, High60 fps, 150–165k triangles solo, 330–410k with a caravan
iPhone 13, Low Power Mode30 fps: iOS caps the screen at 30. Same game speed, choppier look.

Nothing here has been measured on Android phones yet. If you have one, open ?perf and tell us what it says.

The tricks that made phones work

Vehicles

The catalogue is VEHICLES in src/vehicleChoice.js (look, size, toughness). The handling for each is in src/vehiclePhysicsProfiles.js, kept apart on purpose so desktop, phone, solo and multiplayer all share one set of numbers.

NameIdModel fileLength mMass kgToughness ×CaravanBonnet cam
Mr Musclecardodge-shrink.glb4.8914500.95yesyes
70s Saloonescortescort-shrink.glb4.059201.15yesyes
German Beemabmwbmw-SHRINK.glb4.3310801.00yesyes
Mini Roundaboutminimini-SHRINK.glb3.057001.20yesyes
French Cheesefrench-cheesefrench-cheese-SHRINK.glb3.836401.25yesyes
Cuddly Pandapandapanda-SHRINK.glb3.548601.15yesyes
Mobile Chariotmobility-carmobility-car-SHRINK.glb2.903801.40yesyes
Swedish Massagevolvovolvo-SHRINK.glb4.7914200.90yesyes
WASAABIsaabsaab-SHRINK.glb4.6612500.95yesyes
Classic Oil Burnerv8-pilotv8-pilot-SHRINK.glb4.4415100.85yesyes
Happy Campercampervancampervan-shrink.glb4.5012801.10nono
Rootmasterbusbus-SHRINK.glb6.4043000.70nono
Motorhomemotor-homemotorhome-SHRINK.glb4.8528000.80nono

Toughness is a multiplier on the damage a vehicle takes, so below 1 is tougher and above 1 is weaker. A tuk-tuk has a physics profile (520 kg) and a tow ball ready, but no model yet.

Adding a vehicle

  1. Make the model (see Making models) and put it in cars/.
  2. Add a physics profile in src/vehiclePhysicsProfiles.js.
  3. Add an entry in VEHICLES that points at the profile and the file, with its length and toughness.
  4. For a phone copy, add the lite file in cars/lite/ and list it in LITE_MODELS.
  5. To let it tow, add a tow-ball position in HITCH in src/caravan.js.

Damage

Every car has five zones: front, back, left, right and roof. A hit lands on the zone nearest the contact. When any zone reaches zero the car is wrecked and explodes, so where you are hit matters as much as how hard. The numbers are in src/damage.js and the game rules in src/derby.js.

Hit pointsFrontBackLeftRightRoof
Your car256128144144120
A rival7668686860

These are the base figures times TOUGHNESS (0.8; the table above is already scaled), and then each vehicle's own toughness multiplier. Your front end takes half of every hit (the plate welded on).

Visible dents

Dents are done in the vertex shader (src/dents.js): the body mesh is pushed in around the hit, so there is no extra geometry. The wheels are left out. A body needs enough triangles to dent smoothly (about 20–35k is good; a 7k body looks lumpy). How many dents show at once is the tier setting above; the caravan always shows at least 6.

Caravan

The Hastings 2000 is a separate body on a drawbar joint, with its own tyres, weight, damage and sway. Numbers are in CARAVAN in src/caravan.js; the look is in src/caravanView.js.

SettingValueWhat it does
mass450 kgabout half an Escort
hp100battering it takes before it is wrecked (was 220, then 60)
wallMul / carMul14 / 5its thin skin takes more from the same knock than a car does
cog0.5 mweight sits ahead of the axle, so the tail does not wag the car
tyreMu / tyreStiff1.5 / 38sideways grip (raised so the caravan stays behind the car instead of sliding out)
jointK / jointC / jointMax380000 / 11000 / 38000drawbar spring, damping and the most force it can pull
yawDamp1.5extra damping of the swing

The body also rocks a little on its springs when cornering (sway, looks only, at most about 4.5°). The picture lifts the caravan by about what the low side drops, so the wheels never sink into the road (src/caravanView.js, update).

When the caravan is wrecked its wheels fly off and the car drags it along, scraping, with the drag capped at what the towing car can pull. Dents go on the body only.

Hills

Cars have a hill assist so they do not slide backwards on steep slopes: on a climb with the throttle down, the drive force is raised to at least mass × g × slope × 1.4 (fading out above about 9 m/s), and driven tyres get extra grip in proportion to the slope (up to about double on a very steep climb). This holds a car up to about 33°; steeper than about 38° it still slides back.

Cameras and controls

Press C to cycle: Chase (desktop default), High (phone default), Bumper, Bonnet. The chase camera drops back and up when you tow. R (or 0) resets the view.

The bonnet camera

src/bonnetCam.js works out where to put it from the model itself, so it needs no per-car setting. It lines up the highest surface along the middle of the car and walks back from the nose: a bonnet is a low stretch followed by the windscreen climbing at least 0.28 m. The camera sits 0.1 m in front of that step and 0.3 m above the bonnet, tilted so the nose of the bonnet fills about an eighth of the screen. A bus, campervan or motorhome has a flat or steeply sloped nose, so it is skipped and C goes straight on.

Keys

KeyDoesKeyDoes
W SThrottle, brake (and reverse)BBack on the road
A DSteerBackspace / DeleteRestart the race
SpaceBoostCCamera
ShiftDrift (handbrake)VRear-view mirror
HHornPPause (single player)
XBlow up your own car (single player). Handy when you are stuck.MMusic
EEdit this track (desktop)EscMenu

Gamepad and touch controls work too; the key handling is in src/input.js.

Making models

Every vehicle is a .glb exported from Shrink, our own browser tool, using its Racer preset. The recipe:

tools/optimize-car.mjs is an older command-line way to shrink a textured glTF; tools/make-vehicle-thumbs.py makes the menu thumbnails.

File names are case sensitive on GitHub Pages. bmw-SHRINK.glb and bmw-shrink.glb are different files. The name in VEHICLES must match the file exactly, including the capitals. We keep SHRINK in capitals for new files.

House rules and gotchas

Every change

Gotchas

Tracks and game modes

The home page lists the built-in tracks under three headings that open and close (renderMenu in src/main.js; the tracks themselves are in RAW in src/templates.js).

HeadingTracks
Wacky RacingSpeedway, Technical, Croydon Overpass, Random circuit
Stunt and ShuntsThe Oval (Demolition Derby), The hills have highs, The Twister, Bluebell Woods, Thin and Crispy
Sunday DrivingComing soon

Thin and Crispy is a 12 m wide road with no walls, up to about 32 m in the air, with two jumps, banked bends, barrels and one huge dip to a flooded valley floor (pools of water across the whole road) and back up. The heights are TC_Y in templates.js. Pools of water can now be put on any track, not only dirt stages.

Game mode names in the cog menu: Wacky Racing (every rival a different vehicle), Send in the clones (same vehicle, different colours) and Demolition (last one standing). Laps default to 1 everywhere.

Replay and Make my movie

The game quietly records where every car is about 20 times a second (src/replay.js); Replay plays that back from TV-style cameras (src/replayMode.js). Replay and the movie button are for computers: a phone is for quick racing, so the movie button is hidden when the pointer is coarse.

Source map

FileWhat is in it
src/main.jsThe game loop, menus, modes, glue between everything
src/vehicle.js, vehiclePhysicsProfiles.jsCar physics and each vehicle's handling numbers
src/vehicleChoice.jsVehicle catalogue, gallery, lite models, multiplayer vehicle sync
src/carVisual.js, dents.js, wheelDebris.jsDrawing a car, wheel pivots, cameras, dents, flying wheels
src/damage.js, derby.js, ai.jsZones and hit points, the race and demolition rules, rival drivers
src/replay.js, replayMode.js, bestBit.js, mp4Save.jsRecording and playing back a race, picking the best bit, writing the movie
src/caravan.js, caravanView.js, caravanPhone.jsTowing physics, look, phone rule
src/quality.jsTiers, device detection, adaptive resolution, 30 fps hint
src/bonnetCam.js, cameraChoice.js, cameraOrbit.jsBonnet camera, camera cycle, drag-to-orbit
src/track.js, trackGeometry.js, templates.js, editor.jsTracks, built-in tracks, the editor
src/trackLink.jsTracks as links
src/multiplayer.js and mp*.jsPeer-to-peer multiplayer, chat, rejoining, caravan contacts
src/input.js, hud.js, audio.jsKeys, pad and touch; the on-screen display; sound
tests/Headless tests (npm test)