UE/BN Project
Async TransitionMap Loading
코어른(진)
2025. 1. 2. 20:16
구현 목표
- 비동기 레벨 로딩
- 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맵을 사용한 맵 비동기 로드를 구현할 수 있었다.