[Unreal] 비동기 로드

KWONYEONGMIN·2024년 10월 15일

언리얼

목록 보기
10/15

개요

현재 개발 중인 ProjectXZ의 모듈러 시스템에서 DataItemID를 입력받아 해당하는 SkeletalMesh 애셋을 로드하는 기능을 리팩토링하려고 한다.
기존에는 LoadObject를 사용해 애셋을 로드했으나, 이번에 애셋을 캐싱하고 비동기 방식으로 로드하여 최적화하고자 한다.
이를 위해 AssetManager 클래스와 RequestAsyncLoad 함수를 활용하여 개선하였다.

비동기 로드와 동기 로드

비동기 로드 (AsyncLoad)

  • 프로그램이 특정 작업을 백그라운드에서 수행하는 방식
  • 작업이 끝날 때까지 기다리지 않고, 그 사이에 다른 작업을 계속해서 진행

동기 로드 (syncLoad)

  • 특정 작업이 끝날 때까지 프로그램이 멈추거나(블로킹) 다른 작업을 할 수 없는 방식
  • 애셋이 로드될 때까지 다른 프로세스가 대기하게 되어 성능 저하가 발생할 수 있다.


언리얼 비동기 애셋 로딩 및 관리

언리얼 공식 문서 (Unreal 애셋 관리)

언리얼 공식 문서 (비동기 애셋 로딩)

AssetManager 는 애셋 로딩과 캐싱을 관리하는 싱글톤 오브젝트이다.

StreamableManager는 비동기 방식으로 애셋을 로드하며, 애셋이 메모리에 보존될 수 있도록 관리한다. 델리게이트를 통해 로드 완료 시 호출된다.

RequestAsyncLoad 는 애셋 그룹을 비동기 로드하고, 완료 시 델리게이트를 호출하는 함수이다. 로드 중에도 다른 작업을 처리할 수 있다.

Streamable Manager의 비동기 동작 방식

Streamable Manager는 비동기 로드 중인 애셋이 델리게이트가 호출될 때까지 메모리에서 유지되도록 하드 레퍼런스를 유지시킨다.
델리게이트가 호출되기 전에 가비지 컬렉팅되지 않도록 보호하며, 델리게이트가 호출된 이후에는 하드 레퍼런스가 해제된다.
만약 애셋이 계속해서 메모리에 남아 있어야 한다면, 델리게이트 호출 이후에도 어딘가에 하드 레퍼런스를 유지시켜야 가비지 컬렉팅에 의해 제거되지 않는다.



언리얼 비동기 작동 방식

전체 흐름을 정리하면 FAsyncLoadingThread 클래스에서

  • 등록: LoadPackageAsync가 대기열에 넣음 (QueuedPackages.Add)
  • 준비: CreateAsyncPackagesFromQueue가 대기열에서 꺼내서 작업대로 옮김 (AsyncPackages.Add )
    • 함수 내부의 ProcessAsyncPackageRequest 함수에서 실제로 등록한다.
  • 실행: ProcessAsyncLoading이 작업대에서 하나씩 로딩(직렬화)함.

캐싱 과정 분석

  • 언리얼에서 StreamableManager로 로드된 애셋들을 어떻게 캐싱하는지 찾아보고자 엔진 코드를 분석해봤다.
  1. 비동기 로드 요청 (RequestAsyncLoad)
  • FStreamableManager의 RequestAsyncLoad 함수가 호출되어, 필요한 애셋을 비동기 로드 방식으로 가져온다.
  • 이 과정에서 StartHandleRequests 함수가 실행되며 로드 요청을 처리하는 단계가 시작됩니다.
  1. 로드된 애셋 캐싱 (StreamInternal)
    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함수를 통해 비동기 로드가 가능
  • 비동기 로드를 통해 애셋이 로드되는 동안에도 다른 작업을 수행할 수 있다
  • 로드가 완료되면 람다 함수를 통해 SkeletalMesh를 적용한다.


주의할 점

비동기 로드는 더 복잡한 메모리 관리를 필요로 한다. 메모리에서 오브젝트가 가비지 컬렉팅되지 않도록 하드 레퍼런스를 유지해야 하며, 델리게이트가 호출된 후에만 메모리에서 해제될 수 있다. 이를 통해 게임의 멈춤 현상과 메모리 사용량을 줄일 수 있다.

profile
Hello World

0개의 댓글