Beyond the Basics of World Partition
World Partition is UE5's answer to the fundamental problem of open-world development: how do you build a world that's too large to fit in memory? The basic concept is straightforward — divide the world into cells, load cells near the player, unload the rest. But shipping a smooth, hitch-free open world requires mastering the system's deeper features.
This guide goes beyond the setup tutorial and into the decisions that determine whether your open world streams smoothly or stutters every time the player crosses a cell boundary.
Runtime Streaming Architecture
How Cell Loading Works
World Partition divides your level into a grid of streaming cells. Each cell contains the actors within its spatial bounds. At runtime, the Streaming Source (usually the player camera) determines which cells to load.
The loading pipeline:
Player moves → Streaming Source updates position
→ World Partition calculates required cells
→ New cells enter loading range → Async load begins
→ Cell assets load from disk → Actors spawn
→ Old cells exit loading range → Actors destroyed → Memory freed
This happens continuously, every frame. The challenge is making it invisible to the player.
Cell Size Strategy
Cell size is the most impactful configuration decision:
| Cell Size | Pros | Cons | Best For |
|---|---|---|---|
| Small (64m) | Fine-grained streaming, low memory per cell | More cells to manage, more loading operations | Dense urban environments |
| Medium (128m) | Good balance of granularity and overhead | — | Most open-world games |
| Large (256m) | Fewer cells, less management overhead | Higher memory per cell, coarser loading | Sparse landscapes |
The rule: Cell size should be small enough that loading one cell doesn't cause a memory spike, but large enough that cells don't churn (load/unload rapidly) during normal movement.
Loading Range Configuration
The loading range determines how far ahead cells are loaded:
; In WorldPartition settings
RuntimeGrid.CellSize=12800 ; 128 meters
RuntimeGrid.LoadingRange=25600 ; 256 meters (2x cell size)
Loading range should be at least 2x your cell size to ensure cells are fully loaded before the player reaches them. For fast-moving gameplay (vehicles, flying), increase to 3-4x.
Per-Actor Loading Range Overrides
Not every actor needs the same loading range:
// In actor constructor or Blueprint defaults
bOverride_WorldPartitionLoadingRange = true;
WorldPartitionLoadingRange = 50000.f; // 500 meters for large landmarks
Use overrides for:
- Landmarks: Load from far away so players see distant structures
- Audio sources: Extend range for distant ambient sounds
- Gameplay triggers: Ensure triggers load before players can reach them
- Small props: Reduce range to save memory
Data Layers: Selective Content Loading
Data Layers let you organize actors into logical groups that can be loaded or unloaded independently of spatial streaming.
Use Cases
Game state layers: Load different actor sets based on story progression:
DL_Village_Peaceful: NPC merchants, children playing, market stallsDL_Village_Attacked: Burning buildings, enemy soldiers, debrisDL_Village_Rebuilt: Construction scaffolding, new buildings, celebration decorations
Time of day layers:
DL_Daytime: Daytime NPCs, open shops, farmersDL_Nighttime: Night guards, closed shops, nocturnal creatures
Difficulty layers:
DL_Normal: Standard enemy placementDL_Hard: Additional enemies, removed health pickups
Creating and Managing Data Layers
// Activate a data layer at runtime
UWorldPartitionSubsystem* WPSubsystem = GetWorld()->GetSubsystem<UWorldPartitionSubsystem>();
FName LayerName = "DL_Village_Attacked";
WPSubsystem->SetDataLayerRuntimeState(
UDataLayerAsset::FindDataLayerByName(LayerName),
EDataLayerRuntimeState::Activated
);
// Deactivate the peaceful layer
WPSubsystem->SetDataLayerRuntimeState(
UDataLayerAsset::FindDataLayerByName("DL_Village_Peaceful"),
EDataLayerRuntimeState::Unloaded
);
Data Layer Best Practices
- Don't overlap spatial content: Two Data Layers shouldn't place different actors in the same physical space (creates visual popping)
- Transition gracefully: Fade to black or use a loading screen when swapping major Data Layers
- Keep layer count reasonable: Each active layer adds streaming overhead. 5-10 active layers is manageable; 50 is problematic
- Test layer combinations: Ensure all valid layer combinations work together without conflicts
HLOD: Hierarchical Level of Detail
HLOD provides simplified representations of distant content. Instead of loading full-detail actors for cells far from the player, HLOD shows merged, simplified geometry.
The HLOD Pipeline
Full Detail (near) ←→ HLOD Layer 0 (medium) ←→ HLOD Layer 1 (far) ←→ HLOD Layer 2 (very far)
All actors Merged meshes Simplified clusters Impostors/billboards
Full materials Simplified materials Atlas textures Single quads
Full collision No collision No collision No collision
HLOD Layer Configuration
Configure HLOD layers in the World Partition settings:
HLOD Layer 0 (Medium distance):
- Merge nearby static meshes into single draw calls
- Simplify materials (remove parallax, reduce texture resolution)
- Cell size: 2-4x the runtime grid cell size
- Loading range: 4-8x runtime grid loading range
HLOD Layer 1 (Far distance):
- Aggressive mesh simplification (10-20% of original triangle count)
- Single atlas texture per cluster
- Cell size: 8-16x runtime grid cell size
- Loading range: 16-32x runtime grid loading range
HLOD Layer 2 (Very far / skyline):
- Impostor billboards or extremely simplified geometry
- Minimal material complexity
- For landmarks and large structures only
Building HLODs
HLODs are generated offline using the World Partition HLOD builder:
# Command line HLOD build
UnrealEditor-Cmd.exe YourProject.uproject -run=WorldPartitionHLODBuilder -Map=/Game/Maps/OpenWorld
Or from the editor:
- Open World Partition settings
- Navigate to the HLOD section
- Click "Build HLODs"
Build time scales with world size. Budget 30 minutes to several hours for large worlds.
HLOD with Nanite
For Nanite-enabled meshes, HLOD takes a different approach:
- Nanite handles LOD automatically per-mesh
- HLOD still provides draw call merging (fewer instances at distance)
- The combination gives you automatic LOD + merged drawing = optimal distance rendering
- Set HLOD to focus on instance merging rather than mesh simplification for Nanite content
Multi-User Editing with One File Per Actor
The Problem with Traditional Levels
In a traditional .umap level, the entire level is one binary file. Two people editing different corners of the same level creates merge conflicts that can't be resolved.
World Partition's Solution
World Partition stores each actor in its own file (One File Per Actor — OFPA). This means:
- Artist A edits a building in the north → modifies only that building's actor files
- Artist B places trees in the south → modifies only those tree actor files
- No conflict. Both changes merge cleanly.
Setting Up Multi-User Workflows
- Enable One File Per Actor: This is automatic with World Partition
- Use Perforce or Git: Both support file-level locking for binary assets
- Actor locking: The editor shows lock status — locked actors are read-only for other users
- Exclusive checkout: For actors that absolutely shouldn't be edited simultaneously
Workflow Tips
- Divide work by region: Each artist owns a geographic area
- Use Data Layers for team features: One team's gameplay layer, another team's visual layer
- Communicate about shared areas: Town centers, dungeons, and boss arenas need coordination
- Regular integration: Merge everyone's changes daily to catch issues early
Performance Optimization
Streaming Budgets
Set per-frame streaming budgets to prevent hitches:
; Limit how many cells can load per frame
wp.Runtime.MaxStreamingCellsPerFrame=2
; Limit actor spawn count per frame
wp.Runtime.MaxActorsToSpawnPerFrame=10
; Memory budget for streamed content
wp.Runtime.MemoryBudgetMB=2048
Avoiding Streaming Hitches
Common causes of streaming hitches and their fixes:
Large actors straddling cell boundaries: Actor loads when any of its cells load, potentially loading expensive content earlier than expected. Fix: Keep large actors within single cells, or use the bIsSpatiallyLoaded flag for always-loaded actors.
Slow disk I/O: Asset loading is ultimately disk-bound. Fix: Use NVMe storage for development and target platforms. Compress pak files appropriately. Use async loading with priority queues.
Too many actors per cell: Loading a cell spawns all its actors. If one cell has 500 actors, that's a spawn hitch. Fix: Reduce actor density per cell, or use HISM/foliage instances instead of individual actors.
Streaming source prediction: The default streaming source is reactive. By the time the player reaches a cell, it should already be loaded. Fix: Implement predictive streaming based on player velocity and heading.
Profiling World Partition
stat WorldPartition // Streaming state overview
stat Streaming // Asset streaming statistics
wp.Runtime.ToggleDrawRuntimeHash2D // Visualize cell grid in viewport
wp.Runtime.ToggleDrawStreamingSource // Show streaming source and range
Practical Checklist
Before shipping your World Partition world:
- Cell size tuned for your content density and movement speed
- Loading ranges set per-actor-type (landmarks far, props near)
- Data Layers configured for story/state variations
- HLOD layers built and tested (visual quality at all distances)
- Streaming budgets set to prevent per-frame hitches
- Tested on minimum spec hardware with slowest storage
- Multi-user workflow validated (if team project)
- No actors at cell boundaries causing premature loading
- Navigation mesh builds correctly with streamed content
- Save/load correctly preserves streamed world state
World Partition is the foundation of UE5 open-world development. Master it, and your worlds can be as large as your ambition allows — without compromising the player experience.