[UE5] AssetManager (2)

GamzaTori·2024년 10월 8일

UE5 C++

목록 보기
8/27

RAssetData에 에셋을 가져오는 함수 추가

    // header
    public:
    	// 에셋을 캐싱하는 함수
    	virtual void PreSave(FObjectPreSaveContext SaveContext) override;
    	// 캐싱된 것을 가져오는 함수
    	// 이름을 이용해서 에셋을 가져오는 함수
    	FSoftObjectPath GetAssetPathByName(const FName& AssetName);
    	// 라벨을 이용해서 에셋을 가져오는 함수
    	const FAssetSet& GetAssetSetByLabel(const FName& Label);
    	
    private:
    	// C++의 Unordered_map에 가까움
    	UPROPERTY(EditDefaultsOnly)
    	TMap<FName, FAssetSet> AssetGroupNameToSet;
    
    	// EditDefaultsOnly 붙이지 않으면 노출되지 않기 때문에
    	// 임시적으로 캐싱해서 사용하는 헬퍼 역할 
    	UPROPERTY()
    	TMap<FName, FSoftObjectPath> AssetNameToPath;
    
    	UPROPERTY()
    	TMap<FName, FAssetSet> AssetLabelToSet;
    	
    // cpp
    FSoftObjectPath URAssetData::GetAssetPathByName(const FName& AssetName)
    {
    	FSoftObjectPath* AssetPath = AssetNameToPath.Find(AssetName);
    	ensureAlwaysMsgf(AssetPath, TEXT("Can't find Asset Path From Asset Name [%s]"), *AssetName.ToString());
    	return *AssetPath;
    }
    
    const FAssetSet& URAssetData::GetAssetSetByLabel(const FName& Label)
    {
    	const FAssetSet* AssetSet = AssetLabelToSet.Find(Label);
    	ensureAlwaysMsgf(AssetSet, TEXT("Can't find Asset Path From Asset Name [%s]"), *Label.ToString());
    	return *AssetSet;
    }
  • PreSave를 override 하여 에셋을 캐싱할 수 있다

이후 캐싱되어 있는 에셋이 있다면 가져온다

    // PDA를 저장하는 순간 호출
    void URAssetData::PreSave(FObjectPreSaveContext SaveContext)
    {
    	Super::PreSave(SaveContext);
    
    	AssetNameToPath.Empty();	// Clear
    	AssetLabelToSet.Empty();
    
    	AssetGroupNameToSet.KeySort([](const FName& A, const FName& B)
    	{
    		return (A.Compare(B)<0);
    	});
    
    	for(const auto& Pair : AssetGroupNameToSet)
    	{
    		const FAssetSet& AssetSet = Pair.Value;
    		for(FAssetEntry AssetEntry : AssetSet.AssetEntries)
    		{
    			FSoftObjectPath& AssetPath = AssetEntry.AssetPath;
    			//const FString& AssetName = AssetPath.GetAssetName();
    			//if(AssetName.StartsWith(TEXT("BP_")) || AssetName.StartsWith(TEXT("B_")) ||
    			//AssetName.StartsWith(TEXT("GE_")) || AssetName.StartsWith(TEXT("GA_")))
    			//{
    			//	FString AssetPathString = AssetPath.GetAssetPathString();
    			//	AssetPathString.Append(TEXT("_C"));
    			//	AssetPath=FSoftObjectPath(AssetPathString);
    			//}
    
    			AssetNameToPath.Emplace(AssetEntry.AssetName, AssetEntry.AssetPath);
    			for(const FName& Label : AssetEntry.AssetLabels)
    			{
    				AssetLabelToSet.FindOrAdd(Label).AssetEntries.Emplace(AssetEntry);
    			}
    		}
    	}
    }
  • Emplace는 C++ 벡터의 emplace_back
  • AssetNameToPath에는 단순히 이름과 경로를 가지고 있기 때문에 추가하거나 있으면 덮어씌워도 문제가 없습니다.
  • AssetLabelToSet은 Label에 대한 FAssetSet을 가지고 있기 때문에 Label이 존재하지 않으면 추가해야 하기 때문에 FindOrAdd를 통해 추가해야 합니다.

UInputData에 Tag를 통해 InputAction을 가져오는 함수 추가

    // header
    public:
    	const UInputAction* FindInputActionByTag(const FGameplayTag& InputTag) const;
    	
    // cpp
    const UInputAction* URInputData::FindInputActionByTag(const FGameplayTag& InputTag) const
    {
    	for(const FRInputAction& Action : InputActions)
    	{
    		if(Action.InputAction && Action.InputTag == InputTag)
    		{
    			return Action.InputAction;
    		}
    	}
    
    	UE_LOG(LogTemp, Error, TEXT("Can't find InputAction for InputTag [%]s"), *InputTag.ToString());
    
    	return nullptr;
    }
  • FString의 Operator은 FString을 TCHAR로 변환해준다
  • AssetManager에서 에셋을 로딩하는 시점 정하기
    • GameInstance는 1개만 존재하기 때문에 생성되는 시점에 하면 좋다

GameInstance가 생성되는 시점에 AssetManager를 초기화하는 코드 추가

        // header
        public:
        	URGameInstance(const FObjectInitializer& ObjectInitializer);
        
        public:
        	virtual void Init() override;
        	virtual void Shutdown() override;
        
        // cpp
        URGameInstance::URGameInstance(const FObjectInitializer& ObjectInitializer)
        	:Super(ObjectInitializer)
        {
        	
        }
        
        void URGameInstance::Init()
        {
        	Super::Init();
        
        	URAssetManager::Initialize();    // AssetManager 초기화
        }
        
        void URGameInstance::Shutdown()
        {
        	Super::Shutdown();
        
        }

에셋매니저에 동기적으로 에셋을 로드하는 함수 추가

    // header
    public:
    	URAssetManager();
    
    	static URAssetManager& Get();
    
    public:
    	static void Initialize();
    
    	template<typename AssetType>
    	static AssetType* GetAssetByName(const FName& AssetName);
    
    	static void LoadSyncByPath(const FSoftObjectPath& AssetPath);
    	static void LoadSyncByName(const FName& AssetName);
    	static void LoadSyncByLabel(const FName& Label);
    
    private:
    	void LoadPreloadAssets();
    	void AddLoadedAsset(const FName& AssetName, const UObject* Asset);
    
    private:
    	UPROPERTY()
    	TObjectPtr<URAssetData> LoadedAssetData;
    
    	UPROPERTY()
    	TMap<FName, TObjectPtr<const UObject>> NameToLoadedAsset;
    // cpp
    URAssetManager::URAssetManager() : Super()
    {
    	
    }
    
    URAssetManager& URAssetManager::Get()
    {
    	if (URAssetManager* Singleton = Cast<URAssetManager>(GEngine->AssetManager))
    	{
    		return *Singleton;
    	}
    
    	UE_LOG(LogTemp, Fatal, TEXT("Can't find UR1AssetManager"));
    
    	return *NewObject<URAssetManager>();
    }
    
    void URAssetManager::Initialize()
    {
    	Get().LoadPreloadAssets();
    }
    
    void URAssetManager::LoadSyncByPath(const FSoftObjectPath& AssetPath)
    {
    	if (AssetPath.IsValid())
    	{
    		UObject* LoadedAsset = AssetPath.ResolveObject();
    		if (LoadedAsset == nullptr)
    		{
    			if (UAssetManager::IsInitialized())
    			{
    				LoadedAsset = UAssetManager::GetStreamableManager().LoadSynchronous(AssetPath, false);
    			}
    			else
    			{
    				LoadedAsset = AssetPath.TryLoad();
    			}
    		}
    
    		if (LoadedAsset)
    		{
    			Get().AddLoadedAsset(AssetPath.GetAssetFName(), LoadedAsset);
    		}
    		else
    		{
    			UE_LOG(LogTemp, Fatal, TEXT("Failed to load asset [%s]"), *AssetPath.ToString());
    		}
    	}
    }
    
    void URAssetManager::LoadSyncByName(const FName& AssetName)
    {
    	URAssetData* AssetData = Get().LoadedAssetData;
    	check(AssetData);
    
    	const FSoftObjectPath& AssetPath = AssetData->GetAssetPathByName(AssetName);
    	LoadSyncByPath(AssetPath);
    }
    
    void URAssetManager::AddLoadedAsset(const FName& AssetName, const UObject* Asset)
    {
    	if (AssetName.IsValid() && Asset)
    	{
    		//FScopeLock LoadedAssetsLock(&LoadedAssetsCritical);
    
    		if (NameToLoadedAsset.Contains(AssetName) == false)
    		{
    			NameToLoadedAsset.Add(AssetName, Asset);
    		}
    	}
    }
    
    void URAssetManager::LoadSyncByLabel(const FName& Label)
    {
    	if (UAssetManager::IsInitialized() == false)
    	{
    		UE_LOG(LogTemp, Error, TEXT("AssetManager must be initialized"));
    		return;
    	}
    
    	URAssetData* AssetData = Get().LoadedAssetData;
    	check(AssetData);
    
    	TArray<FSoftObjectPath> AssetPaths;
    
    	const FAssetSet& AssetSet = AssetData->GetAssetSetByLabel(Label);
    	for (const FAssetEntry& AssetEntry : AssetSet.AssetEntries)
    	{
    		const FSoftObjectPath& AssetPath = AssetEntry.AssetPath;
    		LoadSyncByPath(AssetPath);
    		if (AssetPath.IsValid())
    		{
    			AssetPaths.Emplace(AssetPath);
    		}
    	}
    
    	GetStreamableManager().RequestSyncLoad(AssetPaths);
    
    	for (const FAssetEntry& AssetEntry : AssetSet.AssetEntries)
    	{
    		const FSoftObjectPath& AssetPath = AssetEntry.AssetPath;
    		if (AssetPath.IsValid())
    		{
    			if (UObject* LoadedAsset = AssetPath.ResolveObject())
    			{
    				Get().AddLoadedAsset(AssetEntry.AssetName, LoadedAsset);
    			}
    			else
    			{
    				UE_LOG(LogTemp, Fatal, TEXT("Failed to load asset [%s]"), *AssetPath.ToString());
    			}
    		}
    	}
    }
    
    void URAssetManager::LoadPreloadAssets()
    {
    	if (LoadedAssetData)
    		return;
    
    	URAssetData* AssetData = nullptr;
    	FPrimaryAssetType PrimaryAssetType(URAssetData::StaticClass()->GetFName());
    	TSharedPtr<FStreamableHandle> Handle = LoadPrimaryAssetsWithType(PrimaryAssetType);
    	if (Handle.IsValid())
    	{
    		Handle->WaitUntilComplete(0.f, false);
    		AssetData = Cast<URAssetData>(Handle->GetLoadedAsset());
    	}
    
    	if (AssetData)
    	{
    		LoadedAssetData = AssetData;
    		LoadSyncByLabel("Preload");
    	}
    	else
    	{
    		UE_LOG(LogTemp, Fatal, TEXT("Failed to load AssetData asset type [%s]."), *PrimaryAssetType.ToString());
    	}
    }
  • check는 표현식이 거짓일 경우 실행을 중단한다
  • LoadSyncByLabel 함수의 흐름을 이해하기 위해서는 중간의 LoadSyncByPath와 그 이후에 일어나는 일들의 목적을 명확히 할 필요가 있습니다. 전체적인 목표는 Label에 해당하는 에셋들을 모두 로드하고, 로드된 에셋들을 관리하는 것입니다. 세부적으로는 두 번의 로딩 과정을 거치며 중복이 있는 것처럼 보일 수 있지만, 이는 다양한 상황에서의 로딩 처리를 보다 안정적으로 하기 위함입니다.

함수의 동작을 단계별로 살펴보면

1. Pre-condition 체크

                if(URAssetManager::IsInitialized() == false)
                {
                    UE_LOG(LogTemp, Error, TEXT("AssetManager must be initialized"));
                    return;
                }
                
  • 여기서는 AssetManager가 초기화되지 않았다면 에러를 기록하고 함수를 종료합니다.

2. Label에 해당하는 AssetSet 얻기

                URAssetData* AssetData = Get().LoadedAssetData;
                check(AssetData);
                
                TArray<FSoftObjectPath> AssetPaths;
                const FAssetSet& AssetSet = AssetData->GetAssetSetByLabel(Label);
                
  • Label에 해당하는 AssetSet을 가져옵니다. AssetSet은 해당 라벨이 붙은 모든 에셋의 정보를 담고 있습니다.

3. 첫 번째 로드 시도 (LoadSyncByPath

                for(const FAssetEntry& AssetEntry : AssetSet.AssetEntries)
                {
                    const FSoftObjectPath& AssetPath = AssetEntry.AssetPath;
                    LoadSyncByPath(AssetPath);
                    if(AssetPath.IsValid())
                    {
                        AssetPaths.Emplace(AssetPath);
                    }
                }
                
  • 각 에셋의 AssetPath에 대해 LoadSyncByPath를 호출합니다. 이 함수는 에셋이 이미 메모리에 로드되어 있는지 (ResolveObject) 확인하고, 없다면 로드합니다. 이후, 로드된 에셋은 AddLoadedAsset에 의해 내부적으로 관리됩니다.

중요한 점은 이 시점에서 AssetPath가 유효한 경우 AssetPaths 배열에 에셋 경로를 저장한다는 점입니다. 여기서 모든 에셋을 개별적으로 로드하면서도, 이후의 일괄적인 비동기 로딩을 위해 경로들을 수집하는 작업을 하고 있습니다.

4. 두 번째 로드 시도 (StreamableManager를 사용한 일괄 로드)

                GetStreamableManager().RequestSyncLoad(AssetPaths);
                
  • AssetPaths에 모인 모든 경로에 대해 StreamableManager를 사용해 다시 한 번 일괄 로드를 요청합니다. 이는 추가적으로 경로가 유효한 에셋들을 메모리에 한 번 더 동기적으로 로드하려는 것입니다.

이 시점의 의미는 첫 번째 로드 과정에서 놓쳤거나 처리되지 않은 에셋들을 확실하게 처리하기 위해 한 번 더 로드 과정을 거치는 것입니다. 또한, StreamableManager를 통해 관리된 방식으로 비동기 로딩과 유사한 방식을 사용하지만, 여기서는 동기적으로 로드를 보장합니다.

5. 두 번째 AddLoadedAsset 호출

                for(const FAssetEntry& AssetEntry : AssetSet.AssetEntries)
                {
                    const FSoftObjectPath& AssetPath = AssetEntry.AssetPath;
                    if(AssetPath.IsValid())
                    {
                        if(UObject* LoadedAsset = AssetPath.ResolveObject())
                        {
                            Get().AddLoadedAsset(AssetEntry.AssetName, LoadedAsset);
                        }
                        else
                        {
                            UE_LOG(LogTemp, Fatal, TEXT("Failed to load Asset [%s]"), *AssetPath.ToString());
                        }
                    }
                }
                
  • 마지막으로, 다시 한번 에셋 경로들을 확인하여 유효한 에셋을 AddLoadedAsset에 추가합니다. 여기서는 StreamableManager를 통한 일괄 로드 결과를 바탕으로, 실제로 로드가 완료되었는지를 확인하고 로드된 에셋을 추가합니다.
          

요약

  • 첫 번째 로드 (LoadSyncByPath): 각 에셋을 개별적으로 로드합니다. 이때 이미 로드된 에셋이 있다면 재로드하지 않으며, 로드된 에셋은 AddLoadedAsset으로 관리됩니다.

  • 두 번째 로드 (RequestSyncLoad): 모든 유효한 에셋 경로들을 모아서 일괄 로드를 수행합니다. 이는 혹시 빠뜨린 에셋을 다시 로드하는 안정성을 보장하기 위함입니다.

  • 최종 확인 후 관리: StreamableManager를 통한 로드가 완료된 에셋들에 대해 다시 한번 AddLoadedAsset으로 등록하여 모든 에셋이 제대로 관리되고 있는지 확인합니다.

두 번 로딩을 시도하는 이유는 다양한 상황에서의 에셋 로딩 실패 가능성을 최소화하고, 누락 없이 모든 에셋을 관리하기 위한 안정성 있는 접근 방식입니다.

에셋매니저에 로딩한 에셋을 이름으로 가져오는 함수 추가

        // header
        template<typename AssetType>
        AssetType* URAssetManager::GetAssetByName(const FName& AssetName)
        {
        	URAssetData* AssetData = Get().LoadedAssetData;
        	check(AssetData);
        
        	AssetType* LoadedAsset = nullptr;
        	const FSoftObjectPath& AssetPath = AssetData->GetAssetPathByName(AssetName);
        	if (AssetPath.IsValid())
        	{
        		LoadedAsset = Cast<AssetType>(AssetPath.ResolveObject());
        		if (LoadedAsset == nullptr)
        		{
        			UE_LOG(LogTemp, Warning, TEXT("Attempted sync loading because asset hadn't loaded yet [%s]."), *AssetPath.ToString());
        			LoadedAsset = Cast<AssetType>(AssetPath.TryLoad());
        		}
        	}
        	return LoadedAsset;
        }

PlayerController 변경

    // header
    public:
    	ARPlayerController(const FObjectInitializer& ObjectInitializer);
    
    protected:
    	virtual void BeginPlay() override;
    	virtual void SetupInputComponent() override;
    
    private:
    	void Input_Move(const FInputActionValue& InputValue);
    	void Input_Turn(const FInputActionValue& InputValue);
    
    // delete
    protected:
    //	UPROPERTY(EditAnywhere, Category = Input)
    //	TObjectPtr<class UInputMappingContext> InputMappingContext;
    //
    //	UPROPERTY(EditAnywhere, Category = Input)
    //	TObjectPtr<class UInputAction> MoveAction;
    //
    //	UPROPERTY(EditAnywhere, Category = Input)
    //	TObjectPtr<class UInputAction> TurnAction;
  • 블루프린트에서 설정해주는 방식에서 만들어준 AssetManager를 사용하는 방식으로 변경

에셋 매니저의 GetAssetByName을 통해 InputData를 가져온다

    // cpp
    void ARPlayerController::BeginPlay()
    {
    	Super::BeginPlay();
    
    	// Subsystem은 범위가 있는 싱글톤으로 플레이어의 생명 주기를 따라감
    	// 대입 하자마자 null check
    	if (const URInputData* InputData = URAssetManager::GetAssetByName<URInputData>("InputData"))
    	{
    		if(auto* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
    		{
    			// Subsystem에 MappingContext 추가
    			Subsystem -> AddMappingContext(InputData->InputMappingContext, 0);		
    		}	
    	}
    	
    }
    
    void ARPlayerController::SetupInputComponent()
    {
    	Super::SetupInputComponent();
    
    	if(const URInputData* InputData = URAssetManager::GetAssetByName<URInputData>("InputData"))
    	{
    		UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(InputComponent);
    		
    		// InputAction에 대한 콜백 함수 바인딩
    		// 객체를 대상으로 실행하는 것이기 때문에 자신에 대한 포인터를 넘겨준다
    		auto Action1 = InputData->FindInputActionByTag(RGameplayTags::Input_Action_Move);
    		EnhancedInputComponent->BindAction(Action1, ETriggerEvent::Triggered, this, &ThisClass::Input_Move);
    		
    		auto Action2 = InputData->FindInputActionByTag(RGameplayTags::Input_Action_Turn);
    		EnhancedInputComponent->BindAction(Action2, ETriggerEvent::Triggered, this, &ThisClass::Input_Turn);
    	}
    }
  • 이후 InputData의 FindInputActionByTag를 통해 InputAction을 가져와 바인딩한다
profile
게임 개발 공부중입니다.

0개의 댓글