Chunk 개념

기본적으로 쿠킹 할 때 해당 디렉터리에 경로를 입력하여 그 안의 에셋들이 쿠킹 되어 같이 배포된다.

 

 

그렇지만 이러한 방식은 게임 업데이트 때 마다 다시 지우고 다운로드를 받고 설치해야 하는데

다음과 같이 Chunk 를 나누게 되면 쿠킹된 에셋 데이터들을 따로따로 다운 받을 수 있다.

위 사진에서의 50MB 는 게임 초기 화면을 띄울 최소한의 데이터 에셋들이고

800MB 는 실제 게임 데이터이므로, 우리는 PlayStore에서 게임을 실행하기 위해 50MB 정도만 다운받으면 되는것이다.

 

게임에 필요한 데이터들을 게임내에서 다운로드 한다.

 

또한 여러개의 Chunk 데이터를 나누게 되면 업데이트 때 마다 변경 사항이 있는 Chunk 데이터만 다시 다운로드 받으면 된다.

 

body를 Chunk로 쪼개서 DLC를 개발하는 것도 마찬가지고 Chunk가 너무 많으면 안 됩니다. 많을수록 압축률이 작아지고 패키지 크기가 커지며, 너무 적으면 업데이트가 번거로워지므로 균형을 잘 맞추는 것이 중요

 

 

청크 관련하여 등록된 PrimaryAsset 을 찾지 못하는 오류는 해결하지 못하였다. (밑에서 설명)

 

 

청크 데이터 나누기

 

청크 데이터를 나누는 방법에는

PrimaryAsset 에 청크 ID를 달아주는 방법과

PrimaryAssetLabel 을 이용하는 방법이 있다.

 

PrimaryAssetData

는 프로젝트 세팅 - 에셋매니저에서 규칙을 지정해주면 되고

DataAsset이 C++에서 가져온 경우 "HasBlueprintClasses"를 선택하지 마세요. 이는 실제로 "IsBlueprintClass"를 의미하고 청사진에서 파생된 DataAsset만 검색하기 때문입니다.

 

IsBlueprintClass -> {에셋이름}_C 으로 가져온다.

 

 

블루프린트 클래스 보유 옵션

 

이 옵션이 뭔지 감이 잡히지 않았었는데

 

말 그대로 블루프린트 클래스를 가져온다.

UBlueprintGenereatedClass 타고 올라가면 UObject

AActor를 상속받는 MapObject가 캐스팅에 실패한 이유

 

 

PrimaryAssetLabel

세컨더리 에셋들을 하나의 청크로 묶는데 필요

매개변수
Rules
Priority 에셋이 동시에 두 개 이상의 라벨의 조건을 만족하는 경우 우선순위 값이 더 큰 라벨에 에셋이 할당 됨
Chunk ID Pak 파일 이름에 해당하는 청크 ID / 고유해야 함
Apply Recursively 재귀적 확장?을 통해 연관된 리소스 찾아내는 옵션 (하위폴더?)

Cook Rule 필요한 것들을 제거하고, 게임에 관련된 부분만 남겨두는 과정
  • UnKnown: 자동 요리와 동일합니다. 이 리소스에 대한 참조가 있으면 쿠킹 되고, 참조가 없으면 쿠킹 되지 않습니다.
  • Never Cook: 절대 쿠킹 하지 마세요. 참조가 있으면 오류가 보고됩니다.
  • Development Cook: 개발패키지 조건에 참고사항이 있으면 쿠킹합니다.
  • Development Always Cook: 개발 패키징 조건에서 항상 쿠킹합니다.
  • Always Cook: 항상 개발 또는 배송 포장 조건에서 쿠킹합니다.
Primary Asset Label
Label Assets in My Dir 라벨이 위치한 폴더 하위까지 에셋들을 한 Chunk 로 묶는 옵션
Is Runtime Label Label 에셋 자체가 쿠킹 되고 런타임에 사용 가능 해야 하는 경우??
비동기 로드에서 사용하는 경우 On 해야 한다고 한다.
리디렉터 포함

<ChatGpt>
프로젝트에서 에셋 경로가 변경되었을 때 원래 경로를 새 경로로 리디렉트하는 경우에 유용합니다. 리디렉터를 포함하면, 경로 변경으로 인해 에셋 로드에 문제가 생기는 것을 방지할 수 있습니다.

-> 청크 데이터로부터 '마운트' 과정을 해야하기 때문에 경로 변경에 있어서 필요한 옵션

Explicit Assets 라벨 수동 지정할 에셋들
Explicit Blueprints 라벨 수동 지정할 BP들
Asset Collection <ChatGpt>
특정 폴더, 태그, 혹은 개별 에셋을 기준으로 컬렉션을 설정할 수 있습니다

지정된 자산 컬렉션에 등록된 자산을 Secondary Asset에 등록

Label Assets in My Dir / Is Runtime Label / 리디렉터 포함 옵션을 모두 활성화 시키고 폴더에 배치 시키면

Art 폴더를 포함한 하위 폴더에 있는 에셋들 모두가 ArtPrimaryAssetLabel 에 지정된 청크 ID 로 묶이는 것을 알 수 있다.

 

 

 

Chunk Downloader 세팅

https://docs.unrealengine.com/5.1/ko/setting-up-the-chunkdownloader-plugin-in-unreal-engine/

 

패키징 - Pak 파일 사용 / 청크 생성 옵션 활성화

Io Store 사용 시 .ucas / .utoc 파일 추가 생성 됨

 

Chunk Downloader 플러그인 활성화

 

ProjectName.Build.cs 에 ChunkDownloader 모듈 추가

PrivateDependencyModuleNames.AddRange(new string[] { "ChunkDownloader" } );

 

 

 

청크데이터 뽑기

https://dev.epicgames.com/documentation/en-us/unreal-engine/how-to-create-a-patch-in-unreal-engine?application_version=5.0

 

 

프로젝트 런처에서 커스텀 실행파일 하나 만든다음 

프로젝트 선택 및 빌드 환경 설정 (청크 -> 쿠킹과정만 할 것이니 무관)

 

 

안드로이드 경우 해당 타입에 맞게 청크데이터 뽑아야 합니다 (ASTC 형식이면 ASTC 로)

Android 가 텍스처 압축 형식 (ASTC DXT ETC2) 모두 포함하는줄 알았는데 아닌 듯

다른 형식 청크를 사용하게되면 다음과 같은 오류가 뜹니다

contains no miplevels

ASTC / DXT / ETC 가 텍스처 압축 포맷 형식이기 때문에 Texture에서만 오류남

해당 경로로 나온 .Pak 파일들 보관

 

 

 

 

그리고 메니페스트 (메타데이터를 포함하는 파일)파일을 작성해 주어야 하는데

https://docs.unrealengine.com/5.1/ko/hosting-a-manifest-and-assets-for-chunkdownloader-in-unreal-engine/#2.%EB%A7%A4%EB%8B%88%ED%8E%98%EC%8A%A4%ED%8A%B8%ED%8C%8C%EC%9D%BC%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0

 

 

프로젝트를 패키징하거나 사용자에게 제공할 파일을 변경하고자 할 때마다 이 프로세스를 실시해야 합니다. 예시에서 최종 매니페스트 파일은 다음과 같습니다.

 

→ 패키징 할 때마다 다른 Pak 파일을 사이즈 까지 다시 수정해 주어야 하고 IIS 폴더에 다시 올려야 하는 상당한 노가다가 필요한데 자동화 과정이 있는지 찾아봐야 할 듯

 

Android / Windows

메니페스트 파일은 대충 이런식으로 작성하면 된다.

파일이름 / 파일 크기 / 버전 / 청크ID / 경로

 

청크 인덱스가 0 인 파일에 대한 정보는 추가할 필요도 복사할 필요도 없습니다.

각 필드는 동일한 줄에 있고 탭으로 구분되어야 합니다. 그렇지 않은 경우 올바르게 구문 분석되지 않습니다.

 

버전 (Version001) 이 패치마다 달라야 합니다

만약 같은 청크 파일인데 버전을 같게 설정해 놓으면 받으려는 청크 파일 크기와 이미 다운된 청크 파일 크기가 다르다며 오류가 납니다.

 

디스크상의 크기 가 아닌 파일 크기 를 사용해야 합니다.

 

이렇게 해서 다음과 같은 폴더 구조를 만든다. (선택 사항이지만 플랫폼 별로 구별하는것이 가장 깔끔하다고 생각)

 

 

 

 

서버에 파일 호스팅

https://docs.unrealengine.com/5.1/ko/hosting-a-manifest-and-assets-for-chunkdownloader-in-unreal-engine/#3.%EB%A1%9C%EC%BB%AC%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%84%9C%EB%B2%84%EC%97%90%ED%8C%8C%EC%9D%BC%ED%98%B8%EC%8A%A4%ED%8C%85

 

위에서 패키징한 파일들을 서버에 호스팅 할 수 있게 ChunkDownloader에 서버의 위치를 알려주는 작업

시작 메뉴(Start Menu) 를 열고, Windows 기능 켜기/끄기(Turn Windows Features on or off)

윈도우 기능 켜기 / 끄기

 

Windows 기능(Windows Features) 메뉴에서 인터넷 정보 서비스(Internet Information Services) 를 활성화

인터넷 정보 서비스 (Internet Information Services) 활성화

 

인터넷 정보 서비스 관리자(IIS Manager) 를 열고 디렉터리 검색(Directory Browsing) 을 활성화

 

창 좌측의 연결(Connections) 메뉴에서 PC-[사용자] 를 클릭

PC-[사용자] 메뉴에서 MIME 형식(MIME Types)

MIME 형식(MIME Types) 메뉴에서 추가(Add)

MIME 형식 추가(Add MIME Type) 창에서 파일 이름 확장명(File Name extension).pak 로 설정하고, MIME 형식(MIME type)application/octet-stream 을 입력합니다. .ucas.utoc 에도 동일한 작업을 수행

이렇게 하면 IIS에서 요청 시 파일을 간단하게 다운로드합니다

 

Default Web Site 폴더로 이동합니다. 기본적으로 이 폴더의 경로는 C:\inetpub\wwwroot 입니다. 사용할 폴더 이름을 생성 (여기선 MoonPatchingCDN)

 

그리고 이 폴더안에 위에서 매니페스트 파일 작성에서 만든 MoonPatchingKey 폴더를 MoonPatchingCDN 으로 복붙

 

프로젝트의 DefaultGame.ini 파일을 열고 다음 정보를 추가하여 CDN 베이스 URL 을 정의

[/Script/Plugins.ChunkDownloader MoonPatchingLive]
+CdnBaseUrls="http://{서버주소}/MoonPatchingCDN"

 

localhost 아니어도 서버에 IIS 세팅이 잘 되어 있다면 연결 된다.

오류가 있는지 http:// 를 붙여주지 않으면 인식을 못한다

 

DeploymentName = (MoonPatchingLive) DefaultGame.ini /Script/Plugins.ChunkDownloader 에 있는 이름

ContentBuildId (MoonPatchingKey) = CDN 안에 있는 폴더 이름 - 버전 별로 폴더 나누어도 될 듯

 

 

패치 코드 구현 (SubSystem)

// Fill out your copyright notice in the Description page of Project Settings.

#include "System/ChunkDownloader/MoonPatchingSubsystem.h"
#include "ChunkDownloader.h"
#include "Misc/CoreDelegates.h"
#include "Kismet/GameplayStatics.h"
#include "System/MoonCommonSettings.h"
#include "System/MoonLog.h"

//TODO: 이미 받아져 있는 팩 파일 검증

namespace MoonConsoleVariables
{
    static FString ContentBuildId = "MoonPatchingKey_";
    static FAutoConsoleVariableRef CVarContentBuildId(
        TEXT("Moon.ContentBuildId"),
        ContentBuildId,
        TEXT("ContentBuildId 변경"),
        ECVF_SetBySystemSettingsIni);

}

const FString DeploymentName = "MoonPatchingLive"; // DefaultGame.ini  /Script/Plugins.ChunkDownloader 에 있는 이름 

void UMoonPatchingSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    Super::Initialize(Collection);
}

void UMoonPatchingSubsystem::Deinitialize()
{
    Super::Deinitialize();
    FChunkDownloader::Shutdown();
}

bool UMoonPatchingSubsystem::ShouldCreateSubsystem(UObject* Outer) const
{
    return UMoonCommonSettings::Get()->bExecutePatchSystem;
}

void UMoonPatchingSubsystem::InitPatching(const FString& PatchName)
{
    // 선택한 플랫폼에서 청크 다운로더를 초기화합니다.
    MOONLOG(Log, TEXT("GetPlatform : %s"), *UGameplayStatics::GetPlatformName());

    // 해당 플랫폼 청크 다운로더 가져옴
    TSharedRef<FChunkDownloader> Downloader = FChunkDownloader::GetOrCreate();
    Downloader->Initialize(UGameplayStatics::GetPlatformName(), 8);
    // TargetDownloadsInFlight - ChunkDownloader가 한 번에 처리하는 최대 다운로드 수

    // 캐싱된 빌드 ID를 로딩합니다
    // 함수는 DeploymentName 을 사용하여 FChunkDownloader::LoadCachedBuild 를 호출합니다.
    // 디스크에 이미 다운로드되어 있는 파일이 있는지 확인하고,
    // 이를 통해 ChunkDownloader가 최신 매니페스트 파일로 업데이트된 경우
    // 두 번째 다운로드를 건너뛸 수 있습니다.
    if (Downloader->LoadCachedBuild(DeploymentName))
    {
        MOONLOG(Log, TEXT("Cache Build Succeeded"));
    }
    else
    {
        MOONLOG(Log, TEXT("Cache Build Failed"));
    }

    // 빌드 매니페스트 파일을 업데이트합니다
    MOONLOG(Log, TEXT("Updating Build Manifest"))
    TFunction<void(bool bSuccess)> ManifestUpdateCompleteCallback = [this](bool bSuccess)
    {
        OnManifestUpdateComplete(bSuccess);
    };

    const FString ContentBuildId = MoonConsoleVariables::ContentBuildId.TrimStart().TrimEnd();
    Downloader->UpdateBuild(DeploymentName, ContentBuildId, ManifestUpdateCompleteCallback);
}


void UMoonPatchingSubsystem::OnManifestUpdateComplete(bool bSuccess)
{
    MOONLOG(Log, TEXT("Manifest Update %s"), bSuccess ? TEXT("Success") : TEXT("Failed"));
    bIsDownloadManifestUpToDate = bSuccess;
    if (bIsDownloadManifestUpToDate)
    {
        PatchGame();
    }
}

void UMoonPatchingSubsystem::GetLoadingProgress(int32& BytesDownloaded, int32& TotalBytesToDownload, float& DownloadPercent, int32& ChunksMounted, int32& TotalChunksToMount, float& MountPercent) const
{
    // 청크 다운로더의 레퍼런스를 구합니다.
    TSharedRef<FChunkDownloader> Downloader = FChunkDownloader::GetChecked();

    // 로딩 통계 구조체를 구합니다.
    FChunkDownloader::FStats LoadingStats = Downloader->GetLoadingStats();

    // 다운로드된 바이트와 다운로드할 바이트를 구합니다.
    BytesDownloaded = LoadingStats.BytesDownloaded;
    TotalBytesToDownload = LoadingStats.TotalBytesToDownload;

    // 마운트된 청크와 다운로드할 청크의 수를 구합니다.
    ChunksMounted = LoadingStats.ChunksMounted;
    TotalChunksToMount = LoadingStats.TotalChunksToMount;

    // 위의 통계를 사용하여 다운로드 및 마운트 퍼센트를 계산합니다.
    DownloadPercent = ((float)BytesDownloaded / (float)TotalBytesToDownload) * 100.0f;
    MountPercent = ((float)ChunksMounted / (float)TotalChunksToMount) * 100.0f;
}

bool UMoonPatchingSubsystem::PatchGame()
{
    // 다운로드 매니페스트를 최신 상태로 유지합니다.
    // 매니페스트를 검증하기 위해 서버와 연락하는 데 실패하여 패치할 수 없었습니다.
    if (!bIsDownloadManifestUpToDate)
    {
        MOONLOG(Log, TEXT("Manifest Update Failed. Can't patch the game"));
        return false;
    }
    MOONLOG(Log, TEXT("Starting the game patch Proccess"));
  
    // 청크 다운로더를 가져옵니다.
    TSharedRef<FChunkDownloader> Downloader = FChunkDownloader::GetChecked();
    // 다운 받을 청크 배열들을 가져옵니다.
    Downloader->GetAllChunkIds(ChunkDownloadList);

    // TEST
    const int DeletedChunkCount = Downloader->ValidateCache();
    MOONLOG(Log, TEXT("Deleted Chunk Counts %d"), DeletedChunkCount);
    Downloader = FChunkDownloader::GetChecked();
    // TEST

    bool bNeedDownLoadChunkFile = false;
    // 현재 청크 상태를 제보합니다.
    for (const int32 ChunkID : ChunkDownloadList)
    {
        const FChunkDownloader::EChunkStatus ChunkStatus = Downloader->GetChunkStatus(ChunkID);
        MOONLOG(Log, TEXT("Chunk %i status: %s"), ChunkID, FChunkDownloader::ChunkStatusToString(ChunkStatus));

        if (ChunkStatus != FChunkDownloader::EChunkStatus::Mounted
            && ChunkStatus != FChunkDownloader::EChunkStatus::Cached)
        {
            bNeedDownLoadChunkFile = true;
        }
    }

    // 로딩 모드를 시작합니다.
    TFunction<void(bool bSuccess)> LoadingModeCompleteCallback = [&](bool bSuccess)
    {
        OnLoadingModeComplete(bSuccess);
    };
    Downloader->BeginLoadingMode(LoadingModeCompleteCallback);

    // 이미 모든 청크 다운받음
    if (!bNeedDownLoadChunkFile)
    {
        MOONLOG(Log, TEXT("Already All Chunks DownLoaded"));
        OnDownloadComplete(true);
        return true;
    }

    // 청크를 다운받아야 함
    MOONLOG(Log, TEXT("Need To DownLoad Chunks Files"));
	TFunction<void(bool bSuccess)> DownloadCompleteCallback = [&](bool bSuccess)
    {
        OnDownloadComplete(bSuccess);
    };
    Downloader->DownloadChunks(ChunkDownloadList, DownloadCompleteCallback, 1);

    return true;
}

// Download / Mount 까지 완료 후 다음 Tick (프레임)에 불림
void UMoonPatchingSubsystem::OnLoadingModeComplete(bool bSuccess)
{
    MOONLOG(Log, TEXT("LoadingModeComplete %s"), bSuccess ? TEXT("Success") : TEXT("Failed"));
    if (bSuccess)
    {
        OnPatchCompleteDelegate.Broadcast(bSuccess);
    }
}

void UMoonPatchingSubsystem::OnAllChunkMountComplete(bool bSuccess)
{
    // (I/O Store 옵션을 On 했을 때) .ucas / .utoc 파일 마운트에 항상 실패하기때문에 False 임
    MOONLOG(Log, TEXT("All Chunk Mount %s"), bSuccess ? TEXT("Success") : TEXT("Failed"));
}

void UMoonPatchingSubsystem::OnChunkMountComplete(uint32 ChunkId, bool bSuccess)
{
    MOONLOG(Log, TEXT("마운트된 청크ID: %d   %s"), ChunkId, bSuccess ? TEXT("Success") : TEXT("Fail"));
}
\
void UMoonPatchingSubsystem::OnDownloadComplete(bool bSuccess)
{
    MOONLOG(Log, TEXT("Chunk Download %s"), bSuccess ? TEXT("Success") : TEXT("Failed"));
    if (bSuccess)
    {
        TSharedRef<FChunkDownloader> Downloader = FChunkDownloader::GetChecked();
        FJsonSerializableArrayInt DownloadedChunks;

        for (int32 ChunkID : ChunkDownloadList)
        {
            DownloadedChunks.Add(ChunkID);
        }

        Downloader->OnChunkMounted.AddUObject(this, &UMoonPatchingSubsystem::OnChunkMountComplete);

        // 청크를 마운트합니다.
        TFunction<void(bool bSuccess1)> MountCompleteCallback = [&](bool bSuccess1)
        {
            OnAllChunkMountComplete(bSuccess1);
        };
        Downloader->MountChunks(DownloadedChunks, MountCompleteCallback);

        OnDownloadCompleteDelegate.Broadcast(true);
    }
    else
    {
        // 델리게이트를 호출합니다.
        OnDownloadCompleteDelegate.Broadcast(false);
    }
}

 

 

ChunkDownloader 테스트

게임 시작 맵 청크 다운로드 레벨로 설정

 

청크 다운로드 맵 빌드 추가 및 쿠킹 경로 제외

PrimaryAssetLabel 로 지정된 적용되지 않는 에셋들 제외하고 전부 쿠킹 디렉터리에서 날려버린다.

 

그리고 다시 패키징 하면 앱 크기가 줄어들어 있는 상태로 시작한다.

왼쪽 - 패키징 경로 뺀 / 오른쪽 - 패키징 경로 모두 추가

 

 

게임을 시작하여 청크데이터를 다운받은 다음 마운트 과정을 거쳐야 한다.

마운트 - Pak 파일을 마운트하는 것은 게임 엔진 런타임 내에서 콘텐츠에 액세스할 수 있도록 하는 과정

 

예를들어 게임 프로젝트 내에서는 /Game/UI/Sprite 경로가 나오는데 청크 데이터에선 ../../{ProjectName}/Content/UI/Sprite 이런식으로 나올것이다.

이 때 ../../{ProjectName}/Content//Game/ 으로 인식하게 끔 하는 작업이 마운트다.

 

 

윈도우 같은 경우 Saved\PersistentDownloadDir\PakCache 경로에 Chunk 파일들이 다운로드 되고, Mount를 시도한다.

.pak 만 Mount 되고, ucas / utoc 파일들은 마운트 되지 않는다.

 

 

그래서 IOStore 옵션 활성화 하였을 때 DownloadChunks 함수 결과가 무조건 false 반환함

랜카드 달아서 빠른 듯

 

해당 청크데이터를 받으면 윈도우의 경우 Windows\{ProjectName}\Saved\PersistentDownloadDir\PakCache 경로에 IIS 에 올려놓았던 청크들이 그대로 다운받아진 것을 확인할 수 있다.

 

 

청크 데이터가 이미 받아져있어도 다음에 실행할 때 청크 데이터와 마운트 하는 과정은 꼭 필요

TSharedRef<FChunkDownloader> Downloader = FChunkDownloader::GetChecked();
Downloader->MountChunks(DownloadedChunks, MountCompleteCallback);

청크 데이터를 사용하면 마운트 할 것

 

 

 

 

 

 

Chunk 데이터로 부터 PrimaryAsset 가져오는 테스트 (실패)

청크 관련하여 등록된 PrimaryAsset 을 찾지 못하는 오류는 해결하지 못하였다.

왼쪽 청크데이터 / 오른쪽 Always Cook

청크데이터를 마운트 하여도 AssetManager 가 찾지를 못한다. 

 

AssetManager Reload

- ChatGpt 설명 -

  1. InvalidatePrimaryAssetDirectory: 이 기능은 기본 자산 디렉토리를 무효화하여 기본적으로 오래된 것으로 표시합니다. 기본 자산 데이터의 AssetManager 내부 캐시를 지웁니다.
  2. RefreshPrimaryAssetDirectory: 이 함수는 기본 자산 디렉터리를 새로 고칩니다. 프로젝트의 콘텐츠 디렉터리를 스캔하고 프로젝트의 현재 상태를 기반으로 기본 자산 데이터의 AssetManager 내부 캐시를 재구축합니다.

→ InvalidatePrimaryAssetDirectory 하면 기존에 있던것들도 nullptr 됨 (LoadPrimaryAssetType 해도 마찬가지)

 

반면에 AssetRegistry 에서 청크데이터를 마운트하면 해당 경로에있는 에셋들을 잘 찾는다.

 

청크 데이터로 부터 PrimaryAsset 는 이미 패키징 된 _0.pak (기본) 파일 외에는 인식하지 못한다.

 

필자같은 경우는 에셋매니저를 없애고 GetAssetRegistry().SearchAllAssets(true); 함수를 사용하고 FARFilter을 이용하여 에셋매니저 처럼 특정 폴더와 특정 클래스만 가져와서 어딘가에 저장을하여 사용하였다.

'언리얼' 카테고리의 다른 글

Save / Load GameData (.sav)  (0) 2024.07.19
SceneCaptureComponent2D  (0) 2024.06.26
소프트 포인터  (1) 2024.02.19
PrimaryAsset  (0) 2024.02.05
Numeric Input Text  (0) 2023.11.15

+ Recent posts