DEV Community

Cover image for Driving a Real Shenzhen in the Browser: OSM Data, Tiles and Stutter Fixes
Gavin Ding
Gavin Ding

Posted on Originally published at gavinding.com

Driving a Real Shenzhen in the Browser: OSM Data, Tiles and Stutter Fixes

Shenzhen Drive is a free 3D free-roam game that runs in the browser. You can drive, get out and walk, or switch to a drone and move across the real road network of Futian, Nanshan, Qianhai and Bao'an, out to the airport. It is a game, not real-world navigation.

This is not a "the AI generated a game in one click" story. It covers three concrete problems that came up when I put a real city map into a browser:

  • where the data comes from,
  • how to avoid making players wait for the whole city to download,
  • why the game stuttered while you were moving.

AI coding assistants (Claude / Codex) wrote much of the code. I set the direction, reviewed the results and made the trade-offs. Every number below names the version and conditions it was measured under.

Drone view over central Bao'an. Roads and building footprints come from OpenStreetMap.
Drone view over central Bao'an. Roads and building footprints come from OpenStreetMap.

1. From procedural blocks to a real map

The first prototype (tagged prototype-v1) was a procedural block about 700 m across. It looked like a city, but none of its roads existed. The next step was to switch to OpenStreetMap (OSM) data:

  • Offline snapshot. Roads, buildings, building parts, parks and water are queried through Overpass. The response is validated first, then the local snapshot is swapped in atomically. The game makes no online map requests at runtime.
  • Simple projection. Coordinates use an approximate equirectangular projection centred on a local origin, with X pointing east and Z pointing south. No GCJ-02 is mixed in.
  • Honest heights. Buildings are extruded from their real polygons, with courtyard holes kept. Tagged height or level values are used where they exist. Everything else gets a deterministic estimate by building type, and the data marks it as an estimate.
    • In the Futian stage, 1,416 of 4,166 building footprints and parts had real height tags. The other 2,750 were estimated.
    • The 599.1 m top of the Ping An Finance Centre comes from an OSM building part.

After expanding to Nanshan, Qianhai and Bao'an, the snapshot grew to:

Roads and paths 43,221
Building footprints and parts 33,821 (10,733 tagged heights, 23,088 estimated)
Land surfaces 5,350
Bounding extent about 29.3 × 24.5 km

The sea comes from OSM's natural=coastline. The directed coastline ways are turned into polygons using the "land on the left, sea on the right" rule, instead of laying down a flat square of water.

Don't trust a truncated response

The Overpass response for the western area ended with an output-timeout error. I didn't throw it away, and I didn't simply trust it either. Instead:

  1. A second out ids query over the same area confirmed that all 72,612 objects matched the downloaded set exactly by type/id.
  2. Coordinate completeness was checked object by object.
  3. Only then was the data accepted.

That check later became a script. It rejects any data whose manifest doesn't match or whose geometry is incomplete.

2. No waiting for the whole city: 1,200 m tiles

Loading the whole city at once worked on my own machine, but it isn't realistic over the public internet. Version 0.4 splits the map into 451 tiles of 1,200 m. Each building belongs to exactly one tile, and the total road length after splitting is checked against the original data.

  • Load by position. The browser first reads the navigation data, a city-wide overview and the tiles near the start point. After that, tiles load based on where the player is. The cache holds up to 48 tiles, and tiles in use are never evicted.
  • Binary navigation graph. City-wide navigation moved from about 10.1 MB of JSON to a 6.08 MB binary topology that keeps the original OSM node IDs. With gzip enabled on the server, about 2.41 MB actually goes over the wire.
  • Less to read at startup. Once the start point settles, the game has read about 13.1 MiB of map assets. The old version read three city-wide JSON files totalling about 32.7 MiB, so that's roughly 60% less, uncompressed. Note that this is the amount of data read, not network traffic: real downloads vary with compression, caching and how far you roam.
  • Level of detail. Nearby buildings use full footprints, while distant ones use simplified blocks. When the drone flies high, street-scale detail is hidden. Under sustained slow frames, the pixel ratio steps down from a maximum of 1.5 to 1.

3. Stutter while moving: moving geometry into a worker

Tiling created a new problem. While driving, a "loading nearby blocks" message kept popping up, along with stutter. Here's what was going on:

  • Downloads and JSON parsing were already in a worker.
  • But building geometry, road detail, batching and collision indexes still ran on the main thread.
  • Movement logic waited until every nearby tile was ready before responding.
  • Every update also redrew a 4096×4096 minimap base layer.

Driving view: nearby blocks are built in the background while you drive through Bao'an.
Driving view: nearby blocks are built in the background while you drive through Bao'an.

The fix had four parts:

  1. A dedicated geometry worker. It talks to the download worker directly over a MessageChannel. Geometry still uses the same deterministic generation rules, with no quiet swap to a simplified version. When it's done, ownership of the ArrayBuffers is transferred to the main thread instead of copied.
  2. Small batches on the main thread. Each batch takes at most 24,576 vertices or 4,096 instances. When everything is ready, it is attached to the scene in one go. Until then, the old coarse model stays visible.
  3. Only wait for what's underfoot. Movement now needs only the tile you're standing on and its collision index, not the surrounding tiles. Ground edges that aren't ready yet still block you, so you can't drive into empty space.
  4. Cached minimap. The city-wide minimap base is cached, and only position and route are updated while moving.

Results. The test ran in a local browser with a fixed 1422×800 drawing buffer, flying the drone over Qianhai in 4 × 5-second segments.

  • Copying one tile from the main thread had previously been measured at 28.8 ms. After the change, the largest single main-thread batch took about 0.8 ms.
  • Heavier builds take about 280 ms in the worker, but they no longer block rendering.
  • p95 frame time during continuous flight was 17.3–17.8 ms.

4. Routing and collision: better unreachable than through a building

  • Directed A*. Routing runs A* over a directed graph built from OSM nodes and respects one-way streets. If two roads cross geometrically but share no node, as with stacked interchange levels, they are not joined into an intersection.
  • Clearance checks. Each road segment is checked every 2 m against the vehicle radius. Edges that pass through buildings never enter the navigation graph. If a destination is unreachable, the game says so instead of falling back to a straight line through buildings.
  • Bridges and ramps. Bridge layers and connections come from OSM. Height is estimated at 6 m per layer, and connecting ramps have a maximum grade of about 6%. Once a vehicle climbs a ramp onto an elevated road, it stays on that layer.
  • Tests. Version 0.4's automated tests cover 156 directed routes between 13 landmarks. The landmark set has since grown to 21.

City map: a route follows the real road network from central Bao'an to Qianhai.
City map: a route follows the real road network from central Bao'an to Qianhai.

Try it

  • It's free, with no sign-up or login. A desktop browser is recommended, and give the first load a few seconds.
  • Controls: WASD / arrow keys to move, F to get in or out, G for the drone, M for the map. Click anywhere on the map to navigate there or jump straight to it.
  • More: AI-assisted indie games you can play in a browser · Game library

Map data © OpenStreetMap contributors, available under ODbL 1.0.


🚗 Play Shenzhen Drive in your browser

Originally published at gavinding.com. By Gavin Ding.

Top comments (0)