// 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;
}
// 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);
}
}
}
}
// 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;
}
// 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());
}
}
if(URAssetManager::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);
}
}
중요한 점은 이 시점에서 AssetPath가 유효한 경우 AssetPaths 배열에 에셋 경로를 저장한다는 점입니다. 여기서 모든 에셋을 개별적으로 로드하면서도, 이후의 일괄적인 비동기 로딩을 위해 경로들을 수집하는 작업을 하고 있습니다.
GetStreamableManager().RequestSyncLoad(AssetPaths);
이 시점의 의미는 첫 번째 로드 과정에서 놓쳤거나 처리되지 않은 에셋들을 확실하게 처리하기 위해 한 번 더 로드 과정을 거치는 것입니다. 또한, StreamableManager를 통해 관리된 방식으로 비동기 로딩과 유사한 방식을 사용하지만, 여기서는 동기적으로 로드를 보장합니다.
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());
}
}
}
첫 번째 로드 (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;
}
// 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;
// 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);
}
}