Content Directories 는 Root 에서 참조를 따라 빌드한다
AssetBundle 은 어느 에셋을 어느 번들에 넣을지 사람이 정합니다. 빠뜨리면 그 에셋은 참조하는 번들마다 몰래 복사되고, 무엇이 어디로 들어갔는지 알려면 리포트를 뒤져야 합니다.
Content Directories 에는 배정하는 단계가 없습니다. Root 하나에서 참조를 따라가며 닿는 것을 전부 담습니다. 그리고 참조를 두 종류로 나눠, 빌드는 둘 다 따라가고 런타임 로드는 한쪽에서 멈추게 했습니다.
빌드는 Root 에서 GUID 를 따라간다
Player 빌드가 Scene List 에서 출발하듯, 콘텐츠 빌드는 Root Asset 에서 출발합니다. Root 의 필드가 가리키는 에셋, 그 에셋이 가리키는 에셋을 차례로 따라가며 모읍니다.
따라가는 길은 에셋 파일에 저장된 참조 값입니다. Root Asset 을 텍스트로 열면 이렇게 들어 있습니다.
bonusPrefab:
m_LoadableObjectId: {fileID: 645800571622001937, loadable: 1, guid: 462de8f7fb5dac54eb97111ce3d2eda1, type: 3}
bonusLevel:
m_SceneGUID: 988cbc230d1f5174ab9aa0a8beda1dfe
guid는 어느 파일인지입니다.Cube.prefab.meta에 적힌 값입니다fileID는 그 파일 안의 어느 오브젝트인지입니다. 씬은 파일 전체가 단위라 GUID 만 씁니다loadable: 1은 “빌드에는 넣되 로드는 끊는다” 는 표시입니다
같은 에셋은 GUID 가 같으니 한 번만 담깁니다. 이 프로젝트에서 Cube 의 Lit.mat 과 씬 머티리얼 여섯 개가 모두 같은 셰이더를 쓰는데, 빌드 결과에는 Lit.shader 파일이 하나뿐이고 일곱 머티리얼이 전부 그 파일을 가리킵니다. 같은 빌드 안의 중복 제거는 따로 설정하는 기능이 아니라 이 수집 방식에서 그냥 나옵니다.
반대로 GUID 가 없는 참조는 그래프에 안 잡힙니다. Shader.Find("…") 나 Resources.Load("…") 처럼 문자열로 찾는 것은 빌드가 따라갈 수 없어서, 에디터에서는 되는데 콘텐츠에는 빠질 수 있습니다.
빌드는 점선까지 가고 로드는 점선에서 멈춘다
참조는 세 가지 모양이 있고, 빌드와 런타임에서 다르게 다뤄집니다.
| 참조 | 빌드 | 런타임 |
|---|---|---|
| 일반 필드 (실선) | 담는다 | 가진 쪽과 같이 올라온다 |
Loadable<T> (점선) |
담는다 | Load() 해야 올라온다 |
LoadableSceneId (점선) |
담는다 | SceneManager.LoadSceneAsync 로 올린다 |
이 프로젝트의 실제 그래프에서 두 경우를 바꿔 보면, 같은 그림 위에서 지나가는 길이 다릅니다.
Build Analysis 창에 같은 숫자가 나옵니다. Root 에서 바로 닿는 것(Direct)은 에셋 2개 6.93 KB 이고, 끝까지 닿는 것(Total)은 에셋 17개 2.85 MB 입니다. 점선 두 개가 등록 때 올라오는 양을 약 420분의 1 로 끊었습니다.
점선은 수명이 갈리는 자리에 둔다
점선을 어디에 두느냐가 곧 “무엇을 같이 올리고 같이 내릴지” 를 정하는 일입니다. 원칙은 하나입니다. 같이 살고 같이 죽는 것은 실선, 수명이 다른 경계에는 점선.
- 항상 필요하고 가벼운 것(설정, 메타데이터)은 Root 의 일반 필드로 둡니다
- 열 때만 쓰는 UI, 특정 스테이지의 적, 보너스 레벨은
Loadable<T>나LoadableSceneId로 끊습니다 Loadable<T>는 Root 에만 두는 것이 아닙니다. 로비 씬의 컨트롤러가 인벤토리 UI 프리팹을Loadable필드로 들고 있다가 열 때만 올리는 식으로 MonoBehaviour 필드에도 둡니다
점선 아래는 실선으로 한꺼번에 따라옵니다. Cube 를 Load() 하면 그 아래 머티리얼과 셰이더는 따로 부르지 않아도 같이 올라옵니다. 그래서 점선 하나가 끊는 범위는 그 아래 실선 전체입니다.
한 가지 예외가 있습니다. 서로를 참조하는 순환이 생기면 어느 것을 먼저 올릴지 정할 수 없어서, 매뉴얼에 따르면 고리 전체를 한 덩어리로 묶고 64 MB 를 넘으면 빌드가 실패합니다.
직접 확인하는 테스트 코드
Root 클래스 하나와 빌드 메뉴 하나면 그래프를 직접 볼 수 있습니다. 빌드한 뒤 Window > Analysis > Build Analysis 에서 Root 의 Direct 와 Total 을 비교하고, Library/BuildHistory/<빌드ID>/ContentLayout.json 에서 파일마다 같이 여는 파일(SerializedFileDependencies)과 점선(LoadableDependencies)을 확인합니다.
[CreateAssetMenu(menuName = "Content/Content Pack Root Asset")]
public class ContentPackRootAsset : ScriptableObject
{
public ContentPackMetadata metadata; // 실선: Direct 에 들어간다
public Loadable<GameObject> bonusPrefab; // 점선: Total 에만 들어간다
public LoadableSceneId bonusLevel; // 점선: Total 에만 들어간다
}
public static class BuildContentPack
{
[MenuItem("Build/Content Directories/Content Pack")]
public static void Build()
{
var report = BuildPipeline.BuildContentDirectory(new BuildContentDirectoryParameters
{
outputPath = "Builds/ContentDirectories/ContentPack",
rootAssetPaths = new[] { "Assets/ContentPacks/ContentPackRootAsset.asset" },
options = BuildContentOptions.None,
});
Debug.Log(report.summary.result);
}
}
이 프로젝트에서는 Direct 6.93 KB, Total 2.85 MB 가 나왔습니다. ContentLayout.json 에서 Root 파일의 같이 여는 파일은 스크립트 정보와 메타데이터 둘뿐이고, Cube 와 씬은 점선 칸에만 적혀 있습니다.
주의할 점
1. .meta 를 잃지 않는다
안 지키면 생기는 문제. 참조는 .meta 에 적힌 GUID 로 이어집니다. 에셋을 옮기면서 .meta 를 빠뜨리거나 버전 관리에서 빼면 새 GUID 가 붙어 Root 에서 그 에셋으로 가는 길이 끊깁니다.
이렇게 쓴다. 에셋은 에디터 안에서 옮기고, .meta 를 에셋과 같이 커밋합니다.
2. 문자열로 찾는 에셋은 Root 에서 닿게 만든다
안 지키면 생기는 문제. Shader.Find 나 Resources.Load 로 찾는 에셋은 그래프에 안 보여서 콘텐츠에 안 들어갑니다. 에디터에서는 프로젝트에 있으니 돌아가고, 빌드한 콘텐츠에서만 비어 나옵니다.
이렇게 쓴다. 필요한 에셋은 Root 나 콘텐츠 에셋의 필드로 참조해서 GUID 로 이어 둡니다.
3. 서로 참조하는 큰 덩어리를 만들지 않는다
안 지키면 생기는 문제. 순환 참조는 고리 전체가 한 번에 올라오고, 64 MB 를 넘으면 빌드가 실패합니다.
이렇게 쓴다. 되돌아가는 참조가 필요하면 한쪽을 Loadable<T> 로 바꿔 고리를 끊습니다.
정리
Content Directories 빌드는 Root 에서 GUID 참조를 따라가며 닿는 것을 한 번씩 담습니다.
- 같은 에셋은 GUID 가 같아서 같은 빌드 안에서는 한 번만 들어갑니다. 문자열로 찾는 에셋은 그래프에 안 보입니다.
- 빌드는 실선과 점선을 다 따라가고, 런타임은 점선에서 멈춥니다. 이 프로젝트에서 등록 때 올라오는 양은 6.93 KB, 전체는 2.85 MB 입니다.
- 같이 살고 같이 죽는 것은 실선, 수명이 다른 경계에는
Loadable<T>나LoadableSceneId를 둡니다.