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 | 필요한 것들을 제거하고, 게임에 관련된 부분만 남겨두는 과정
|
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 파일 사용 / 청크 생성 옵션 활성화
Chunk Downloader 플러그인 활성화
ProjectName.Build.cs 에 ChunkDownloader 모듈 추가
PrivateDependencyModuleNames.AddRange(new string[] { "ChunkDownloader" } );
청크데이터 뽑기
프로젝트 런처에서 커스텀 실행파일 하나 만든다음
프로젝트 선택 및 빌드 환경 설정 (청크 -> 쿠킹과정만 할 것이니 무관)
안드로이드 경우 해당 타입에 맞게 청크데이터 뽑아야 합니다 (ASTC 형식이면 ASTC 로)
Android 가 텍스처 압축 형식 (ASTC DXT ETC2) 모두 포함하는줄 알았는데 아닌 듯
다른 형식 청크를 사용하게되면 다음과 같은 오류가 뜹니다
ASTC / DXT / ETC 가 텍스처 압축 포맷 형식이기 때문에 Texture에서만 오류남
해당 경로로 나온 .Pak 파일들 보관
그리고 메니페스트 (메타데이터를 포함하는 파일)파일을 작성해 주어야 하는데
프로젝트를 패키징하거나 사용자에게 제공할 파일을 변경하고자 할 때마다 이 프로세스를 실시해야 합니다. 예시에서 최종 매니페스트 파일은 다음과 같습니다.
→ 패키징 할 때마다 다른 Pak 파일을 사이즈 까지 다시 수정해 주어야 하고 IIS 폴더에 다시 올려야 하는 상당한 노가다가 필요한데 자동화 과정이 있는지 찾아봐야 할 듯
메니페스트 파일은 대충 이런식으로 작성하면 된다.
파일이름 / 파일 크기 / 버전 / 청크ID / 경로
청크 인덱스가 0 인 파일에 대한 정보는 추가할 필요도 복사할 필요도 없습니다.
각 필드는 동일한 줄에 있고 탭으로 구분되어야 합니다. 그렇지 않은 경우 올바르게 구문 분석되지 않습니다.
버전 (Version001) 이 패치마다 달라야 합니다
만약 같은 청크 파일인데 버전을 같게 설정해 놓으면 받으려는 청크 파일 크기와 이미 다운된 청크 파일 크기가 다르다며 오류가 납니다.
디스크상의 크기 가 아닌 파일 크기 를 사용해야 합니다.
이렇게 해서 다음과 같은 폴더 구조를 만든다. (선택 사항이지만 플랫폼 별로 구별하는것이 가장 깔끔하다고 생각)
서버에 파일 호스팅
위에서 패키징한 파일들을 서버에 호스팅 할 수 있게 ChunkDownloader에 서버의 위치를 알려주는 작업
시작 메뉴(Start Menu) 를 열고, Windows 기능 켜기/끄기(Turn Windows Features on or off)
Windows 기능(Windows Features) 메뉴에서 인터넷 정보 서비스(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 을 찾지 못하는 오류는 해결하지 못하였다.
청크데이터를 마운트 하여도 AssetManager 가 찾지를 못한다.
AssetManager Reload
- ChatGpt 설명 -
- InvalidatePrimaryAssetDirectory: 이 기능은 기본 자산 디렉토리를 무효화하여 기본적으로 오래된 것으로 표시합니다. 기본 자산 데이터의 AssetManager 내부 캐시를 지웁니다.
- 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 |