콘텐츠로 이동

UE5 Level Streaming

Level Streaming은 큰 월드를 여러 서브레벨로 분할하고 필요할 때만 로드해 메모리와 CPU 비용을 줄이는 기법입니다. UE5에서는 World Partition이 자동 스트리밍을 제공하지만, 레거시 프로젝트나 세밀한 제어가 필요한 경우 여전히 수동 Level Streaming이 쓰입니다.


항목Level StreamingWorld Partition
설정 방식서브레벨 수동 배치에디터 자동 셀 분할
제어 단위서브레벨 전체셀(Cell) 단위
런타임 APILoadStreamLevel / UnloadStreamLevel자동 (플레이어 위치 기반)
적합 규모중소형, 구획이 명확한 맵오픈월드
UE5 권장레거시 / 특수 목적신규 오픈월드

Windows > Levels 패널
→ Levels 드롭다운 → Add Existing Level
→ 서브레벨 선택 후 Streaming Method: Blueprint (런타임 제어) 또는 Always Loaded

Streaming Method를 Blueprint로 설정해야 런타임에 C++/Blueprint로 로드 상태를 제어할 수 있습니다.


LevelStreamingManager.h
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "LevelStreamingManager.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnLevelLoaded, FName, LevelName);
UCLASS()
class MYGAME_API ULevelStreamingManager : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintAssignable)
FOnLevelLoaded OnLevelLoaded;
UFUNCTION(BlueprintCallable)
void LoadLevel(FName LevelName, bool bMakeVisible = true);
UFUNCTION(BlueprintCallable)
void UnloadLevel(FName LevelName);
UFUNCTION(BlueprintCallable, BlueprintPure)
bool IsLevelLoaded(FName LevelName) const;
};
LevelStreamingManager.cpp
#include "LevelStreamingManager.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/LevelStreamingDynamic.h"
void ULevelStreamingManager::LoadLevel(FName LevelName, bool bMakeVisible)
{
UWorld* World = GetWorld();
if (!World) return;
FLatentActionInfo LatentInfo;
LatentInfo.CallbackTarget = this;
LatentInfo.ExecutionFunction = FName("OnLoadComplete");
LatentInfo.Linkage = 0;
LatentInfo.UUID = GetTypeHash(LevelName);
UGameplayStatics::LoadStreamLevel(
World, LevelName, bMakeVisible, /*bShouldBlockOnLoad=*/false, LatentInfo);
}
void ULevelStreamingManager::UnloadLevel(FName LevelName)
{
UWorld* World = GetWorld();
if (!World) return;
FLatentActionInfo LatentInfo;
LatentInfo.CallbackTarget = this;
LatentInfo.ExecutionFunction = FName("OnUnloadComplete");
LatentInfo.Linkage = 0;
LatentInfo.UUID = GetTypeHash(LevelName) + 1000;
UGameplayStatics::UnloadStreamLevel(
World, LevelName, LatentInfo, /*bShouldBlockOnUnload=*/false);
}
bool ULevelStreamingManager::IsLevelLoaded(FName LevelName) const
{
UWorld* World = GetWorld();
if (!World) return false;
ULevelStreaming* StreamingLevel = UGameplayStatics::GetStreamingLevel(
World, LevelName);
return StreamingLevel && StreamingLevel->IsLevelLoaded();
}
// LatentInfo 콜백 (UFUNCTION 필요)
UFUNCTION()
void ULevelStreamingManager::OnLoadComplete()
{
// 로드 완료 후 처리
UE_LOG(LogTemp, Log, TEXT("Level load complete"));
OnLevelLoaded.Broadcast(NAME_None); // 실제 이름 추적 필요
}

4. 동적 서브레벨 생성 (런타임 인스턴스)

섹션 제목: “4. 동적 서브레벨 생성 (런타임 인스턴스)”

에디터에 사전 등록 없이 런타임에 새 레벨 인스턴스를 생성합니다.

bool bSuccess = false;
ULevelStreamingDynamic* StreamingLevel = ULevelStreamingDynamic::LoadLevelInstance(
GetWorld(),
TEXT("/Game/Maps/DungeonRoom"), // 레벨 에셋 경로
FVector(1000.f, 0.f, 0.f), // 배치 위치
FRotator::ZeroRotator,
bSuccess
);
if (bSuccess && StreamingLevel)
{
// 로드 완료 콜백 등록
StreamingLevel->OnLevelLoaded.AddDynamic(
this, &AMyActor::OnDynamicLevelLoaded);
}

이 방식은 던전 생성, 인스턴스 방 같은 프로시저럴 레벨 배치에 유용합니다.


void AMyActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
ULevelStreaming* Level = UGameplayStatics::GetStreamingLevel(
GetWorld(), TEXT("DungeonRoom_01"));
if (!Level) return;
ELevelStreamingState State = ULevelStreamingBlueprintLibrary::GetStreamingState(
GetWorld(), TEXT("DungeonRoom_01"));
switch (State)
{
case ELevelStreamingState::Loading:
// 로딩 UI 표시
break;
case ELevelStreamingState::LoadedVisible:
// 레벨 완전히 표시됨 → 플레이어 이동 허용
AllowPlayerEntry();
break;
case ELevelStreamingState::Unloaded:
// 언로드 완료
break;
default:
break;
}
}

문 열기처럼 레벨 전환이 즉각적으로 느껴져야 할 때는 로딩이 완료될 때까지 트리거를 블로킹합니다.

void ADoorActor::TryOpen()
{
FName TargetLevel = TEXT("NextRoom");
if (ULevelStreamingManager* Mgr = GetGameInstance()->GetSubsystem<ULevelStreamingManager>())
{
if (!Mgr->IsLevelLoaded(TargetLevel))
{
// 아직 로드 안 됨 → 로드 시작 + 문 잠금
bDoorLocked = true;
Mgr->LoadLevel(TargetLevel);
Mgr->OnLevelLoaded.AddDynamic(this, &ADoorActor::OnNextRoomReady);
}
else
{
OpenDoor();
}
}
}
void ADoorActor::OnNextRoomReady(FName LevelName)
{
bDoorLocked = false;
OpenDoor();
}

7. Seamless Travel — 멀티플레이어 레벨 전환

섹션 제목: “7. Seamless Travel — 멀티플레이어 레벨 전환”

싱글플레이어에서는 OpenLevel로 레벨 전환이 가능하지만, 멀티플레이어에서는 연결을 유지하면서 레벨을 바꾸는 Seamless Travel이 필요합니다.

// GameMode에서 Seamless Travel 설정
AMyGameMode::AMyGameMode()
{
// Seamless Travel 활성화 (기본값 false)
bUseSeamlessTravel = true;
}
// 서버에서 모든 클라이언트를 새 레벨로 이동
void AMyGameMode::TravelToNextLevel(const FString& NextMapName)
{
// "?listen" 옵션으로 서버 소켓 유지
FString TravelURL = FString::Printf(TEXT("/Game/Maps/%s?listen"), *NextMapName);
GetWorld()->ServerTravel(TravelURL, /*bAbsolute=*/true);
}
// Seamless Travel 단계별 콜백 — AGameMode에서 재정의 가능
virtual void GetSeamlessTravelActorList(
bool bToEntry, TArray<AActor*>& ActorList) override
{
Super::GetSeamlessTravelActorList(bToEntry, ActorList);
// 전환 중 유지할 액터 추가 (PlayerController는 자동 유지)
// ActorList.Add(GameStateActor);
}
// 클라이언트에서 Seamless Travel 완료 감지
void AMyPlayerController::PostSeamlessTravel()
{
Super::PostSeamlessTravel();
// 새 레벨 로드 완료 후 HUD 재초기화 등
RecreateHUD();
}

Seamless Travel 흐름:

  1. 서버 ServerTravel 호출 → Entry Level 로드 (전환용 임시 레벨)
  2. 클라이언트 → Entry Level로 자동 이동 (연결 유지)
  3. Entry Level → 목적지 레벨 이동
  4. PostSeamlessTravel 콜백 실행

비교:

항목OpenLevel (Hard)ServerTravel (Seamless)
연결 유지X (재연결)O
로딩 시간동기 블로킹비동기
멀티플레이어 적합XO
전환 중 로딩 화면직접 구현Entry Level로 처리

  • Level Streaming은 Windows > Levels 패널에서 서브레벨을 등록하고 Streaming Method를 Blueprint로 설정해야 런타임 제어가 가능하다.
  • UGameplayStatics::LoadStreamLevel / UnloadStreamLevel은 비동기 LatentAction 기반이므로 완료 콜백을 활용한다.
  • 프로시저럴 레벨 배치가 필요하면 ULevelStreamingDynamic::LoadLevelInstance를 사용한다.
  • 멀티플레이어 레벨 전환은 bUseSeamlessTravel = true + ServerTravel로 클라이언트 연결을 유지한다.
  • 오픈월드 신규 프로젝트라면 World Partition이 더 적합하며, Level Streaming은 구획이 명확한 선형 맵이나 레거시 프로젝트에 적합하다.