icon안동민 개발노트

에셋 관리와 로딩


 언리얼 엔진에서 효율적인 에셋 관리와 로딩은 게임의 성능과 사용자 경험에 큰 영향을 미칩니다.

 이 절에서는 C++를 사용하여 에셋을 관리하고 로딩하는 다양한 기법을 살펴보겠습니다.

에셋 레퍼런스 시스템 이해 및 활용

 언리얼 엔진의 에셋 레퍼런스 시스템은 에셋의 로딩, 언로딩, 가비지 컬렉션을 관리합니다.

UCLASS()
class MYGAME_API AMyActor : public AActor
{
    GENERATED_BODY()
 
public:
    // 소프트 레퍼런스 (지연 로딩 가능)
    UPROPERTY(EditAnywhere, Category = "Assets")
    TSoftObjectPtr<UStaticMesh> MeshAsset;
 
    // 하드 레퍼런스 (즉시 로딩)
    UPROPERTY(EditAnywhere, Category = "Assets")
    UStaticMesh* LoadedMesh;
 
    void LoadMeshAsset()
    {
        if (MeshAsset.IsValid())
        {
            LoadedMesh = MeshAsset.Get();
        }
        else
        {
            LoadedMesh = MeshAsset.LoadSynchronous();
        }
    }
};

동적 에셋 로딩 구현

 런타임에 에셋을 동적으로 로딩하는 방법

void AMyActor::LoadDynamicAsset(const FString& AssetPath)
{
    UStaticMesh* LoadedAsset = Cast<UStaticMesh>(StaticLoadObject(UStaticMesh::StaticClass(), nullptr, *AssetPath));
    if (LoadedAsset)
    {
        // 로드된 에셋 사용
        UStaticMeshComponent* MeshComponent = GetComponentByClass<UStaticMeshComponent>();
        if (MeshComponent)
        {
            MeshComponent->SetStaticMesh(LoadedAsset);
        }
    }
}

비동기 에셋 로딩 기법

 대용량 에셋을 비동기적으로 로딩하여 게임 성능 향상

void AMyActor::LoadAssetAsync(const FSoftObjectPath& AssetPath)
{
    TAsyncLoadingRequest<UStaticMesh> AsyncRequest = UAssetManager::GetStreamableManager().RequestAsyncLoad(
        AssetPath,
        FStreamableDelegate::CreateUObject(this, &AMyActor::OnAssetLoaded)
    );
}
 
void AMyActor::OnAssetLoaded()
{
    UStaticMesh* LoadedMesh = Cast<UStaticMesh>(AssetPath.ResolveObject());
    if (LoadedMesh)
    {
        // 로드된 메시 사용
    }
}

에셋 번들링 전략

 에셋 번들을 사용하여 관련 에셋을 그룹화하고 효율적으로 관리하는 방법

UCLASS()
class MYGAME_API UMyAssetBundle : public UPrimaryDataAsset
{
    GENERATED_BODY()
 
public:
    UPROPERTY(EditAnywhere, Category = "Bundle")
    TArray<FSoftObjectPath> BundledAssets;
 
    virtual FPrimaryAssetId GetPrimaryAssetId() const override
    {
        return FPrimaryAssetId("AssetBundle", GetFName());
    }
};
 
// 에셋 번들 로딩
void AMyGameMode::LoadAssetBundle(const FPrimaryAssetId& BundleId)
{
    UAssetManager::Get().LoadPrimaryAsset(
        BundleId,
        TArray<FName>(),
        FStreamableDelegate::CreateUObject(this, &AMyGameMode::OnBundleLoaded)
    );
}

스트리밍 레벨과 에셋 관리

 스트리밍 레벨을 사용하여 대규모 월드의 에셋을 효율적으로 관리하는 방법

UCLASS()
class MYGAME_API AMyLevelStreamer : public AActor
{
    GENERATED_BODY()
 
public:
    UFUNCTION(BlueprintCallable)
    void StreamInLevel(const FName& LevelName)
    {
        FLatentActionInfo LatentInfo;
        LatentInfo.CallbackTarget = this;
        LatentInfo.ExecutionFunction = "OnLevelLoaded";
        LatentInfo.UUID = FGuid::NewGuid().A;
        LatentInfo.Linkage = 0;
 
        UGameplayStatics::LoadStreamLevel(this, LevelName, true, true, LatentInfo);
    }
 
    UFUNCTION()
    void OnLevelLoaded()
    {
        // 레벨 로드 완료 후 처리
    }
};

메모리 내 에셋 캐싱 기법

 자주 사용되는 에셋을 메모리에 캐싱하여 로딩 시간 단축하는 방법

UCLASS()
class MYGAME_API UAssetCache : public UObject
{
    GENERATED_BODY()
 
private:
    UPROPERTY()
    TMap<FString, UObject*> CachedAssets;
 
public:
    template<class T>
    T* GetOrLoadAsset(const FString& AssetPath)
    {
        if (UObject** FoundAsset = CachedAssets.Find(AssetPath))
        {
            return Cast<T>(*FoundAsset);
        }
 
        T* LoadedAsset = Cast<T>(StaticLoadObject(T::StaticClass(), nullptr, *AssetPath));
        if (LoadedAsset)
        {
            CachedAssets.Add(AssetPath, LoadedAsset);
        }
        return LoadedAsset;
    }
};

대용량 에셋 처리 전략

 대용량 에셋을 효율적으로 처리하기 위한 전략

  1. 에셋 분할 : 대용량 에셋을 더 작은 단위로 분할하여 관리
  2. LOD (Level of Detail) 시스템 활용 : 거리에 따라 다른 상세도의 에셋 사용
  3. 스트리밍 텍스처 : 필요에 따라 텍스처 해상도를 동적으로 조정
UCLASS()
class MYGAME_API UMyStreamingTexture : public UStreamableRenderAsset
{
    GENERATED_BODY()
 
public:
    virtual void BeginDestroy() override
    {
        Super::BeginDestroy();
        // 스트리밍 리소스 정리
    }
 
    virtual void CancelPendingStreamingRequest() override
    {
        // 진행 중인 스트리밍 요청 취소
    }
};

에셋 로딩 시 성능 최적화 기법

  1. 백그라운드 로딩 : 게임플레이에 영향을 주지 않도록 별도 스레드에서 에셋 로딩
  2. 에셋 프리로딩 : 예상되는 에셋을 미리 로딩하여 로딩 시간 단축
  3. 에셋 언로딩 : 불필요한 에셋을 적시에 언로딩하여 메모리 사용 최적화
void AMyGameMode::PreloadAssets()
{
    TArray<FSoftObjectPath> AssetsToLoad;
    // AssetsToLoad에 프리로드할 에셋 경로 추가
 
    UAssetManager::GetStreamableManager().RequestAsyncLoad(
        AssetsToLoad,
        FStreamableDelegate::CreateUObject(this, &AMyGameMode::OnAssetsPreloaded)
    );
}

다양한 플랫폼에 따른 에셋 관리 전략

  1. 모바일 : 메모리 제약을 고려한 에셋 최적화 (텍스처 압축, 메시 단순화 등)
  2. 콘솔 : 플랫폼별 특화된 에셋 포맷 및 로딩 전략 사용
  3. PC : 다양한 하드웨어 사양을 고려한 스케일러블 에셋 관리
##if PLATFORM_ANDROID || PLATFORM_IOS
    ##define USE_COMPRESSED_TEXTURES 1
##else
    ##define USE_COMPRESSED_TEXTURES 0
##endif
 
void AMyGameMode::LoadPlatformSpecificAssets()
{
    ##if USE_COMPRESSED_TEXTURES
        // 압축된 텍스처 로딩
    ##else
        // 비압축 텍스처 로딩
    ##endif
}

에셋 버전 관리

 에셋 버전 관리를 통해 에셋 업데이트 및 호환성을 유지하는 방법

USTRUCT()
struct FAssetVersion
{
    GENERATED_BODY()
 
    UPROPERTY()
    int32 MajorVersion;
 
    UPROPERTY()
    int32 MinorVersion;
};
 
UCLASS()
class MYGAME_API UVersionedAsset : public UObject
{
    GENERATED_BODY()
 
public:
    UPROPERTY()
    FAssetVersion Version;
 
    virtual void Serialize(FArchive& Ar) override
    {
        Super::Serialize(Ar);
        Ar << Version;
 
        if (Ar.IsLoading())
        {
            // 버전에 따른 로딩 로직
        }
    }
};

에셋 의존성 처리

 에셋 간 의존성을 관리하여 일관성 있는 에셋 로딩 보장하기

UCLASS()
class MYGAME_API UMyAssetManager : public UAssetManager
{
    GENERATED_BODY()
 
public:
    virtual void StartInitialLoading() override
    {
        Super::StartInitialLoading();
 
        // 의존성 그래프 구축
        BuildAssetDependencyGraph();
    }
 
private:
    void BuildAssetDependencyGraph()
    {
        // 에셋 간 의존성 분석 및 그래프 구축
    }
};

런타임 에셋 생성 및 파괴

 동적으로 에셋을 생성하고 파괴하는 Best Practices

UCLASS()
class MYGAME_API AAssetGenerator : public AActor
{
    GENERATED_BODY()
 
public:
    UFUNCTION(BlueprintCallable)
    UStaticMesh* CreateDynamicMesh()
    {
        UStaticMesh* NewMesh = NewObject<UStaticMesh>(this);
        // 메시 데이터 설정
        NewMesh->CreateMeshDescription();
        NewMesh->BuildFromMeshDescription();
        return NewMesh;
    }
 
    UFUNCTION(BlueprintCallable)
    void DestroyDynamicAsset(UObject* AssetToDestroy)
    {
        if (AssetToDestroy && !AssetToDestroy->IsUnreachable())
        {
            AssetToDestroy->ConditionalBeginDestroy();
        }
    }
};

 효율적인 에셋 관리와 로딩은 게임의 성능과 사용자 경험에 크게 기여합니다.

 에셋 레퍼런스 시스템을 이해하고 활용하며, 동적 및 비동기 로딩 기법을 적절히 사용하는 것이 중요합니다