Make a drivable car

Two crossing streets at sunset, lined with buildings, lamps and trees, and a car you can drive into all of them. The car and its handling come straight from City Drive; wgpu3d packages them as a prefab and a controller, so the whole game fits on one screen of code.

Click the scene, then drive with WASD or the arrow keys. Space is the handbrake. On a phone, drag up to drive and sideways to steer.

The whole program

import { createApp, createCar, createGround, createRoad, createBuilding, createTree, createLampPost, VehicleController, createRandom } from "wgpu3d";

const app = await createApp({ canvas: document.querySelector("#drive-demo"), environment: "sunset" });
const random = createRandom(7);

app.add(createGround({ size: 260, tiles: 52 }));
app.add(createRoad({ length: 240, width: 9 }));
app.add(createRoad({ length: 240, width: 9, yaw: Math.PI / 2, height: 0.04 }));

for (let block = -5; block <= 5; block++) {
  if (block === 0) continue;
  for (const side of [-1, 1]) {
    const along = block * 20;
    const height = random.range(8, 26);
    app.add(createBuilding({ seed: random.integer(1, 9999), width: 12, depth: 12, height, position: [side * 14, 0, along], litShare: 0.25 }));
    app.add(createBuilding({ seed: random.integer(1, 9999), width: 12, depth: 12, height: random.range(8, 26), position: [along, 0, side * 14], litShare: 0.25 }));
    app.add(createLampPost({ position: [side * 6, 0, along + 10] }));
  }
}

for (let index = 0; index < 60; index++) {
  const x = random.range(-120, 120), z = random.range(-120, 120);
  if (Math.abs(x) < 24 || Math.abs(z) < 24) continue;
  app.add(createTree({ seed: index, height: random.range(4, 8), kind: random() < 0.4 ? "pine" : "round", position: [x, 0, z] }));
}

const car = createCar({ paint: "#1d6fd8" });
car.addBehavior(new VehicleController({ bounds: { minX: -125, maxX: 125, minZ: -125, maxZ: 125 } }));
app.add(car);
app.follow(car, { speedFieldOfView: 12 });
app.start();

Laying out the world

The world is built from prefabs, functions that return a ready-made SceneObject. createRoad gives an asphalt strip with lane markings; the second road is rotated a quarter turn with yaw and raised with height so it sits cleanly on top of the first where they cross. Buildings get a random height and a litShare, the fraction of windows that glow, which is what makes the street look lived-in once the sun is low.

All the randomness comes from createRandom(7), a seeded generator. The same seed always lays out the same street, so a bug you find can be reproduced, and changing one number gives you a new town. The trees loop skips spots within 24 metres of either road so nothing grows in the traffic lanes.

The car

createCar builds the City Drive car: a body lofted from twelve cross-sections, a glass cabin, trim, headlights and four wheels. The wheels and body are separate child objects listed in car.parts, so a controller can spin and steer the wheels and tilt the body without rebuilding any geometry. All four wheels share one mesh.

VehicleController is a behavior that turns key presses into motion, using the same model as City Drive's car physics:

Every one of those numbers is an option. A heavier truck is new VehicleController({ maxSpeed: 20, acceleration: 9, steering: 1.2 }).

Collisions

Buildings, lamp posts and trees come with box colliders, rotated with the object. Each frame the controller treats the car as a circle, finds any colliders it overlaps and pushes it out along the shortest way. That is cheaper and steadier than a physics engine, and it is exactly what a driving game needs. bounds keeps the car on the map.

The chase camera

app.follow(car) puts a chase camera behind the car. It aims at a point ahead of the car rather than the car itself, so you see where you are going, and it follows with springy smoothing so turns feel weighty. speedFieldOfView: 12 widens the view by up to 12 degrees as you speed up, an old racing-game trick for a sense of speed.

Things to try