Guide
Game level streaming explained
Harbor Outpost's coastal expansion shipped as one continuous 4 km map. Playtest telemetry was brutal: mobile devices exhausted RAM during the initial load, and PC players hit 400–800 ms frame spikes whenever they crossed the forest-to-shore biome border. The design team had built a beautiful open world on paper, but the engine was treating it like a single monolithic level. Season 2 refactored the map into 256 m streaming cells with a three-ring load policy (active, prefetch, dormant). Cold-start memory dropped 62%, zone-border hitches vanished, and average session length in the new biome rose 14%. Level streaming is how you ship worlds larger than your device can hold in RAM at once: partition space into addressable chunks, load geometry and gameplay data asynchronously as the player moves, and unload what falls behind. This guide covers partitioning strategies, load triggers and retention policies, async pipelines, seamless vs loading-screen transitions, persistence across cells, integration with LOD and object pooling, a Harbor Outpost worked example, an architecture decision table, common pitfalls, and a production checklist.
Streaming vs LOD vs asset bundles
Three techniques reduce memory and improve performance in large worlds. They stack but solve different problems:
- Level streaming — load and unload entire scene sections (geometry, actors, navmesh slices) based on player position. Answers: “What part of the world exists in memory right now?”
- LOD (level of detail) — swap mesh and texture fidelity for content that is loaded. Answers: “How detailed should this loaded rock look at 200 m?”
- Asset bundles / addressables — package and fetch individual assets on demand (props, audio banks, UI skins). Answers: “When do I fetch this specific tree prefab?”
A streaming cell might contain hundreds of assets referenced through an addressable catalog; LOD reduces draw cost inside each loaded cell. Confusing the three leads to loading entire biomes because one hero prop was not bundled correctly, or applying LOD to geometry that should have been unloaded entirely.
Partitioning the world
Every streaming system starts by dividing continuous space into cells with stable IDs. Common schemes:
Uniform grid
Split the map into fixed-size squares (e.g. 128–512 m). Simple to implement, easy to prefetch neighbors, works well for outdoor terrain. Downsides: dense urban blocks and sparse wilderness share the same cell size unless you add hierarchical grids.
Manual zones
Designers draw irregular volumes (caves, towns, dungeons) as separate sub-levels. Maximum art control, common in narrative games with discrete areas. Transitions are often explicit doors or loading screens rather than fully seamless travel.
Engine-native partitions
Unreal's World Partition and Unity's scene streaming tools bake grid metadata into the editor. They automate cell bounds, data layers, and HLOD generation. Worth adopting early; retrofitting a monolithic map into cells is expensive (Harbor's coastal refactor took six engineer-weeks).
Procedural worlds
When terrain is generated from seeds, cells are defined by chunk
coordinates ((cx, cy)) rather than hand-placed files. The
streaming layer requests “generate chunk (12, −4)” from
your
procedural
pipeline and caches the result. Deterministic noise ensures the same
chunk always produces the same geography.
Load triggers and unload policies
A streaming manager tracks the player (or camera) position each frame and maintains concentric rings around them:
- Active ring — fully simulated, rendered, collision-enabled. Typically 1–2 cells in each direction.
- Prefetch ring — geometry and static props loading asynchronously; AI and physics may be dormant. One extra cell beyond active.
- Unload ring — cells beyond prefetch are candidates for teardown.
Hysteresis prevents thrashing at boundaries: load when the player enters a prefetch zone, but do not unload until they are two cells away. Harbor uses a 1-cell load margin and 2-cell unload margin. Fast travel skips prefetch and forces a synchronous load with a fade — acceptable because the player expects a transition.
Priority and budgets
Cap concurrent async loads (4–8 on console, 2–4 on mobile). Prioritize cells the player moves toward using velocity vector projection. Defer cosmetic props until core collision and navmesh slices are ready — an NPC that spawns before walkable mesh exists reads as a bug, not a loading artifact.
Async loading pipeline
Blocking the game thread on disk I/O causes hitches. A production pipeline looks like:
- Request — streaming manager enqueues cell ID with priority.
- IO thread — read serialized scene or bundle from SSD; decompress if needed.
- Deserialize — parse on a worker thread; build GPU-ready meshes and collision hulls.
- Main-thread commit — register actors, wire references, enable rendering. Keep this stage under 2 ms per cell slice.
- Activation — enable AI, spawn dynamic props, register with physics broadphase.
Instrument each stage. Harbor found 70% of border hitches came from main-thread commit, not disk — too many individual actor registrations. Batching into prefab roots cut commit time from 12 ms to 1.8 ms per cell.
Seamless vs loading screens
Seamless streaming hides loads behind travel time or visual occlusion (tunnels, fog walls). Requires aggressive prefetch and strict per-cell budgets. Zoned streaming uses explicit transitions — doorways, elevators, fade-to-black — and allows larger per-zone content because only one zone is resident. Pick based on genre promise: exploration sandboxes lean seamless; mission-based action games often prefer zones.
Persistence and cross-cell state
When a cell unloads, what happens to the chest the player opened or the tree they chopped? Three patterns:
- Cell snapshot — serialize mutable state into a sidecar file keyed by cell ID. Reload applies diffs on next visit. Simple, but file count grows with player interaction breadth.
- Global state store — a database or key-value
map (
entity_id → state) independent of streaming. Cells query it on activation. Scales for MMO-scale persistence. - Regenerate on return — acceptable for respawning resources (herbs, minor enemies) but never for player-built structures without clear UX justification.
Harbor Outpost stores player-placed buildings in a global store and reinjects them when any overlapping cell loads. Destructible props use per-cell snapshots so unloaded wilderness does not bloat the save file.
Worked example: Harbor Outpost coastal refactor
Before: one CoastalBiome.umap (1.8 GB
on disk, 2.4 GB RAM resident). Mobile killed the app; PC averaged
340 ms spikes at the inland border where foliage density doubled.
Partition: 16×16 grid of 256 m cells. Each
cell target: <120 MB resident, <2 ms main-thread commit.
Shared assets (player, UI, global sky) live in a persistent
PersistentLevel that never unloads.
Prefetch tuning: player on foot triggers 1-cell prefetch; mounted travel triggers 2-cell prefetch based on speed >8 m/s. Coastal cliffs use manual zone overrides so ocean reflection probes load as a unit.
Results: cold-start RAM 920 MB → 350 MB on mid-tier Android; zero frames >33 ms at biome borders in 95th-percentile playtests; navmesh rebake per cell runs offline in CI instead of at runtime.
Architecture decision table
| Approach | Best for | Trade-offs |
|---|---|---|
| Uniform grid streaming | Large outdoor open worlds, procedural terrain | Awkward cell boundaries in dense interiors |
| Manual zone sub-levels | Hub-and-spoke RPGs, linear campaigns with optional hubs | Visible loads unless heavily disguised |
| Instanced dungeons | Repeatable content, multiplayer shards | Not a substitute for overworld streaming |
| Procedural chunk cache | Infinite or seed-driven worlds | Determinism and save compatibility are hard problems |
| Monolithic level | Arena fighters, small single-screen games | Does not scale past device RAM |
Common pitfalls
- Streaming without a memory budget per cell. Individual cells creep to 500 MB and prefetch rings exceed device limits.
- Cross-cell references. A quest NPC in cell A points to a trigger in cell B; unloading A breaks the quest silently.
- Main-thread deserialization. Parsing JSON or XML scene graphs on the render thread causes border hitches.
- No hysteresis. Players walking back and forth on a boundary reload the same cell every second.
- Spawning AI before navmesh. Agents path into walls or fall through terrain during partial loads.
- Ignoring audio and VFX streaming. Geometry streams but ambient loops from unloaded cells cut abruptly.
- Retrofitting too late. Monolithic maps require rebaking lighting, navmesh, and occlusion per cell — plan streaming before art lock.
- Multiplayer desync. Clients load cells at different rates; authority must gate interactions until all peers have the cell active.
Production checklist
- Choose partitioning scheme (grid, zones, procedural) before vertical slice.
- Set per-cell RAM and main-thread commit budgets; enforce in CI.
- Implement three-ring load policy with hysteresis margins.
- Cap concurrent async loads per platform tier.
- Prioritize loads by player velocity vector.
- Batch actor registration to minimize main-thread commit time.
- Stream navmesh and collision before enabling AI.
- Define persistence model (cell snapshot vs global store) per entity type.
- Validate no cross-cell hard references in quest or scripting graphs.
- Profile border crossings on min-spec hardware with tracers enabled.
- Test fast travel and respawn paths for synchronous load UX.
- Document cell grid coordinates for level design and QA teleport commands.
Key takeaways
- Streaming answers “what exists in memory” — LOD answers “how detailed is it.”
- Prefetch rings and hysteresis prevent border hitches and reload thrashing.
- Async IO is necessary but not sufficient — main-thread commit is often the real bottleneck.
- Persistence must survive unload or players lose work when cells recycle.
- Plan cells early — retrofitting monolithic maps is expensive and error-prone.
Related reading
- Game open world design explained — density, traversal, and when streaming serves the design
- Game LOD explained — reduce detail inside loaded cells
- Game procedural generation explained — seed-driven chunk content
- Game asset pipeline explained — bundle and addressable packaging for streaming