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()
    {
        // 에셋 간 의존성 분석 및 그래프 구축
    }
};

런타임 에셋 생성 및 파괴

 동적으로 에셋을 생성하고 파괴하는 베스트 프랙티스

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();
        }
    }
};

 효율적인 에셋 관리와 로딩은 게임의 성능과 사용자 경험에 크게 기여합니다. 에셋 레퍼런스 시스템을 이해하고 활용하며, 동적 및 비동기 로딩 기법을 적절히 사용하는 것이 중요합니다. 에셋 번들링, 스트리밍 레벨, 메모리 캐싱 등의 전략을 통해 대규모 게임에서도 효율적인 에셋 관리가 가능합니다.

 플랫폼별 특성을 고려한 에셋 관리 전략을 수립하고, 버전 관리와 의존성 처리를 통해 에셋의 일관성과 호환성을 유지해야 합니다. 런타임 에셋 생성 및 파괴 시에는 메모리 관리에 주의를 기울여야 합니다.

 이러한 기법들을 적절히 조합하여 사용하면, 다양한 규모와 플랫폼의 게임에서 효율적이고 안정적인 에셋 관리 시스템을 구축할 수 있습니다.