$npx -y skills add quodsoler/unreal-engine-skills --skill ue-world-level-streamingUse this skill when working with World Partition, level streaming, level travel, OpenLevel, ServerTravel, data layer, world subsystem, level instance, sub-level, seamless travel, open world, or HLOD. See references/streaming-patterns.md for configuration patterns by game type.
| 1 | # UE World & Level Streaming |
| 2 | |
| 3 | You are an expert in Unreal Engine's world management and level streaming systems. |
| 4 | |
| 5 | --- |
| 6 | |
| 7 | ## Context |
| 8 | |
| 9 | Read `.agents/ue-project-context.md` before advising. Pay attention to: |
| 10 | - **Engine version** — World Partition is UE5 only; sub-level streaming works in both UE4 and UE5. |
| 11 | - **Build targets** — Dedicated server has no rendering-driven streaming; streaming must be server-safe. |
| 12 | - **World size** — Determines whether World Partition or manual sub-level streaming is appropriate. |
| 13 | - **Multiplayer** — Seamless travel requirements and per-player streaming radius. |
| 14 | |
| 15 | --- |
| 16 | |
| 17 | ## Information to Gather |
| 18 | |
| 19 | Before recommending a streaming approach, confirm: |
| 20 | |
| 21 | 1. **World size and type**: Is this an open world (World Partition), a set of discrete levels, or a hub-and-spoke map? |
| 22 | 2. **Multiplayer**: Are you running a dedicated server? Are per-player streaming radii needed? |
| 23 | 3. **Streaming control**: Does gameplay code need to control load/unload explicitly, or should proximity drive it? |
| 24 | 4. **Level travel**: Non-seamless (lobby flows), seamless (multiplayer round transitions), or no travel? |
| 25 | 5. **Persistent data**: What must survive a level transition — player state, inventory, session state? |
| 26 | |
| 27 | --- |
| 28 | |
| 29 | ## World Partition (UE5) |
| 30 | |
| 31 | ### Enabling World Partition |
| 32 | |
| 33 | Enable via the Level menu: **World -> World Partition -> Convert Level**. Once enabled, all actors in the level are managed by World Partition's grid. The level can no longer have traditional sub-levels. Use **One File Per Actor (OFPA)** for collaborative editing: each actor is saved as its own `.uasset` under `__ExternalActors__`. |
| 34 | |
| 35 | ### Runtime Data Layers |
| 36 | |
| 37 | Data layers replace the old sub-level toggle pattern. A runtime data layer can be loaded/unloaded at runtime without traveling to a new map. |
| 38 | |
| 39 | ```cpp |
| 40 | // MyGameMode.cpp |
| 41 | #include "WorldPartition/DataLayer/DataLayerManager.h" |
| 42 | |
| 43 | void AMyGameMode::ActivateDungeonDataLayer() |
| 44 | { |
| 45 | UDataLayerManager* DLMgr = UDataLayerManager::GetDataLayerManager(GetWorld()); |
| 46 | if (!DLMgr) return; |
| 47 | |
| 48 | // Get by asset reference (set up in editor as a UDataLayerAsset) |
| 49 | UDataLayerAsset* DungeonLayer = DungeonDataLayerAsset.LoadSynchronous(); |
| 50 | DLMgr->SetDataLayerRuntimeState(DungeonLayer, EDataLayerRuntimeState::Activated); |
| 51 | } |
| 52 | |
| 53 | void AMyGameMode::DeactivateDungeonDataLayer() |
| 54 | { |
| 55 | UDataLayerManager* DLMgr = UDataLayerManager::GetDataLayerManager(GetWorld()); |
| 56 | if (!DLMgr) return; |
| 57 | |
| 58 | UDataLayerAsset* DungeonLayer = DungeonDataLayerAsset.LoadSynchronous(); |
| 59 | DLMgr->SetDataLayerRuntimeState(DungeonLayer, EDataLayerRuntimeState::Unloaded); |
| 60 | } |
| 61 | ``` |
| 62 | |
| 63 | **Data layer states:** |
| 64 | - `Unloaded` — not loaded, not visible. |
| 65 | - `Loaded` — loaded into memory, not visible (pre-warming). |
| 66 | - `Activated` — loaded and visible (fully active). |
| 67 | |
| 68 | ### Streaming Sources |
| 69 | |
| 70 | Each player controller is a streaming source by default. For custom sources (cinematic cameras, AI directors), implement `IWorldPartitionStreamingSourceProvider`. |
| 71 | |
| 72 | ### HLOD |
| 73 | |
| 74 | HLOD provides distant merged-mesh representations of World Partition cells. Configure HLOD layers in the World Partition editor; build before shipping via **Build -> Build World Partition HLODs**. Without HLOD, content beyond the streaming radius is simply absent. |
| 75 | |
| 76 | ### Converting Sub-Levels to World Partition |
| 77 | |
| 78 | Use **Tools -> World Partition -> Convert Level**. Actors migrate into the persistent level under WP management. Audit cross-level references beforehand — hard references to converted actors become invalid. |
| 79 | |
| 80 | ### World Partition and Multiplayer |
| 81 | |
| 82 | In a multiplayer session, each player controller acts as a streaming source with a configurable radius. The server streams based on server-side sources; clients receive visibility updates via `AServerStreamingLevelsVisibility`. On dedicated servers, rendering-based streaming does not apply — streaming is driven by server-side sources only. |
| 83 | |
| 84 | Streaming radius is configured per-partition in the World Partition editor UI (`LoadingRange` on `URuntimePartition`), not via ini. |
| 85 | |
| 86 | --- |
| 87 | |
| 88 | ## Level Streaming (Manual Sub-Levels) |
| 89 | |
| 90 | ### ULevelStreaming State Machine |
| 91 | |
| 92 | From `LevelStreaming.h`, the full state sequence is: |
| 93 | |
| 94 | ``` |
| 95 | Removed -> Unloaded -> Loading -> LoadedNotVisible -> MakingVisible -> LoadedVisible -> MakingInvisible -> LoadedNotVisible |
| 96 | | |
| 97 | FailedToLoad (check logs; level asset missing or corrupt) |
| 98 | ``` |
| 99 | |
| 100 | Query state with: |
| 101 | |
| 102 | ```cpp |
| 103 | ULevelStreaming* StreamingLevel = /* ... */; |
| 104 | ELevelStreamingState State = StreamingLevel->GetLevelStreamingState(); |
| 105 | |
| 106 | switch (State) |
| 107 | { |
| 108 | case ELevelStreamingState::Unloaded: /* not in memory */ break; |
| 109 | case ELevelStreamingSta |