UE5 C++ 어셋 로딩 완전 가이드
개요 — 어셋 로딩 전략의 중요성
Section titled “개요 — 어셋 로딩 전략의 중요성”어셋 로딩 방식의 선택은 메모리 사용량, 로딩 시간, 프레임 드랍 모두에 직접적인 영향을 줍니다. 잘못된 참조 방식은 레벨 로드 시 불필요한 어셋을 통째로 메모리에 올려 로딩 시간을 수 배로 늘리는 원인이 됩니다.
| 방식 | 메모리 | 로딩 시점 | 사용 시기 |
|---|---|---|---|
| Hard Reference (직접 포인터) | 항상 로드됨 | 소유 오브젝트 로드 시 | 반드시 필요한 코어 어셋 |
| Soft Reference (TSoftObjectPtr) | 필요 시 로드 | 명시적 요청 시 | 선택적·대용량 어셋 |
| 동기 로딩 | 즉시 로드 | 호출 즉시 (블로킹) | 소형 어셋, 초기화 시 |
| 비동기 로딩 | 백그라운드 로드 | 완료 콜백 | 대형 어셋, 런타임 |
1. Hard Reference vs Soft Reference
Section titled “1. Hard Reference vs Soft Reference”1.1 Hard Reference — 의존성 전파
Section titled “1.1 Hard Reference — 의존성 전파”UPROPERTY에 TObjectPtr<UTexture2D> 또는 UStaticMesh*를 선언하면 소유 오브젝트가 로드될 때 참조된 어셋도 강제로 메모리에 로드됩니다.
// ❌ Hard Reference — 무기 BP 로드 시 모든 무기 메시·텍스처가 메모리에 올라옴UCLASS()class MYGAME_API AWeaponBase : public AActor{ GENERATED_BODY()
// 이 하나의 참조가 체인을 만듦: // AWeaponBase → UStaticMesh → UMaterial → UTexture2D (수십 MB) UPROPERTY(EditDefaultsOnly) TObjectPtr<UStaticMesh> WeaponMesh; // Hard Reference
UPROPERTY(EditDefaultsOnly) TObjectPtr<USoundCue> FireSound; // Hard Reference};1.2 Soft Reference — 지연 로딩
Section titled “1.2 Soft Reference — 지연 로딩”// ✅ Soft Reference — 선언만 하고 실제 로드는 필요 시점에UCLASS()class MYGAME_API AWeaponBase : public AActor{ GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, Category = "Assets") TSoftObjectPtr<UStaticMesh> WeaponMeshRef; // Soft — 경로만 저장
UPROPERTY(EditDefaultsOnly, Category = "Assets") TSoftObjectPtr<USoundCue> FireSoundRef; // Soft
UPROPERTY(EditDefaultsOnly, Category = "Assets") TSoftClassPtr<AActor> ProjectileClass; // Soft Class Reference};TSoftObjectPtr는 내부적으로 FSoftObjectPath(문자열 경로)를 저장합니다. 메모리 영향 없이 에디터에서 어셋을 지정할 수 있습니다.
2. 동기 로딩
Section titled “2. 동기 로딩”2.1 LoadObject — 런타임 동기 로딩
Section titled “2.1 LoadObject — 런타임 동기 로딩”// 경로로 어셋 동기 로딩 — 메인 스레드 블로킹 주의UStaticMesh* Mesh = LoadObject<UStaticMesh>( nullptr, TEXT("/Game/Meshes/Weapons/SM_Rifle"));
if (IsValid(Mesh)){ MeshComponent->SetStaticMesh(Mesh);}
// TSoftObjectPtr에서 동기 로딩if (!WeaponMeshRef.IsNull()){ UStaticMesh* LoadedMesh = WeaponMeshRef.LoadSynchronous(); if (IsValid(LoadedMesh)) { MeshComponent->SetStaticMesh(LoadedMesh); }}
LoadSynchronous()는 메인 스레드를 블로킹합니다. 대용량 어셋에 사용하면 프레임 드랍이 발생합니다.
2.2 ConstructorHelpers — 생성자 전용 로딩
Section titled “2.2 ConstructorHelpers — 생성자 전용 로딩”AMyCharacter::AMyCharacter(){ // 생성자에서만 사용 가능 — 런타임에는 사용 불가 static ConstructorHelpers::FObjectFinder<USkeletalMesh> MeshFinder(TEXT("/Game/Characters/Hero/SK_Hero"));
if (MeshFinder.Succeeded()) { GetMesh()->SetSkeletalMesh(MeshFinder.Object); }
// BP 클래스 참조 static ConstructorHelpers::FClassFinder<AMyProjectile> ProjectileFinder(TEXT("/Game/Blueprints/BP_Projectile"));
if (ProjectileFinder.Succeeded()) { ProjectileClass = ProjectileFinder.Class; }}3. 비동기 로딩 — FStreamableManager
Section titled “3. 비동기 로딩 — FStreamableManager”3.1 단일 어셋 비동기 로딩
Section titled “3.1 단일 어셋 비동기 로딩”#pragma once
#include "CoreMinimal.h"#include "Engine/StreamableManager.h"
class MYGAME_API FMyAssetLoader{public: // 단일 어셋 비동기 로딩 static void LoadMeshAsync(TSoftObjectPtr<UStaticMesh> MeshRef, TFunction<void(UStaticMesh*)> OnLoaded) { if (MeshRef.IsNull()) { OnLoaded(nullptr); return; }
// 이미 로드되어 있으면 즉시 반환 if (MeshRef.IsValid()) { OnLoaded(MeshRef.Get()); return; }
// UAssetManager의 StreamableManager 사용 FStreamableManager& Streamable = UAssetManager::GetStreamableManager();
Streamable.RequestAsyncLoad( MeshRef.ToSoftObjectPath(), FStreamableDelegate::CreateLambda([MeshRef, OnLoaded]() { OnLoaded(MeshRef.Get()); })); }};3.2 다중 어셋 일괄 비동기 로딩
Section titled “3.2 다중 어셋 일괄 비동기 로딩”void AMyGameMode::PreloadLevelAssets(TArray<FSoftObjectPath> AssetPaths, TFunction<void()> OnAllLoaded){ FStreamableManager& Streamable = UAssetManager::GetStreamableManager();
// 모든 어셋 로드 완료 시 콜백 TSharedPtr<FStreamableHandle> Handle = Streamable.RequestAsyncLoad( AssetPaths, FStreamableDelegate::CreateWeakLambda(this, [this, OnAllLoaded]() { UE_LOG(LogTemp, Log, TEXT("모든 레벨 어셋 로드 완료")); OnAllLoaded(); }), FStreamableManager::AsyncLoadHighPriority // 높은 우선순위 );
// Handle 보존 — GC 방지 PreloadHandle = Handle;}
// 사용 예시void AMyGameMode::BeginPlay(){ Super::BeginPlay();
TArray<FSoftObjectPath> Paths = { FSoftObjectPath(TEXT("/Game/Meshes/Environment/SM_Tree01")), FSoftObjectPath(TEXT("/Game/Textures/T_Grass_D")), FSoftObjectPath(TEXT("/Game/Sounds/Ambient/SC_Forest")), };
PreloadLevelAssets(Paths, [this]() { // 어셋 로드 완료 후 게임 시작 StartGameplay(); });}3.3 핸들로 진행률 추적
Section titled “3.3 핸들로 진행률 추적”void AMyLoadingScreen::UpdateLoadingProgress(){ if (LoadHandle.IsValid()) { float Progress = LoadHandle->GetProgress(); ProgressBar->SetPercent(Progress);
if (LoadHandle->HasLoadCompleted()) { HideLoadingScreen(); } }}4. UAssetManager — Primary Asset 관리
Section titled “4. UAssetManager — Primary Asset 관리”UAssetManager는 Primary Asset(UPrimaryDataAsset 서브클래스)을 관리합니다. 번들 단위로 로드·언로드하는 고수준 API를 제공합니다.
// PrimaryDataAsset 서브클래스UCLASS()class MYGAME_API UWeaponData : public UPrimaryDataAsset{ GENERATED_BODY()
public: // Primary Asset ID 반환 virtual FPrimaryAssetId GetPrimaryAssetId() const override { return FPrimaryAssetId(TEXT("WeaponData"), GetFName()); }
UPROPERTY(EditDefaultsOnly, Category = "Stats") float Damage = 30.f;
UPROPERTY(EditDefaultsOnly, Category = "Assets") TSoftObjectPtr<UStaticMesh> WeaponMesh;};// AssetManager로 Primary Asset 로딩void AMyInventory::LoadWeaponData(FPrimaryAssetId WeaponAssetId){ UAssetManager& AssetManager = UAssetManager::Get();
// Primary Asset 비동기 로딩 AssetManager.LoadPrimaryAsset( WeaponAssetId, TArray<FName>(), // 번들 이름 (없으면 기본) FStreamableDelegate::CreateUObject(this, &AMyInventory::OnWeaponDataLoaded, WeaponAssetId) );}
void AMyInventory::OnWeaponDataLoaded(FPrimaryAssetId WeaponAssetId){ UAssetManager& AssetManager = UAssetManager::Get();
UWeaponData* WeaponData = Cast<UWeaponData>( AssetManager.GetPrimaryAssetObject(WeaponAssetId));
if (IsValid(WeaponData)) { EquipWeapon(WeaponData); }}
// 사용 완료 후 언로드void AMyInventory::UnloadWeaponData(FPrimaryAssetId WeaponAssetId){ UAssetManager& AssetManager = UAssetManager::Get(); AssetManager.UnloadPrimaryAsset(WeaponAssetId);}5. 메모리 관리 & 언로드 전략
Section titled “5. 메모리 관리 & 언로드 전략”5.1 FStreamableHandle 수명 관리
Section titled “5.1 FStreamableHandle 수명 관리”// ❌ Handle을 저장하지 않으면 즉시 언로드될 수 있음void AMyActor::LoadAsset(){ // Handle을 로컬 변수로만 받으면 스코프 종료 시 언로드 FStreamableManager& Streamable = UAssetManager::GetStreamableManager(); Streamable.RequestAsyncLoad(AssetPath, Callback); // Handle 저장 안 함 — 위험}
// ✅ 멤버 변수로 Handle 보존UPROPERTY() // TSharedPtr은 UPROPERTY 불필요하지만 명시적 수명 관리TSharedPtr<FStreamableHandle> LoadHandle;
void AMyActor::LoadAsset(){ FStreamableManager& Streamable = UAssetManager::GetStreamableManager(); LoadHandle = Streamable.RequestAsyncLoad(AssetPath, Callback);}
void AMyActor::UnloadAsset(){ if (LoadHandle.IsValid()) { LoadHandle->ReleaseHandle(); // 강제 언로드 LoadHandle.Reset(); }}5.2 레퍼런스 카운팅과 GC
Section titled “5.2 레퍼런스 카운팅과 GC”// UObject 어셋은 참조가 남아있는 한 GC되지 않음// 더 이상 필요 없으면 참조를 null로 설정void AMyActor::ReleaseTemporaryAsset(){ // UPROPERTY 포인터를 null로 설정하면 GC 대상이 됨 TemporaryMesh = nullptr;
// 또는 강제 GC (주의: 성능 영향) // GEngine->ForceGarbageCollection(true);}| 상황 | 권장 방법 |
|---|---|
| 코어 어셋 (항상 필요) | Hard Reference (UPROPERTY 직접 포인터) |
| 선택적 어셋 | TSoftObjectPtr + LoadSynchronous() 또는 비동기 |
| 대용량 어셋 | FStreamableManager::RequestAsyncLoad() |
| Primary Data Asset | UAssetManager::LoadPrimaryAsset() |
| 생성자 내 로딩 | ConstructorHelpers::FObjectFinder |
| 런타임 경로 로딩 | LoadObject<T>() (소형) 또는 비동기 (대형) |
핵심 규칙: Soft Reference는 메모리를 아끼고, Hard Reference는 편의를 제공합니다. 대형 어셋, 런타임 선택 어셋, 선택 사항인 어셋에는 반드시 Soft Reference를 사용하세요.