# Ue World Level Streaming

> Use 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.

- Skill: `quodsoler/ue-world-level-streaming` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add quodsoler/ue-world-level-streaming`
- Raw SKILL.md: https://api.skillmd.com/api/skills/quodsoler/ue-world-level-streaming/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: quodsoler (https://skillmd.com/u/quodsoler)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/quodsoler/ue-world-level-streaming

---


# UE World & Level Streaming

You are an expert in Unreal Engine's world management and level streaming systems.

---

## Context

Read `.agents/ue-project-context.md` before advising. Pay attention to:
- **Engine version** — World Partition is UE5 only; sub-level streaming works in both UE4 and UE5.
- **Build targets** — Dedicated server has no rendering-driven streaming; streaming must be server-safe.
- **World size** — Determines whether World Partition or manual sub-level streaming is appropriate.
- **Multiplayer** — Seamless travel requirements and per-player streaming radius.

---

## Information to Gather

Before recommending a streaming approach, confirm:

1. **World size and type**: Is this an open world (World Partition), a set of discrete levels, or a hub-and-spoke map?
2. **Multiplayer**: Are you running a dedicated server? Are per-player streaming radii needed?
3. **Streaming control**: Does gameplay code need to control load/unload explicitly, or should proximity drive it?
4. **Level travel**: Non-seamless (lobby flows), seamless (multiplayer round transitions), or no travel?
5. **Persistent data**: What must survive a level transition — player state, inventory, session state?

---

## World Partition (UE5)

### Enabling World Partition

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__`.

### Runtime Data Layers

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.

```cpp
// MyGameMode.cpp
#include "WorldPartition/DataLayer/DataLayerManager.h"

void AMyGameMode::ActivateDungeonDataLayer()
{
    UDataLayerManager* DLMgr = UDataLayerManager::GetDataLayerManager(GetWorld());
    if (!DLMgr) return;

    // Get by asset reference (set up in editor as a UDataLayerAsset)
    UDataLayerAsset* DungeonLayer = DungeonDataLayerAsset.LoadSynchronous();
    DLMgr->SetDataLayerRuntimeState(DungeonLayer, EDataLayerRuntimeState::Activated);
}

void AMyGameMode::DeactivateDungeonDataLayer()
{
    UDataLayerManager* DLMgr = UDataLayerManager::GetDataLayerManager(GetWorld());
    if (!DLMgr) return;

    UDataLayerAsset* DungeonLayer = DungeonDataLayerAsset.LoadSynchronous();
    DLMgr->SetDataLayerRuntimeState(DungeonLayer, EDataLayerRuntimeState::Unloaded);
}
```

**Data layer states:**
- `Unloaded` — not loaded, not visible.
- `Loaded` — loaded into memory, not visible (pre-warming).
- `Activated` — loaded and visible (fully active).

### Streaming Sources

Each player controller is a streaming source by default. For custom sources (cinematic cameras, AI directors), implement `IWorldPartitionStreamingSourceProvider`.

### HLOD

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.

### Converting Sub-Levels to World Partition

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.

### World Partition and Multiplayer

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.

Streaming radius is configured per-partition in the World Partition editor UI (`LoadingRange` on `URuntimePartition`), not via ini.

---

## Level Streaming (Manual Sub-Levels)

### ULevelStreaming State Machine

From `LevelStreaming.h`, the full state sequence is:

```
Removed -> Unloaded -> Loading -> LoadedNotVisible -> MakingVisible -> LoadedVisible -> MakingInvisible -> LoadedNotVisible
                                      |
                                 FailedToLoad   (check logs; level asset missing or corrupt)
```

Query state with:

```cpp
ULevelStreaming* StreamingLevel = /* ... */;
ELevelStreamingState State = StreamingLevel->GetLevelStreamingState();

switch (State)
{
    case ELevelStreamingState::Unloaded:         /* not in memory */ break;
    case ELevelStreamingState::Loading:          /* async load in progress */ break;
    case ELevelStreamingState::LoadedNotVisible: /* in memory, not rendered */ break;
    case ELevelStreamingState::MakingVisible:    /* adding to world */ break;
    case ELevelStreamingState::LoadedVisible:    /* fully active */ break;
    case ELevelStreamingState::MakingInvisible:  /* removing from rendering */ break;
    case ELevelStreamingState::FailedToLoad:     /* check logs */ break;
}
```

### UGameplayStatics: LoadStreamLevel / UnloadStreamLevel

For Blueprint-friendly async streaming with latent actions (from `GameplayStatics.h`):

```cpp
// MyActor.cpp — async load using FLatentActionInfo
#include "Kismet/GameplayStatics.h"

void AMyActor::StreamInRoom(FName LevelName)
{
    FLatentActionInfo LatentInfo;
    LatentInfo.CallbackTarget = this;
    LatentInfo.ExecutionFunction = FName("OnRoomLoaded");
    LatentInfo.Linkage = 0;
    LatentInfo.UUID = GetUniqueID();

    UGameplayStatics::LoadStreamLevel(
        this,           // WorldContextObject
        LevelName,      // e.g., FName("Room_01")
        true,           // bMakeVisibleAfterLoad
        false,          // bShouldBlockOnLoad — keep false for async
        LatentInfo
    );
}

UFUNCTION()
void AMyActor::OnRoomLoaded()
{
    // Room is now loaded and visible
}

void AMyActor::StreamOutRoom(FName LevelName)
{
    FLatentActionInfo LatentInfo;
    LatentInfo.CallbackTarget = this;
    LatentInfo.ExecutionFunction = FName("OnRoomUnloaded");
    LatentInfo.Linkage = 0;
    LatentInfo.UUID = GetUniqueID() + 1;

    UGameplayStatics::UnloadStreamLevel(
        this,
        LevelName,
        LatentInfo,
        false // bShouldBlockOnUnload
    );
}
```

For soft object pointers (preferred for packaging safety), use `LoadStreamLevelBySoftObjectPtr` with the same arguments.

### ULevelStreamingDynamic: Runtime Level Instances

Use `ULevelStreamingDynamic::LoadLevelInstance` to load the same level package multiple times at different transforms — for procedural dungeons, modular buildings, or instanced rooms (from `LevelStreamingDynamic.h`):

```cpp
#include "Engine/LevelStreamingDynamic.h"

void AMyDungeonGenerator::SpawnRoom(FVector Location, FRotator Rotation)
{
    bool bSuccess = false;
    ULevelStreamingDynamic* StreamingLevel = ULevelStreamingDynamic::LoadLevelInstance(
        this,                              // WorldContextObject
        TEXT("/Game/Levels/Room_Corridor"), // LongPackageName — full path
        Location,
        Rotation,
        bSuccess
    );

    if (bSuccess && StreamingLevel)
    {
        // Bind to delegate to know when visible
        StreamingLevel->OnLevelShown.AddDynamic(this, &AMyDungeonGenerator::OnRoomShown);
        StreamingLevel->OnLevelHidden.AddDynamic(this, &AMyDungeonGenerator::OnRoomHidden);

        LoadedRooms.Add(StreamingLevel);
    }
}

void AMyDungeonGenerator::UnloadRoom(ULevelStreamingDynamic* StreamingLevel)
{
    if (StreamingLevel)
    {
        StreamingLevel->SetShouldBeLoaded(false);
        StreamingLevel->SetShouldBeVisible(false);
        StreamingLevel->SetIsRequestingUnloadAndRemoval(true);
    }
}
```

For networking: use `OptionalLevelNameOverride` to give all clients and server the same package name for a given instance. Without this, names are auto-generated uniquely per process and will not match across connections.

```cpp
ULevelStreamingDynamic::FLoadLevelInstanceParams Params(
    GetWorld(),
    TEXT("/Game/Levels/Room_Corridor"),
    FTransform(Rotation, Location)
);
Params.OptionalLevelNameOverride = &InstanceName; // FString, same on server and clients
Params.bInitiallyVisible = true;

bool bSuccess = false;
ULevelStreamingDynamic* Level = ULevelStreamingDynamic::LoadLevelInstance(Params, bSuccess);
```

### OnLevelShown / OnLevelHidden Delegates

From `LevelStreaming.h` — four `BlueprintAssignable` delegates: `OnLevelLoaded`, `OnLevelUnloaded`, `OnLevelShown`, `OnLevelHidden`. Bind with `AddDynamic`:

```cpp
StreamingLevel->OnLevelShown.AddDynamic(this, &UMyManager::HandleLevelShown);
StreamingLevel->OnLevelLoaded.AddDynamic(this, &UMyManager::HandleLevelLoaded);
```

### Streaming Volumes

`ALevelStreamingVolume` automatically controls sub-level loading when the player camera is inside or outside the volume. From `LevelStreamingVolume.h`:

```cpp
// EStreamingVolumeUsage — set on the volume in editor
SVB_Loading                 // load but do not make visible
SVB_LoadingAndVisibility    // load and make visible (most common)
SVB_VisibilityBlockingOnLoad // force blocking load when entering
SVB_BlockingOnLoad          // block load of associated levels
SVB_LoadingNotVisible       // load, keep invisible (pre-warm)
```

Volumes are assigned to a sub-level via its `EditorStreamingVolumes` array. Disable volume-driven streaming for a level with `ULevelStreaming::bDisableDistanceStreaming = true` when you want code-only control.

### Manual Visibility Control

```cpp
// Get streaming level reference from world
const TArray<ULevelStreaming*>& Levels = GetWorld()->GetStreamingLevels();
for (ULevelStreaming* Level : Levels)
{
    if (Level->GetWorldAssetPackageFName() == FName("/Game/Levels/MySubLevel"))
    {
        Level->SetShouldBeLoaded(true);
        Level->SetShouldBeVisible(true);
        break;
    }
}
```

Force flush all streaming (blocks until complete — use sparingly):
```cpp
UGameplayStatics::FlushLevelStreaming(this);
```

---

## Level Instances

`ALevelInstance` places a level as a reusable chunk in the editor. Actors inside are editable as a unit. For runtime instancing, see `ULevelStreamingDynamic` above.

**Packed Level Actors** merge instance meshes into a single static mesh for performance. Enable via right-click on Level Instance → **Pack Level Actor**.

**Per-instance property overrides (UE5.1+):** Each placed `ALevelInstance` can override individual actor properties (materials, gameplay values) without modifying the source level. Configure overrides in the Details panel; overridden values bake into packed level data at cook time.

---

## Level Travel

### Non-Seamless: UGameplayStatics::OpenLevel

Destroys the current world and loads a new one; all clients disconnect. From `GameplayStatics.h`:

```cpp
UGameplayStatics::OpenLevel(this, FName("/Game/Maps/MainMenu"), true);
UGameplayStatics::OpenLevel(this, FName("/Game/Maps/GameLevel"), true, TEXT("?Difficulty=Hard"));
UGameplayStatics::OpenLevelBySoftObjectPtr(this, GameLevelAsset, true); // packaging-safe
```

### Server Travel (Multiplayer, Non-Seamless)

Initiated on the server; all connected clients follow (`World.h`):

```cpp
GetWorld()->ServerTravel(TEXT("/Game/Maps/Level02?listen"), /*bAbsolute=*/false);
```

### Seamless Travel

Seamless travel loads the destination map in the background via a transition (midpoint) map. Clients stay connected. From `World.h`:

```cpp
void UWorld::SeamlessTravel(const FString& InURL, bool bAbsolute);
bool UWorld::IsInSeamlessTravel() const;
void UWorld::SetSeamlessTravelMidpointPause(bool bNowPaused);
```

**Setup requirements:**

1. Set `bUseSeamlessTravel = true` on `AGameModeBase`:

```cpp
// bUseSeamlessTravel is already declared in AGameModeBase — do NOT redeclare it.
// Just set it in the constructor:

// MyGameMode.cpp constructor
bUseSeamlessTravel = true;
```

2. Set a transition map in `DefaultEngine.ini`:

```ini
[/Script/Engine.GameMapsSettings]
TransitionMap=/Game/Maps/Transition
```

3. Override `GetSeamlessTravelActorList` to control which actors persist:

```cpp
// GameMode — called on server side during transition
void AMyGameMode::GetSeamlessTravelActorList(bool bToTransition, TArray<AActor*>& ActorList)
{
    Super::GetSeamlessTravelActorList(bToTransition, ActorList);

    if (!bToTransition)
    {
        // bToTransition=false means we're moving TO the destination
        // Add actors that should survive (e.g., GameState, custom managers)
        ActorList.Add(MyPersistentManager);
    }
}

// GameMode — called after destination map is loaded
void AMyGameMode::PostSeamlessTravel()
{
    Super::PostSeamlessTravel();
    // Re-initialize any post-travel systems
}

// GameMode — handle re-possessing players after travel
void AMyGameMode::HandleSeamlessTravelPlayer(AController*& C)
{
    Super::HandleSeamlessTravelPlayer(C);
    // Restore player-specific state here
}
```

4. Trigger on server:

```cpp
// From GameMode, server-only
GetWorld()->ServerTravel(TEXT("/Game/Maps/Level02?listen"));
// Seamless travel is automatic because bUseSeamlessTravel is true
```

**Travel sequence:** current world -> transition map -> destination world. Use `SetSeamlessTravelMidpointPause(true)` to pause at midpoint for pre-loading.

### Client Travel

For client-initiated travel (join server, change options), call `APlayerController::ClientTravel(URL, ETravelType::TRAVEL_Absolute)` from the player controller.

---

## World Subsystems

`UWorldSubsystem` (from `Subsystems/WorldSubsystem.h`) is auto-instantiated once per `UWorld`. It is destroyed when the world is destroyed — including on level travel. It is the correct place for per-world singleton logic: streaming managers, zone trackers, world-state caches.

```cpp
// MyStreamingManager.h
UCLASS()
class MYGAME_API UMyStreamingManager : public UWorldSubsystem
{
    GENERATED_BODY()
public:
    virtual void PostInitialize() override;                          // after all subsystems init
    virtual void OnWorldBeginPlay(UWorld& InWorld) override;         // after all BeginPlay
    virtual void PreDeinitialize() override;                         // cleanup hook
    virtual bool ShouldCreateSubsystem(UObject* Outer) const override; // filter world type

    void RequestLoadZone(FName ZoneName);
    void RequestUnloadZone(FName ZoneName);
private:
    TMap<FName, TWeakObjectPtr<ULevelStreaming>> ActiveZones;
};
```

Access from anywhere with a world context:

```cpp
UMyStreamingManager* Manager = GetWorld()->GetSubsystem<UMyStreamingManager>();
if (Manager)
{
    Manager->RequestLoadZone(FName("Zone_A"));
}
```

### UTickableWorldSubsystem

For per-frame updates (distance checks, zone detection). Inherit from `UTickableWorldSubsystem`. Must call `Super::Initialize` and `Super::Deinitialize` to enable/disable ticking. Implement `GetStatId` returning a `RETURN_QUICK_DECLARE_CYCLE_STAT`.

---

## Persistent Data Across Level Transitions

| Mechanism | Lifetime | Use Case |
|---|---|---|
| `UGameInstance` | Entire application session | Cross-level player state, session config |
| `UGameInstanceSubsystem` | Entire application session | Services that outlive any world |
| Seamless travel actor list | Transition only | Actors that physically cross (GameState, managers) |
| `USaveGame` + `SaveGameToSlot` | Disk-persistent | Long-term saves, progression |
| `UWorldSubsystem` | Per world | World-scoped cache; push data to `UGameInstance` in `Deinitialize()` before travel clears it |

### GameInstance Pattern

Store cross-level data in `UGameInstance` properties (survives all level travel). Access from anywhere with a world context:

```cpp
UMyGameInstance* GI = GetGameInstance<UMyGameInstance>();
if (GI) GI->PlayerScore += 100;
```

---

## Common Mistakes and Anti-Patterns

**Loading everything at once.** Setting `bShouldBlockOnLoad = true` on many sub-levels causes hitches. Use async loading and the latent action pattern. Only block on load when the game is behind a loading screen.

**Streaming volume gaps.** Overlapping volumes cause spurious unload/reload cycles. Use `MinTimeBetweenVolumeUnloadRequests` on the streaming level to add a cooldown and prevent flickering.

**Broken seamless travel in multiplayer.** If `bUseSeamlessTravel` is true but no transition map is set, seamless travel silently falls back to non-seamless. Always set `TransitionMap` in `DefaultEngine.ini`.

**Cross-level hard references.** Hard object references (`UPROPERTY() UObject*`) between actors in different streaming levels cause the entire referenced level to stay loaded. Always use `TSoftObjectPtr` or `TSoftClassPtr` across level boundaries.

**Dynamic streaming level names not matching server and client.** When using `ULevelStreamingDynamic::LoadLevelInstance`, each process generates a unique name. In multiplayer, supply `OptionalLevelNameOverride` with the same name on server and all clients.

**World Partition on dedicated server.** The server does not use rendering-driven streaming. Streaming sources must be explicitly added server-side (e.g., player positions) or World Partition will not stream in actors correctly on the server.

**Modifying `StreamingLevels` directly.** Do not add to `UWorld::StreamingLevels` directly. Use `AddStreamingLevels`, `AddUniqueStreamingLevels`, and `RemoveStreamingLevels` (from `World.h`) which handle internal bookkeeping and `StreamingLevelsToConsider`.

**Forgetting to call `Super::Initialize` / `Super::Deinitialize` in `UTickableWorldSubsystem`.** These calls enable and disable ticking respectively. Skipping them results in a subsystem that never ticks or never stops ticking.

---

## Quick Reference: Key APIs

| API | Header | Notes |
|---|---|---|
| `UGameplayStatics::LoadStreamLevel` | `Kismet/GameplayStatics.h` | Async latent load of named sub-level |
| `UGameplayStatics::UnloadStreamLevel` | `Kismet/GameplayStatics.h` | Async latent unload |
| `UGameplayStatics::FlushLevelStreaming` | `Kismet/GameplayStatics.h` | Blocking flush — use behind loading screens |
| `UGameplayStatics::OpenLevel` | `Kismet/GameplayStatics.h` | Non-seamless level travel |
| `ULevelStreamingDynamic::LoadLevelInstance` | `Engine/LevelStreamingDynamic.h` | Runtime level instancing |
| `ULevelStreaming::GetLevelStreamingState` | `Engine/LevelStreaming.h` | Query current stream state |
| `ULevelStreaming::SetShouldBeLoaded` | `Engine/LevelStreaming.h` | Drive load state |
| `ULevelStreaming::SetShouldBeVisible` | `Engine/LevelStreaming.h` | Drive visibility |
| `ULevelStreaming::SetIsRequestingUnloadAndRemoval` | `Engine/LevelStreaming.h` | Remove level from world |
| `UWorld::ServerTravel` | `Engine/World.h` | Multiplayer level transition |
| `UWorld::SeamlessTravel` | `Engine/World.h` | Background seamless transition |
| `UWorld::GetStreamingLevels` | `Engine/World.h` | Iterate all streaming levels |
| `UDataLayerManager::SetDataLayerRuntimeState` | `WorldPartition/DataLayer/DataLayerManager.h` | World Partition data layer control (use `UDataLayerManager::GetDataLayerManager(World)`) |
| `UWorldSubsystem::OnWorldBeginPlay` | `Subsystems/WorldSubsystem.h` | Post-BeginPlay init hook |
| `AGameModeBase::GetSeamlessTravelActorList` | `GameFramework/GameModeBase.h` | Control actor persistence |

---

## Related Skills

- `ue-gameplay-framework` — GameMode travel callbacks, `PostSeamlessTravel`, actor persistence rules.
- `ue-data-assets-tables` — async asset loading patterns that complement level streaming.
- `ue-networking-replication` — net visibility transactions, server streaming authority.
- `ue-cpp-foundations` — subsystem patterns, `UGameInstance` lifetime.

