Transmission #004: Chunk Buffer, Location Map, Overpass Retries, and Collision

Today’s focus was turning the map from a single fixed grid into a streaming 3×3 chunk system, making Overpass more resilient, and giving the world proper collision so the player can’t fall through roads or walk through buildings and water.

Where Things Stood

The map was one OSMMapDisplay building a fixed tile grid (e.g. 2×2) around a center set once at start (GPS or inspector). No streaming, no follow-the-player. Overpass did one request per fetch with no retries. Terrain had a collider, but roads and water were mesh-only; buildings from cubes had BoxCollider; prefabs and decor depended on whatever the prefab had—so plenty of holes to fall through or walk through.

Target Behavior

Chunk grid: A 3×3 set of chunks centered on the tile under the player. One chunk = one OSM tile. When the player moves to another tile, we unload chunks that leave the 3×3 and load the new ones (e.g. moving east: unload a,d,g; load j,k,l). Unload when a chunk is 2+ tiles away from center. Same (tileX, tileY) always produces the same OSM/elevation data, so coming back to a,d,g reloads them identically—and later we can cache by that key.

Single world space: A fixed world origin (e.g. center of the tile the player started on). All chunk GameObjects are positioned in that world so coordinates are stable for future harvest/mine and gameplay.

Location-based load: Initial center from GPS (mobile) or a default/inspector fallback, so the first 3×3 is around the player’s location.

Overpass: Retries (e.g. 3 attempts with backoff) in the existing Overpass request path.

Collision: Roads, water, buildings, and decor get colliders so the player can’t fall through roads, walk through houses or trees, or pass through water.

How tiles load and unload as you move

The coordinator always keeps a 3×3 window of chunks centered on the tile under the player. Each cell below is one OSM tile (one chunk). The player is in the center tile.

Initial state — player in center tile e:

        North
    a      b      c
    d    [ e ]    f     ← center tile (player here)
    g      h      i
       West   East
        South

Player moves east into tile f. The center tile becomes f, so the 3×3 window shifts east. Chunks more than 1 tile away from the new center are unloaded; chunks that are now within the 3×3 are loaded:

  Unload: a, d, g   (west column, now 2 tiles left of center)
  Load:   j, k, l   (new column to the east of c, f, i)

        b      c      j
        e    [ f ]    k     ← new center (player here)
        h      i      l

Same idea for any direction: move north and the window shifts north (unload g,h,i; load the row above a,b,c); move diagonally and you unload/load the corner chunks. The world origin stays fixed, so chunk positions in world space are stable and returning to a previous tile reloads the same data.

1. Coordinate and Tile Helpers (MapCoordinateHelper)

We need two helpers that stay stateless and reusable (and cache-friendly later):

2. MapChunkCoordinator (3×3 Load/Unload)

New script: MapChunkCoordinator. It holds zoom, world scale per tile, and tile grid size. It has a follow target (e.g. player or flying beast). Origin tile (originTileX, originTileY) is set once at first run (from first GPS fix or initial lat/lon); world (0,0,0) is the center of that tile. Each frame (or at interval) we take the target’s world XZ, run WorldToTile, and get the current center tile. We keep a 3×3 set of chunk keys around that center.

Loaded chunks live in a dictionary keyed by (tileX, tileY). Each chunk GameObject is a child of the coordinator and is positioned so that +X = East, +Z = North, and the origin chunk sits at (0,0). When a (tileX, tileY) enters the 3×3, we instantiate a chunk (prefab or built in code), set its position, set its center lat/lon via TileToLatLon, and let it build (raster + terrain + Overpass + 3D). When a chunk leaves the 3×3 (Chebyshev distance > 1), we unload it and remove it from bookkeeping.

The coordinator also exposes the map API for the player and GPS: IsMapReady, HasTerrainForHeight, SampleHeightAtWorld, LatLonToWorld. So PlayerController and GPSFlyingController can talk to the coordinator instead of a single OSMMapDisplay when we’re in chunked mode. Height sampling: figure out which loaded chunk contains the world XZ (from chunk bounds), then sample that chunk’s terrain and add the chunk’s world Y if needed. LatLonToWorld uses the origin tile and world scale to convert lat/lon to world XZ in one consistent formula for the whole world.

Initial center: on start, if using device location we wait for GPS (same pattern as OSMMapDisplay’s WaitForLocationAndSetCenter), set origin tile from that, then create the initial 3×3; otherwise we use inspector lat/lon.

3. MapChunk (Per-Chunk Setup)

New script: MapChunk. It holds tileX, tileY (set by the coordinator). In Awake, it finds OSMMapDisplay in self or children, sets centerLatitude and centerLongitude from TileToLatLon, and sets useDeviceLocationForCenter = false, tileGridSize = 1. So when that map’s Start runs, it builds one tile at the right place. Optionally it can expose “is this chunk ready” or its Terrain for height sampling.

4. OSMMapDisplay

When used as a chunk, we don’t override center with device location—MapChunk sets center and disables useDeviceLocationForCenter, so no change to OSMMapDisplay logic beyond that. We can optionally expose center tile as read-only for height lookup; the “set in Awake, build in Start” flow is enough for this plan.

5. DEMTerrainBuilder / Overpass / OSM3DBuilder Per Chunk

They already read mapDisplay (and terrain, etc.) from the same GameObject or parent. So each chunk has its own child with OSMMapDisplay + DEMTerrainBuilder + OSMOverpassClient + OSM3DBuilder; they run in Start for that chunk with that chunk’s center and tileGridSize = 1. No code changes required for per-chunk operation. For height, the coordinator doesn’t use a single global mapDisplay; it uses SampleHeightAtWorld to find the right chunk and sample its terrain. OSM3DBuilder and DEMTerrainBuilder on each chunk stay as-is for local use.

6. Overpass Retries (OSMOverpassClient)

In FetchOSMDataCoroutine, wrap the single Overpass request in a retry loop: e.g. maxRetries = 3, retryDelaySeconds = 2f (configurable). On failure (request error or response with “error”/“remark”), if attempt < maxRetries, wait then retry with the same bbox/query. On success or after all retries exhausted, call onComplete (with data or null). Same query and bbox every attempt so behavior is deterministic.

7. Collision for Map Features (OSM3DBuilder)

No new physics layers are strictly required if we use existing ones; if we add “Map”, we assign it in the builder and include it in PlayerController’s groundMask.

8. Player and GPS Using the Coordinator

Introduce a small IMapProvider (or equivalent) that exposes IsMapReady, HasTerrainForHeight, SampleHeightAtWorld, LatLonToWorld. MapChunkCoordinator implements it. OSMMapDisplay implements it too so we can keep one field. In the scene, when using chunked map, we assign the coordinator to PlayerController and GPSFlyingController as the map provider. So: IMapProvider with those four members; both OSMMapDisplay and MapChunkCoordinator implement it; change the player/GPS field to IMapProvider (e.g. mapProvider or keep mapDisplay as the name).

9. Scene and File Checklist

Item Action
MapCoordinateHelper Add TileToLatLon, WorldToTile
MapChunkCoordinator New: 3×3 logic, follow target, origin, load/unload, map API
MapChunk New: set chunk center from (tileX, tileY) in Awake
OSMOverpassClient Add retry loop in FetchOSMDataCoroutine
OSM3DBuilder Add MeshCollider to roads and water; ensure/add colliders for buildings and decor
OSMMapDisplay Optional: expose center tile; MapChunk sets center in Awake
PlayerController / GPSFlyingController Use coordinator as map source (IMapProvider); assign in scene
Scene Add MapChunkCoordinator; use chunk prefab or template; set follow target; remove or disable single full-map build

Future-Proofing

Chunk key is (tileX, tileY, zoom). Load/unload and coordinate logic can later call a cache service (e.g. Redis/S3) that returns or stores tile/Overpass/DEM data for that key; coordinator and MapChunk stay the same. Harvest/mine and other gameplay can key off world position or (tileX, tileY) and store state per chunk or per world cell—chunk identity and world positions are stable thanks to the fixed origin.

That’s the plan for the chunk buffer, location-based map, Overpass retries, and collision. More soon as we implement it.