TIL_094 : Hard/Soft Reference, 언리얼 데이터 에셋

김펭귄·2026년 1월 12일

Today What I Learned (TIL)

목록 보기
94/109

1. Hard/Soft Reference

1.1. Hard Reference

UCLASS()
class AMyCharacter : public ACharacter
{
    USkeletalMesh* CharacterMesh;  
    TObjectPtr<UTexture2D> CharacterIcon;     
};
  • 자동으로 메모리에 필요한 모든 것들을 다 올려놓은 상태

  • 즉, 메모리에 올라와있는 에셋이나 액터의 주소를 저장하고 있음

  • 위 코드예시에서 Mesh뿐만이 아니라 메시와 연관되고 참조하고 있는 매터리얼, Texture들도 다 함께 엮여서 올라감

  • 그래도 무조건 사용되고 필요한 에셋에는 하드 레퍼런스를 사용해야함

1.2. Soft Reference

class ALyraCharacter : public ACharacter
{
    TSoftObjectPtr<USkeletalMesh> CharacterMesh;  
    TSoftObjectPtr<UTexture2D> CharacterIcon;  
};
  • 메모리에 로드하지 않고, 경로만 문자열로 저장 (몇 byte)

  • 나중에 필요할 때 경로를 참조하여 메모리에 로드함

  • 그래서 보통 에셋을 가리킬 때 주로 사용

  • Asset Registry : .uasset이라는 에셋 이름과 실제 파일 경로를 연결해주는 인덱스

  • 나중에 에셋이 필요하면, Asset Registry를 통해 disk경로로 찾아가서 에셋을 메모리에 로드함

2. TSubclassOf vs TSoftClassPtr

2.1. TSubclassOf

  • 하드 레퍼런스 방식 (UClass*)

  • 특정 클래스나 그 하위 클래스 타입만 선택적으로 담을 수 있는 유효성 검사 기능이 포함된 포인터

  • 클래스의 경우 위에서 봤던 에셋들을 엄청나게 가지고 있는 설계도이기 때문에, 하드레퍼런스를 하면 메모리 엄청 먹게 됨

  • 그래서 무조건 사용되어야할 클래스이거나 유효성 검사가 필요할 때 사용

2.2. TSoftClassPtr

  • 소프트 레퍼런스 방식 (UClass*)

  • 마찬가지로 경로만 가지고 있다가, 필요할 때 로드

  • 메모리 사용을 엄청 아낄 수 있음

  • 지금 당장 필요하지 않은 클래스일때 사용

UCLASS()
class AWeaponSpawner : public AActor
{
    GENERATED_BODY()
    
public:
    UPROPERTY(EditDefaultsOnly, Category = "Weapon")
    TSoftClassPtr<AWeapon> WeaponClassToSpawn;
    
    void SpawnWeapon()
    {
        // 스폰 시점에 클래스를 메모리에 로드
        if (TSubclassOf<AWeapon> WeaponClass = WeaponClassToSpawn.LoadSynchronous())
        {
            // 로드된 클래스로 인스턴스 생성
            AWeapon* NewWeapon = GetWorld()->SpawnActor<AWeapon>(
                WeaponClass,
                GetActorLocation(),
                GetActorRotation());
        }
    }
};

3. Soft Reference 사용

3.1. Synchronous (동기 로드)

// 경로 정보가 있는지 먼저 확인
if (!CharacterMesh.IsNull())
{
    // 즉시 로드
    USkeletalMesh* LoadedMesh = CharacterMesh.LoadSynchronous();
}
  • 언리얼 게임이 진행되고 있는 메인 스레드에서 잠시 작업(게임흐름)을 멈추고, 디스크에서 해당 에셋을 가져옴

  • 당연히 I/O작업이므로 느리며, 그 동안 게임도 멈추게 됨

  • 보통 맵 로딩같이 플레이어가 로딩될 것이라고 예측하는 상황에서 주로 사용

3.2. Asynchronous (비동기 로드)

if (!CharacterMesh.IsNull())
{
    // AssetManager에게 백그라운드에서 로드하도록 요청
    FStreamableManager& Streamable = UAssetManager::GetStreamableManager();

    Streamable.RequestAsyncLoad(
            CharacterMesh.ToSoftObjectPath(),
            FStreamableDelegate::CreateWeakLambda(this, [this]()
        {
            // 로드 완료시
            if (USkeletalMesh* LoadedMesh = CharacterMesh.Get())
            {
                GetMesh()->SetSkeletalMesh(LoadedMesh);
            }
            else
            {
                UE_LOG(LogTemp, Error, TEXT("메시 로드 실패!"));
            }
        })
    );
}
  • 동기 로드와 다르게, 워크 스레드 풀에서 스레드 하나를 할당 받음

  • 할당 받은 워크 스레드에서 로딩 작업을 수행함

  • 따라서 메인 스레드에선 게임을 계속 진행하다가, 로딩이 완료되면 워크 스레드가 메인 스레드에게 콜백함수를 부름

  • 주의할 점으로는, 이 작업을 실행한 객체가 중간에 사라지는 경우에도 로딩 완료 후에 콜백함수가 호출되므로, WeakLambda를 꼭 사용해야함

4. Asset Manager

  • 하지만 TSoftObjectPtr같은 SoftReference는 대규모 게임에서는 단점이 많음

  • TObjectPtr처럼 GC가 메모리 관리를 해주지 않음

  • 하나하나 직접 로드해줘야하므로, 대량으로 한 번에 로드하고 소환이 불가능

  • 그리고 상황에 맞게 자동으로 로드, 참조카운트 등 여러 기능들을 지원하지 않아, 대규모 게임에선 현실적으로 안 맞는 부분이 있다

  • 그래서 Asset Manager라는 하나의 클래스가 여러 에셋들을 한 번에 관리하게끔 함

  • 이 에셋 매니저한테 관리 받으려면, 에셋은 UPrimaryDataAsset을 상속받아야 관리받을 수 있음

  • 나머지 일반 에셋들은 Asset Registry로 관리

4.1. Primary Data Asset 상속

class UWeaponDataAsset : public UPrimaryDataAsset  // 상속받아야 관리가능
{
    GENERATED_BODY()
    
public:
    // AssetManager가 식별할 수 있는 고유 ID 생성
    virtual FPrimaryAssetId GetPrimaryAssetId() const override
    {
        // Type과 DataAsset파일 이름으로 고유 ID를 생성
        return FPrimaryAssetId(TEXT("Weapon"), GetFName());
    }
    
    // 소프트 레퍼런스 데이터
    UPROPERTY(EditDefaultsOnly)
    TSoftObjectPtr<USkeletalMesh> WeaponMesh;
    
    UPROPERTY(EditDefaultsOnly)
    TSoftObjectPtr<UTexture2D> WeaponIcon;
};
  • Asset Manager는 해시map을 사용하여 PrimaryAssetId를 키로하고, 해당 데이터에셋을 값으로 하여 저장함

  • 따라서 상수시간에 에셋을 로드 가능

4.2. Bundle System

  • 총이라는 에셋은 사운드, 메시, 텍스쳐, 이름 등 여러 에셋들을 가짐

  • 하지만 캐릭터에게는 다 필요할지라도 서버에선 사운드, 메시, 텍스쳐가 필요없고 무기상점에서도 사운드는 필요 없음

  • 즉, Primary Asset에서도 묶음으로 필요한 부분만 로드할 수 있게 하는 것이 Bundle System

4.3. 사용법

// DataAsset 헤더

class MYGAME_API UMyGameDataAsset : public UPrimaryDataAsset
{
    GENERATED_BODY()

public:
    // 1. 번들에 포함될 에셋들 (소프트 포인터 사용), 번들 설정
    UPROPERTY(EditAnywhere, meta = (AssetBundles = "MeshBundle"))
    TSoftObjectPtr<UStaticMesh> CharacterMesh;

    UPROPERTY(EditAnywhere, meta = (AssetBundles = "TextureBundle"))
    TSoftObjectPtr<UTexture2D> CharacterIcon;

    // 2. GetPrimaryAssetId 오버라이드 (Asset Manager가 인식하기 위해 필수)
    virtual FPrimaryAssetId GetPrimaryAssetId() const override
    {
        return FPrimaryAssetId("MyGameData", GetFName());
    }
};
  • PrimaryDataAsset을 상속받아 번들과 함께 데이터에셋 생성

  • 에디터에서 Asset Manager가 인식할 수 있도록 아래의 과정을 수행

    1. Project Settings\Asset Manager에서 Primary Asset Types to Scan에 MyGameData를 추가
    2. Asset Base Class를 MyGameDataAsset로 선택
    3. Directories를 해당 에셋이 저장될 폴더로 지정
// 에셋 ID로 에셋 로드

void AMyCharacter::LoadMyBundle(FPrimaryAssetId AssetId)
{
	// 에셋 매니저는 싱글톤
    UAssetManager& AssetManager = UAssetManager::Get();

    // 로드할 번들 이름을 추가
    TArray<FName> BundlesToLoad;
    BundlesToLoad.Add(FName("MeshBundle"));

    // 번들 로드되면 실행할 콜백함수 설정
    FStreamableDelegate Delegate = 
    	FStreamableDelegate::CreateUObject(this, 
        								   &AMyCharacter::OnBundleLoadCompleted, 
                                           AssetId);
                           
	// 번들로 에셋 비동기 로드. 완료시 델리게이트로 콜백함수 실행
    AssetManager.LoadPrimaryAsset(AssetId, BundlesToLoad, Delegate);
}

// 로드 완료시 콜백함수. 엔진이 불러줘야하므로, UFUNCTION()으로 선언
void AMyCharacter::OnBundleLoadCompleted(FPrimaryAssetId AssetId)
{
    UAssetManager& AssetManager = UAssetManager::Get();
    // 로드된 에셋 가져오기
    UMyGameDataAsset* LoadedAsset = 
    	AssetManager.GetPrimaryAssetObject<UMyGameDataAsset>(AssetId);

    if (LoadedAsset)
    {
        // 데이터에셋에 MeshBundle에 속했던 CharacterMesh는 로드된 상태
        UStaticMesh* Mesh = LoadedAsset->CharacterMesh.get();
        GetMesh()->SetStaticMesh(Mesh);	// 캐릭터 메시로 설정
    }
}
profile
반갑습니다

0개의 댓글