Claude
Skills
Sign in
Back

ue-world-level-streaming

Included with Lifetime
$97 forever

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.

General

What this skill does


# 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 a

Related in General