레벨 스트리밍 (Level Streaming)

타입·2023년 11월 15일
0

언리얼 엔진 공부

목록 보기
1/5

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/level-streaming-in-unreal-engine

레벨 스트리밍 개념

Level Streaming: 플레이 중 맵을 로드/언로드하거나 표시/숨김
Persistent Level: 항상 로딩되어 있는 메인 레벨, 스트리밍 불가능
Streaming Level: 레벨 스트리밍으로 로딩되는 서브 레벨, 항상 로드됨 옵션이 켜진 경우 퍼시스턴트 레벨과 함께 로딩

스트리밍 방식

  • 블루프린트, C++, 볼륨 총 세가지 방법 존재

Blueprint

Trigger를 배치하여 레벨 블루프린트와 같은 블루프린트의 이벤트 그래프에서 Overlap 시 LoadStreamLevel() 호출하여 로드, UnloadStreamLevel() 호출하여 언로드

레벨 블루프린트 열기로 이벤트 그래프 구현 확인할 수 있음

레벨 로딩이 완료되었을 때 커스텀 이벤트로 문이 열리도록 구현

C++

아래의 스태틱 함수를 호출하여 로드/언로드
레벨의 이름이 아닌 SoftObjectPtr를 넘기는 버전도 존재 (결국 LevelName으로 변환하여 호출)

// GameplayStatics.h

/** Stream the level (by Name); Calling again before it finishes has no effect */
UFUNCTION(BlueprintCallable, meta=(WorldContext="WorldContextObject", Latent = "", LatentInfo = "LatentInfo", DisplayName = "Load Stream Level (by Name)"), Category="Game")
static void LoadStreamLevel(const UObject* WorldContextObject, FName LevelName, bool bMakeVisibleAfterLoad, bool bShouldBlockOnLoad, FLatentActionInfo LatentInfo);

/** Unload a streamed in level (by Name)*/
UFUNCTION(BlueprintCallable, meta=(WorldContext="WorldContextObject", Latent = "", LatentInfo = "LatentInfo", DisplayName = "Unload Stream Level (by Name)"), Category="Game")
static void UnloadStreamLevel(const UObject* WorldContextObject, FName LevelName, FLatentActionInfo LatentInfo, bool bShouldBlockOnUnload);

////////////////////////////////////////////////////////////////////////////////////////////////////

// GameplayStatics.cpp

void UGameplayStatics::LoadStreamLevel(const UObject* WorldContextObject, FName LevelName, bool bMakeVisibleAfterLoad, bool bShouldBlockOnLoad, FLatentActionInfo LatentInfo)
{
	if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
	{
		FLatentActionManager& LatentManager = World->GetLatentActionManager();
		if (LatentManager.FindExistingAction<FStreamLevelAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr)
		{
			FStreamLevelAction* NewAction = new FStreamLevelAction(true, LevelName, bMakeVisibleAfterLoad, bShouldBlockOnLoad, LatentInfo, World);
			LatentManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, NewAction);
		}
	}
}

void UGameplayStatics::UnloadStreamLevel(const UObject* WorldContextObject, FName LevelName, FLatentActionInfo LatentInfo, bool bShouldBlockOnUnload)
{
	if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
	{
		FLatentActionManager& LatentManager = World->GetLatentActionManager();
		if (LatentManager.FindExistingAction<FStreamLevelAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr)
		{
			FStreamLevelAction* NewAction = new FStreamLevelAction(false, LevelName, false, bShouldBlockOnUnload, LatentInfo, World );
			LatentManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, NewAction );
		}
	}
}

Streaming Volume

볼륨은 별도 구현 없이 볼륨에 겹치면 로드, 겹치는게 끝나면 언로드
겹치는 대상은 캐릭터가 아닌 활성화된 카메라 위치를 추적
레벨 디테일 창에서 스트리밍 레벨에 연결하여 사용

매 틱마다 아래 함수가 호출되어 플레이어가 볼륨 안에 있는지, 밖에 있는지 확인하여 레벨 로드/언로드 요청

// World.h

/**
 * Issues level streaming load/unload requests based on whether
 * local players are inside/outside level streaming volumes.
 *
 * @param OverrideViewLocation Optional position used to override the location used to calculate current streaming volumes
 */
void ProcessLevelStreamingVolumes(FVector* OverrideViewLocation=NULL);

// 구현부는 LevelTick.cpp
// 대강 설명하자면 플레이어 컨트롤러를 통해 카메라 위치를 ViewLocation으로 가져오고
// LevelStreamingObjectsWithVolume(StreamingVolume을 가진 레벨)을 돌며 ViewLocation이 볼륨 안에 위치하는지 검사
// 볼륨 안에 있으면 StreamingSettings에 마킹하여 VisibleLevelStreamingObjects에 추가
// 이후 LevelStreamingObjectsWithVolume을 한 번 더 돌며 VisibleLevelStreamingObjects에 추가된 레벨이 있는지 검사
// 추가된 게 있으면 ShouldBeLoaded를 true로 세팅하고 기존 값과 비교해서 변경된 게 있다면 플레이어 컨트롤러의 LevelStreamingStatusChanged()로 Notify줘서 로드
// 만약 없으면 NewStreamingSettings가 nullptr이라 볼륨 밖에 있는 걸로 판단하여 bShouldBeLoaded가 false라 언로드

bDisabled로 볼륨 비활성화

아직 접근할 수 없는 서브 레벨을 스트리밍하지 않도록 방지

볼륨 언로드 요청 최소 간격 (Min Time Between Volume Unload Requests)

볼륨의 경계를 왔다갔다 할 때 바로 언로딩되지 않도록 방지

Editor Pre Vis Only

에디터 미리보기 전용

그 외 팁

레벨은 여러 스트리밍 볼륨을 가질 수 있고, 스트리밍 볼륨은 여러 레벨에서 사용 가능
에디터에서 실행 시 레벨이 이미 로딩되어 있으므로 성능 테스트 시엔 독립형 게임으로 플레이

스트리밍 코드 분석

// LevelStreaming.h

/** 
 * Try to find loaded level in memory, issue a loading request otherwise
 *
 * @param	PersistentWorld			Persistent world
 * @param	bAllowLevelLoadRequests	Whether to allow level load requests
 * @param	BlockPolicy				Whether loading operation should block
 * @return							true if the load request was issued or a package was already loaded
 */
bool RequestLevel(UWorld* PersistentWorld, bool bAllowLevelLoadRequests, EReqLevelBlock BlockPolicy);


// UWorld::UpdateLevelStreaming()에서 StreamingLevelsToConsider에 있는 것들 돌면서
// FStreamingLevelPrivateAccessor::UpdateStreamingState() 호출하면
// ULevelStreaming::UpdateStreamingState()가 호출되는데 여기서 CurrentState와 TargetState 값을 보며 레벨을 보여줘야하는 경우 RequestLevel() 호출
// ULevelStreaming::RequestLevel()에서 메모리에 이미 로딩된 레벨이 있는지 확인하고 없으면 레벨 로딩 요청
// LoadPackageAsync()으로 요청하고 완료 시 ULevelStreaming::AsyncLevelLoadComplete() 호출하는 듯 함

// 결국 ULevelStreaming::UpdateStreamingState()의 AddToWorld() / RemoveFromWorld()로 월드에 추가/제거

스트리밍 볼륨에 카메라가 들어가거나 나갈 때 서버에서 클라로 쏴주는 RPC 함수

LevelStreaming에 서버로부터 받은 값을 세팅해줬으니 UpdateLevelStreaming() 돌면서 이후 진행

스트리밍 방식

로딩을 제어할 지, 로드 및 Visibility를 모두 제어할지, Visibility만 제어할지 여부
SVB Loading
SVB Loading Not Visible
SVB Loading and Visibility
SVB Visibility Blocking on Load
SVB Blocking on Load

	FVisibleLevelStreamingSettings( EStreamingVolumeUsage Usage )
	{
		switch( Usage )
		{
		case SVB_Loading:
			bShouldBeVisible		= false;
			bShouldBlockOnLoad		= false;
			bShouldChangeVisibility	= false;
			break;
		case SVB_LoadingNotVisible:
			bShouldBeVisible		= false;
			bShouldBlockOnLoad		= false;
			bShouldChangeVisibility	= true;
			break;
		case SVB_LoadingAndVisibility:
			bShouldBeVisible		= true;
			bShouldBlockOnLoad		= false;
			bShouldChangeVisibility	= true;
			break;
		case SVB_VisibilityBlockingOnLoad:
			bShouldBeVisible		= true;
			bShouldBlockOnLoad		= true;
			bShouldChangeVisibility	= true;
			break;
		case SVB_BlockingOnLoad:
			bShouldBeVisible		= false;
			bShouldBlockOnLoad		= true;
			bShouldChangeVisibility	= false;
			break;
		default:
			UE_LOG(LogLevel, Fatal,TEXT("Unsupported usage %i"),(int32)Usage);
		}
	}
    
// bShouldBeVisible은 로딩 완료하면 보여줄 것인지
// bShouldBlockOnLoad는 로딩 시 멈추는 듯하고
// bShouldChangeVisibility는 true인 경우 ShouldBeVisible() 함수에서 bShouldBeVisible를 반환해주고 false라면 받아온 인자값을 그대로 넘겨주는 역할
// 그래서 SVB_LoadingNotVisible일 때 Visible을 강제로 끄기 위해 bShouldChangeVisibility를 true로 두고 bShouldBeVisible를 false로 둬서 무조건 Visibility가 바뀌도록 세팅된 것처럼 보임
// 반대로 bShouldBeVisible이 true고 bShouldChangeVisibility를 false로 세팅하는 enum 값은 안보이는게 앞서 한말이 맞는듯함

// 잘 모르겠으면 SVB Visibility Blocking on Load 사용 (공식 문서에서 사용함)

서브레벨 공동작업

https://docs.unrealengine.com/4.27/ko/Basics/Levels/CollaborateWithSublevels/
레벨은 바이너리 파일이므로 공동작업이 어려움 (충돌 문제)
메인 레벨에 서브 레벨을 항상 로드됨(Always Streaming) 옵션을 켜서 배치하여 작업 가능
작업한 액터는 '선택된 액터를 레벨로 이동'으로 메인 레벨로 이동 가능
서브 레벨은 구역 별로(A지역, B지역..) 나누거나 기능 별로(오디오, 스태틱메시..) 나누는 방식이 있을 수 있음
Level Coloration 옵션으로 서브 레벨을 색깔별로 구분 가능

  • LevelStreamingDynamic
    LevelStreaming을 상속받는 클래스
    Persistent Level의 하위 레벨로 존재하지 않는 레벨을 스트리밍할 수 있음
    단, 패키징엔 해당 umap이 포함되어야 함
    원하는 위치, 회전 정보로 똑같은 레벨을 여러 인스턴스로 생성할 수 있음
profile
주니어 언리얼 프로그래머

1개의 댓글

comment-user-thumbnail
2024년 8월 27일

좋은 분석글 감사합니다

답글 달기