관리 메뉴

기억을 위한 기록들

[UNREAL] Asset Manager(애셋 매니저) 란? 본문

카테고리 없음

[UNREAL] Asset Manager(애셋 매니저) 란?

에드윈H 2024. 3. 6. 11:49

 

에픽게임즈에서 제공하는 Lyra 프로젝트 코드를  분석해 보다가 AssetManager라는 엔진의 프레임워크로 많지 않은 애셋들을 관리하길래 궁금해서 찾아보고 정리를 위해 작성하였습니다.

 

Asset Manager란?

- 언리얼엔진에서 제공하는 엔진 서브시스템과 같은 싱글톤 UObject 클래스
- 맵 또는 모드별로 존재하지 않음
- Asset Registry를 사용하여 언로딩된 애셋을 분류하고 쿼리 한다.
- 글로벌 애셋 로드 상태 유지
- 쿠킹과 비동기 로딩과 같같은 기존 시스템 통합
- 게임에 의해 오버라이딩되도록 설계

 

 Asset Registry의 기능은?

에디터가 로드되면서 로드되지 않은 애셋에 대한 정보를 비동기적으로 모으는 에디터 서브시스템

- 에디터가 애셋을 로드하지 않고 목록을 만들 수 있도록 메모리에 저장.(Asset Manager를 위한 데이터 저장소 제공)

- 에디터 시작 또는 cook 시작 시 새로고침

- 대부분의 데이터는 패키지 게임에서도 사용할 수 있다.

 

Asset Manager가 존재하는 이유?

- 기존 ObjectLibrary의 확장
- 로드가능한 인벤토리 아이템 쿼리
- 블루프린트 클래스의 복잡성 줄이기
- 긴 로드 시간 및 높은 메모리 사용량 처리
- 히치 하드 참조에서 비동기 로드된 참조로의 지원 이동
- 다양한 클라/서버/메뉴/ 게임플레이 로딩 상태 처리
- 복잡한 패킹 및 청크 규칙 설정

 

언리얼엔진 애셋들은 크게 2가지로  Primary Asset과 Secondary Asset으로 나누어진다.

Primary Asset : 애셋 매니저가 직접적으로 처리하고 로딩 가능한 애셋들은 Primary Asset으로 분류된다.

Secondary Asset : 애셋 매니저가 직접적으로 처리하고 로딩 불가능한 애셋들(대부분은 Secondary Asset)이다. (텍스쳐 등 주요 애셋이 아닌 것

 

 

 

 

Chunking(청킹)이란?

- 개별 애셋을 서로 다른 pak/staged 파일에 할당
- 스테이징 된 파일당 청크 식별자 1개, 0은 기본 값입니다.
- bGenerateChunks 패키징 설정으로 활성화
- ChunkDependencyInfo를 사용하여 계층 생성
- 언어별 다운로드 등 플랫폼별 기능에 필요
- 새 ChunkDownloade 플러그인과 합계 동작.

 

 

사용 예 : Lyra 프로젝트 참고

 

void ALyraGameMode::HandleMatchAssignmentIfNotExpectingOne()
{
	FPrimaryAssetId ExperienceId;
	FString ExperienceIdSource;

	//...중간 생략

	// Final fallback to the default experience
	if (!ExperienceId.IsValid())
	{
		if (TryDedicatedServerLogin())
		{
			// This will start to host as a dedicated server
			return;
		}

		//@TODO: Pull this from a config setting or something
		ExperienceId = FPrimaryAssetId(FPrimaryAssetType("LyraExperienceDefinition"), FName("B_LyraDefaultExperience"));
		ExperienceIdSource = TEXT("Default");
	}

	OnMatchAssignmentGiven(ExperienceId, ExperienceIdSource);
}

 

프로젝트 설정에서 입력된 Primary Asset Type에 "LyraExperienceDefinition"가 어떻게 키보면 데이터 키 역할을 한다

 

그리고 본격적으로 로드하는 부분

void ULyraExperienceManagerComponent::StartExperienceLoad()
{
	check(CurrentExperience != nullptr);
	check(LoadState == ELyraExperienceLoadState::Unloaded);

	UE_LOG(LogLyraExperience, Log, TEXT("EXPERIENCE: StartExperienceLoad(CurrentExperience = %s, %s)"),
		*CurrentExperience->GetPrimaryAssetId().ToString(),
		*GetClientServerContextString(this));

	LoadState = ELyraExperienceLoadState::Loading;

	ULyraAssetManager& AssetManager = ULyraAssetManager::Get();

	TSet<FPrimaryAssetId> BundleAssetList;
	TSet<FSoftObjectPath> RawAssetList;

	BundleAssetList.Add(CurrentExperience->GetPrimaryAssetId());
	for (const TObjectPtr<ULyraExperienceActionSet>& ActionSet : CurrentExperience->ActionSets)
	{
		if (ActionSet != nullptr)
		{
			BundleAssetList.Add(ActionSet->GetPrimaryAssetId());
		}
	}

	// Load assets associated with the experience

	TArray<FName> BundlesToLoad;
	BundlesToLoad.Add(FLyraBundles::Equipped);

	//@TODO: Centralize this client/server stuff into the LyraAssetManager
	const ENetMode OwnerNetMode = GetOwner()->GetNetMode();
	const bool bLoadClient = GIsEditor || (OwnerNetMode != NM_DedicatedServer);
	const bool bLoadServer = GIsEditor || (OwnerNetMode != NM_Client);
	if (bLoadClient)
	{
		BundlesToLoad.Add(UGameFeaturesSubsystemSettings::LoadStateClient);
	}
	if (bLoadServer)
	{
		BundlesToLoad.Add(UGameFeaturesSubsystemSettings::LoadStateServer);
	}

	TSharedPtr<FStreamableHandle> BundleLoadHandle = nullptr;
	if (BundleAssetList.Num() > 0)
	{
		BundleLoadHandle = AssetManager.ChangeBundleStateForPrimaryAssets(BundleAssetList.Array(), BundlesToLoad, {}, false, FStreamableDelegate(), FStreamableManager::AsyncLoadHighPriority);
	}

	TSharedPtr<FStreamableHandle> RawLoadHandle = nullptr;
	if (RawAssetList.Num() > 0)
	{
		RawLoadHandle = AssetManager.LoadAssetList(RawAssetList.Array(), FStreamableDelegate(), FStreamableManager::AsyncLoadHighPriority, TEXT("StartExperienceLoad()"));
	}

	// If both async loads are running, combine them
	TSharedPtr<FStreamableHandle> Handle = nullptr;
	if (BundleLoadHandle.IsValid() && RawLoadHandle.IsValid())
	{
		Handle = AssetManager.GetStreamableManager().CreateCombinedHandle({ BundleLoadHandle, RawLoadHandle });
	}
	else
	{
		Handle = BundleLoadHandle.IsValid() ? BundleLoadHandle : RawLoadHandle;
	}

	FStreamableDelegate OnAssetsLoadedDelegate = FStreamableDelegate::CreateUObject(this, &ThisClass::OnExperienceLoadComplete);
	if (!Handle.IsValid() || Handle->HasLoadCompleted())
	{
		// Assets were already loaded, call the delegate now
		FStreamableHandle::ExecuteDelegate(OnAssetsLoadedDelegate);
	}
	else
	{
		Handle->BindCompleteDelegate(OnAssetsLoadedDelegate);

		Handle->BindCancelDelegate(FStreamableDelegate::CreateLambda([OnAssetsLoadedDelegate]()
			{
				OnAssetsLoadedDelegate.ExecuteIfBound();
			}));
	}

	// This set of assets gets preloaded, but we don't block the start of the experience based on it
	TSet<FPrimaryAssetId> PreloadAssetList;
	//@TODO: Determine assets to preload (but not blocking-ly)
	if (PreloadAssetList.Num() > 0)
	{
		AssetManager.ChangeBundleStateForPrimaryAssets(PreloadAssetList.Array(), BundlesToLoad, {});
	}
}

 

 

2021년 자료 :&nbsp;https://www.youtube.com/watch?v=9MGHBU5eNu0

 

Loading Best Pracitces

- 스크린 로딩 중에 로드만 동기화하거나, 입력에 대한 빠른 응답

- 모드 전환 중 번들 상태 변경 및 캐시 지우기

- 로드를 동기화 하거나, 메모리에 저장하기 위해 StreamableHandles 사용

- 딜리게이트와 로드 vs Preload + FallBack

- 절대 안전하지 않은 람다를 딜리게이트로 사용하지 마세요.

- 샘플 및 기존 시스템은 개선의 여지가 있다.

 

 

 

참고자료 :

https://www.youtube.com/watch?v=9MGHBU5eNu0