UCLASS()
class AMyCharacter : public ACharacter
{
USkeletalMesh* CharacterMesh;
TObjectPtr<UTexture2D> CharacterIcon;
};
자동으로 메모리에 필요한 모든 것들을 다 올려놓은 상태
즉, 메모리에 올라와있는 에셋이나 액터의 주소를 저장하고 있음
위 코드예시에서 Mesh뿐만이 아니라 메시와 연관되고 참조하고 있는 매터리얼, Texture들도 다 함께 엮여서 올라감
그래도 무조건 사용되고 필요한 에셋에는 하드 레퍼런스를 사용해야함
class ALyraCharacter : public ACharacter
{
TSoftObjectPtr<USkeletalMesh> CharacterMesh;
TSoftObjectPtr<UTexture2D> CharacterIcon;
};
메모리에 로드하지 않고, 경로만 문자열로 저장 (몇 byte)
나중에 필요할 때 경로를 참조하여 메모리에 로드함
그래서 보통 에셋을 가리킬 때 주로 사용
Asset Registry : .uasset이라는 에셋 이름과 실제 파일 경로를 연결해주는 인덱스
나중에 에셋이 필요하면, Asset Registry를 통해 disk경로로 찾아가서 에셋을 메모리에 로드함
하드 레퍼런스 방식 (UClass*)
특정 클래스나 그 하위 클래스 타입만 선택적으로 담을 수 있는 유효성 검사 기능이 포함된 포인터
클래스의 경우 위에서 봤던 에셋들을 엄청나게 가지고 있는 설계도이기 때문에, 하드레퍼런스를 하면 메모리 엄청 먹게 됨
그래서 무조건 사용되어야할 클래스이거나 유효성 검사가 필요할 때 사용
소프트 레퍼런스 방식 (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());
}
}
};
// 경로 정보가 있는지 먼저 확인
if (!CharacterMesh.IsNull())
{
// 즉시 로드
USkeletalMesh* LoadedMesh = CharacterMesh.LoadSynchronous();
}
언리얼 게임이 진행되고 있는 메인 스레드에서 잠시 작업(게임흐름)을 멈추고, 디스크에서 해당 에셋을 가져옴
당연히 I/O작업이므로 느리며, 그 동안 게임도 멈추게 됨
보통 맵 로딩같이 플레이어가 로딩될 것이라고 예측하는 상황에서 주로 사용
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를 꼭 사용해야함
하지만 TSoftObjectPtr같은 SoftReference는 대규모 게임에서는 단점이 많음
TObjectPtr처럼 GC가 메모리 관리를 해주지 않음
하나하나 직접 로드해줘야하므로, 대량으로 한 번에 로드하고 소환이 불가능
그리고 상황에 맞게 자동으로 로드, 참조카운트 등 여러 기능들을 지원하지 않아, 대규모 게임에선 현실적으로 안 맞는 부분이 있다
그래서 Asset Manager라는 하나의 클래스가 여러 에셋들을 한 번에 관리하게끔 함
이 에셋 매니저한테 관리 받으려면, 에셋은 UPrimaryDataAsset을 상속받아야 관리받을 수 있음
나머지 일반 에셋들은 Asset Registry로 관리
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를 키로하고, 해당 데이터에셋을 값으로 하여 저장함
따라서 상수시간에 에셋을 로드 가능
총이라는 에셋은 사운드, 메시, 텍스쳐, 이름 등 여러 에셋들을 가짐
하지만 캐릭터에게는 다 필요할지라도 서버에선 사운드, 메시, 텍스쳐가 필요없고 무기상점에서도 사운드는 필요 없음
즉, Primary Asset에서도 묶음으로 필요한 부분만 로드할 수 있게 하는 것이 Bundle System
// 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가 인식할 수 있도록 아래의 과정을 수행
Project Settings\Asset Manager에서 Primary Asset Types to Scan에 MyGameData를 추가// 에셋 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); // 캐릭터 메시로 설정
}
}