언리얼
에셋 참조
gamedevlab
2025. 5. 13. 22:40
에셋 참조는 언리얼 엔진에서 한 애셋이 다른 애셋을 사용하는 방식을 정의한다. 참조 방식은 메모리 관리, 로딩 시간, 패키징에 직접적인 영향을 미친다.
- 강참조 (Hard Reference) 강참조는 오브젝트 A가 오브젝트 B를 직접 포인터로 참조하는 형태이다. 이 경우 A가 로드될 때 B도 반드시 함께 로드된다. 이는 의존성이 있는 애셋들이 항상 함께 존재하도록 보장하지만, 참조 체인이 길어지면 불필요한 애셋까지 메모리에 로드하여 초기 로딩 시간을 증가시키고 메모리 사용량을 늘릴 수 있다.
- 약참조 (Soft Reference) 약참조는 오브젝트 A가 오브젝트 B를 직접적인 메모리 주소 대신, 경로 문자열(String Path)이나 ID 형태로 참조하는 방식이다. A를 로드할 때 B가 자동으로 로드되지 않는다. B의 사용이 필요한 시점에 명시적으로 로드 요청을 해야 한다. 이는 초기 로딩 시간을 줄이고 메모리 관리를 유연하게 하지만, 참조된 애셋의 로드 상태를 개발자가 직접 관리해야 하는 부담이 있다. FSoftObjectPath, TSoftObjectPtr, TSoftClassPtr 등이 약참조 구현에 사용된다.
애셋 참조 방식
- 직접 프로퍼티 참조 (Direct Property Reference) 애셋 참조의 가장 일반적인 방식은 UPROPERTY 매크로를 통해 프로퍼티를 에디터에 노출하고, 레벨 디자이너나 아티스트가 블루프린트 에디터 또는 레벨 에디터의 디테일 패널에서 해당 애셋을 직접 지정하는 것이다. 이는 강참조로 동작한다.이 방식은 설정이 간편하고 직관적이나, 참조된 애셋이 항상 함께 로드되므로 필요 이상으로 많은 애셋이 메모리에 상주할 위험이 있다.
/** construction start sound stinger */ UPROPERTY(EditDefaultsOnly, Category=Building) USoundCue* ConstructionStartStinger; // USoundCue 애셋에 대한 강참조
- 생성 시간 참조 (Construction Time Reference) 이는 C++ 클래스의 생성자 내에서 특정 애셋을 찾아 프로퍼티에 할당하는 방식이다. ConstructorHelpers 유틸리티 클래스를 사용하여 생성자 실행 단계에서 애셋이나 클래스를 검색하여 참조한다. 이 방식 역시 강참조로 취급된다.ConstructorHelpers는 생성자에서만 사용해야 하며, 경로가 하드코딩되므로 애셋 경로 변경 시 코드 수정이 필요하다.
/** gray health bar texture */ UPROPERTY() // 직접적인 에디터 노출은 아니지만, 내부적으로 강참조 유지 class UTexture2D* BarFillTexture; AStrategyHUD::AStrategyHUD(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { // 생성자에서 FObjectFinder를 사용한 애셋 검색 및 로드 static ConstructorHelpers::FObjectFinder<UTexture2D> BarFillObj(TEXT("/Game/UI/HUD/BarFill")); if (BarFillObj.Succeeded()) // 로드 성공 확인 { BarFillTexture = BarFillObj.Object; // 프로퍼티에 할당 } // FClassFinder를 사용하여 클래스 참조를 로드할 수도 있다. // static ConstructorHelpers::FClassFinder<UBlueprint> MyBlueprintClass(TEXT("/Game/Blueprints/MyBP")); // if (MyBlueprintClass.Succeeded()) { SomeBlueprintClassProperty = MyBlueprintClass.Class; } }
- 간접 프로퍼티 참조 (Indirect Property Reference / Soft Referencing) TSoftObjectPtr 또는 TSoftClassPtr을 사용하여 애셋 로드 시점을 제어한다. 이는 약참조 방식이다. 디자이너는 에디터에서 이를 직접 프로퍼티 레퍼런스와 유사하게 지정할 수 있지만, 실제 애셋 데이터는 참조 시점에 자동으로 로드되지 않는다.
- FSoftObjectPath: 애셋의 경로를 문자열 형태로 저장하는 구조체이다. 자체적으로 애셋을 로드하거나 유효성을 검증하는 기능은 없다. 가장 기본적인 약참조 형태이다.
- TSoftObjectPtr<T>: FSoftObjectPath를 래핑하며, 템플릿으로 지정된 특정 UObject 파생 타입의 애셋을 가리킨다. 애셋이 로드되면 내부적으로 해당 객체에 대한 TWeakObjectPtr를 유지하여 접근을 제공할 수 있다. .IsPending()로 로드 대기 상태를, .Get()으로 로드된 객체를 (로드되지 않았으면 nullptr), .ToSoftObjectPath()로 FSoftObjectPath를 얻을 수 있다.
- TSoftClassPtr<T>: TSoftObjectPtr과 유사하나, UClass 객체(주로 블루프린트 클래스)를 참조하는 데 특화되어 있다.
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category=Building) TSoftObjectPtr<UStaticMesh> BaseMesh; // UStaticMesh 애셋에 대한 약참조 // FStreamableManager 인스턴스가 사용 가능하다고 가정 (예: Streamable 멤버 변수 또는 싱글톤 접근) // FStreamableManager& StreamMgr = UAssetManager::GetStreamableManager(); UStaticMesh* GetLazyLoadedMesh() { if (BaseMesh.IsPending()) // 아직 로드되지 않았는지 확인 { // FSoftObjectPath로 변환하여 로드 요청 const FSoftObjectPath& AssetRef = BaseMesh.ToSoftObjectPath(); // 동기 로드 예시. 실제 게임에서는 비동기 로드가 권장된다. // BaseMesh = Cast<UStaticMesh>(Streamable.SynchronousLoad(AssetRef)); // 또는 BaseMesh = Cast<UStaticMesh>(StaticLoadObject(UStaticMesh::StaticClass(), nullptr, *AssetRef.ToString())); // 비동기 로드를 위해서는 FStreamableManager 사용이 필요하다. } return BaseMesh.Get(); // 로드된 애셋 반환, 실패 시 nullptr }
오브젝트 검색/로드
- FindObject<T>(): 이미 메모리에 로드된 UObject를 특정 Outer와 이름으로 검색한다. 애셋을 로드하지는 않는다.
AFunctionalTest* TestToRun = FindObject<AFunctionalTest>(TestsOuter, *TestName);
- LoadObject<T>(): 지정된 경로의 애셋을 동기적으로 로드한다. 내부적으로 FindObject와 유사한 검색을 먼저 수행하고, 없으면 디스크에서 로드한다. 메인 스레드를 차단하므로 게임플레이 중 빈번한 사용은 피해야 한다.
// LoadObject 인자: Outer(보통 NULL), 경로(TEXT 매크로 사용), 이름(보통 NULL), 로드 플래그(LOAD_None 등), 패키지(보통 NULL) GridTexture = LoadObject<UTexture2D>(NULL, TEXT("/Engine/EngineMaterials/DefaultWhiteGrid.DefaultWhiteGrid"), NULL, LOAD_None, NULL);
- LoadClass<T>(): LoadObject<UClass>()와 유사하게 동작하며, 지정된 경로의 UClass를 동기적으로 로드한다.
DefaultPreviewPawnClass = LoadClass<APawn>(NULL, *PreviewPawnName, NULL, LOAD_None, NULL); // LoadObject<UClass>를 사용한 동일 동작 // UClass* LoadedClass = LoadObject<UClass>(NULL, *PreviewPawnName, NULL, LOAD_None, NULL); // if (LoadedClass && LoadedClass->IsChildOf(APawn::StaticClass())) // 유효성 및 타입 검사 // { // DefaultPreviewPawnClass = LoadedClass; // } // else // { // DefaultPreviewPawnClass = nullptr; // }
- StaticLoadObject() / StaticLoadClass(): LoadObject / LoadClass와 기능적으로 유사하며, 주로 에디터 코드나 특정 동기 로드가 불가피한 상황에서 사용된다. 마찬가지로 메인 스레드를 차단한다.
비동기 로딩 및 스트리밍 (Asynchronous Loading and Streaming)
대규모 애셋이나 게임플레이 도중 애셋 로드로 인한 프레임 드롭을 방지하기 위해 비동기 로딩이 필수적이다.
- FStreamableManager: 비동기 애셋 로딩 및 스트리밍을 관리하는 중앙 시스템이다. FSoftObjectPath 배열을 사용하여 다수의 애셋을 동시에 비동기적으로 로드하도록 요청할 수 있다. 로드가 완료되면 지정된 델리게이트가 호출된다.StreamableHandle을 통해 로드 상태를 추적하거나 로드를 취소할 수 있다.
// FStreamableManager 인스턴스 획득 (UGameGlobals는 예시, 실제로는 엔진이나 애셋 매니저 통해 접근) // FStreamableManager& Streamable = UGameGlobals::Get().StreamableManager; 또는 FStreamableManager& Streamable = UAssetManager::GetStreamableManager(); TArray<FSoftObjectPath> AssetsToLoad; AssetsToLoad.Add(BaseMesh.ToSoftObjectPath()); // TSoftObjectPtr로부터 경로 추출 // 비동기 로드 요청. 로드 완료 시 MyLoadCallback 함수 호출 StreamableHandle = Streamable.RequestAsyncLoad(AssetsToLoad, FStreamableDelegate::CreateUObject(this, &UMyClass::MyLoadCallback));
- 블루프린트에서의 비동기 로드: 블루프린트에서는 "Async Load Asset" 노드를 사용하여 유사한 비동기 로딩 기능을 구현할 수 있다. 이 노드는 TSoftObjectPtr나 TSoftClassPtr을 입력으로 받고, 로드가 완료되면 "Completed" 실행 핀을 통해 로드된 애셋을 제공한다.
애셋 매니저 (Asset Manager)
UAssetManager는 대규모 프로젝트에서 애셋의 검색, 로드, 언로드, 감사를 체계적으로 관리하기 위한 프레임워크이다.
- FPrimaryAssetId: 애셋을 고유하게 식별하는 ID이다 (PrimaryAssetType과 PrimaryAssetName으로 구성). 애셋 매니저는 프라이머리 애셋 ID를 사용하여 애셋을 관리하며, 이는 경로 문자열보다 안정적인 참조 방식이다.
- 프라이머리 애셋 (Primary Assets): 맵, 캐릭터, 무기 아이템 등 게임의 주요 콘텐츠 단위를 나타낸다. 애셋 매니저는 이러한 프라이머리 애셋과 그 의존성을 관리하여 쿠킹, 청킹, DLC 배포 등을 효율화한다.
- 애셋 번들 (Asset Bundles): 프라이머리 애셋 내에서 특정 상황(예: 특정 게임 모드, 해상도)에만 필요한 애셋들을 그룹화하는 기능이다. 이를 통해 필요한 애셋만 선택적으로 로드할 수 있다.
애셋 매니저는 특정 애셋이 언제 로드되고 언로드되어야 하는지에 대한 규칙을 정의하고, 이를 통해 메모리 사용을 최적화하고 로딩 시간을 관리한다.
참조와 패키징 (References and Packaging)
- 강참조된 애셋: 참조하는 애셋이 쿠킹될 때 함께 쿠킹되어 패키지에 포함된다.
- 약참조된 애셋: 기본적으로는 참조하는 애셋이 쿠킹된다고 해서 약참조된 애셋이 자동으로 쿠킹되지 않는다. 약참조된 애셋이 패키지에 포함되려면 다음 중 하나의 조건을 만족해야 한다:
- 다른 곳에서 해당 애셋을 강참조한다.
- 애셋 매니저에 프라이머리 애셋으로 등록되어 관리된다.
- 명시적으로 쿠킹하도록 설정된 디렉토리(예: "Always Cook" 규칙이 적용된 디렉토리)에 포함된다.
- 기타 쿠킹 규칙에 의해 포함된다.
따라서 약참조를 사용할 경우, 해당 애셋이 실제 빌드에 포함되도록 관리하는 전략이 필요하다. 이는 패키지 크기를 최적화하고 불필요한 데이터 포함을 막는 데 중요하다.