현재 개발 중인 ProjectXZ의 모듈러 시스템에서 DataItemID를 입력받아 해당하는 SkeletalMesh 애셋을 로드하는 기능을 리팩토링하려고 한다.
기존에는 LoadObject를 사용해 애셋을 로드했으나, 이번에 애셋을 캐싱하고 비동기 방식으로 로드하여 최적화하고자 한다.
이를 위해 AssetManager 클래스와 RequestAsyncLoad 함수를 활용하여 개선하였다.
비동기 로드 (AsyncLoad)
- 프로그램이 특정 작업을 백그라운드에서 수행하는 방식
- 작업이 끝날 때까지 기다리지 않고, 그 사이에 다른 작업을 계속해서 진행
동기 로드 (syncLoad)
- 특정 작업이 끝날 때까지 프로그램이 멈추거나(블로킹) 다른 작업을 할 수 없는 방식
- 애셋이 로드될 때까지 다른 프로세스가 대기하게 되어 성능 저하가 발생할 수 있다.
AssetManager 는 애셋 로딩과 캐싱을 관리하는 싱글톤 오브젝트이다.
StreamableManager는 비동기 방식으로 애셋을 로드하며, 애셋이 메모리에 보존될 수 있도록 관리한다. 델리게이트를 통해 로드 완료 시 호출된다.
RequestAsyncLoad 는 애셋 그룹을 비동기 로드하고, 완료 시 델리게이트를 호출하는 함수이다. 로드 중에도 다른 작업을 처리할 수 있다.
Streamable Manager의 비동기 동작 방식Streamable Manager는 비동기 로드 중인 애셋이 델리게이트가 호출될 때까지 메모리에서 유지되도록 하드 레퍼런스를 유지시킨다.
델리게이트가 호출되기 전에 가비지 컬렉팅되지 않도록 보호하며, 델리게이트가 호출된 이후에는 하드 레퍼런스가 해제된다.
만약 애셋이 계속해서 메모리에 남아 있어야 한다면, 델리게이트 호출 이후에도 어딘가에 하드 레퍼런스를 유지시켜야 가비지 컬렉팅에 의해 제거되지 않는다.
전체 흐름을 정리하면 FAsyncLoadingThread 클래스에서
LoadPackageAsync가 대기열에 넣음 (QueuedPackages.Add)CreateAsyncPackagesFromQueue가 대기열에서 꺼내서 작업대로 옮김 (AsyncPackages.Add )ProcessAsyncPackageRequest 함수에서 실제로 등록한다.ProcessAsyncLoading이 작업대에서 하나씩 로딩(직렬화)함.
RequestAsyncLoad 함수가 호출되어, 필요한 애셋을 비동기 로드 방식으로 가져온다. StreamInternal 함수는 요청된 애셋이 이미 캐싱되어 있는지 확인한다. if ( USkeletalMesh* SkeletalMesh = LoadObject<USkeletalMesh>(this,*ModuleAsset->ASSETPATH)
{
SkeletalMeshComponent->SetSkeletalMesh(SkeletalMesh);
if ( SkeletalMeshComponent == Character->GetMesh() )
{
return;
}
...
}
}
LoadObject는 애셋을 캐싱하지 않음. UXZAssetManager& AssetManager = UXZAssetManager::GetXZAssetManager();
FSoftObjectPath AssetPath = ModuleAsset->ASSETPATH;
AssetManager.GetStreamableManager().RequestAsyncLoad(AssetPath, FStreamableDelegate::CreateLambda([this, SkeletalMeshComponent, AssetPath]()
{
if (USkeletalMesh* SkeletalMesh = Cast<USkeletalMesh>(AssetPath.TryLoad()))
{
SkeletalMeshComponent->SetSkeletalMesh(SkeletalMesh);
...
}
}));
AssetManager를 사용해 애셋을 로드하여 캐싱이 이루어지기 때문에, 동일한 애셋을 여러 번 로드할 때 캐시에서 가져오기 때문에 성능이 향상된다. LoadObject는 매번 새로 로드하여 불필요하게 성능 저하가 발생할 가능성이 있었음RequestAsyncLoad함수를 통해 비동기 로드가 가능비동기 로드는 더 복잡한 메모리 관리를 필요로 한다. 메모리에서 오브젝트가 가비지 컬렉팅되지 않도록 하드 레퍼런스를 유지해야 하며, 델리게이트가 호출된 후에만 메모리에서 해제될 수 있다. 이를 통해 게임의 멈춤 현상과 메모리 사용량을 줄일 수 있다.