Skip to content

UE5 C++ 어셋 로딩 완전 가이드

개요 — 어셋 로딩 전략의 중요성

Section titled “개요 — 어셋 로딩 전략의 중요성”

어셋 로딩 방식의 선택은 메모리 사용량, 로딩 시간, 프레임 드랍 모두에 직접적인 영향을 줍니다. 잘못된 참조 방식은 레벨 로드 시 불필요한 어셋을 통째로 메모리에 올려 로딩 시간을 수 배로 늘리는 원인이 됩니다.

방식메모리로딩 시점사용 시기
Hard Reference (직접 포인터)항상 로드됨소유 오브젝트 로드 시반드시 필요한 코어 어셋
Soft Reference (TSoftObjectPtr)필요 시 로드명시적 요청 시선택적·대용량 어셋
동기 로딩즉시 로드호출 즉시 (블로킹)소형 어셋, 초기화 시
비동기 로딩백그라운드 로드완료 콜백대형 어셋, 런타임

UPROPERTYTObjectPtr<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
};
// ✅ 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.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”
MyAssetLoader.h
#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());
}));
}
};
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();
});
}
void AMyLoadingScreen::UpdateLoadingProgress()
{
if (LoadHandle.IsValid())
{
float Progress = LoadHandle->GetProgress();
ProgressBar->SetPercent(Progress);
if (LoadHandle->HasLoadCompleted())
{
HideLoadingScreen();
}
}
}

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);
}

// ❌ 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();
}
}
// UObject 어셋은 참조가 남아있는 한 GC되지 않음
// 더 이상 필요 없으면 참조를 null로 설정
void AMyActor::ReleaseTemporaryAsset()
{
// UPROPERTY 포인터를 null로 설정하면 GC 대상이 됨
TemporaryMesh = nullptr;
// 또는 강제 GC (주의: 성능 영향)
// GEngine->ForceGarbageCollection(true);
}

상황권장 방법
코어 어셋 (항상 필요)Hard Reference (UPROPERTY 직접 포인터)
선택적 어셋TSoftObjectPtr + LoadSynchronous() 또는 비동기
대용량 어셋FStreamableManager::RequestAsyncLoad()
Primary Data AssetUAssetManager::LoadPrimaryAsset()
생성자 내 로딩ConstructorHelpers::FObjectFinder
런타임 경로 로딩LoadObject<T>() (소형) 또는 비동기 (대형)

핵심 규칙: Soft Reference는 메모리를 아끼고, Hard Reference는 편의를 제공합니다. 대형 어셋, 런타임 선택 어셋, 선택 사항인 어셋에는 반드시 Soft Reference를 사용하세요.