1. 에셋 참조 방식: 하드(Hard) vs 소프트(Soft)
① 하드 래퍼런스 (Hard Reference)
- 특징: UPROPERTY(EditAnywhere)로 블루프린트에서 직접 할당하거나 생성자에서 ConstructorHelpers로 찾는 방식.
- 장단점: 참조된 에셋을 즉시 사용할 수 있어 편리하지만, 캐릭터가 스폰될 때 들고 있는 무기 수십 개가 메모리에 한꺼번에 로드되므로 메모리 부하 및 로딩 지연이 발생할 수 있습니다.
② 소프트 래퍼런스 (Soft Reference)
- 특징: 에셋의 경로 정보만 딱 가지고 있다가, 개발자가 '지금 당장 필요해!'라고 부를 때(동기/비동기) 비로소 메모리에 로드하는 방식.
- 장점: 초기 로딩 속도 개선, 메모리 점유율 최소화, 비동기 로딩을 통한 프레임 드롭(렉) 방지.
- 사용처: 인벤토리 내 수백 개의 아이콘, 특정 보스 몬스터, 무거운 고해상도 텍스처/사운드 등.
- 선언 방법:
- 인스턴스(객체/에셋) 참조: TSoftObjectPtr<UStaticMesh>
- 클래스(붕어빵 틀) 참조: TSoftClassPtr<AActor>
2. 소프트 참조 로드 방식 (동기 vs 비동기)
① 동기 로드 (Synchronous Load)
메모리에 올릴 때까지 메인 스레드(게임)가 멈춰서 기다리는 방식입니다.
C++
UPROPERTY(EditAnywhere)
TSoftObjectPtr<UStaticMesh> MySoftMesh;
void LoadWithSoftPtr()
{
if (MySoftMesh.IsNull()) return;
UStaticMesh* FinalMesh = nullptr;
if (MySoftMesh.IsValid()) {
// 이미 메모리에 로드된 상태면 바로 가져옴
FinalMesh = MySoftMesh.Get();
} else {
// 메모리에 없으면 메인 스레드를 멈추고 강제로 로드!
FinalMesh = MySoftMesh.LoadSynchronous();
}
}
② 비동기 로드 (Asynchronous Load)
에셋 매니저를 통해 뒷단(서브 스레드)에서 로드하라고 시키고, 게임은 계속 진행하는 방식입니다. 로드가 끝나면 델리게이트를 통해 결과를 돌려받습니다.
C++
#include "Engine/AssetManager.h"
#include "Engine/StreamableManager.h"
void LoadAsync()
{
if (MySoftMesh.IsValid()) {
OnLoadCompleted(); // 이미 로드되어 있으면 바로 함수 실행
return;
}
// 에셋 매니저의 StreamableManager를 통해 비동기 로드 요청
FStreamableManager& Streamable = UAssetManager::GetStreamableManager();
Streamable.RequestAsyncLoad(
MySoftMesh.ToSoftObjectPath(), // 로드할 경로
FStreamableDelegate::CreateUObject(this, &AMySoftReferenceTestActor::OnLoadCompleted) // 완료 시 호출될 델리게이트
);
}
void OnLoadCompleted()
{
// 비동기 로드가 끝나면 이 함수가 실행되며 에셋을 꺼내 씀!
UStaticMesh* FinalMesh = MySoftMesh.Get();
}
3. 데이터 관리: DataTable vs DataAsset
수많은 아이템 데이터를 관리할 때 무엇을 써야 할까요? 상황에 따라 섞어 쓰는 것이 베스트입니다.
| 구분 | DataTable (데이터 테이블) | DataAsset (데이터 에셋) |
| 특징 | 엑셀 형태의 표. 한눈에 보기 좋음. | 개별 포장된 상품. 독립된 에셋 파일. |
| 장점 | 공격력, 드랍률 등 1000개의 수치를 쫙 펴놓고 밸런스 비교/수정하기 좋음. | 게임 중 특정 아이템 1개만 콕 집어서 메모리에 비동기 로드하기 좋음. |
| 단점 | 행 하나만 필요해도 표 전체를 로드해야 함. | 아이템이 1000개면 파일도 1000개라 전체 수치 비교/수정이 매우 불편함. |
💡 핵심 전략: 기본 스탯은 DataTable로 기획자가 관리하고, 실제 무거운 메쉬나 사운드 같은 리소스는 DataAsset에 TSoftObjectPtr로 연결해 둔 뒤, 테이블에서 데이터 에셋의 ID만 참조하는 식으로 섞어 씁니다!
4. 실전: PrimaryDataAsset 구현 및 로드
에셋 매니저가 고유 ID를 통해 빠르게 에셋을 식별하고 로드할 수 있도록 UPrimaryDataAsset을 상속받아 사용합니다.
① PrimaryDataAsset 세팅 (데이터 구조 만들기)
C++
UCLASS()
class UMyItemData : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
// 에셋 매니저가 이 파일을 식별할 수 있는 고유 ID 규칙 생성
virtual FPrimaryAssetId GetPrimaryAssetId() const override
{
return FPrimaryAssetId(ItemType, GetFName());
}
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FPrimaryAssetType ItemType;
// 무거운 데이터는 반드시 TSoftObjectPtr로 선언!
// meta = (AssetBundles = "Mesh")를 통해 특정 번들만 골라서 로드 가능
UPROPERTY(EditAnywhere, meta = (AssetBundles = "Mesh"))
TSoftObjectPtr<USkeletalMesh> ItemMesh;
};
(주의: 생성 후 프로젝트 세팅 -> Asset Manager에서 지정한 ItemType과 폴더 경로를 꼭 등록해야 엔진이 인식합니다!)
② 에셋 매니저로 특정 데이터 에셋 비동기 로드하기
C++
#include "Engine/AssetManager.h"
UPROPERTY(EditAnywhere)
FPrimaryAssetId ItemToLoad; // 에디터에서 로드할 대상의 ID 지정
// 로딩 진행 상황을 쥐고 있을 핸들 (안 들고 있으면 가비지 컬렉터가 날려버림!)
TSharedPtr<FStreamableHandle> LoadingHandle;
void StartLoading()
{
if (!ItemToLoad.IsValid()) return;
UAssetManager& AssetManager = UAssetManager::Get();
TArray<FName> Bundles; // 특정 번들 이름, 비워두면 전체 로드
// ID를 주고 로드 요청! (끝나면 OnLoadFinished 호출)
LoadingHandle = AssetManager.LoadPrimaryAsset(
ItemToLoad,
Bundles,
FStreamableDelegate::CreateUObject(this, &AMyDataAssetTestActor::OnLoadFinished, ItemToLoad)
);
}
// 로드 완료 후 콜백 함수
void OnLoadFinished(FPrimaryAssetId LoadedId)
{
// 로드된 ID를 바탕으로 진짜 UMyItemData 객체를 캐스팅해서 꺼내옴
UMyItemData* LoadedItem = Cast<UMyItemData>(UAssetManager::Get().GetPrimaryAssetObject(LoadedId));
if (LoadedItem) {
// 이제 LoadedItem->ItemMesh.Get() 등을 통해 사용 가능!
}
}