언리얼 엔진(UE) 은 에셋을 자동으로 로드하고 언로드합니다. 덕분에 개발자는 필요한 에셋을 즉시 언리얼 엔진에 요청할 수 있습니다. 그러나 에셋을 검색, 로드, 검사할 시기와 방법을 개발자가 정확하게 제어하려는 경우가 있습니다. 이러한 경우에는 에셋 매니저(Asset Manager) 가 유용합니다.
에셋 매니저는 에디터와 패키지로 만든 게임에 고유한 글로벌 오브젝트입니다. 특정 프로젝트에 맞게 오버라이드하고 커스터마이징할 수 있으며, 프로젝트 컨텍스트에 따라 콘텐츠를 청크로 분할할 수 있는 에셋 관리 프레임워크로서 기능합니다. 또한, 언리얼 엔진에서 지원되는 느슨한 패키지 아키텍처의 장점도 제공합니다.
디스크 및 메모리 사용량을 검사하는 데 유용한 툴 세트 외에도, 게임을 배포할 때 쿠킹 및 청킹을 고려하여 에셋 구성을 최적화하는 데 필요한 정보를 제공합니다.
프라이머리 에셋과 세컨더리 에셋
언리얼 엔진의 에셋 관리 시스템은 모든 에셋을 두 가지 타입(프라이머리 에셋 및 세컨더리 에셋 )으로 구분합니다. 에셋 매니저는 GetPrimaryAssetId
함수를 호출하여 확보한 프라이머리 에셋 ID 에서 프라이머리 에셋을 직접 조작할 수 있습니다. 특정한 UObject
클래스로 제작한 에셋을 프라이머리 에셋으로 지정하려면 'GetPrimaryAssetId' 함수를 오버라이드하여 유효한 FPrimaryAssetId
구조체를 반환하면 됩니다. 에셋 매니저는 세컨더리 에셋을 직접 처리하지 않습니다. 즉, 프라이머리 에셋이 세컨더리 에셋을 참조하거나 사용하려는 경우 언리얼 엔진이 자동으로 세컨더리 에셋을 로드합니다. 기본적으로 UWorld
에셋(레벨)만 프라이머리 에셋이고 나머지 에셋은 모두 세컨더리 에셋입니다.
세컨더리 에셋을 프라이머리 에셋으로 변경하려면 해당 클래스의 GetPrimaryAssetId
함수를 오버라이드하여 유효한 FPrimaryAssetId
구조체를 반환해야 합니다. 프라이머리 에셋 ID는 에셋 그룹을 알려주는 고유한 프라이머리 에셋 타입과 해당 프라이머리 에셋의 이름 등 두 요소로 구성됩니다. 프라이머리 에셋 이름은 콘텐츠 브라우저(Content Browser) 에 표시되는 에셋 이름을 기본값으로 사용합니다.
블루프린트 클래스 에셋 및 데이터 에셋
에셋 매니저(Asset Manager)는 두 가지 타입의 에셋을 처리합니다. 하나는 블루프린트 클래스, 다른 하나는 레벨이나 UDataAsset
클래스의 에셋 인스턴스인 데이터 에셋 같은 블루프린트 이외의 에셋입니다. 각 프라이머리 에셋 타입에는 특정한 베이스 클래스가 연결되며 환경설정에 블루프린트 클래스를 저장할지 여부가 지정됩니다(아래의 설명 참조).
블루프린트 클래스
새로운 블루프린트 프라이머리 에셋을 생성하려는 경우 콘텐츠 브라우저 로 이동하여 GetPrimaryAssetId
함수를 오버라이드하는 클래스의 후손인 새 블루프린트 클래스를 생성하면 됩니다. 이와 같은 베이스 클래스가 될 수 있는 요소로는 프라이머리 데이터 에셋이나 그 자손 외에도 GetPrimaryAssetId
를 오버라이드하는 액터 서브클래스가 있습니다. 블루프린트 프라이머리 에셋에 액세스하려는 경우 C++ 코드에서 GetPrimaryAssetObjectClass
같은 함수를 호출하거나, 이름에 'Class'라는 단어가 포함된 블루프린트 에셋 매니저 함수를 사용하면 됩니다. 블루프린트 프라이머리 에셋 클래스를 확보하고 나면 다른 블루프린트 클래스처럼 취급하여 새 인스턴스를 스폰하는 데 사용하거나, Get Defaults 함수를 사용하여 블루프린트와 연결된 클래스 디폴트 오브젝트의 읽기 전용 데이터에 액세스할 수 있습니다.
블루프린트 클래스를 인스턴스화할 필요가 없는 경우 UPrimaryDataAsset
으로부터 상속받은 데이터 전용 블루프린트에 데이터를 저장할 수 있습니다. 또한 베이스 클래스에서 자손 클래스(예: 블루프린트 기반 자손 클래스)를 도출할 수도 있습니다. 예를 들어 C++에서 UPrimaryDataAsset
을 확장하는 UMyShape
같은 베이스 클래스를 생성하고, UMyShape
가 부모인 BP_MyRectangle
이라는 블루프린트 기반 서브클래스를 생성한 다음, BP_MySquare
라는 BP_MyRectangle
의 블루프린트 기반 자손을 생성할 수 있습니다. 디폴트 세팅을 유지할 경우 마지막으로 생성되는 클래스의 PrimaryAssetId는 MyShape:BP_MySquare
입니다.
블루프린트 이외의 에셋
프라이머리 에셋 타입이 블루프린트 데이터를 저장할 필요가 없는 경우에는 블루프린트 이외의 에셋을 사용하면 됩니다. 블루프린트 이외의 에셋은 코드로 액세스하기가 더 간편하고 메모리 효율도 더 우수합니다. 에디터에서 블루프린트 이외의 프라이머리 에셋을 새로 생성하려는 경우 고급 콘텐츠 브라우저 창에서 새 데이터 에셋을 생성하거나 커스텀 UI를 사용하여 새 레벨을 생성하면 됩니다. 에셋이 생성되는 원리는 블루프린트 클래스가 생성되는 원리와 다릅니다. 다시 말해서, 생성한 에셋은 클래스 자체가 아니라 클래스의 인스턴스입니다. 클래스에 액세스하려는 경우 GetPrimaryAssetObject
같은 C++ 함수나 이름에 'Class'가 포함되지 않은 블루프린트 함수로 로드하면 됩니다. 클래스가 로드되면 직접 액세스하여 해당 클래스의 데이터를 읽을 수 있습니다.
이러한 에셋은 클래스가 아니라 인스턴스이므로 클래스나 다른 에셋을 상속받을 수 없습니다. 예를 들어 명시적으로 오버라이드하는 값을 제외하고 부모의 값을 상속받는 자손 에셋을 생성하고 싶다면 블루프린트 클래스를 대신 사용해야 합니다.
에셋 매니저와 스트리머블 매니저
에셋 매니저 오브젝트는 프라이머리 에셋의 검색 및 로드 프로세스를 관리하는 싱글톤입니다. 언리얼 엔진에 포함된 베이스 에셋 매니저 클래스는 기본적인 관리 기능만 갖추고 있지만, 프로젝트에 필요한 경우 그에 맞게 확장할 수도 있습니다. 에셋 매니저에 포함된 스트리머블 매니저(Streamable Manager) 구조체는 오브젝트를 비동기식으로 로드하는 작업을 실제로 수행할 뿐만 아니라, 오브젝트가 필요 없어져 언로드될 수 있을 때까지 스트리머블 핸들(Streamable Handle) 을 사용하여 오브젝트를 메모리에 보존합니다. 싱글톤 에셋 매니저와 달리 언리얼 엔진의 다양한 부분에는 서로 다른 용도의 스트리머블 매니저가 여럿 존재합니다.
에셋 번들
에셋 번들(Asset Bundle) 이란 프라이머리 에셋과 관련된 특정 에셋의 이름이 지정된 목록입니다. 에셋 번들을 생성하려면 메타 태그가 'AssetBundles'인 UObject
의 TSoftObjectPtr
또는 FStringAssetReference
멤버로 구성된 UPROPERTY
섹션에 태그를 지정해야 합니다. 태그 값은 세컨더리 에셋을 저장할 번들의 이름을 나타냅니다. 예를 들어 MeshPtr
이라는 멤버 변수에 저장된 아래의 스태틱 메시 에셋은 UObject를 저장했을 때 'TestBundle'이라는 에셋 번들에 추가됩니다.
/** 메시 */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = Display, AssetRegistrySearchable, meta = (AssetBundles = "TestBundle"))
TSoftObjectPtr<UStaticMesh> MeshPtr;
에셋 번들을 사용하는 두 번째 방법은 런타임 시점에 프로젝트의 에셋 매니저 클래스로 등록하는 것입니다. 이 경우 프로그래머가 FAssetBundleData
구조체를 채워 에셋 매니저에 전달하는 코드를 작성해야 합니다. 프로그래머는 이러한 목적으로 UpdateAssetBundleData
함수를 오버라이드하거나 번들의 세컨더리 에셋과 연결하려는 프라이머리 에셋 ID로 AddDynamicAsset
을 호출할 수 있습니다.
디스크에 저장된 프라이머리 에셋 등록 및 로드
대부분의 프라이머리 에셋은 콘텐츠 브라우저 에 표시되고 디스크에 저장된 에셋 파일로 존재하므로 아티스트나 디자이너가 편집할 수 있습니다. 프로그래머가 이런 용도로 사용할 수 있는 클래스를 생성하는 가장 간단한 방법은 UDataAsset
자손 클래스인 UPrimaryDataAsset
으로부터 상속받는 것입니다. UPrimaryDataAsset
에는 에셋 번들 데이터를 로드하고 저장하는 기능이 내장되어 있습니다. 다른 베이스 클래스(예: APawn
)를 사용하려는 경우 UPrimaryDataAsset
을 살펴보는 것이 좋습니다. 이 함수는 에셋 번들을 클래스에 사용하기 위해 구현해야 하는 기능을 알 수 있는 가장 간단한 예시이기 때문입니다. 아래의 클래스는 가상 게임의 존 타입을 지정하는 방법을 알 수 있는 예시입니다. 존 타입은 게임의 전체 맵 화면에 월드를 시각적으로 표현할 때 사용할 아트 에셋을 게임에 알려줍니다.
/** 맵 화면에서 사용자가 선택할 수 있는 존 */
UCLASS(Blueprintable)
class MYGAME_API UMyGameZoneTheme : public UPrimaryDataAsset
{
GENERATED_BODY()
/** 존 이름 */
UPROPERTY(EditDefaultsOnly, Category=Zone)
FText ZoneName;
/** 이 존에 진입할 때 로드될 레벨 */
UPROPERTY(EditDefaultsOnly, Category=Zone)
TSoftObjectPtr<UWorld> LevelToLoad;
/** 맵에 이 존을 표현하는 데 사용되는 블루프린트 클래스 */
UPROPERTY(EditDefaultsOnly, Category=Visual, meta=(AssetBundles = "Menu"))
TSoftClassPtr<class AGameMapTile> MapTileClass;
};
이 클래스는 UPrimaryDataAsset
으로부터 상속받습니다. 따라서 이 클래스에는 에셋의 약식 이름과 네이티브 클래스를 사용하는 유효한 버전의 GetPrimaryAssetId
가 존재합니다. 예를 들어 'Forest'라는 이름으로 저장된 UMyGameZoneTheme
의 프라이머리 에셋 ID는 'MyGameZoneTheme:Forest'입니다. 에디터에서 UMyGameZoneTheme
에셋을 저장할 때마다 PrimaryDataAsset
의 AssetBundleData
멤버가 업데이트되어 해당 에셋을 세컨더리 에셋으로 추가합니다.
프라이머리 에셋을 등록하고 로드하려면 다음 액션을 수행해야 합니다.
-
프로젝트의 커스텀 에셋 매니저 클래스를 언리얼 엔진에 알립니다. 프로젝트에 특수 기능이 필요한 경우에만 디폴트 에셋 매니저 클래스인
UAssetManager
를 오버라이드해야 합니다. 프로젝트에 특수 기능이 필요하지 않은 경우 이 단계를 생략해도 무방합니다. 오버라이드하려면 프로젝트의DefaultEngine.ini
파일을 수정하고[/Script/Engine.Engine]
섹션의AssetManagerClassName
변수를 설정합니다. 최종 값의 형식은 다음과 같아야 합니다.[/Script/Engine.Engine] AssetManagerClassName=/Script/Module.UClassName
여기에서 'Module'은 프로젝트의 모듈 이름을 뜻하고, 'UClassName'은 사용하려는
UClass
의 이름을 뜻합니다. 이 예시에서 프로젝트의 모듈 이름은 'MyGame'이고, 사용하려는 클래스는UFortAssetManager
(즉,UClass
이름이FortAssetManager
) 이므로 두 번째 줄의 내용은 다음과 같습니다.AssetManagerClassName=/Script/FortniteGame.FortAssetManager
-
프라이머리 에셋을 에셋 매니저에 등록합니다. 프로젝트 세팅(Project Settings) 메뉴에서 구성하거나 시작 시점에 프라이머리 에셋을 등록하도록 에셋 매니저 클래스를 프로그래밍하면 됩니다.
- (게임/에셋 매니저(Game / Asset Manager) 섹션에 있는) 프로젝트 세팅(Project Settings) 구성 방법은 아래와 같습니다.
프라이머리 에셋 스캔 경로를 환경설정할 수 있습니다.
세팅 효과 스캔할 프라이머리 에셋 타입(Primary Asset Types to Scan) 검색하고 등록할 프라이머리 에셋의 타입과 프라이머리 에셋의 위치 및 용도를 나열합니다. 제외할 디렉터리(Directories to Exclude) 프라이머리 에셋 스캔 대상에서 명시적으로 제외되는 디렉터리입니다. 테스트 에셋을 제외하는 데 유용합니다. 프라이머리 에셋 규칙(Primary Asset Rules) 에셋 처리 방법이 명시된 구체적인 규칙 오버라이드를 나열합니다. 자세한 내용은 쿠킹 및 청킹을 참고하세요. 프로덕션 에셋만 쿠킹(Only Cook Production Assets) 이 옵션을 선택하면 DevelopmentCook으로 지정된 에셋이 쿠킹 과정에서 오류를 유발합니다. 최종 배포 빌드에서 테스트 에셋을 제거하는 데 유용합니다. 프라이머리 에셋 ID 리디렉트(Primary Asset ID Redirects) 에셋 매니저가 이 목록에 ID가 표시되는 프라이머리 에셋 관련 데이터를 조회할 때 제공된 대체 ID로 ID가 교체됩니다. 프라이머리 에셋 타입 리디렉트(Primary Asset Type Redirects) 에셋 매니저가 프라이머리 에셋 관련 데이터를 조회할 때 원래 타입 대신 이 목록에 제공된 타입 이름이 사용됩니다. 프라이머리 에셋 이름 리디렉트(Primary Asset Name Redirects) 에셋 매니저가 프라이머리 에셋 관련 데이터를 조회할 때 원래 이름 대신 이 목록에 제공된 에셋 이름이 사용됩니다. - 프라이머리 에셋을 코드로 직접 등록하려면 에셋 매니저 클래스의
StartInitialLoading
함수를 오버라이드하고 해당 위치에서ScanPathsForPrimaryAssets
를 호출해야 합니다. 이 경우 동일한 타입의 프라이머리 에셋을 모두 하나의 서브폴더에 넣는 것이 좋습니다. 검색 및 등록 속도가 빨라지기 때문입니다.
- 에셋을 로드합니다. 에셋 매니저 함수인
LoadPrimaryAssets
,LoadPrimaryAsset
,LoadPrimaryAssetsWithType
을 사용하면 적시에 프라이머리 에셋을 로드할 수 있습니다. 나중에UnloadPrimaryAssets
,UnloadPrimaryAsset
,UnloadPrimaryAssetsWithType
을 사용하여 에셋을 언로드할 수 있습니다. 이와 같은 로드 함수를 사용할 때 에셋 번들 목록을 지정할 수 있습니다. 이 방식으로 로드하면 에셋 매니저가 앞서 설명한 대로 해당 에셋 번들에서 참조하는 세컨더리 에셋을 로드합니다.
동적으로 생성된 프라이머리 에셋 등록 및 로드
런타임 시점에 프라이머리 에셋 번들을 동적으로 등록하고 로드할 수도 있습니다. 이와 같은 작업을 수행하는 이유를 이해하는 데 유용한 두 가지 에셋 매니저 함수가 있습니다.
- 첫 번째 함수는 주어진
UScriptStruct
의 모든UPROPERTY
멤버를 검사하고 에셋 레퍼런스를 식별한 뒤 에셋 이름 배열에 저장하는ExtractSoftObjectPaths
입니다. 이 배열은 에셋 번들을 생성할 때 사용할 수 있습니다.ExtractSoftObjectPaths
파라미터:파라미터 용도 Struct
에셋 레퍼런스를 검색할 UStruct입니다. StructValue
구조체를 가리키는 void 포인터입니다. FoundAssetReferences
구조체에서 찾은 에셋 레퍼런스를 반환하는 데 사용되는 배열입니다. PropertiesToSkip
반환 배열에서 제외할 프로퍼티 이름 배열입니다. - 두 번째 함수는 프라이머리 에셋과 관련된 모든 레퍼런스를 찾고 재귀적 확장을 통해 에셋 번들의 모든 종속성을 찾아내는
RecursivelyExpandBundleData
입니다. 이 경우 위의 ZoneTheme에서 참조되는 TheaterMapTileClass가 AssetBundleData에 추가됩니다. 그런 다음 그 이름으로 된 다이내믹 에셋을 등록하고 로드합니다.RecursivelyExpandBundleData
파라미터:파라미터 용도 BundleData
에셋 레퍼런스가 포함되어 있는 번들 데이터입니다. 재귀적으로 확장되며 연관된 에셋 세트를 로드하는 데 유용할 수 있습니다.
예를 들어 'MyGame' 프로젝트는 커스텀 에셋 매니저 클래스에서 아래의 코드를 사용하여 게임 중에 다운로드한 'theater' 데이터를 토대로 에셋을 구성하고 로드할 수 있습니다.
// theater ID에서 추출한 이름 구성
UMyGameAssetManager& AssetManager = UMyGameAssetManager::Get();
FPrimaryAssetId WorldMapAssetId = FPrimaryAssetId(UMyGameAssetManager::WorldMapInfoType, FName(*WorldMapData.UniqueId));
TArray<FSoftObjectPath> AssetReferences;
AssetManager.ExtractSoftObjectPaths(FMyGameWorldMapData::StaticStruct(), &WorldMapData, AssetReferences);
FAssetBundleData GameDataBundles;
GameDataBundles.AddBundleAssets(UMyGameAssetManager::LoadStateMenu, AssetReferences);
// 재귀적 레퍼런스 확장을 통해 존에서 타일 블루프린트 픽업
AssetManager.RecursivelyExpandBundleData(GameDataBundles);
// 다이내믹 에셋 등록
AssetManager.AddDynamicAsset(WorldMapAssetId, FSoftObjectPath(), GameDataBundles);
// 프리로드 시작
AssetManager.LoadPrimaryAsset(WorldMapAssetId, AssetManager.GetDefaultBundleState());