UE/BN Project

Async TransitionMap Loading

코어른(진) 2025. 1. 2. 20:16

구현 목표

  1. 비동기 레벨 로딩
  2. Transition Map

 

When?

Level을 바꿀 때 사용한다.

 

 

Why?

Level을 바꾸는 정보를 가진 주체는 여러가지 일 수 있다.

예를 들어, 버튼이 될 수도 있고, 어떤 포탈 혹은 UObject의 로직이 될 수도 있다.

따라서 다양한 경우에 대응할 수 있어야 한다.

 

 

How?

Interface를 상속받은 객체는 레벨 전환에 사용할 구조체를 반환하는 함수를 구현한다.

USTRUCT(BlueprintType)
struct FBN_LevelInfo
{
	GENERATED_BODY()

	UPROPERTY(EditDefaultsOnly, meta = (AllowedTypes = "Map"))
	FPrimaryAssetId TargetMapID;

	UPROPERTY(EditDefaultsOnly, meta = (AllowedTypes = "Map"))
	FPrimaryAssetId TransitionMapID;
};
class PROJECTBN_API IBN_LevelChangeable
{
	GENERATED_BODY()

public:

	UFUNCTION(BlueprintCallable, BlueprintNativeEvent)
	FBN_LevelInfo GetLevelInfo();
};

 

이제 ChangeLevel을 하는 부분을 구현해야 하는데, 이 부분은 LevelInfo를 가지고 같은 동작을 수행한다.

Interface에도 기능을 강제하는 함수를 추가할 수 있지만, 이런 것은 BlueprintLibrary로 빼서 구현해야 하는 것과, 사용해야 하는 것에 대한 구분을 둘 수 있다.

void UBN_LevelChangeableStatics::ChangeLevel(TScriptInterface<IBN_LevelChangeable> InChangeable, float InMarginTime)
{
	if(UObject* Loc_WorldObj = InChangeable.GetObject())
	{
		// BP / Native 분기
		FBN_LevelInfo Loc_LevelInfo = InChangeable.GetInterface() ?
			InChangeable->GetLevelInfo() : IBN_LevelChangeable::Execute_GetLevelInfo(Loc_WorldObj);
		
		UGameplayStatics::GetGameInstance(Loc_WorldObj)->GetSubsystem<UBN_LevelChangeSubsystem>()->ChangeLevel(Loc_LevelInfo.TargetMapID, InMarginTime);
		UGameplayStatics::OpenLevel(Loc_WorldObj, Loc_LevelInfo.TransitionMapID.PrimaryAssetName);
	}
}

 

ChangeLevel에서 GameInstanceSubsystem을 쓴 것을 볼 수 있다.

원래 맵 A, 트랜지션 맵 B, 타겟 맵 C가 있다고 했을 때, B가 없다면, C를 비동기 로딩하여, A에 로딩 정도를 표시할 수는 있으나, A와 C가 맵의 크기가 큰 맵일 경우 문제가 될 수 있다.

따라서 깡통 UI맵 B를 로드하고, A를 언로드하고, C를 비동기 로드하는 과정이 필요하다.

이 과정에서 World 삭제, 생성 중 Delegate Handle을 유지해야하는 부분이 생겼고, 그 부분을 위해 GameInstance의 Subsystem을 만들었다.

void UBN_LevelChangeSubsystem::ChangeLevel(FPrimaryAssetId InTargetMapID, float InMarginTime)
{
	OnPostWorldCreationHandle = FWorldDelegates::OnPostWorldCreation.AddLambda([InTargetMapID, InMarginTime, this](UWorld* InWorld)
	{
    	// ++ 위치를 아래에서 변경 -> Async가 바로 완료되는 경우 터짐
    	if(FWorldDelegates::OnPostWorldCreation.Remove(OnPostWorldCreationHandle))
			GEngine->AddOnScreenDebugMessage(1, 3, FColor::Red, "OnPostWorldCreation Delegate Removed.");
            
		FStreamableManager& Loc_StreamableManager = UAssetManager::GetStreamableManager();
		
		TSharedPtr<FStreamableHandle> Loc_StreamHandle = Loc_StreamableManager.RequestAsyncLoad(UAssetManager::Get().GetPrimaryAssetPath(InTargetMapID),
		FStreamableDelegate::CreateLambda([InTargetMapID, InWorld, InMarginTime]()
		{
			// UGameplayStatics::OpenLevel(InWorld, InTargetMapID.PrimaryAssetName);
			FTimerHandle Loc_TimerHandle;
			InWorld->GetTimerManager().SetTimer(Loc_TimerHandle, [InWorld, InTargetMapID]()
			{
				UGameplayStatics::OpenLevel(InWorld, InTargetMapID.PrimaryAssetName);
			}, InMarginTime, false);
		}));
		
		
	});
}

 

 

FWorldDelegates, FStreamableManager를 사용하여, Transition맵을 사용한 맵 비동기 로드를 구현할 수 있었다.

https://youtu.be/YTADCmllgH4